ret) {
+ return Result.SUCCESS == ret.getCode();
+ }
+
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/enums/UserStatus.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/enums/UserStatus.java
new file mode 100644
index 0000000..32ff39a
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/enums/UserStatus.java
@@ -0,0 +1,26 @@
+package com.muyu.common.core.enums;
+
+/**
+ * 用户状态
+ *
+ * @author muyu
+ */
+public enum UserStatus {
+ OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除");
+
+ private final String code;
+ private final String info;
+
+ UserStatus (String code, String info) {
+ this.code = code;
+ this.info = info;
+ }
+
+ public String getCode () {
+ return code;
+ }
+
+ public String getInfo () {
+ return info;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/CaptchaException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/CaptchaException.java
new file mode 100644
index 0000000..eb32d0b
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/CaptchaException.java
@@ -0,0 +1,14 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 验证码错误异常类
+ *
+ * @author muyu
+ */
+public class CaptchaException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaException (String msg) {
+ super(msg);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/CheckedException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/CheckedException.java
new file mode 100644
index 0000000..4f12893
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/CheckedException.java
@@ -0,0 +1,26 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 检查异常
+ *
+ * @author muyu
+ */
+public class CheckedException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public CheckedException (String message) {
+ super(message);
+ }
+
+ public CheckedException (Throwable cause) {
+ super(cause);
+ }
+
+ public CheckedException (String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public CheckedException (String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/DemoModeException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/DemoModeException.java
new file mode 100644
index 0000000..82249cf
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/DemoModeException.java
@@ -0,0 +1,13 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 演示模式异常
+ *
+ * @author muyu
+ */
+public class DemoModeException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public DemoModeException () {
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/GlobalException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/GlobalException.java
new file mode 100644
index 0000000..b14e03c
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/GlobalException.java
@@ -0,0 +1,51 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 全局异常
+ *
+ * @author muyu
+ */
+public class GlobalException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ *
+ * 和 {@link CommonResult#getDetailMessage()} 一致的设计
+ */
+ private String detailMessage;
+
+ /**
+ * 空构造方法,避免反序列化问题
+ */
+ public GlobalException () {
+ }
+
+ public GlobalException (String message) {
+ this.message = message;
+ }
+
+ public String getDetailMessage () {
+ return detailMessage;
+ }
+
+ public GlobalException setDetailMessage (String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+
+ @Override
+ public String getMessage () {
+ return message;
+ }
+
+ public GlobalException setMessage (String message) {
+ this.message = message;
+ return this;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/InnerAuthException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/InnerAuthException.java
new file mode 100644
index 0000000..f211c7f
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/InnerAuthException.java
@@ -0,0 +1,14 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 内部认证异常
+ *
+ * @author muyu
+ */
+public class InnerAuthException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public InnerAuthException (String message) {
+ super(message);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/PreAuthorizeException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/PreAuthorizeException.java
new file mode 100644
index 0000000..6cb8636
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/PreAuthorizeException.java
@@ -0,0 +1,13 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 权限异常
+ *
+ * @author muyu
+ */
+public class PreAuthorizeException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public PreAuthorizeException () {
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/ServiceException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/ServiceException.java
new file mode 100644
index 0000000..5039bc0
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/ServiceException.java
@@ -0,0 +1,65 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 业务异常
+ *
+ * @author muyu
+ */
+public final class ServiceException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误码
+ */
+ private Integer code;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ *
+ * 和 {@link CommonResult#getDetailMessage()} 一致的设计
+ */
+ private String detailMessage;
+
+ /**
+ * 空构造方法,避免反序列化问题
+ */
+ public ServiceException () {
+ }
+
+ public ServiceException (String message) {
+ this.message = message;
+ }
+
+ public ServiceException (String message, Integer code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ public String getDetailMessage () {
+ return detailMessage;
+ }
+
+ public ServiceException setDetailMessage (String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+
+ @Override
+ public String getMessage () {
+ return message;
+ }
+
+ public ServiceException setMessage (String message) {
+ this.message = message;
+ return this;
+ }
+
+ public Integer getCode () {
+ return code;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/UtilException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/UtilException.java
new file mode 100644
index 0000000..8de4bbf
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/UtilException.java
@@ -0,0 +1,22 @@
+package com.muyu.common.core.exception;
+
+/**
+ * 工具类异常
+ *
+ * @author muyu
+ */
+public class UtilException extends RuntimeException {
+ private static final long serialVersionUID = 8247610319171014183L;
+
+ public UtilException (Throwable e) {
+ super(e.getMessage(), e);
+ }
+
+ public UtilException (String message) {
+ super(message);
+ }
+
+ public UtilException (String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotLoginException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotLoginException.java
new file mode 100644
index 0000000..40293bf
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotLoginException.java
@@ -0,0 +1,14 @@
+package com.muyu.common.core.exception.auth;
+
+/**
+ * 未能通过的登录认证异常
+ *
+ * @author muyu
+ */
+public class NotLoginException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public NotLoginException (String message) {
+ super(message);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotPermissionException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotPermissionException.java
new file mode 100644
index 0000000..e464840
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotPermissionException.java
@@ -0,0 +1,20 @@
+package com.muyu.common.core.exception.auth;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 未能通过的权限认证异常
+ *
+ * @author muyu
+ */
+public class NotPermissionException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public NotPermissionException (String permission) {
+ super(permission);
+ }
+
+ public NotPermissionException (String[] permissions) {
+ super(StringUtils.join(permissions, ","));
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotRoleException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotRoleException.java
new file mode 100644
index 0000000..53a1522
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/auth/NotRoleException.java
@@ -0,0 +1,20 @@
+package com.muyu.common.core.exception.auth;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 未能通过的角色认证异常
+ *
+ * @author muyu
+ */
+public class NotRoleException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public NotRoleException (String role) {
+ super(role);
+ }
+
+ public NotRoleException (String[] roles) {
+ super(StringUtils.join(roles, ","));
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/base/BaseException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/base/BaseException.java
new file mode 100644
index 0000000..9bb1356
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/base/BaseException.java
@@ -0,0 +1,69 @@
+package com.muyu.common.core.exception.base;
+
+/**
+ * 基础异常
+ *
+ * @author muyu
+ */
+public class BaseException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 所属模块
+ */
+ private String module;
+
+ /**
+ * 错误码
+ */
+ private String code;
+
+ /**
+ * 错误码对应的参数
+ */
+ private Object[] args;
+
+ /**
+ * 错误消息
+ */
+ private String defaultMessage;
+
+ public BaseException (String module, String code, Object[] args, String defaultMessage) {
+ this.module = module;
+ this.code = code;
+ this.args = args;
+ this.defaultMessage = defaultMessage;
+ }
+
+ public BaseException (String module, String code, Object[] args) {
+ this(module, code, args, null);
+ }
+
+ public BaseException (String module, String defaultMessage) {
+ this(module, null, null, defaultMessage);
+ }
+
+ public BaseException (String code, Object[] args) {
+ this(null, code, args, null);
+ }
+
+ public BaseException (String defaultMessage) {
+ this(null, null, null, defaultMessage);
+ }
+
+ public String getModule () {
+ return module;
+ }
+
+ public String getCode () {
+ return code;
+ }
+
+ public Object[] getArgs () {
+ return args;
+ }
+
+ public String getDefaultMessage () {
+ return defaultMessage;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileException.java
new file mode 100644
index 0000000..ae2e184
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileException.java
@@ -0,0 +1,17 @@
+package com.muyu.common.core.exception.file;
+
+import com.muyu.common.core.exception.base.BaseException;
+
+/**
+ * 文件信息异常类
+ *
+ * @author muyu
+ */
+public class FileException extends BaseException {
+ private static final long serialVersionUID = 1L;
+
+ public FileException (String code, Object[] args, String msg) {
+ super("file", code, args, msg);
+ }
+
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileNameLengthLimitExceededException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileNameLengthLimitExceededException.java
new file mode 100644
index 0000000..3a85df3
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileNameLengthLimitExceededException.java
@@ -0,0 +1,14 @@
+package com.muyu.common.core.exception.file;
+
+/**
+ * 文件名称超长限制异常类
+ *
+ * @author muyu
+ */
+public class FileNameLengthLimitExceededException extends FileException {
+ private static final long serialVersionUID = 1L;
+
+ public FileNameLengthLimitExceededException (int defaultFileNameLength) {
+ super("upload.filename.exceed.length", new Object[]{defaultFileNameLength}, "the filename is too long");
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileSizeLimitExceededException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileSizeLimitExceededException.java
new file mode 100644
index 0000000..7570be5
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileSizeLimitExceededException.java
@@ -0,0 +1,14 @@
+package com.muyu.common.core.exception.file;
+
+/**
+ * 文件名大小限制异常类
+ *
+ * @author muyu
+ */
+public class FileSizeLimitExceededException extends FileException {
+ private static final long serialVersionUID = 1L;
+
+ public FileSizeLimitExceededException (long defaultMaxSize) {
+ super("upload.exceed.maxSize", new Object[]{defaultMaxSize}, "the filesize is too large");
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileUploadException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileUploadException.java
new file mode 100644
index 0000000..94341ab
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/FileUploadException.java
@@ -0,0 +1,52 @@
+package com.muyu.common.core.exception.file;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * 文件上传异常类
+ *
+ * @author muyu
+ */
+public class FileUploadException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Throwable cause;
+
+ public FileUploadException () {
+ this(null, null);
+ }
+
+ public FileUploadException (final String msg) {
+ this(msg, null);
+ }
+
+ public FileUploadException (String msg, Throwable cause) {
+ super(msg);
+ this.cause = cause;
+ }
+
+ @Override
+ public void printStackTrace (PrintStream stream) {
+ super.printStackTrace(stream);
+ if (cause != null) {
+ stream.println("Caused by:");
+ cause.printStackTrace(stream);
+ }
+ }
+
+ @Override
+ public void printStackTrace (PrintWriter writer) {
+ super.printStackTrace(writer);
+ if (cause != null) {
+ writer.println("Caused by:");
+ cause.printStackTrace(writer);
+ }
+ }
+
+ @Override
+ public Throwable getCause () {
+ return cause;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/InvalidExtensionException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/InvalidExtensionException.java
new file mode 100644
index 0000000..3a993c2
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/file/InvalidExtensionException.java
@@ -0,0 +1,67 @@
+package com.muyu.common.core.exception.file;
+
+import java.util.Arrays;
+
+/**
+ * 文件上传 误异常类
+ *
+ * @author muyu
+ */
+public class InvalidExtensionException extends FileUploadException {
+ private static final long serialVersionUID = 1L;
+
+ private String[] allowedExtension;
+ private String extension;
+ private String filename;
+
+ public InvalidExtensionException (String[] allowedExtension, String extension, String filename) {
+ super("filename : [" + filename + "], extension : [" + extension + "], allowed extension : [" + Arrays.toString(allowedExtension) + "]");
+ this.allowedExtension = allowedExtension;
+ this.extension = extension;
+ this.filename = filename;
+ }
+
+ public String[] getAllowedExtension () {
+ return allowedExtension;
+ }
+
+ public String getExtension () {
+ return extension;
+ }
+
+ public String getFilename () {
+ return filename;
+ }
+
+ public static class InvalidImageExtensionException extends InvalidExtensionException {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidImageExtensionException (String[] allowedExtension, String extension, String filename) {
+ super(allowedExtension, extension, filename);
+ }
+ }
+
+ public static class InvalidFlashExtensionException extends InvalidExtensionException {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidFlashExtensionException (String[] allowedExtension, String extension, String filename) {
+ super(allowedExtension, extension, filename);
+ }
+ }
+
+ public static class InvalidMediaExtensionException extends InvalidExtensionException {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidMediaExtensionException (String[] allowedExtension, String extension, String filename) {
+ super(allowedExtension, extension, filename);
+ }
+ }
+
+ public static class InvalidVideoExtensionException extends InvalidExtensionException {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidVideoExtensionException (String[] allowedExtension, String extension, String filename) {
+ super(allowedExtension, extension, filename);
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/job/TaskException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/job/TaskException.java
new file mode 100644
index 0000000..aee364a
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/job/TaskException.java
@@ -0,0 +1,29 @@
+package com.muyu.common.core.exception.job;
+
+/**
+ * 计划策略异常
+ *
+ * @author muyu
+ */
+public class TaskException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private Code code;
+
+ public TaskException (String msg, Code code) {
+ this(msg, code, null);
+ }
+
+ public TaskException (String msg, Code code, Exception nestedEx) {
+ super(msg, nestedEx);
+ this.code = code;
+ }
+
+ public Code getCode () {
+ return code;
+ }
+
+ public enum Code {
+ TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/CaptchaExpireException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/CaptchaExpireException.java
new file mode 100644
index 0000000..a95a57b
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/CaptchaExpireException.java
@@ -0,0 +1,14 @@
+package com.muyu.common.core.exception.user;
+
+/**
+ * 验证码失效异常类
+ *
+ * @author muyu
+ */
+public class CaptchaExpireException extends UserException {
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaExpireException () {
+ super("user.jcaptcha.expire", null);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/UserException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/UserException.java
new file mode 100644
index 0000000..f113749
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/UserException.java
@@ -0,0 +1,16 @@
+package com.muyu.common.core.exception.user;
+
+import com.muyu.common.core.exception.base.BaseException;
+
+/**
+ * 用户信息异常类
+ *
+ * @author muyu
+ */
+public class UserException extends BaseException {
+ private static final long serialVersionUID = 1L;
+
+ public UserException (String code, Object[] args) {
+ super("user", code, args, null);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/UserPasswordNotMatchException.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/UserPasswordNotMatchException.java
new file mode 100644
index 0000000..7615cda
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/exception/user/UserPasswordNotMatchException.java
@@ -0,0 +1,14 @@
+package com.muyu.common.core.exception.user;
+
+/**
+ * 用户密码不正确或不符合规范异常类
+ *
+ * @author muyu
+ */
+public class UserPasswordNotMatchException extends UserException {
+ private static final long serialVersionUID = 1L;
+
+ public UserPasswordNotMatchException () {
+ super("user.password.not.match", null);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/CharsetKit.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/CharsetKit.java
new file mode 100644
index 0000000..fe5e9fa
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/CharsetKit.java
@@ -0,0 +1,94 @@
+package com.muyu.common.core.text;
+
+import com.muyu.common.core.utils.StringUtils;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 字符集工具类
+ *
+ * @author muyu
+ */
+public class CharsetKit {
+ /**
+ * ISO-8859-1
+ */
+ public static final String ISO_8859_1 = "ISO-8859-1";
+ /**
+ * UTF-8
+ */
+ public static final String UTF_8 = "UTF-8";
+ /**
+ * GBK
+ */
+ public static final String GBK = "GBK";
+
+ /**
+ * ISO-8859-1
+ */
+ public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
+ /**
+ * UTF-8
+ */
+ public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
+ /**
+ * GBK
+ */
+ public static final Charset CHARSET_GBK = Charset.forName(GBK);
+
+ /**
+ * 转换为Charset对象
+ *
+ * @param charset 字符集,为空则返回默认字符集
+ *
+ * @return Charset
+ */
+ public static Charset charset (String charset) {
+ return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
+ }
+
+ /**
+ * 转换字符串的字符集编码
+ *
+ * @param source 字符串
+ * @param srcCharset 源字符集,默认ISO-8859-1
+ * @param destCharset 目标字符集,默认UTF-8
+ *
+ * @return 转换后的字符集
+ */
+ public static String convert (String source, String srcCharset, String destCharset) {
+ return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
+ }
+
+ /**
+ * 转换字符串的字符集编码
+ *
+ * @param source 字符串
+ * @param srcCharset 源字符集,默认ISO-8859-1
+ * @param destCharset 目标字符集,默认UTF-8
+ *
+ * @return 转换后的字符集
+ */
+ public static String convert (String source, Charset srcCharset, Charset destCharset) {
+ if (null == srcCharset) {
+ srcCharset = StandardCharsets.ISO_8859_1;
+ }
+
+ if (null == destCharset) {
+ destCharset = StandardCharsets.UTF_8;
+ }
+
+ if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) {
+ return source;
+ }
+ return new String(source.getBytes(srcCharset), destCharset);
+ }
+
+ /**
+ * @return 系统字符集编码
+ */
+ public static String systemCharset () {
+ return Charset.defaultCharset().name();
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/Convert.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/Convert.java
new file mode 100644
index 0000000..fb57cb9
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/Convert.java
@@ -0,0 +1,903 @@
+package com.muyu.common.core.text;
+
+import com.muyu.common.core.utils.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.text.NumberFormat;
+import java.util.Set;
+
+/**
+ * 类型转换器
+ *
+ * @author muyu
+ */
+public class Convert {
+ /**
+ * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static String toStr (Object value, String defaultValue) {
+ if (null == value) {
+ return defaultValue;
+ }
+ if (value instanceof String) {
+ return (String) value;
+ }
+ return value.toString();
+ }
+
+ /**
+ * 转换为字符串
+ * 如果给定的值为null
,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static String toStr (Object value) {
+ return toStr(value, null);
+ }
+
+ /**
+ * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Character toChar (Object value, Character defaultValue) {
+ if (null == value) {
+ return defaultValue;
+ }
+ if (value instanceof Character) {
+ return (Character) value;
+ }
+
+ final String valueStr = toStr(value, null);
+ return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0);
+ }
+
+ /**
+ * 转换为字符
+ * 如果给定的值为null
,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Character toChar (Object value) {
+ return toChar(value, null);
+ }
+
+ /**
+ * 转换为byte
+ * 如果给定的值为null
,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Byte toByte (Object value, Byte defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Byte) {
+ return (Byte) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).byteValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return Byte.parseByte(valueStr);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为byte
+ * 如果给定的值为null
,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Byte toByte (Object value) {
+ return toByte(value, null);
+ }
+
+ /**
+ * 转换为Short
+ * 如果给定的值为null
,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Short toShort (Object value, Short defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Short) {
+ return (Short) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).shortValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return Short.parseShort(valueStr.trim());
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Short
+ * 如果给定的值为null
,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Short toShort (Object value) {
+ return toShort(value, null);
+ }
+
+ /**
+ * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Number toNumber (Object value, Number defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Number) {
+ return (Number) value;
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return NumberFormat.getInstance().parse(valueStr);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Number toNumber (Object value) {
+ return toNumber(value, null);
+ }
+
+ /**
+ * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Integer toInt (Object value, Integer defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Integer) {
+ return (Integer) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return Integer.parseInt(valueStr.trim());
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为int
+ * 如果给定的值为null
,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Integer toInt (Object value) {
+ return toInt(value, null);
+ }
+
+ /**
+ * 转换为Integer数组
+ *
+ * @param str 被转换的值
+ *
+ * @return 结果
+ */
+ public static Integer[] toIntArray (String str) {
+ return toIntArray(",", str);
+ }
+
+ /**
+ * 转换为Long数组
+ *
+ * @param str 被转换的值
+ *
+ * @return 结果
+ */
+ public static Long[] toLongArray (String str) {
+ return toLongArray(",", str);
+ }
+
+ /**
+ * 转换为Integer数组
+ *
+ * @param split 分隔符
+ * @param str 被转换的值
+ *
+ * @return 结果
+ */
+ public static Integer[] toIntArray (String split, String str) {
+ if (StringUtils.isEmpty(str)) {
+ return new Integer[]{};
+ }
+ String[] arr = str.split(split);
+ final Integer[] ints = new Integer[arr.length];
+ for (int i = 0 ; i < arr.length ; i++) {
+ final Integer v = toInt(arr[i], 0);
+ ints[i] = v;
+ }
+ return ints;
+ }
+
+ /**
+ * 转换为Long数组
+ *
+ * @param split 分隔符
+ * @param str 被转换的值
+ *
+ * @return 结果
+ */
+ public static Long[] toLongArray (String split, String str) {
+ if (StringUtils.isEmpty(str)) {
+ return new Long[]{};
+ }
+ String[] arr = str.split(split);
+ final Long[] longs = new Long[arr.length];
+ for (int i = 0 ; i < arr.length ; i++) {
+ final Long v = toLong(arr[i], null);
+ longs[i] = v;
+ }
+ return longs;
+ }
+
+ /**
+ * 转换为String数组
+ *
+ * @param str 被转换的值
+ *
+ * @return 结果
+ */
+ public static String[] toStrArray (String str) {
+ return toStrArray(",", str);
+ }
+
+ /**
+ * 转换为String数组
+ *
+ * @param split 分隔符
+ * @param str 被转换的值
+ *
+ * @return 结果
+ */
+ public static String[] toStrArray (String split, String str) {
+ return str.split(split);
+ }
+
+ /**
+ * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Long toLong (Object value, Long defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Long) {
+ return (Long) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).longValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ // 支持科学计数法
+ return new BigDecimal(valueStr.trim()).longValue();
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为long
+ * 如果给定的值为null
,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Long toLong (Object value) {
+ return toLong(value, null);
+ }
+
+ /**
+ * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Double toDouble (Object value, Double defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Double) {
+ return (Double) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).doubleValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ // 支持科学计数法
+ return new BigDecimal(valueStr.trim()).doubleValue();
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Double toDouble (Object value) {
+ return toDouble(value, null);
+ }
+
+ /**
+ * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Float toFloat (Object value, Float defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Float) {
+ return (Float) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).floatValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return Float.parseFloat(valueStr.trim());
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Float toFloat (Object value) {
+ return toFloat(value, null);
+ }
+
+ /**
+ * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static Boolean toBool (Object value, Boolean defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ }
+ String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ valueStr = valueStr.trim().toLowerCase();
+ switch (valueStr) {
+ case "true":
+ case "yes":
+ case "ok":
+ case "1":
+ return true;
+ case "false":
+ case "no":
+ case "0":
+ return false;
+ default:
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static Boolean toBool (Object value) {
+ return toBool(value, null);
+ }
+
+ /**
+ * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ *
+ * @param clazz Enum的Class
+ * @param value 值
+ * @param defaultValue 默认值
+ *
+ * @return Enum
+ */
+ public static > E toEnum (Class clazz, Object value, E defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (clazz.isAssignableFrom(value.getClass())) {
+ @SuppressWarnings("unchecked")
+ E myE = (E) value;
+ return myE;
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return Enum.valueOf(clazz, valueStr);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ *
+ * @param clazz Enum的Class
+ * @param value 值
+ *
+ * @return Enum
+ */
+ public static > E toEnum (Class clazz, Object value) {
+ return toEnum(clazz, value, null);
+ }
+
+ /**
+ * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static BigInteger toBigInteger (Object value, BigInteger defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof BigInteger) {
+ return (BigInteger) value;
+ }
+ if (value instanceof Long) {
+ return BigInteger.valueOf((Long) value);
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return new BigInteger(valueStr);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static BigInteger toBigInteger (Object value) {
+ return toBigInteger(value, null);
+ }
+
+ /**
+ * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ *
+ * @return 结果
+ */
+ public static BigDecimal toBigDecimal (Object value, BigDecimal defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof BigDecimal) {
+ return (BigDecimal) value;
+ }
+ if (value instanceof Long) {
+ return new BigDecimal((Long) value);
+ }
+ if (value instanceof Double) {
+ return BigDecimal.valueOf((Double) value);
+ }
+ if (value instanceof Integer) {
+ return new BigDecimal((Integer) value);
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr)) {
+ return defaultValue;
+ }
+ try {
+ return new BigDecimal(valueStr);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ *
+ * @return 结果
+ */
+ public static BigDecimal toBigDecimal (Object value) {
+ return toBigDecimal(value, null);
+ }
+
+ /**
+ * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+ *
+ * @param obj 对象
+ *
+ * @return 字符串
+ */
+ public static String utf8Str (Object obj) {
+ return str(obj, CharsetKit.CHARSET_UTF_8);
+ }
+
+ /**
+ * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+ *
+ * @param obj 对象
+ * @param charsetName 字符集
+ *
+ * @return 字符串
+ */
+ public static String str (Object obj, String charsetName) {
+ return str(obj, Charset.forName(charsetName));
+ }
+
+ /**
+ * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+ *
+ * @param obj 对象
+ * @param charset 字符集
+ *
+ * @return 字符串
+ */
+ public static String str (Object obj, Charset charset) {
+ if (null == obj) {
+ return null;
+ }
+
+ if (obj instanceof String) {
+ return (String) obj;
+ } else if (obj instanceof byte[] || obj instanceof Byte[]) {
+ if (obj instanceof byte[]) {
+ return str((byte[]) obj, charset);
+ } else {
+ Byte[] bytes = (Byte[]) obj;
+ int length = bytes.length;
+ byte[] dest = new byte[length];
+ for (int i = 0 ; i < length ; i++) {
+ dest[i] = bytes[i];
+ }
+ return str(dest, charset);
+ }
+ } else if (obj instanceof ByteBuffer) {
+ return str((ByteBuffer) obj, charset);
+ }
+ return obj.toString();
+ }
+
+ /**
+ * 将byte数组转为字符串
+ *
+ * @param bytes byte数组
+ * @param charset 字符集
+ *
+ * @return 字符串
+ */
+ public static String str (byte[] bytes, String charset) {
+ return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset));
+ }
+
+ /**
+ * 解码字节码
+ *
+ * @param data 字符串
+ * @param charset 字符集,如果此字段为空,则解码的结果取决于平台
+ *
+ * @return 解码后的字符串
+ */
+ public static String str (byte[] data, Charset charset) {
+ if (data == null) {
+ return null;
+ }
+
+ if (null == charset) {
+ return new String(data);
+ }
+ return new String(data, charset);
+ }
+
+ /**
+ * 将编码的byteBuffer数据转换为字符串
+ *
+ * @param data 数据
+ * @param charset 字符集,如果为空使用当前系统字符集
+ *
+ * @return 字符串
+ */
+ public static String str (ByteBuffer data, String charset) {
+ if (data == null) {
+ return null;
+ }
+
+ return str(data, Charset.forName(charset));
+ }
+
+ /**
+ * 将编码的byteBuffer数据转换为字符串
+ *
+ * @param data 数据
+ * @param charset 字符集,如果为空使用当前系统字符集
+ *
+ * @return 字符串
+ */
+ public static String str (ByteBuffer data, Charset charset) {
+ if (null == charset) {
+ charset = Charset.defaultCharset();
+ }
+ return charset.decode(data).toString();
+ }
+
+ // ----------------------------------------------------------------------- 全角半角转换
+
+ /**
+ * 半角转全角
+ *
+ * @param input String.
+ *
+ * @return 全角字符串.
+ */
+ public static String toSBC (String input) {
+ return toSBC(input, null);
+ }
+
+ /**
+ * 半角转全角
+ *
+ * @param input String
+ * @param notConvertSet 不替换的字符集合
+ *
+ * @return 全角字符串.
+ */
+ public static String toSBC (String input, Set notConvertSet) {
+ char[] c = input.toCharArray();
+ for (int i = 0 ; i < c.length ; i++) {
+ if (null != notConvertSet && notConvertSet.contains(c[i])) {
+ // 跳过不替换的字符
+ continue;
+ }
+
+ if (c[i] == ' ') {
+ c[i] = '\u3000';
+ } else if (c[i] < '\177') {
+ c[i] = (char) (c[i] + 65248);
+
+ }
+ }
+ return new String(c);
+ }
+
+ /**
+ * 全角转半角
+ *
+ * @param input String.
+ *
+ * @return 半角字符串
+ */
+ public static String toDBC (String input) {
+ return toDBC(input, null);
+ }
+
+ /**
+ * 替换全角为半角
+ *
+ * @param text 文本
+ * @param notConvertSet 不替换的字符集合
+ *
+ * @return 替换后的字符
+ */
+ public static String toDBC (String text, Set notConvertSet) {
+ char[] c = text.toCharArray();
+ for (int i = 0 ; i < c.length ; i++) {
+ if (null != notConvertSet && notConvertSet.contains(c[i])) {
+ // 跳过不替换的字符
+ continue;
+ }
+
+ if (c[i] == '\u3000') {
+ c[i] = ' ';
+ } else if (c[i] > '\uFF00' && c[i] < '\uFF5F') {
+ c[i] = (char) (c[i] - 65248);
+ }
+ }
+ return new String(c);
+ }
+
+ /**
+ * 数字金额大写转换 先写个完整的然后将如零拾替换成零
+ *
+ * @param n 数字
+ *
+ * @return 中文大写数字
+ */
+ public static String digitUppercase (double n) {
+ String[] fraction = {"角", "分"};
+ String[] digit = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
+ String[][] unit = {{"元", "万", "亿"}, {"", "拾", "佰", "仟"}};
+
+ String head = n < 0 ? "负" : "";
+ n = Math.abs(n);
+
+ String s = "";
+ for (int i = 0 ; i < fraction.length ; i++) {
+ // 优化double计算精度丢失问题
+ BigDecimal nNum = new BigDecimal(n);
+ BigDecimal decimal = new BigDecimal(10);
+ BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN);
+ double d = scale.doubleValue();
+ s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", "");
+ }
+ if (s.length() < 1) {
+ s = "整";
+ }
+ int integerPart = (int) Math.floor(n);
+
+ for (int i = 0 ; i < unit[0].length && integerPart > 0 ; i++) {
+ String p = "";
+ for (int j = 0 ; j < unit[1].length && n > 0 ; j++) {
+ p = digit[integerPart % 10] + unit[1][j] + p;
+ integerPart = integerPart / 10;
+ }
+ s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s;
+ }
+ return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整");
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/StrFormatter.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/StrFormatter.java
new file mode 100644
index 0000000..0c07cf5
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/text/StrFormatter.java
@@ -0,0 +1,77 @@
+package com.muyu.common.core.text;
+
+import com.muyu.common.core.utils.StringUtils;
+
+/**
+ * 字符串格式化
+ *
+ * @author muyu
+ */
+public class StrFormatter {
+ public static final String EMPTY_JSON = "{}";
+ public static final char C_BACKSLASH = '\\';
+ public static final char C_DELIM_START = '{';
+ public static final char C_DELIM_END = '}';
+
+ /**
+ * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ *
+ * @param strPattern 字符串模板
+ * @param argArray 参数列表
+ *
+ * @return 结果
+ */
+ public static String format (final String strPattern, final Object... argArray) {
+ if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) {
+ return strPattern;
+ }
+ final int strPatternLength = strPattern.length();
+
+ // 初始化定义好的长度以获得更好的性能
+ StringBuilder sbuf = new StringBuilder(strPatternLength + 50);
+
+ int handledPosition = 0;
+ int delimIndex;// 占位符所在位置
+ for (int argIndex = 0 ; argIndex < argArray.length ; argIndex++) {
+ delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition);
+ if (delimIndex == -1) {
+ if (handledPosition == 0) {
+ return strPattern;
+ } else { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果
+ sbuf.append(strPattern, handledPosition, strPatternLength);
+ return sbuf.toString();
+ }
+ } else {
+ if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) {
+ if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) {
+ // 转义符之前还有一个转义符,占位符依旧有效
+ sbuf.append(strPattern, handledPosition, delimIndex - 1);
+ sbuf.append(Convert.utf8Str(argArray[argIndex]));
+ handledPosition = delimIndex + 2;
+ } else {
+ // 占位符被转义
+ argIndex--;
+ sbuf.append(strPattern, handledPosition, delimIndex - 1);
+ sbuf.append(C_DELIM_START);
+ handledPosition = delimIndex + 1;
+ }
+ } else {
+ // 正常占位符
+ sbuf.append(strPattern, handledPosition, delimIndex);
+ sbuf.append(Convert.utf8Str(argArray[argIndex]));
+ handledPosition = delimIndex + 2;
+ }
+ }
+ }
+ // 加入最后一个占位符后所有的字符
+ sbuf.append(strPattern, handledPosition, strPattern.length());
+
+ return sbuf.toString();
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/DateUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/DateUtils.java
new file mode 100644
index 0000000..fb15c59
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/DateUtils.java
@@ -0,0 +1,158 @@
+package com.muyu.common.core.utils;
+
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.*;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ *
+ * @author muyu
+ */
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+ public static String YYYY = "yyyy";
+
+ public static String YYYY_MM = "yyyy-MM";
+
+ public static String YYYY_MM_DD = "yyyy-MM-dd";
+
+ public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+ public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+ private static String[] parsePatterns = {
+ "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+ "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+ "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+ /**
+ * 获取当前Date型日期
+ *
+ * @return Date() 当前日期
+ */
+ public static Date getNowDate () {
+ return new Date();
+ }
+
+ /**
+ * 获取当前日期, 默认格式为yyyy-MM-dd
+ *
+ * @return String
+ */
+ public static String getDate () {
+ return dateTimeNow(YYYY_MM_DD);
+ }
+
+ public static final String getTime () {
+ return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+ }
+
+ public static final String dateTimeNow () {
+ return dateTimeNow(YYYYMMDDHHMMSS);
+ }
+
+ public static final String dateTimeNow (final String format) {
+ return parseDateToStr(format, new Date());
+ }
+
+ public static final String dateTime (final Date date) {
+ return parseDateToStr(YYYY_MM_DD, date);
+ }
+
+ public static final String parseDateToStr (final String format, final Date date) {
+ return new SimpleDateFormat(format).format(date);
+ }
+
+ public static final Date dateTime (final String format, final String ts) {
+ try {
+ return new SimpleDateFormat(format).parse(ts);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 日期路径 即年/月/日 如2018/08/08
+ */
+ public static final String datePath () {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyy/MM/dd");
+ }
+
+ /**
+ * 日期路径 即年/月/日 如20180808
+ */
+ public static final String dateTime () {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyyMMdd");
+ }
+
+ /**
+ * 日期型字符串转化为日期 格式
+ */
+ public static Date parseDate (Object str) {
+ if (str == null) {
+ return null;
+ }
+ try {
+ return parseDate(str.toString(), parsePatterns);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取服务器启动时间
+ */
+ public static Date getServerStartDate () {
+ long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+ return new Date(time);
+ }
+
+ /**
+ * 计算时间差
+ *
+ * @param endDate 最后时间
+ * @param startTime 开始时间
+ *
+ * @return 时间差(天/小时/分钟)
+ */
+ public static String timeDistance (Date endDate, Date startTime) {
+ long nd = 1000 * 24 * 60 * 60;
+ long nh = 1000 * 60 * 60;
+ long nm = 1000 * 60;
+ // long ns = 1000;
+ // 获得两个时间的毫秒时间差异
+ long diff = endDate.getTime() - startTime.getTime();
+ // 计算差多少天
+ long day = diff / nd;
+ // 计算差多少小时
+ long hour = diff % nd / nh;
+ // 计算差多少分钟
+ long min = diff % nd % nh / nm;
+ // 计算差多少秒//输出结果
+ // long sec = diff % nd % nh % nm / ns;
+ return day + "天" + hour + "小时" + min + "分钟";
+ }
+
+ /**
+ * 增加 LocalDateTime ==> Date
+ */
+ public static Date toDate (LocalDateTime temporalAccessor) {
+ ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+
+ /**
+ * 增加 LocalDate ==> Date
+ */
+ public static Date toDate (LocalDate temporalAccessor) {
+ LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+ ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ExceptionUtil.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ExceptionUtil.java
new file mode 100644
index 0000000..e6abdf9
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ExceptionUtil.java
@@ -0,0 +1,35 @@
+package com.muyu.common.core.utils;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * 错误信息处理类。
+ *
+ * @author muyu
+ */
+public class ExceptionUtil {
+ /**
+ * 获取exception的详细错误信息。
+ */
+ public static String getExceptionMessage (Throwable e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw, true));
+ return sw.toString();
+ }
+
+ public static String getRootErrorMessage (Exception e) {
+ Throwable root = ExceptionUtils.getRootCause(e);
+ root = (root == null ? e : root);
+ if (root == null) {
+ return "";
+ }
+ String msg = root.getMessage();
+ if (msg == null) {
+ return "null";
+ }
+ return StringUtils.defaultString(msg);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/JwtUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/JwtUtils.java
new file mode 100644
index 0000000..a12b839
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/JwtUtils.java
@@ -0,0 +1,123 @@
+package com.muyu.common.core.utils;
+
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.core.constant.TokenConstants;
+import com.muyu.common.core.text.Convert;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import java.util.Map;
+
+/**
+ * Jwt工具类
+ *
+ * @author muyu
+ */
+public class JwtUtils {
+ public static String secret = TokenConstants.SECRET;
+
+ /**
+ * 从数据声明生成令牌
+ *
+ * @param claims 数据声明
+ *
+ * @return 令牌
+ */
+ public static String createToken (Map claims) {
+ String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
+ return token;
+ }
+
+ /**
+ * 从令牌中获取数据声明
+ *
+ * @param token 令牌
+ *
+ * @return 数据声明
+ */
+ public static Claims parseToken (String token) {
+ return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
+ }
+
+ /**
+ * 根据令牌获取用户标识
+ *
+ * @param token 令牌
+ *
+ * @return 用户ID
+ */
+ public static String getUserKey (String token) {
+ Claims claims = parseToken(token);
+ return getValue(claims, SecurityConstants.USER_KEY);
+ }
+
+ /**
+ * 根据令牌获取用户标识
+ *
+ * @param claims 身份信息
+ *
+ * @return 用户ID
+ */
+ public static String getUserKey (Claims claims) {
+ return getValue(claims, SecurityConstants.USER_KEY);
+ }
+
+ /**
+ * 根据令牌获取用户ID
+ *
+ * @param token 令牌
+ *
+ * @return 用户ID
+ */
+ public static String getUserId (String token) {
+ Claims claims = parseToken(token);
+ return getValue(claims, SecurityConstants.DETAILS_USER_ID);
+ }
+
+ /**
+ * 根据身份信息获取用户ID
+ *
+ * @param claims 身份信息
+ *
+ * @return 用户ID
+ */
+ public static String getUserId (Claims claims) {
+ return getValue(claims, SecurityConstants.DETAILS_USER_ID);
+ }
+
+ /**
+ * 根据令牌获取用户名
+ *
+ * @param token 令牌
+ *
+ * @return 用户名
+ */
+ public static String getUserName (String token) {
+ Claims claims = parseToken(token);
+ return getValue(claims, SecurityConstants.DETAILS_USERNAME);
+ }
+
+ /**
+ * 根据身份信息获取用户名
+ *
+ * @param claims 身份信息
+ *
+ * @return 用户名
+ */
+ public static String getUserName (Claims claims) {
+ return getValue(claims, SecurityConstants.DETAILS_USERNAME);
+ }
+
+ /**
+ * 根据身份信息获取键值
+ *
+ * @param claims 身份信息
+ * @param key 键
+ *
+ * @return 值
+ */
+ public static String getValue (Claims claims, String key) {
+ return Convert.toStr(claims.get(key), "");
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/PageUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/PageUtils.java
new file mode 100644
index 0000000..d4b0554
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/PageUtils.java
@@ -0,0 +1,32 @@
+package com.muyu.common.core.utils;
+
+import com.github.pagehelper.PageHelper;
+import com.muyu.common.core.utils.sql.SqlUtil;
+import com.muyu.common.core.web.page.PageDomain;
+import com.muyu.common.core.web.page.TableSupport;
+
+/**
+ * 分页工具类
+ *
+ * @author muyu
+ */
+public class PageUtils extends PageHelper {
+ /**
+ * 设置请求分页数据
+ */
+ public static void startPage () {
+ PageDomain pageDomain = TableSupport.buildPageRequest();
+ Integer pageNum = pageDomain.getPageNum();
+ Integer pageSize = pageDomain.getPageSize();
+ String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
+ Boolean reasonable = pageDomain.getReasonable();
+ PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
+ }
+
+ /**
+ * 清理分页的线程变量
+ */
+ public static void clearPage () {
+ PageHelper.clearPage();
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ServletUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ServletUtils.java
new file mode 100644
index 0000000..c9a8998
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ServletUtils.java
@@ -0,0 +1,294 @@
+package com.muyu.common.core.utils;
+
+import com.alibaba.fastjson2.JSON;
+import com.muyu.common.core.constant.Constants;
+import com.muyu.common.core.domain.Result;
+import com.muyu.common.core.text.Convert;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import reactor.core.publisher.Mono;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author muyu
+ */
+public class ServletUtils {
+ /**
+ * 获取String参数
+ */
+ public static String getParameter (String name) {
+ return getRequest().getParameter(name);
+ }
+
+ /**
+ * 获取String参数
+ */
+ public static String getParameter (String name, String defaultValue) {
+ return Convert.toStr(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt (String name) {
+ return Convert.toInt(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt (String name, Integer defaultValue) {
+ return Convert.toInt(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool (String name) {
+ return Convert.toBool(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool (String name, Boolean defaultValue) {
+ return Convert.toBool(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ *
+ * @return Map
+ */
+ public static Map getParams (ServletRequest request) {
+ final Map map = request.getParameterMap();
+ return Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ *
+ * @return Map
+ */
+ public static Map getParamMap (ServletRequest request) {
+ Map params = new HashMap<>();
+ for (Map.Entry entry : getParams(request).entrySet()) {
+ params.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
+ }
+ return params;
+ }
+
+ /**
+ * 获取request
+ */
+ public static HttpServletRequest getRequest () {
+ try {
+ return getRequestAttributes().getRequest();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取response
+ */
+ public static HttpServletResponse getResponse () {
+ try {
+ return getRequestAttributes().getResponse();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取session
+ */
+ public static HttpSession getSession () {
+ return getRequest().getSession();
+ }
+
+ public static ServletRequestAttributes getRequestAttributes () {
+ try {
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ return (ServletRequestAttributes) attributes;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static String getHeader (HttpServletRequest request, String name) {
+ String value = request.getHeader(name);
+ if (StringUtils.isEmpty(value)) {
+ return StringUtils.EMPTY;
+ }
+ return urlDecode(value);
+ }
+
+ public static Map getHeaders (HttpServletRequest request) {
+ Map map = new LinkedCaseInsensitiveMap<>();
+ Enumeration enumeration = request.getHeaderNames();
+ if (enumeration != null) {
+ while (enumeration.hasMoreElements()) {
+ String key = enumeration.nextElement();
+ String value = request.getHeader(key);
+ map.put(key, value);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * 将字符串渲染到客户端
+ *
+ * @param response 渲染对象
+ * @param string 待渲染的字符串
+ */
+ public static void renderString (HttpServletResponse response, String string) {
+ try {
+ response.setStatus(200);
+ response.setContentType("application/json");
+ response.setCharacterEncoding("utf-8");
+ response.getWriter().print(string);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 是否是Ajax异步请求
+ *
+ * @param request
+ */
+ public static boolean isAjaxRequest (HttpServletRequest request) {
+ String accept = request.getHeader("accept");
+ if (accept != null && accept.contains("application/json")) {
+ return true;
+ }
+
+ String xRequestedWith = request.getHeader("X-Requested-With");
+ if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
+ return true;
+ }
+
+ String uri = request.getRequestURI();
+ if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) {
+ return true;
+ }
+
+ String ajax = request.getParameter("__ajax");
+ return StringUtils.inStringIgnoreCase(ajax, "json", "xml");
+ }
+
+ /**
+ * 内容编码
+ *
+ * @param str 内容
+ *
+ * @return 编码后的内容
+ */
+ public static String urlEncode (String str) {
+ try {
+ return URLEncoder.encode(str, Constants.UTF8);
+ } catch (UnsupportedEncodingException e) {
+ return StringUtils.EMPTY;
+ }
+ }
+
+ /**
+ * 内容解码
+ *
+ * @param str 内容
+ *
+ * @return 解码后的内容
+ */
+ public static String urlDecode (String str) {
+ try {
+ return URLDecoder.decode(str, Constants.UTF8);
+ } catch (UnsupportedEncodingException e) {
+ return StringUtils.EMPTY;
+ }
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param value 响应内容
+ *
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter (ServerHttpResponse response, Object value) {
+ return webFluxResponseWriter(response, HttpStatus.OK, value, Result.FAIL);
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param code 响应状态码
+ * @param value 响应内容
+ *
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter (ServerHttpResponse response, Object value, int code) {
+ return webFluxResponseWriter(response, HttpStatus.OK, value, code);
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param status http状态码
+ * @param code 响应状态码
+ * @param value 响应内容
+ *
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter (ServerHttpResponse response, HttpStatus status, Object value, int code) {
+ return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code);
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param contentType content-type
+ * @param status http状态码
+ * @param code 响应状态码
+ * @param value 响应内容
+ *
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter (ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code) {
+ response.setStatusCode(status);
+ response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
+ Result> result = Result.error(code, value.toString());
+ DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
+ return response.writeWith(Mono.just(dataBuffer));
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/SpringUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/SpringUtils.java
new file mode 100644
index 0000000..c37a65c
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/SpringUtils.java
@@ -0,0 +1,114 @@
+package com.muyu.common.core.utils;
+
+import org.springframework.aop.framework.AopContext;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring工具类 方便在非spring管理环境中获取bean
+ *
+ * @author muyu
+ */
+@Component
+public final class SpringUtils implements BeanFactoryPostProcessor {
+ /**
+ * Spring应用上下文环境
+ */
+ private static ConfigurableListableBeanFactory beanFactory;
+
+ /**
+ * 获取对象
+ *
+ * @param name
+ *
+ * @return Object 一个以所给名字注册的bean的实例
+ *
+ * @throws org.springframework.beans.BeansException
+ */
+ @SuppressWarnings("unchecked")
+ public static T getBean (String name) throws BeansException {
+ return (T) beanFactory.getBean(name);
+ }
+
+ /**
+ * 获取类型为requiredType的对象
+ *
+ * @param clz
+ *
+ * @return
+ *
+ * @throws org.springframework.beans.BeansException
+ */
+ public static T getBean (Class clz) throws BeansException {
+ T result = (T) beanFactory.getBean(clz);
+ return result;
+ }
+
+ /**
+ * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
+ *
+ * @param name
+ *
+ * @return boolean
+ */
+ public static boolean containsBean (String name) {
+ return beanFactory.containsBean(name);
+ }
+
+ /**
+ * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
+ *
+ * @param name
+ *
+ * @return boolean
+ *
+ * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
+ */
+ public static boolean isSingleton (String name) throws NoSuchBeanDefinitionException {
+ return beanFactory.isSingleton(name);
+ }
+
+ /**
+ * @param name
+ *
+ * @return Class 注册对象的类型
+ *
+ * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
+ */
+ public static Class> getType (String name) throws NoSuchBeanDefinitionException {
+ return beanFactory.getType(name);
+ }
+
+ /**
+ * 如果给定的bean名字在bean定义中有别名,则返回这些别名
+ *
+ * @param name
+ *
+ * @return
+ *
+ * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
+ */
+ public static String[] getAliases (String name) throws NoSuchBeanDefinitionException {
+ return beanFactory.getAliases(name);
+ }
+
+ /**
+ * 获取aop代理对象
+ *
+ * @param invoker
+ *
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ public static T getAopProxy (T invoker) {
+ return (T) AopContext.currentProxy();
+ }
+
+ @Override
+ public void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ SpringUtils.beanFactory = beanFactory;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/StringUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/StringUtils.java
new file mode 100644
index 0000000..ec8f557
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/StringUtils.java
@@ -0,0 +1,504 @@
+package com.muyu.common.core.utils;
+
+import com.muyu.common.core.constant.Constants;
+import com.muyu.common.core.text.StrFormatter;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 字符串工具类
+ *
+ * @author muyu
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+ /**
+ * 空字符串
+ */
+ private static final String NULLSTR = "";
+
+ /**
+ * 下划线
+ */
+ private static final char SEPARATOR = '_';
+
+ /**
+ * 获取参数不为空值
+ *
+ * @param value defaultValue 要判断的value
+ *
+ * @return value 返回值
+ */
+ public static T nvl (T value, T defaultValue) {
+ return value != null ? value : defaultValue;
+ }
+
+ /**
+ * * 判断一个Collection是否为空, 包含List,Set,Queue
+ *
+ * @param coll 要判断的Collection
+ *
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty (Collection> coll) {
+ return isNull(coll) || coll.isEmpty();
+ }
+
+ /**
+ * * 判断一个Collection是否非空,包含List,Set,Queue
+ *
+ * @param coll 要判断的Collection
+ *
+ * @return true:非空 false:空
+ */
+ public static boolean isNotEmpty (Collection> coll) {
+ return !isEmpty(coll);
+ }
+
+ /**
+ * * 判断一个对象数组是否为空
+ *
+ * @param objects 要判断的对象数组
+ * * @return true:为空 false:非空
+ */
+ public static boolean isEmpty (Object[] objects) {
+ return isNull(objects) || (objects.length == 0);
+ }
+
+ /**
+ * * 判断一个对象数组是否非空
+ *
+ * @param objects 要判断的对象数组
+ *
+ * @return true:非空 false:空
+ */
+ public static boolean isNotEmpty (Object[] objects) {
+ return !isEmpty(objects);
+ }
+
+ /**
+ * * 判断一个Map是否为空
+ *
+ * @param map 要判断的Map
+ *
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty (Map, ?> map) {
+ return isNull(map) || map.isEmpty();
+ }
+
+ /**
+ * * 判断一个Map是否为空
+ *
+ * @param map 要判断的Map
+ *
+ * @return true:非空 false:空
+ */
+ public static boolean isNotEmpty (Map, ?> map) {
+ return !isEmpty(map);
+ }
+
+ /**
+ * * 判断一个字符串是否为空串
+ *
+ * @param str String
+ *
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty (String str) {
+ return isNull(str) || NULLSTR.equals(str.trim());
+ }
+
+ /**
+ * * 判断一个字符串是否为非空串
+ *
+ * @param str String
+ *
+ * @return true:非空串 false:空串
+ */
+ public static boolean isNotEmpty (String str) {
+ return !isEmpty(str);
+ }
+
+ /**
+ * * 判断一个对象是否为空
+ *
+ * @param object Object
+ *
+ * @return true:为空 false:非空
+ */
+ public static boolean isNull (Object object) {
+ return object == null;
+ }
+
+ /**
+ * * 判断一个对象是否非空
+ *
+ * @param object Object
+ *
+ * @return true:非空 false:空
+ */
+ public static boolean isNotNull (Object object) {
+ return !isNull(object);
+ }
+
+ /**
+ * * 判断一个对象是否是数组类型(Java基本型别的数组)
+ *
+ * @param object 对象
+ *
+ * @return true:是数组 false:不是数组
+ */
+ public static boolean isArray (Object object) {
+ return isNotNull(object) && object.getClass().isArray();
+ }
+
+ /**
+ * 去空格
+ */
+ public static String trim (String str) {
+ return (str == null ? "" : str.trim());
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ *
+ * @return 结果
+ */
+ public static String substring (final String str, int start) {
+ if (str == null) {
+ return NULLSTR;
+ }
+
+ if (start < 0) {
+ start = str.length() + start;
+ }
+
+ if (start < 0) {
+ start = 0;
+ }
+ if (start > str.length()) {
+ return NULLSTR;
+ }
+
+ return str.substring(start);
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ * @param end 结束
+ *
+ * @return 结果
+ */
+ public static String substring (final String str, int start, int end) {
+ if (str == null) {
+ return NULLSTR;
+ }
+
+ if (end < 0) {
+ end = str.length() + end;
+ }
+ if (start < 0) {
+ start = str.length() + start;
+ }
+
+ if (end > str.length()) {
+ end = str.length();
+ }
+
+ if (start > end) {
+ return NULLSTR;
+ }
+
+ if (start < 0) {
+ start = 0;
+ }
+ if (end < 0) {
+ end = 0;
+ }
+
+ return str.substring(start, end);
+ }
+
+ /**
+ * 判断是否为空,并且不是空白字符
+ *
+ * @param str 要判断的value
+ *
+ * @return 结果
+ */
+ public static boolean hasText (String str) {
+ return (str != null && !str.isEmpty() && containsText(str));
+ }
+
+ private static boolean containsText (CharSequence str) {
+ int strLen = str.length();
+ for (int i = 0 ; i < strLen ; i++) {
+ if (!Character.isWhitespace(str.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ *
+ * @param template 文本模板,被替换的部分用 {} 表示
+ * @param params 参数值
+ *
+ * @return 格式化后的文本
+ */
+ public static String format (String template, Object... params) {
+ if (isEmpty(params) || isEmpty(template)) {
+ return template;
+ }
+ return StrFormatter.format(template, params);
+ }
+
+ /**
+ * 是否为http(s)://开头
+ *
+ * @param link 链接
+ *
+ * @return 结果
+ */
+ public static boolean ishttp (String link) {
+ return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
+ }
+
+ /**
+ * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
+ *
+ * @param collection 给定的集合
+ * @param array 给定的数组
+ *
+ * @return boolean 结果
+ */
+ public static boolean containsAny (Collection collection, String... array) {
+ if (isEmpty(collection) || isEmpty(array)) {
+ return false;
+ } else {
+ for (String str : array) {
+ if (collection.contains(str)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * 驼峰转下划线命名
+ */
+ public static String toUnderScoreCase (String str) {
+ if (str == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ // 前置字符是否大写
+ boolean preCharIsUpperCase = true;
+ // 当前字符是否大写
+ boolean curreCharIsUpperCase = true;
+ // 下一字符是否大写
+ boolean nexteCharIsUpperCase = true;
+ for (int i = 0 ; i < str.length() ; i++) {
+ char c = str.charAt(i);
+ if (i > 0) {
+ preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
+ } else {
+ preCharIsUpperCase = false;
+ }
+
+ curreCharIsUpperCase = Character.isUpperCase(c);
+
+ if (i < (str.length() - 1)) {
+ nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
+ }
+
+ if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) {
+ sb.append(SEPARATOR);
+ } else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) {
+ sb.append(SEPARATOR);
+ }
+ sb.append(Character.toLowerCase(c));
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * 是否包含字符串
+ *
+ * @param str 验证字符串
+ * @param strs 字符串组
+ *
+ * @return 包含返回true
+ */
+ public static boolean inStringIgnoreCase (String str, String... strs) {
+ if (str != null && strs != null) {
+ for (String s : strs) {
+ if (str.equalsIgnoreCase(trim(s))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+ *
+ * @param name 转换前的下划线大写方式命名的字符串
+ *
+ * @return 转换后的驼峰式命名的字符串
+ */
+ public static String convertToCamelCase (String name) {
+ StringBuilder result = new StringBuilder();
+ // 快速检查
+ if (name == null || name.isEmpty()) {
+ // 没必要转换
+ return "";
+ } else if (!name.contains("_")) {
+ // 不含下划线,仅将首字母大写
+ return name.substring(0, 1).toUpperCase() + name.substring(1);
+ }
+ // 用下划线将原始字符串分割
+ String[] camels = name.split("_");
+ for (String camel : camels) {
+ // 跳过原始字符串中开头、结尾的下换线或双重下划线
+ if (camel.isEmpty()) {
+ continue;
+ }
+ // 首字母大写
+ result.append(camel.substring(0, 1).toUpperCase());
+ result.append(camel.substring(1).toLowerCase());
+ }
+ return result.toString();
+ }
+
+ /**
+ * 驼峰式命名法
+ * 例如:user_name->userName
+ */
+ public static String toCamelCase (String s) {
+ if (s == null) {
+ return null;
+ }
+ if (s.indexOf(SEPARATOR) == -1) {
+ return s;
+ }
+ s = s.toLowerCase();
+ StringBuilder sb = new StringBuilder(s.length());
+ boolean upperCase = false;
+ for (int i = 0 ; i < s.length() ; i++) {
+ char c = s.charAt(i);
+
+ if (c == SEPARATOR) {
+ upperCase = true;
+ } else if (upperCase) {
+ sb.append(Character.toUpperCase(c));
+ upperCase = false;
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+ *
+ * @param str 指定字符串
+ * @param strs 需要检查的字符串数组
+ *
+ * @return 是否匹配
+ */
+ public static boolean matches (String str, List strs) {
+ if (isEmpty(str) || isEmpty(strs)) {
+ return false;
+ }
+ for (String pattern : strs) {
+ if (isMatch(pattern, str)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断url是否与规则配置:
+ * ? 表示单个字符;
+ * * 表示一层路径内的任意字符串,不可跨层级;
+ * ** 表示任意层路径;
+ *
+ * @param pattern 匹配规则
+ * @param url 需要匹配的url
+ *
+ * @return
+ */
+ public static boolean isMatch (String pattern, String url) {
+ AntPathMatcher matcher = new AntPathMatcher();
+ return matcher.match(pattern, url);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T cast (Object obj) {
+ return (T) obj;
+ }
+
+ /**
+ * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+ *
+ * @param num 数字对象
+ * @param size 字符串指定长度
+ *
+ * @return 返回数字的字符串格式,该字符串为指定长度。
+ */
+ public static final String padl (final Number num, final int size) {
+ return padl(num.toString(), size, '0');
+ }
+
+ /**
+ * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+ *
+ * @param s 原始字符串
+ * @param size 字符串指定长度
+ * @param c 用于补齐的字符
+ *
+ * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+ */
+ public static final String padl (final String s, final int size, final char c) {
+ final StringBuilder sb = new StringBuilder(size);
+ if (s != null) {
+ final int len = s.length();
+ if (s.length() <= size) {
+ for (int i = size - len ; i > 0 ; i--) {
+ sb.append(c);
+ }
+ sb.append(s);
+ } else {
+ return s.substring(len - size, len);
+ }
+ } else {
+ for (int i = size ; i > 0 ; i--) {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/bean/BeanUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/bean/BeanUtils.java
new file mode 100644
index 0000000..d44b351
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/bean/BeanUtils.java
@@ -0,0 +1,107 @@
+package com.muyu.common.core.utils.bean;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Bean 工具类
+ *
+ * @author muyu
+ */
+public class BeanUtils extends org.springframework.beans.BeanUtils {
+ /**
+ * Bean方法名中属性名开始的下标
+ */
+ private static final int BEAN_METHOD_PROP_INDEX = 3;
+
+ /**
+ * 匹配getter方法的正则表达式
+ */
+ private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)");
+
+ /**
+ * 匹配setter方法的正则表达式
+ */
+ private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)");
+
+ /**
+ * Bean属性复制工具方法。
+ *
+ * @param dest 目标对象
+ * @param src 源对象
+ */
+ public static void copyBeanProp (Object dest, Object src) {
+ try {
+ copyProperties(src, dest);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 获取对象的setter方法。
+ *
+ * @param obj 对象
+ *
+ * @return 对象的setter方法列表
+ */
+ public static List getSetterMethods (Object obj) {
+ // setter方法列表
+ List setterMethods = new ArrayList();
+
+ // 获取所有方法
+ Method[] methods = obj.getClass().getMethods();
+
+ // 查找setter方法
+
+ for (Method method : methods) {
+ Matcher m = SET_PATTERN.matcher(method.getName());
+ if (m.matches() && (method.getParameterTypes().length == 1)) {
+ setterMethods.add(method);
+ }
+ }
+ // 返回setter方法列表
+ return setterMethods;
+ }
+
+ /**
+ * 获取对象的getter方法。
+ *
+ * @param obj 对象
+ *
+ * @return 对象的getter方法列表
+ */
+
+ public static List getGetterMethods (Object obj) {
+ // getter方法列表
+ List getterMethods = new ArrayList();
+ // 获取所有方法
+ Method[] methods = obj.getClass().getMethods();
+ // 查找getter方法
+ for (Method method : methods) {
+ Matcher m = GET_PATTERN.matcher(method.getName());
+ if (m.matches() && (method.getParameterTypes().length == 0)) {
+ getterMethods.add(method);
+ }
+ }
+ // 返回getter方法列表
+ return getterMethods;
+ }
+
+ /**
+ * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。
+ *
+ * @param m1 方法名1
+ * @param m2 方法名2
+ *
+ * @return 属性名一样返回true,否则返回false
+ */
+
+ public static boolean isMethodPropEquals (String m1, String m2) {
+ return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX));
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/bean/BeanValidators.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/bean/BeanValidators.java
new file mode 100644
index 0000000..abbb157
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/bean/BeanValidators.java
@@ -0,0 +1,21 @@
+package com.muyu.common.core.utils.bean;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validator;
+import java.util.Set;
+
+/**
+ * bean对象属性验证
+ *
+ * @author muyu
+ */
+public class BeanValidators {
+ public static void validateWithException (Validator validator, Object object, Class>... groups)
+ throws ConstraintViolationException {
+ Set> constraintViolations = validator.validate(object, groups);
+ if (!constraintViolations.isEmpty()) {
+ throw new ConstraintViolationException(constraintViolations);
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/FileTypeUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/FileTypeUtils.java
new file mode 100644
index 0000000..dde7e85
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/FileTypeUtils.java
@@ -0,0 +1,85 @@
+package com.muyu.common.core.utils.file;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.util.Objects;
+
+/**
+ * 文件类型工具类
+ *
+ * @author muyu
+ */
+public class FileTypeUtils {
+ /**
+ * 获取文件类型
+ *
+ * 例如: muyu.txt, 返回: txt
+ *
+ * @param file 文件名
+ *
+ * @return 后缀(不含".")
+ */
+ public static String getFileType (File file) {
+ if (null == file) {
+ return StringUtils.EMPTY;
+ }
+ return getFileType(file.getName());
+ }
+
+ /**
+ * 获取文件类型
+ *
+ * 例如: muyu.txt, 返回: txt
+ *
+ * @param fileName 文件名
+ *
+ * @return 后缀(不含".")
+ */
+ public static String getFileType (String fileName) {
+ int separatorIndex = fileName.lastIndexOf(".");
+ if (separatorIndex < 0) {
+ return "";
+ }
+ return fileName.substring(separatorIndex + 1).toLowerCase();
+ }
+
+ /**
+ * 获取文件名的后缀
+ *
+ * @param file 表单文件
+ *
+ * @return 后缀名
+ */
+ public static final String getExtension (MultipartFile file) {
+ String extension = FilenameUtils.getExtension(file.getOriginalFilename());
+ if (StringUtils.isEmpty(extension)) {
+ extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
+ }
+ return extension;
+ }
+
+ /**
+ * 获取文件类型
+ *
+ * @param photoByte 文件字节码
+ *
+ * @return 后缀(不含".")
+ */
+ public static String getFileExtendName (byte[] photoByte) {
+ String strFileExtendName = "JPG";
+ if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56)
+ && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) {
+ strFileExtendName = "GIF";
+ } else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) {
+ strFileExtendName = "JPG";
+ } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) {
+ strFileExtendName = "BMP";
+ } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) {
+ strFileExtendName = "PNG";
+ }
+ return strFileExtendName;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/FileUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/FileUtils.java
new file mode 100644
index 0000000..30f01b8
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/FileUtils.java
@@ -0,0 +1,223 @@
+package com.muyu.common.core.utils.file;
+
+import com.muyu.common.core.utils.StringUtils;
+import org.apache.commons.lang3.ArrayUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 文件处理工具类
+ *
+ * @author muyu
+ */
+public class FileUtils {
+ /**
+ * 字符常量:斜杠 {@code '/'}
+ */
+ public static final char SLASH = '/';
+
+ /**
+ * 字符常量:反斜杠 {@code '\\'}
+ */
+ public static final char BACKSLASH = '\\';
+
+ public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+";
+
+ /**
+ * 输出指定文件的byte数组
+ *
+ * @param filePath 文件路径
+ * @param os 输出流
+ *
+ * @return
+ */
+ public static void writeBytes (String filePath, OutputStream os) throws IOException {
+ FileInputStream fis = null;
+ try {
+ File file = new File(filePath);
+ if (!file.exists()) {
+ throw new FileNotFoundException(filePath);
+ }
+ fis = new FileInputStream(file);
+ byte[] b = new byte[1024];
+ int length;
+ while ((length = fis.read(b)) > 0) {
+ os.write(b, 0, length);
+ }
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * 删除文件
+ *
+ * @param filePath 文件
+ *
+ * @return
+ */
+ public static boolean deleteFile (String filePath) {
+ boolean flag = false;
+ File file = new File(filePath);
+ // 路径为文件且不为空则进行删除
+ if (file.isFile() && file.exists()) {
+ flag = file.delete();
+ }
+ return flag;
+ }
+
+ /**
+ * 文件名称验证
+ *
+ * @param filename 文件名称
+ *
+ * @return true 正常 false 非法
+ */
+ public static boolean isValidFilename (String filename) {
+ return filename.matches(FILENAME_PATTERN);
+ }
+
+ /**
+ * 检查文件是否可下载
+ *
+ * @param resource 需要下载的文件
+ *
+ * @return true 正常 false 非法
+ */
+ public static boolean checkAllowDownload (String resource) {
+ // 禁止目录上跳级别
+ if (StringUtils.contains(resource, "..")) {
+ return false;
+ }
+ // 判断是否在允许下载的文件规则内
+ return ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource));
+ }
+
+ /**
+ * 下载文件名重新编码
+ *
+ * @param request 请求对象
+ * @param fileName 文件名
+ *
+ * @return 编码后的文件名
+ */
+ public static String setFileDownloadHeader (HttpServletRequest request, String fileName) throws UnsupportedEncodingException {
+ final String agent = request.getHeader("USER-AGENT");
+ String filename = fileName;
+ if (agent.contains("MSIE")) {
+ // IE浏览器
+ filename = URLEncoder.encode(filename, "utf-8");
+ filename = filename.replace("+", " ");
+ } else if (agent.contains("Firefox")) {
+ // 火狐浏览器
+ filename = new String(fileName.getBytes(), "ISO8859-1");
+ } else if (agent.contains("Chrome")) {
+ // google浏览器
+ filename = URLEncoder.encode(filename, "utf-8");
+ } else {
+ // 其它浏览器
+ filename = URLEncoder.encode(filename, "utf-8");
+ }
+ return filename;
+ }
+
+ /**
+ * 返回文件名
+ *
+ * @param filePath 文件
+ *
+ * @return 文件名
+ */
+ public static String getName (String filePath) {
+ if (null == filePath) {
+ return null;
+ }
+ int len = filePath.length();
+ if (0 == len) {
+ return filePath;
+ }
+ if (isFileSeparator(filePath.charAt(len - 1))) {
+ // 以分隔符结尾的去掉结尾分隔符
+ len--;
+ }
+
+ int begin = 0;
+ char c;
+ for (int i = len - 1 ; i > -1 ; i--) {
+ c = filePath.charAt(i);
+ if (isFileSeparator(c)) {
+ // 查找最后一个路径分隔符(/或者\)
+ begin = i + 1;
+ break;
+ }
+ }
+
+ return filePath.substring(begin, len);
+ }
+
+ /**
+ * 是否为Windows或者Linux(Unix)文件分隔符
+ * Windows平台下分隔符为\,Linux(Unix)为/
+ *
+ * @param c 字符
+ *
+ * @return 是否为Windows或者Linux(Unix)文件分隔符
+ */
+ public static boolean isFileSeparator (char c) {
+ return SLASH == c || BACKSLASH == c;
+ }
+
+ /**
+ * 下载文件名重新编码
+ *
+ * @param response 响应对象
+ * @param realFileName 真实文件名
+ *
+ * @return
+ */
+ public static void setAttachmentResponseHeader (HttpServletResponse response, String realFileName) throws UnsupportedEncodingException {
+ String percentEncodedFileName = percentEncode(realFileName);
+
+ StringBuilder contentDispositionValue = new StringBuilder();
+ contentDispositionValue.append("attachment; filename=")
+ .append(percentEncodedFileName)
+ .append(";")
+ .append("filename*=")
+ .append("utf-8''")
+ .append(percentEncodedFileName);
+
+ response.setHeader("Content-disposition", contentDispositionValue.toString());
+ response.setHeader("download-filename", percentEncodedFileName);
+ }
+
+ /**
+ * 百分号编码工具方法
+ *
+ * @param s 需要百分号编码的字符串
+ *
+ * @return 百分号编码后的字符串
+ */
+ public static String percentEncode (String s) throws UnsupportedEncodingException {
+ String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString());
+ return encode.replaceAll("\\+", "%20");
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/ImageUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/ImageUtils.java
new file mode 100644
index 0000000..7e23345
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/ImageUtils.java
@@ -0,0 +1,69 @@
+package com.muyu.common.core.utils.file;
+
+import org.apache.poi.util.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+
+/**
+ * 图片处理工具类
+ *
+ * @author muyu
+ */
+public class ImageUtils {
+ private static final Logger log = LoggerFactory.getLogger(ImageUtils.class);
+
+ public static byte[] getImage (String imagePath) {
+ InputStream is = getFile(imagePath);
+ try {
+ return IOUtils.toByteArray(is);
+ } catch (Exception e) {
+ log.error("图片加载异常 {}", e);
+ return null;
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ }
+
+ public static InputStream getFile (String imagePath) {
+ try {
+ byte[] result = readFile(imagePath);
+ result = Arrays.copyOf(result, result.length);
+ return new ByteArrayInputStream(result);
+ } catch (Exception e) {
+ log.error("获取图片异常 {}", e);
+ }
+ return null;
+ }
+
+ /**
+ * 读取文件为字节数据
+ *
+ * @param url 地址
+ *
+ * @return 字节数据
+ */
+ public static byte[] readFile (String url) {
+ InputStream in = null;
+ try {
+ // 网络地址
+ URL urlObj = new URL(url);
+ URLConnection urlConnection = urlObj.openConnection();
+ urlConnection.setConnectTimeout(30 * 1000);
+ urlConnection.setReadTimeout(60 * 1000);
+ urlConnection.setDoInput(true);
+ in = urlConnection.getInputStream();
+ return IOUtils.toByteArray(in);
+ } catch (Exception e) {
+ log.error("访问文件异常 {}", e);
+ return null;
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/MimeTypeUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/MimeTypeUtils.java
new file mode 100644
index 0000000..9eb1d84
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/file/MimeTypeUtils.java
@@ -0,0 +1,56 @@
+package com.muyu.common.core.utils.file;
+
+/**
+ * 媒体类型工具类
+ *
+ * @author muyu
+ */
+public class MimeTypeUtils {
+ public static final String IMAGE_PNG = "image/png";
+
+ public static final String IMAGE_JPG = "image/jpg";
+
+ public static final String IMAGE_JPEG = "image/jpeg";
+
+ public static final String IMAGE_BMP = "image/bmp";
+
+ public static final String IMAGE_GIF = "image/gif";
+
+ public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+ public static final String[] FLASH_EXTENSION = {"swf", "flv"};
+
+ public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+ "asf", "rm", "rmvb"};
+
+ public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+ public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+ // 图片
+ "bmp", "gif", "jpg", "jpeg", "png",
+ // word excel powerpoint
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+ // 压缩文件
+ "rar", "zip", "gz", "bz2",
+ // 视频格式
+ "mp4", "avi", "rmvb",
+ // pdf
+ "pdf"};
+
+ public static String getExtension (String prefix) {
+ switch (prefix) {
+ case IMAGE_PNG:
+ return "png";
+ case IMAGE_JPG:
+ return "jpg";
+ case IMAGE_JPEG:
+ return "jpeg";
+ case IMAGE_BMP:
+ return "bmp";
+ case IMAGE_GIF:
+ return "gif";
+ default:
+ return "";
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/html/EscapeUtil.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/html/EscapeUtil.java
new file mode 100644
index 0000000..7dba9be
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/html/EscapeUtil.java
@@ -0,0 +1,145 @@
+package com.muyu.common.core.utils.html;
+
+import com.muyu.common.core.utils.StringUtils;
+
+/**
+ * 转义和反转义工具类
+ *
+ * @author muyu
+ */
+public class EscapeUtil {
+ public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
+
+ private static final char[][] TEXT = new char[64][];
+
+ static {
+ for (int i = 0 ; i < 64 ; i++) {
+ TEXT[i] = new char[]{(char) i};
+ }
+
+ // special HTML characters
+ TEXT['\''] = "'".toCharArray(); // 单引号
+ TEXT['"'] = """.toCharArray(); // 双引号
+ TEXT['&'] = "&".toCharArray(); // &符
+ TEXT['<'] = "<".toCharArray(); // 小于号
+ TEXT['>'] = ">".toCharArray(); // 大于号
+ }
+
+ /**
+ * 转义文本中的HTML字符为安全的字符
+ *
+ * @param text 被转义的文本
+ *
+ * @return 转义后的文本
+ */
+ public static String escape (String text) {
+ return encode(text);
+ }
+
+ /**
+ * 还原被转义的HTML特殊字符
+ *
+ * @param content 包含转义符的HTML内容
+ *
+ * @return 转换后的字符串
+ */
+ public static String unescape (String content) {
+ return decode(content);
+ }
+
+ /**
+ * 清除所有HTML标签,但是不删除标签内的内容
+ *
+ * @param content 文本
+ *
+ * @return 清除标签后的文本
+ */
+ public static String clean (String content) {
+ return new HTMLFilter().filter(content);
+ }
+
+ /**
+ * Escape编码
+ *
+ * @param text 被编码的文本
+ *
+ * @return 编码后的字符
+ */
+ private static String encode (String text) {
+ if (StringUtils.isEmpty(text)) {
+ return StringUtils.EMPTY;
+ }
+
+ final StringBuilder tmp = new StringBuilder(text.length() * 6);
+ char c;
+ for (int i = 0 ; i < text.length() ; i++) {
+ c = text.charAt(i);
+ if (c < 256) {
+ tmp.append("%");
+ if (c < 16) {
+ tmp.append("0");
+ }
+ tmp.append(Integer.toString(c, 16));
+ } else {
+ tmp.append("%u");
+ if (c <= 0xfff) {
+ // issue#I49JU8@Gitee
+ tmp.append("0");
+ }
+ tmp.append(Integer.toString(c, 16));
+ }
+ }
+ return tmp.toString();
+ }
+
+ /**
+ * Escape解码
+ *
+ * @param content 被转义的内容
+ *
+ * @return 解码后的字符串
+ */
+ public static String decode (String content) {
+ if (StringUtils.isEmpty(content)) {
+ return content;
+ }
+
+ StringBuilder tmp = new StringBuilder(content.length());
+ int lastPos = 0, pos = 0;
+ char ch;
+ while (lastPos < content.length()) {
+ pos = content.indexOf("%", lastPos);
+ if (pos == lastPos) {
+ if (content.charAt(pos + 1) == 'u') {
+ ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
+ tmp.append(ch);
+ lastPos = pos + 6;
+ } else {
+ ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
+ tmp.append(ch);
+ lastPos = pos + 3;
+ }
+ } else {
+ if (pos == -1) {
+ tmp.append(content.substring(lastPos));
+ lastPos = content.length();
+ } else {
+ tmp.append(content.substring(lastPos, pos));
+ lastPos = pos;
+ }
+ }
+ }
+ return tmp.toString();
+ }
+
+ public static void main (String[] args) {
+ String html = "";
+ String escape = EscapeUtil.escape(html);
+ // String html = "ipt>alert(\"XSS\") ipt>";
+ // String html = "<123";
+ // String html = "123>";
+ System.out.println("clean: " + EscapeUtil.clean(html));
+ System.out.println("escape: " + escape);
+ System.out.println("unescape: " + EscapeUtil.unescape(escape));
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/html/HTMLFilter.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/html/HTMLFilter.java
new file mode 100644
index 0000000..68221f3
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/html/HTMLFilter.java
@@ -0,0 +1,498 @@
+package com.muyu.common.core.utils.html;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HTML过滤器,用于去除XSS漏洞隐患。
+ *
+ * @author muyu
+ */
+public final class HTMLFilter {
+ /**
+ * regex flag union representing /si modifiers in php
+ **/
+ private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
+ private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL);
+ private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
+ private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
+ private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
+ private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
+ private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
+ private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
+ private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
+ private static final Pattern P_ENTITY = Pattern.compile("(\\d+);?");
+ private static final Pattern P_ENTITY_UNICODE = Pattern.compile("([0-9a-f]+);?");
+ private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
+ private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
+ private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
+ private static final Pattern P_END_ARROW = Pattern.compile("^>");
+ private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
+ private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
+ private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
+ private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
+ private static final Pattern P_AMP = Pattern.compile("&");
+ private static final Pattern P_QUOTE = Pattern.compile("\"");
+ private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
+ private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
+ private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
+
+ // @xxx could grow large... maybe use sesat's ReferenceMap
+ private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>();
+ private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>();
+
+ /**
+ * set of allowed html elements, along with allowed attributes for each element
+ **/
+ private final Map> vAllowed;
+ /**
+ * counts of open tags for each (allowable) html element
+ **/
+ private final Map vTagCounts = new HashMap<>();
+
+ /**
+ * html elements which must always be self-closing (e.g. " ")
+ **/
+ private final String[] vSelfClosingTags;
+ /**
+ * html elements which must always have separate opening and closing tags (e.g. " ")
+ **/
+ private final String[] vNeedClosingTags;
+ /**
+ * set of disallowed html elements
+ **/
+ private final String[] vDisallowed;
+ /**
+ * attributes which should be checked for valid protocols
+ **/
+ private final String[] vProtocolAtts;
+ /**
+ * allowed protocols
+ **/
+ private final String[] vAllowedProtocols;
+ /**
+ * tags which should be removed if they contain no content (e.g. " " or " ")
+ **/
+ private final String[] vRemoveBlanks;
+ /**
+ * entities allowed within html markup
+ **/
+ private final String[] vAllowedEntities;
+ /**
+ * flag determining whether comments are allowed in input String.
+ */
+ private final boolean stripComment;
+ private final boolean encodeQuotes;
+ /**
+ * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. ""
+ * becomes " text "). If set to false, unbalanced angle brackets will be html escaped.
+ */
+ private final boolean alwaysMakeTags;
+
+ /**
+ * Default constructor.
+ */
+ public HTMLFilter () {
+ vAllowed = new HashMap<>();
+
+ final ArrayList a_atts = new ArrayList<>();
+ a_atts.add("href");
+ a_atts.add("target");
+ vAllowed.put("a", a_atts);
+
+ final ArrayList img_atts = new ArrayList<>();
+ img_atts.add("src");
+ img_atts.add("width");
+ img_atts.add("height");
+ img_atts.add("alt");
+ vAllowed.put("img", img_atts);
+
+ final ArrayList no_atts = new ArrayList<>();
+ vAllowed.put("b", no_atts);
+ vAllowed.put("strong", no_atts);
+ vAllowed.put("i", no_atts);
+ vAllowed.put("em", no_atts);
+
+ vSelfClosingTags = new String[]{"img"};
+ vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"};
+ vDisallowed = new String[]{};
+ vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp.
+ vProtocolAtts = new String[]{"src", "href"};
+ vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"};
+ vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"};
+ stripComment = true;
+ encodeQuotes = true;
+ alwaysMakeTags = false;
+ }
+
+ /**
+ * Map-parameter configurable constructor.
+ *
+ * @param conf map containing configuration. keys match field names.
+ */
+ @SuppressWarnings("unchecked")
+ public HTMLFilter (final Map conf) {
+
+ assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
+ assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
+ assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
+ assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
+ assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
+ assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts";
+ assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
+ assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
+
+ vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed"));
+ vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
+ vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
+ vDisallowed = (String[]) conf.get("vDisallowed");
+ vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
+ vProtocolAtts = (String[]) conf.get("vProtocolAtts");
+ vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
+ vAllowedEntities = (String[]) conf.get("vAllowedEntities");
+ stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
+ encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
+ alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
+ }
+
+ // ---------------------------------------------------------------
+ // my versions of some PHP library functions
+ public static String chr (final int decimal) {
+ return String.valueOf((char) decimal);
+ }
+
+ public static String htmlSpecialChars (final String s) {
+ String result = s;
+ result = regexReplace(P_AMP, "&", result);
+ result = regexReplace(P_QUOTE, """, result);
+ result = regexReplace(P_LEFT_ARROW, "<", result);
+ result = regexReplace(P_RIGHT_ARROW, ">", result);
+ return result;
+ }
+
+ private static String regexReplace (final Pattern regex_pattern, final String replacement, final String s) {
+ Matcher m = regex_pattern.matcher(s);
+ return m.replaceAll(replacement);
+ }
+
+ // ---------------------------------------------------------------
+
+ private static boolean inArray (final String s, final String[] array) {
+ for (String item : array) {
+ if (item != null && item.equals(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void reset () {
+ vTagCounts.clear();
+ }
+
+ /**
+ * given a user submitted input String, filter out any invalid or restricted html.
+ *
+ * @param input text (i.e. submitted by a user) than may contain html
+ *
+ * @return "clean" version of input, with only valid, whitelisted html elements allowed
+ */
+ public String filter (final String input) {
+ reset();
+ String s = input;
+
+ s = escapeComments(s);
+
+ s = balanceHTML(s);
+
+ s = checkTags(s);
+
+ s = processRemoveBlanks(s);
+
+ // s = validateEntities(s);
+
+ return s;
+ }
+
+ public boolean isAlwaysMakeTags () {
+ return alwaysMakeTags;
+ }
+
+ public boolean isStripComments () {
+ return stripComment;
+ }
+
+ private String escapeComments (final String s) {
+ final Matcher m = P_COMMENTS.matcher(s);
+ final StringBuffer buf = new StringBuffer();
+ if (m.find()) {
+ final String match = m.group(1); // (.*?)
+ m.appendReplacement(buf, Matcher.quoteReplacement(""));
+ }
+ m.appendTail(buf);
+
+ return buf.toString();
+ }
+
+ private String balanceHTML (String s) {
+ if (alwaysMakeTags) {
+ //
+ // try and form html
+ //
+ s = regexReplace(P_END_ARROW, "", s);
+ // 不追加结束标签
+ s = regexReplace(P_BODY_TO_END, "<$1>", s);
+ s = regexReplace(P_XML_CONTENT, "$1<$2", s);
+
+ } else {
+ //
+ // escape stray brackets
+ //
+ s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s);
+ s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s);
+
+ //
+ // the last regexp causes '<>' entities to appear
+ // (we need to do a lookahead assertion so that the last bracket can
+ // be used in the next pass of the regexp)
+ //
+ s = regexReplace(P_BOTH_ARROWS, "", s);
+ }
+
+ return s;
+ }
+
+ private String checkTags (String s) {
+ Matcher m = P_TAGS.matcher(s);
+
+ final StringBuffer buf = new StringBuffer();
+ while (m.find()) {
+ String replaceStr = m.group(1);
+ replaceStr = processTag(replaceStr);
+ m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
+ }
+ m.appendTail(buf);
+
+ // these get tallied in processTag
+ // (remember to reset before subsequent calls to filter method)
+ final StringBuilder sBuilder = new StringBuilder(buf.toString());
+ for (String key : vTagCounts.keySet()) {
+ for (int ii = 0 ; ii < vTagCounts.get(key) ; ii++) {
+ sBuilder.append("").append(key).append(">");
+ }
+ }
+ s = sBuilder.toString();
+
+ return s;
+ }
+
+ private String processRemoveBlanks (final String s) {
+ String result = s;
+ for (String tag : vRemoveBlanks) {
+ if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) {
+ P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>" + tag + ">"));
+ }
+ result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
+ if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) {
+ P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
+ }
+ result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
+ }
+
+ return result;
+ }
+
+ private String processTag (final String s) {
+ // ending tags
+ Matcher m = P_END_TAG.matcher(s);
+ if (m.find()) {
+ final String name = m.group(1).toLowerCase();
+ if (allowed(name)) {
+ if (!inArray(name, vSelfClosingTags)) {
+ if (vTagCounts.containsKey(name)) {
+ vTagCounts.put(name, vTagCounts.get(name) - 1);
+ return "" + name + ">";
+ }
+ }
+ }
+ }
+
+ // starting tags
+ m = P_START_TAG.matcher(s);
+ if (m.find()) {
+ final String name = m.group(1).toLowerCase();
+ final String body = m.group(2);
+ String ending = m.group(3);
+
+ // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
+ if (allowed(name)) {
+ final StringBuilder params = new StringBuilder();
+
+ final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
+ final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
+ final List paramNames = new ArrayList<>();
+ final List paramValues = new ArrayList<>();
+ while (m2.find()) {
+ paramNames.add(m2.group(1)); // ([a-z0-9]+)
+ paramValues.add(m2.group(3)); // (.*?)
+ }
+ while (m3.find()) {
+ paramNames.add(m3.group(1)); // ([a-z0-9]+)
+ paramValues.add(m3.group(3)); // ([^\"\\s']+)
+ }
+
+ String paramName, paramValue;
+ for (int ii = 0 ; ii < paramNames.size() ; ii++) {
+ paramName = paramNames.get(ii).toLowerCase();
+ paramValue = paramValues.get(ii);
+
+ // debug( "paramName='" + paramName + "'" );
+ // debug( "paramValue='" + paramValue + "'" );
+ // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
+
+ if (allowedAttribute(name, paramName)) {
+ if (inArray(paramName, vProtocolAtts)) {
+ paramValue = processParamProtocol(paramValue);
+ }
+ params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\"");
+ }
+ }
+
+ if (inArray(name, vSelfClosingTags)) {
+ ending = " /";
+ }
+
+ if (inArray(name, vNeedClosingTags)) {
+ ending = "";
+ }
+
+ if (ending == null || ending.length() < 1) {
+ if (vTagCounts.containsKey(name)) {
+ vTagCounts.put(name, vTagCounts.get(name) + 1);
+ } else {
+ vTagCounts.put(name, 1);
+ }
+ } else {
+ ending = " /";
+ }
+ return "<" + name + params + ending + ">";
+ } else {
+ return "";
+ }
+ }
+
+ // comments
+ m = P_COMMENT.matcher(s);
+ if (!stripComment && m.find()) {
+ return "<" + m.group() + ">";
+ }
+
+ return "";
+ }
+
+ private String processParamProtocol (String s) {
+ s = decodeEntities(s);
+ final Matcher m = P_PROTOCOL.matcher(s);
+ if (m.find()) {
+ final String protocol = m.group(1);
+ if (!inArray(protocol, vAllowedProtocols)) {
+ // bad protocol, turn into local anchor link instead
+ s = "#" + s.substring(protocol.length() + 1);
+ if (s.startsWith("#//")) {
+ s = "#" + s.substring(3);
+ }
+ }
+ }
+
+ return s;
+ }
+
+ private String decodeEntities (String s) {
+ StringBuffer buf = new StringBuffer();
+
+ Matcher m = P_ENTITY.matcher(s);
+ while (m.find()) {
+ final String match = m.group(1);
+ final int decimal = Integer.decode(match).intValue();
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ buf = new StringBuffer();
+ m = P_ENTITY_UNICODE.matcher(s);
+ while (m.find()) {
+ final String match = m.group(1);
+ final int decimal = Integer.valueOf(match, 16).intValue();
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ buf = new StringBuffer();
+ m = P_ENCODE.matcher(s);
+ while (m.find()) {
+ final String match = m.group(1);
+ final int decimal = Integer.valueOf(match, 16).intValue();
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ s = validateEntities(s);
+ return s;
+ }
+
+ private String validateEntities (final String s) {
+ StringBuffer buf = new StringBuffer();
+
+ // validate entities throughout the string
+ Matcher m = P_VALID_ENTITIES.matcher(s);
+ while (m.find()) {
+ final String one = m.group(1); // ([^&;]*)
+ final String two = m.group(2); // (?=(;|&|$))
+ m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
+ }
+ m.appendTail(buf);
+
+ return encodeQuotes(buf.toString());
+ }
+
+ private String encodeQuotes (final String s) {
+ if (encodeQuotes) {
+ StringBuffer buf = new StringBuffer();
+ Matcher m = P_VALID_QUOTES.matcher(s);
+ while (m.find()) {
+ final String one = m.group(1); // (>|^)
+ final String two = m.group(2); // ([^<]+?)
+ final String three = m.group(3); // (<|$)
+ // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two)
+ m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three));
+ }
+ m.appendTail(buf);
+ return buf.toString();
+ } else {
+ return s;
+ }
+ }
+
+ private String checkEntity (final String preamble, final String term) {
+
+ return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble;
+ }
+
+ private boolean isValidEntity (final String entity) {
+ return inArray(entity, vAllowedEntities);
+ }
+
+ private boolean allowed (final String name) {
+ return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
+ }
+
+ private boolean allowedAttribute (final String name, final String paramName) {
+ return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ip/IpUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ip/IpUtils.java
new file mode 100644
index 0000000..f7ad9bf
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/ip/IpUtils.java
@@ -0,0 +1,331 @@
+package com.muyu.common.core.utils.ip;
+
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * 获取IP方法
+ *
+ * @author muyu
+ */
+public class IpUtils {
+ public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)";
+ // 匹配 ip
+ public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")";
+ // 匹配网段
+ public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")";
+ public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))";
+
+ /**
+ * 获取客户端IP
+ *
+ * @return IP地址
+ */
+ public static String getIpAddr () {
+ return getIpAddr(ServletUtils.getRequest());
+ }
+
+ /**
+ * 获取客户端IP
+ *
+ * @param request 请求对象
+ *
+ * @return IP地址
+ */
+ public static String getIpAddr (HttpServletRequest request) {
+ if (request == null) {
+ return "unknown";
+ }
+ String ip = request.getHeader("x-forwarded-for");
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("X-Forwarded-For");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("X-Real-IP");
+ }
+
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getRemoteAddr();
+ }
+
+ return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
+ }
+
+ /**
+ * 检查是否为内部IP地址
+ *
+ * @param ip IP地址
+ *
+ * @return 结果
+ */
+ public static boolean internalIp (String ip) {
+ byte[] addr = textToNumericFormatV4(ip);
+ return internalIp(addr) || "127.0.0.1".equals(ip);
+ }
+
+ /**
+ * 检查是否为内部IP地址
+ *
+ * @param addr byte地址
+ *
+ * @return 结果
+ */
+ private static boolean internalIp (byte[] addr) {
+ if (StringUtils.isNull(addr) || addr.length < 2) {
+ return true;
+ }
+ final byte b0 = addr[0];
+ final byte b1 = addr[1];
+ // 10.x.x.x/8
+ final byte SECTION_1 = 0x0A;
+ // 172.16.x.x/12
+ final byte SECTION_2 = (byte) 0xAC;
+ final byte SECTION_3 = (byte) 0x10;
+ final byte SECTION_4 = (byte) 0x1F;
+ // 192.168.x.x/16
+ final byte SECTION_5 = (byte) 0xC0;
+ final byte SECTION_6 = (byte) 0xA8;
+ switch (b0) {
+ case SECTION_1:
+ return true;
+ case SECTION_2:
+ if (b1 >= SECTION_3 && b1 <= SECTION_4) {
+ return true;
+ }
+ case SECTION_5:
+ switch (b1) {
+ case SECTION_6:
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * 将IPv4地址转换成字节
+ *
+ * @param text IPv4地址
+ *
+ * @return byte 字节
+ */
+ public static byte[] textToNumericFormatV4 (String text) {
+ if (text.length() == 0) {
+ return null;
+ }
+
+ byte[] bytes = new byte[4];
+ String[] elements = text.split("\\.", -1);
+ try {
+ long l;
+ int i;
+ switch (elements.length) {
+ case 1:
+ l = Long.parseLong(elements[0]);
+ if ((l < 0L) || (l > 4294967295L)) {
+ return null;
+ }
+ bytes[0] = (byte) (int) (l >> 24 & 0xFF);
+ bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
+ bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 2:
+ l = Integer.parseInt(elements[0]);
+ if ((l < 0L) || (l > 255L)) {
+ return null;
+ }
+ bytes[0] = (byte) (int) (l & 0xFF);
+ l = Integer.parseInt(elements[1]);
+ if ((l < 0L) || (l > 16777215L)) {
+ return null;
+ }
+ bytes[1] = (byte) (int) (l >> 16 & 0xFF);
+ bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 3:
+ for (i = 0; i < 2 ; ++i) {
+ l = Integer.parseInt(elements[i]);
+ if ((l < 0L) || (l > 255L)) {
+ return null;
+ }
+ bytes[i] = (byte) (int) (l & 0xFF);
+ }
+ l = Integer.parseInt(elements[2]);
+ if ((l < 0L) || (l > 65535L)) {
+ return null;
+ }
+ bytes[2] = (byte) (int) (l >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 4:
+ for (i = 0; i < 4 ; ++i) {
+ l = Integer.parseInt(elements[i]);
+ if ((l < 0L) || (l > 255L)) {
+ return null;
+ }
+ bytes[i] = (byte) (int) (l & 0xFF);
+ }
+ break;
+ default:
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return bytes;
+ }
+
+ /**
+ * 获取IP地址
+ *
+ * @return 本地IP地址
+ */
+ public static String getHostIp () {
+ try {
+ return InetAddress.getLocalHost().getHostAddress();
+ } catch (UnknownHostException e) {
+ }
+ return "127.0.0.1";
+ }
+
+ /**
+ * 获取主机名
+ *
+ * @return 本地主机名
+ */
+ public static String getHostName () {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ }
+ return "未知";
+ }
+
+ /**
+ * 从多级反向代理中获得第一个非unknown IP地址
+ *
+ * @param ip 获得的IP地址
+ *
+ * @return 第一个非unknown IP地址
+ */
+ public static String getMultistageReverseProxyIp (String ip) {
+ // 多级反向代理检测
+ if (ip != null && ip.indexOf(",") > 0) {
+ final String[] ips = ip.trim().split(",");
+ for (String subIp : ips) {
+ if (false == isUnknown(subIp)) {
+ ip = subIp;
+ break;
+ }
+ }
+ }
+ return StringUtils.substring(ip, 0, 255);
+ }
+
+ /**
+ * 检测给定字符串是否为未知,多用于检测HTTP请求相关
+ *
+ * @param checkString 被检测的字符串
+ *
+ * @return 是否未知
+ */
+ public static boolean isUnknown (String checkString) {
+ return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
+ }
+
+ /**
+ * 是否为IP
+ */
+ public static boolean isIP (String ip) {
+ return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP);
+ }
+
+ /**
+ * 是否为IP,或 *为间隔的通配符地址
+ */
+ public static boolean isIpWildCard (String ip) {
+ return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD);
+ }
+
+ /**
+ * 检测参数是否在ip通配符里
+ */
+ public static boolean ipIsInWildCardNoCheck (String ipWildCard, String ip) {
+ String[] s1 = ipWildCard.split("\\.");
+ String[] s2 = ip.split("\\.");
+ boolean isMatchedSeg = true;
+ for (int i = 0 ; i < s1.length && !s1[i].equals("*") ; i++) {
+ if (!s1[i].equals(s2[i])) {
+ isMatchedSeg = false;
+ break;
+ }
+ }
+ return isMatchedSeg;
+ }
+
+ /**
+ * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串
+ */
+ public static boolean isIPSegment (String ipSeg) {
+ return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG);
+ }
+
+ /**
+ * 判断ip是否在指定网段中
+ */
+ public static boolean ipIsInNetNoCheck (String iparea, String ip) {
+ int idx = iparea.indexOf('-');
+ String[] sips = iparea.substring(0, idx).split("\\.");
+ String[] sipe = iparea.substring(idx + 1).split("\\.");
+ String[] sipt = ip.split("\\.");
+ long ips = 0L, ipe = 0L, ipt = 0L;
+ for (int i = 0 ; i < 4 ; ++i) {
+ ips = ips << 8 | Integer.parseInt(sips[i]);
+ ipe = ipe << 8 | Integer.parseInt(sipe[i]);
+ ipt = ipt << 8 | Integer.parseInt(sipt[i]);
+ }
+ if (ips > ipe) {
+ long t = ips;
+ ips = ipe;
+ ipe = t;
+ }
+ return ips <= ipt && ipt <= ipe;
+ }
+
+ /**
+ * 校验ip是否符合过滤串规则
+ *
+ * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99`
+ * @param ip 校验IP地址
+ *
+ * @return boolean 结果
+ */
+ public static boolean isMatchedIp (String filter, String ip) {
+ if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) {
+ return false;
+ }
+ String[] ips = filter.split(";");
+ for (String iStr : ips) {
+ if (isIP(iStr) && iStr.equals(ip)) {
+ return true;
+ } else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) {
+ return true;
+ } else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/poi/ExcelHandlerAdapter.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/poi/ExcelHandlerAdapter.java
new file mode 100644
index 0000000..e2041fc
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/poi/ExcelHandlerAdapter.java
@@ -0,0 +1,23 @@
+package com.muyu.common.core.utils.poi;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Workbook;
+
+/**
+ * Excel数据格式处理适配器
+ *
+ * @author muyu
+ */
+public interface ExcelHandlerAdapter {
+ /**
+ * 格式化
+ *
+ * @param value 单元格数据值
+ * @param args excel注解args参数组
+ * @param cell 单元格对象
+ * @param wb 工作簿对象
+ *
+ * @return 处理后的值
+ */
+ Object format (Object value, String[] args, Cell cell, Workbook wb);
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/poi/ExcelUtil.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/poi/ExcelUtil.java
new file mode 100644
index 0000000..69795db
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/poi/ExcelUtil.java
@@ -0,0 +1,1230 @@
+package com.muyu.common.core.utils.poi;
+
+import com.muyu.common.core.annotation.Excel;
+import com.muyu.common.core.annotation.Excel.ColumnType;
+import com.muyu.common.core.annotation.Excel.Type;
+import com.muyu.common.core.annotation.Excels;
+import com.muyu.common.core.exception.UtilException;
+import com.muyu.common.core.text.Convert;
+import com.muyu.common.core.utils.DateUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.core.utils.file.FileTypeUtils;
+import com.muyu.common.core.utils.file.ImageUtils;
+import com.muyu.common.core.utils.reflect.ReflectUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Excel相关处理
+ *
+ * @author muyu
+ */
+public class ExcelUtil {
+ public static final String FORMULA_REGEX_STR = "=|-|\\+|@";
+ public static final String[] FORMULA_STR = {"=", "-", "+", "@"};
+ /**
+ * Excel sheet最大行数,默认65536
+ */
+ public static final int sheetSize = 65536;
+ private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);
+ /**
+ * 数字格式
+ */
+ private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");
+ /**
+ * 实体对象
+ */
+ public Class clazz;
+ /**
+ * 需要排除列属性
+ */
+ public String[] excludeFields;
+ /**
+ * 工作表名称
+ */
+ private String sheetName;
+ /**
+ * 导出类型(EXPORT:导出数据;IMPORT:导入模板)
+ */
+ private Type type;
+ /**
+ * 工作薄对象
+ */
+ private Workbook wb;
+ /**
+ * 工作表对象
+ */
+ private Sheet sheet;
+ /**
+ * 样式列表
+ */
+ private Map styles;
+ /**
+ * 导入导出数据列表
+ */
+ private List list;
+ /**
+ * 注解列表
+ */
+ private List fields;
+ /**
+ * 当前行号
+ */
+ private int rownum;
+ /**
+ * 标题
+ */
+ private String title;
+ /**
+ * 最大高度
+ */
+ private short maxHeight;
+ /**
+ * 合并后最后行数
+ */
+ private int subMergedLastRowNum = 0;
+ /**
+ * 合并后开始行数
+ */
+ private int subMergedFirstRowNum = 1;
+ /**
+ * 对象的子列表方法
+ */
+ private Method subMethod;
+ /**
+ * 对象的子列表属性
+ */
+ private List subFields;
+ /**
+ * 统计列表
+ */
+ private Map statistics = new HashMap();
+
+ public ExcelUtil (Class clazz) {
+ this.clazz = clazz;
+ }
+
+ /**
+ * 获取画布
+ */
+ public static Drawing> getDrawingPatriarch (Sheet sheet) {
+ if (sheet.getDrawingPatriarch() == null) {
+ sheet.createDrawingPatriarch();
+ }
+ return sheet.getDrawingPatriarch();
+ }
+
+ /**
+ * 解析导出值 0=男,1=女,2=未知
+ *
+ * @param propertyValue 参数值
+ * @param converterExp 翻译注解
+ * @param separator 分隔符
+ *
+ * @return 解析后值
+ */
+ public static String convertByExp (String propertyValue, String converterExp, String separator) {
+ StringBuilder propertyString = new StringBuilder();
+ String[] convertSource = converterExp.split(",");
+ for (String item : convertSource) {
+ String[] itemArray = item.split("=");
+ if (StringUtils.containsAny(propertyValue, separator)) {
+ for (String value : propertyValue.split(separator)) {
+ if (itemArray[0].equals(value)) {
+ propertyString.append(itemArray[1] + separator);
+ break;
+ }
+ }
+ } else {
+ if (itemArray[0].equals(propertyValue)) {
+ return itemArray[1];
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 反向解析值 男=0,女=1,未知=2
+ *
+ * @param propertyValue 参数值
+ * @param converterExp 翻译注解
+ * @param separator 分隔符
+ *
+ * @return 解析后值
+ */
+ public static String reverseByExp (String propertyValue, String converterExp, String separator) {
+ StringBuilder propertyString = new StringBuilder();
+ String[] convertSource = converterExp.split(",");
+ for (String item : convertSource) {
+ String[] itemArray = item.split("=");
+ if (StringUtils.containsAny(propertyValue, separator)) {
+ for (String value : propertyValue.split(separator)) {
+ if (itemArray[1].equals(value)) {
+ propertyString.append(itemArray[0] + separator);
+ break;
+ }
+ }
+ } else {
+ if (itemArray[1].equals(propertyValue)) {
+ return itemArray[0];
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 隐藏Excel中列属性
+ *
+ * @param fields 列属性名 示例[单个"name"/多个"id","name"]
+ *
+ * @throws Exception
+ */
+ public void hideColumn (String... fields) {
+ this.excludeFields = fields;
+ }
+
+ public void init (List list, String sheetName, String title, Type type) {
+ if (list == null) {
+ list = new ArrayList();
+ }
+ this.list = list;
+ this.sheetName = sheetName;
+ this.type = type;
+ this.title = title;
+ createExcelField();
+ createWorkbook();
+ createTitle();
+ createSubHead();
+ }
+
+ /**
+ * 创建excel第一行标题
+ */
+ public void createTitle () {
+ if (StringUtils.isNotEmpty(title)) {
+ subMergedFirstRowNum++;
+ subMergedLastRowNum++;
+ int titleLastCol = this.fields.size() - 1;
+ if (isSubList()) {
+ titleLastCol = titleLastCol + subFields.size() - 1;
+ }
+ Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
+ titleRow.setHeightInPoints(30);
+ Cell titleCell = titleRow.createCell(0);
+ titleCell.setCellStyle(styles.get("title"));
+ titleCell.setCellValue(title);
+ sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol));
+ }
+ }
+
+ /**
+ * 创建对象的子列表名称
+ */
+ public void createSubHead () {
+ if (isSubList()) {
+ subMergedFirstRowNum++;
+ subMergedLastRowNum++;
+ Row subRow = sheet.createRow(rownum);
+ int excelNum = 0;
+ for (Object[] objects : fields) {
+ Excel attr = (Excel) objects[1];
+ Cell headCell1 = subRow.createCell(excelNum);
+ headCell1.setCellValue(attr.name());
+ headCell1.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+ excelNum++;
+ }
+ int headFirstRow = excelNum - 1;
+ int headLastRow = headFirstRow + subFields.size() - 1;
+ if (headLastRow > headFirstRow) {
+ sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow));
+ }
+ rownum++;
+ }
+ }
+
+ /**
+ * 对excel表单默认第一个索引名转换成list
+ *
+ * @param is 输入流
+ *
+ * @return 转换后集合
+ */
+ public List importExcel (InputStream is) {
+ List list = null;
+ try {
+ list = importExcel(is, 0);
+ } catch (Exception e) {
+ log.error("导入Excel异常{}", e.getMessage());
+ throw new UtilException(e.getMessage());
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ return list;
+ }
+
+ /**
+ * 对excel表单默认第一个索引名转换成list
+ *
+ * @param is 输入流
+ * @param titleNum 标题占用行数
+ *
+ * @return 转换后集合
+ */
+ public List importExcel (InputStream is, int titleNum) throws Exception {
+ return importExcel(StringUtils.EMPTY, is, titleNum);
+ }
+
+ /**
+ * 对excel表单指定表格索引名转换成list
+ *
+ * @param sheetName 表格索引名
+ * @param titleNum 标题占用行数
+ * @param is 输入流
+ *
+ * @return 转换后集合
+ */
+ public List importExcel (String sheetName, InputStream is, int titleNum) throws Exception {
+ this.type = Type.IMPORT;
+ this.wb = WorkbookFactory.create(is);
+ List list = new ArrayList();
+ // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
+ Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
+ if (sheet == null) {
+ throw new IOException("文件sheet不存在");
+ }
+
+ // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
+ int rows = sheet.getLastRowNum();
+ if (rows > 0) {
+ // 定义一个map用于存放excel列的序号和field.
+ Map cellMap = new HashMap();
+ // 获取表头
+ Row heard = sheet.getRow(titleNum);
+ for (int i = 0 ; i < heard.getPhysicalNumberOfCells() ; i++) {
+ Cell cell = heard.getCell(i);
+ if (StringUtils.isNotNull(cell)) {
+ String value = this.getCellValue(heard, i).toString();
+ cellMap.put(value, i);
+ } else {
+ cellMap.put(null, i);
+ }
+ }
+ // 有数据时才处理 得到类的所有field.
+ List fields = this.getFields();
+ Map fieldsMap = new HashMap();
+ for (Object[] objects : fields) {
+ Excel attr = (Excel) objects[1];
+ Integer column = cellMap.get(attr.name());
+ if (column != null) {
+ fieldsMap.put(column, objects);
+ }
+ }
+ for (int i = titleNum + 1 ; i <= rows ; i++) {
+ // 从第2行开始取数据,默认第一行是表头.
+ Row row = sheet.getRow(i);
+ // 判断当前行是否是空行
+ if (isRowEmpty(row)) {
+ continue;
+ }
+ T entity = null;
+ for (Map.Entry entry : fieldsMap.entrySet()) {
+ Object val = this.getCellValue(row, entry.getKey());
+
+ // 如果不存在实例则新建.
+ entity = (entity == null ? clazz.newInstance() : entity);
+ // 从map中得到对应列的field.
+ Field field = (Field) entry.getValue()[0];
+ Excel attr = (Excel) entry.getValue()[1];
+ // 取得类型,并根据对象类型设置值.
+ Class> fieldType = field.getType();
+ if (String.class == fieldType) {
+ String s = Convert.toStr(val);
+ if (StringUtils.endsWith(s, ".0")) {
+ val = StringUtils.substringBefore(s, ".0");
+ } else {
+ String dateFormat = field.getAnnotation(Excel.class).dateFormat();
+ if (StringUtils.isNotEmpty(dateFormat)) {
+ val = parseDateToStr(dateFormat, val);
+ } else {
+ val = Convert.toStr(val);
+ }
+ }
+ } else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) {
+ val = Convert.toInt(val);
+ } else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) {
+ val = Convert.toLong(val);
+ } else if (Double.TYPE == fieldType || Double.class == fieldType) {
+ val = Convert.toDouble(val);
+ } else if (Float.TYPE == fieldType || Float.class == fieldType) {
+ val = Convert.toFloat(val);
+ } else if (BigDecimal.class == fieldType) {
+ val = Convert.toBigDecimal(val);
+ } else if (Date.class == fieldType) {
+ if (val instanceof String) {
+ val = DateUtils.parseDate(val);
+ } else if (val instanceof Double) {
+ val = DateUtil.getJavaDate((Double) val);
+ }
+ } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
+ val = Convert.toBool(val, false);
+ }
+ if (StringUtils.isNotNull(fieldType)) {
+ String propertyName = field.getName();
+ if (StringUtils.isNotEmpty(attr.targetAttr())) {
+ propertyName = field.getName() + "." + attr.targetAttr();
+ }
+ if (StringUtils.isNotEmpty(attr.readConverterExp())) {
+ val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
+ } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) {
+ val = dataFormatHandlerAdapter(val, attr, null);
+ }
+ ReflectUtils.invokeSetter(entity, propertyName, val);
+ }
+ }
+ list.add(entity);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param response 返回数据
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ *
+ * @return 结果
+ */
+ public void exportExcel (HttpServletResponse response, List list, String sheetName) {
+ exportExcel(response, list, sheetName, StringUtils.EMPTY);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param response 返回数据
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param title 标题
+ *
+ * @return 结果
+ */
+ public void exportExcel (HttpServletResponse response, List list, String sheetName, String title) {
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ response.setCharacterEncoding("utf-8");
+ this.init(list, sheetName, title, Type.EXPORT);
+ exportExcel(response);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param sheetName 工作表的名称
+ *
+ * @return 结果
+ */
+ public void importTemplateExcel (HttpServletResponse response, String sheetName) {
+ importTemplateExcel(response, sheetName, StringUtils.EMPTY);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param sheetName 工作表的名称
+ * @param title 标题
+ *
+ * @return 结果
+ */
+ public void importTemplateExcel (HttpServletResponse response, String sheetName, String title) {
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ response.setCharacterEncoding("utf-8");
+ this.init(null, sheetName, title, Type.IMPORT);
+ exportExcel(response);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @return 结果
+ */
+ public void exportExcel (HttpServletResponse response) {
+ try {
+ writeSheet();
+ wb.write(response.getOutputStream());
+ } catch (Exception e) {
+ log.error("导出Excel异常{}", e.getMessage());
+ } finally {
+ IOUtils.closeQuietly(wb);
+ }
+ }
+
+ /**
+ * 创建写入数据到Sheet
+ */
+ public void writeSheet () {
+ // 取出一共有多少个sheet.
+ int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
+ for (int index = 0 ; index < sheetNo ; index++) {
+ createSheet(sheetNo, index);
+
+ // 产生一行
+ Row row = sheet.createRow(rownum);
+ int column = 0;
+ // 写入各个字段的列头名称
+ for (Object[] os : fields) {
+ Field field = (Field) os[0];
+ Excel excel = (Excel) os[1];
+ if (Collection.class.isAssignableFrom(field.getType())) {
+ for (Field subField : subFields) {
+ Excel subExcel = subField.getAnnotation(Excel.class);
+ this.createHeadCell(subExcel, row, column++);
+ }
+ } else {
+ this.createHeadCell(excel, row, column++);
+ }
+ }
+ if (Type.EXPORT.equals(type)) {
+ fillExcelData(index, row);
+ addStatisticsRow();
+ }
+ }
+ }
+
+ /**
+ * 填充excel数据
+ *
+ * @param index 序号
+ * @param row 单元格行
+ */
+ @SuppressWarnings("unchecked")
+ public void fillExcelData (int index, Row row) {
+ int startNo = index * sheetSize;
+ int endNo = Math.min(startNo + sheetSize, list.size());
+ int rowNo = (1 + rownum) - startNo;
+ for (int i = startNo ; i < endNo ; i++) {
+ rowNo = isSubList() ? (i > 1 ? rowNo + 1 : rowNo + i) : i + 1 + rownum - startNo;
+ row = sheet.createRow(rowNo);
+ // 得到导出对象.
+ T vo = (T) list.get(i);
+ Collection> subList = null;
+ if (isSubList()) {
+ if (isSubListValue(vo)) {
+ subList = getListCellValue(vo);
+ subMergedLastRowNum = subMergedLastRowNum + subList.size();
+ } else {
+ subMergedFirstRowNum++;
+ subMergedLastRowNum++;
+ }
+ }
+ int column = 0;
+ for (Object[] os : fields) {
+ Field field = (Field) os[0];
+ Excel excel = (Excel) os[1];
+ if (Collection.class.isAssignableFrom(field.getType()) && StringUtils.isNotNull(subList)) {
+ boolean subFirst = false;
+ for (Object obj : subList) {
+ if (subFirst) {
+ rowNo++;
+ row = sheet.createRow(rowNo);
+ }
+ List subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class);
+ int subIndex = 0;
+ for (Field subField : subFields) {
+ if (subField.isAnnotationPresent(Excel.class)) {
+ subField.setAccessible(true);
+ Excel attr = subField.getAnnotation(Excel.class);
+ this.addCell(attr, row, (T) obj, subField, column + subIndex);
+ }
+ subIndex++;
+ }
+ subFirst = true;
+ }
+ this.subMergedFirstRowNum = this.subMergedFirstRowNum + subList.size();
+ } else {
+ this.addCell(excel, row, vo, field, column++);
+ }
+ }
+ }
+ }
+
+ /**
+ * 创建表格样式
+ *
+ * @param wb 工作薄对象
+ *
+ * @return 样式列表
+ */
+ private Map createStyles (Workbook wb) {
+ // 写入各条记录,每条记录对应excel表中的一行
+ Map styles = new HashMap();
+ CellStyle style = wb.createCellStyle();
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ Font titleFont = wb.createFont();
+ titleFont.setFontName("Arial");
+ titleFont.setFontHeightInPoints((short) 16);
+ titleFont.setBold(true);
+ style.setFont(titleFont);
+ styles.put("title", style);
+
+ style = wb.createCellStyle();
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ style.setBorderRight(BorderStyle.THIN);
+ style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderLeft(BorderStyle.THIN);
+ style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderTop(BorderStyle.THIN);
+ style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderBottom(BorderStyle.THIN);
+ style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ Font dataFont = wb.createFont();
+ dataFont.setFontName("Arial");
+ dataFont.setFontHeightInPoints((short) 10);
+ style.setFont(dataFont);
+ styles.put("data", style);
+
+ style = wb.createCellStyle();
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ Font totalFont = wb.createFont();
+ totalFont.setFontName("Arial");
+ totalFont.setFontHeightInPoints((short) 10);
+ style.setFont(totalFont);
+ styles.put("total", style);
+
+ styles.putAll(annotationHeaderStyles(wb, styles));
+
+ styles.putAll(annotationDataStyles(wb));
+
+ return styles;
+ }
+
+ /**
+ * 根据Excel注解创建表格头样式
+ *
+ * @param wb 工作薄对象
+ *
+ * @return 自定义样式列表
+ */
+ private Map annotationHeaderStyles (Workbook wb, Map styles) {
+ Map headerStyles = new HashMap();
+ for (Object[] os : fields) {
+ Excel excel = (Excel) os[1];
+ String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor());
+ if (!headerStyles.containsKey(key)) {
+ CellStyle style = wb.createCellStyle();
+ style.cloneStyleFrom(styles.get("data"));
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ style.setFillForegroundColor(excel.headerBackgroundColor().index);
+ style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+ Font headerFont = wb.createFont();
+ headerFont.setFontName("Arial");
+ headerFont.setFontHeightInPoints((short) 10);
+ headerFont.setBold(true);
+ headerFont.setColor(excel.headerColor().index);
+ style.setFont(headerFont);
+ headerStyles.put(key, style);
+ }
+ }
+ return headerStyles;
+ }
+
+ /**
+ * 根据Excel注解创建表格列样式
+ *
+ * @param wb 工作薄对象
+ *
+ * @return 自定义样式列表
+ */
+ private Map annotationDataStyles (Workbook wb) {
+ Map styles = new HashMap();
+ for (Object[] os : fields) {
+ Excel excel = (Excel) os[1];
+ String key = StringUtils.format("data_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor());
+ if (!styles.containsKey(key)) {
+ CellStyle style = wb.createCellStyle();
+ style.setAlignment(excel.align());
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ style.setBorderRight(BorderStyle.THIN);
+ style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderLeft(BorderStyle.THIN);
+ style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderTop(BorderStyle.THIN);
+ style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderBottom(BorderStyle.THIN);
+ style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+ style.setFillForegroundColor(excel.backgroundColor().getIndex());
+ Font dataFont = wb.createFont();
+ dataFont.setFontName("Arial");
+ dataFont.setFontHeightInPoints((short) 10);
+ dataFont.setColor(excel.color().index);
+ style.setFont(dataFont);
+ styles.put(key, style);
+ }
+ }
+ return styles;
+ }
+
+ /**
+ * 创建单元格
+ */
+ public Cell createHeadCell (Excel attr, Row row, int column) {
+ // 创建列
+ Cell cell = row.createCell(column);
+ // 写入列信息
+ cell.setCellValue(attr.name());
+ setDataValidation(attr, row, column);
+ cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+ if (isSubList()) {
+ // 填充默认样式,防止合并单元格样式失效
+ sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor())));
+ if (attr.needMerge()) {
+ sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column));
+ }
+ }
+ return cell;
+ }
+
+ /**
+ * 设置单元格信息
+ *
+ * @param value 单元格值
+ * @param attr 注解相关
+ * @param cell 单元格信息
+ */
+ public void setCellVo (Object value, Excel attr, Cell cell) {
+ if (ColumnType.STRING == attr.cellType()) {
+ String cellValue = Convert.toStr(value);
+ // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。
+ if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) {
+ cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0");
+ }
+ if (value instanceof Collection && StringUtils.equals("[]", cellValue)) {
+ cellValue = StringUtils.EMPTY;
+ }
+ cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix());
+ } else if (ColumnType.NUMERIC == attr.cellType()) {
+ if (StringUtils.isNotNull(value)) {
+ cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value));
+ }
+ } else if (ColumnType.IMAGE == attr.cellType()) {
+ ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1);
+ String imagePath = Convert.toStr(value);
+ if (StringUtils.isNotEmpty(imagePath)) {
+ byte[] data = ImageUtils.getImage(imagePath);
+ getDrawingPatriarch(cell.getSheet()).createPicture(anchor,
+ cell.getSheet().getWorkbook().addPicture(data, getImageType(data)));
+ }
+ }
+ }
+
+ /**
+ * 获取图片类型,设置图片插入类型
+ */
+ public int getImageType (byte[] value) {
+ String type = FileTypeUtils.getFileExtendName(value);
+ if ("JPG".equalsIgnoreCase(type)) {
+ return Workbook.PICTURE_TYPE_JPEG;
+ } else if ("PNG".equalsIgnoreCase(type)) {
+ return Workbook.PICTURE_TYPE_PNG;
+ }
+ return Workbook.PICTURE_TYPE_JPEG;
+ }
+
+ /**
+ * 创建表格样式
+ */
+ public void setDataValidation (Excel attr, Row row, int column) {
+ if (attr.name().indexOf("注:") >= 0) {
+ sheet.setColumnWidth(column, 6000);
+ } else {
+ // 设置列宽
+ sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256));
+ }
+ if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0) {
+ if (attr.combo().length > 15 || StringUtils.join(attr.combo()).length() > 255) {
+ // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到
+ setXSSFValidationWithHidden(sheet, attr.combo(), attr.prompt(), 1, 100, column, column);
+ } else {
+ // 提示信息或只能选择不能输入的列内容.
+ setPromptOrValidation(sheet, attr.combo(), attr.prompt(), 1, 100, column, column);
+ }
+ }
+ }
+
+ /**
+ * 添加单元格
+ */
+ public Cell addCell (Excel attr, Row row, T vo, Field field, int column) {
+ Cell cell = null;
+ try {
+ // 设置行高
+ row.setHeight(maxHeight);
+ // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
+ if (attr.isExport()) {
+ // 创建cell
+ cell = row.createCell(column);
+ if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) {
+ CellRangeAddress cellAddress = new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column);
+ sheet.addMergedRegion(cellAddress);
+ }
+ cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor())));
+
+ // 用于读取对象中的属性
+ Object value = getTargetValue(vo, field, attr);
+ String dateFormat = attr.dateFormat();
+ String readConverterExp = attr.readConverterExp();
+ String separator = attr.separator();
+ if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) {
+ cell.setCellValue(parseDateToStr(dateFormat, value));
+ } else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) {
+ cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator));
+ } else if (value instanceof BigDecimal && -1 != attr.scale()) {
+ cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue());
+ } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) {
+ cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell));
+ } else {
+ // 设置列类型
+ setCellVo(value, attr, cell);
+ }
+ addStatisticsData(column, Convert.toStr(value), attr);
+ }
+ } catch (Exception e) {
+ log.error("导出Excel失败{}", e);
+ }
+ return cell;
+ }
+
+ /**
+ * 设置 POI XSSFSheet 单元格提示或选择框
+ *
+ * @param sheet 表单
+ * @param textlist 下拉框显示的内容
+ * @param promptContent 提示内容
+ * @param firstRow 开始行
+ * @param endRow 结束行
+ * @param firstCol 开始列
+ * @param endCol 结束列
+ */
+ public void setPromptOrValidation (Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow,
+ int firstCol, int endCol) {
+ DataValidationHelper helper = sheet.getDataValidationHelper();
+ DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1");
+ CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+ DataValidation dataValidation = helper.createValidation(constraint, regions);
+ if (StringUtils.isNotEmpty(promptContent)) {
+ // 如果设置了提示信息则鼠标放上去提示
+ dataValidation.createPromptBox("", promptContent);
+ dataValidation.setShowPromptBox(true);
+ }
+ // 处理Excel兼容性问题
+ if (dataValidation instanceof XSSFDataValidation) {
+ dataValidation.setSuppressDropDownArrow(true);
+ dataValidation.setShowErrorBox(true);
+ } else {
+ dataValidation.setSuppressDropDownArrow(false);
+ }
+ sheet.addValidationData(dataValidation);
+ }
+
+ /**
+ * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框).
+ *
+ * @param sheet 要设置的sheet.
+ * @param textlist 下拉框显示的内容
+ * @param promptContent 提示内容
+ * @param firstRow 开始行
+ * @param endRow 结束行
+ * @param firstCol 开始列
+ * @param endCol 结束列
+ */
+ public void setXSSFValidationWithHidden (Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) {
+ String hideSheetName = "combo_" + firstCol + "_" + endCol;
+ Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据
+ for (int i = 0 ; i < textlist.length ; i++) {
+ hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]);
+ }
+ // 创建名称,可被其他单元格引用
+ Name name = wb.createName();
+ name.setNameName(hideSheetName + "_data");
+ name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length);
+ DataValidationHelper helper = sheet.getDataValidationHelper();
+ // 加载下拉列表内容
+ DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data");
+ // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+ CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+ // 数据有效性对象
+ DataValidation dataValidation = helper.createValidation(constraint, regions);
+ if (StringUtils.isNotEmpty(promptContent)) {
+ // 如果设置了提示信息则鼠标放上去提示
+ dataValidation.createPromptBox("", promptContent);
+ dataValidation.setShowPromptBox(true);
+ }
+ // 处理Excel兼容性问题
+ if (dataValidation instanceof XSSFDataValidation) {
+ dataValidation.setSuppressDropDownArrow(true);
+ dataValidation.setShowErrorBox(true);
+ } else {
+ dataValidation.setSuppressDropDownArrow(false);
+ }
+
+ sheet.addValidationData(dataValidation);
+ // 设置hiddenSheet隐藏
+ wb.setSheetHidden(wb.getSheetIndex(hideSheet), true);
+ }
+
+ /**
+ * 数据处理器
+ *
+ * @param value 数据值
+ * @param excel 数据注解
+ *
+ * @return
+ */
+ public String dataFormatHandlerAdapter (Object value, Excel excel, Cell cell) {
+ try {
+ Object instance = excel.handler().newInstance();
+ Method formatMethod = excel.handler().getMethod("format", new Class[]{Object.class, String[].class, Cell.class, Workbook.class});
+ value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb);
+ } catch (Exception e) {
+ log.error("不能格式化数据 " + excel.handler(), e.getMessage());
+ }
+ return Convert.toStr(value);
+ }
+
+ /**
+ * 合计统计信息
+ */
+ private void addStatisticsData (Integer index, String text, Excel entity) {
+ if (entity != null && entity.isStatistics()) {
+ Double temp = 0D;
+ if (!statistics.containsKey(index)) {
+ statistics.put(index, temp);
+ }
+ try {
+ temp = Double.valueOf(text);
+ } catch (NumberFormatException e) {
+ }
+ statistics.put(index, statistics.get(index) + temp);
+ }
+ }
+
+ /**
+ * 创建统计行
+ */
+ public void addStatisticsRow () {
+ if (statistics.size() > 0) {
+ Row row = sheet.createRow(sheet.getLastRowNum() + 1);
+ Set keys = statistics.keySet();
+ Cell cell = row.createCell(0);
+ cell.setCellStyle(styles.get("total"));
+ cell.setCellValue("合计");
+
+ for (Integer key : keys) {
+ cell = row.createCell(key);
+ cell.setCellStyle(styles.get("total"));
+ cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key)));
+ }
+ statistics.clear();
+ }
+ }
+
+ /**
+ * 获取bean中的属性值
+ *
+ * @param vo 实体对象
+ * @param field 字段
+ * @param excel 注解
+ *
+ * @return 最终的属性值
+ *
+ * @throws Exception
+ */
+ private Object getTargetValue (T vo, Field field, Excel excel) throws Exception {
+ Object o = field.get(vo);
+ if (StringUtils.isNotEmpty(excel.targetAttr())) {
+ String target = excel.targetAttr();
+ if (target.contains(".")) {
+ String[] targets = target.split("[.]");
+ for (String name : targets) {
+ o = getValue(o, name);
+ }
+ } else {
+ o = getValue(o, target);
+ }
+ }
+ return o;
+ }
+
+ /**
+ * 以类的属性的get方法方法形式获取值
+ *
+ * @param o
+ * @param name
+ *
+ * @return value
+ *
+ * @throws Exception
+ */
+ private Object getValue (Object o, String name) throws Exception {
+ if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) {
+ Class> clazz = o.getClass();
+ Field field = clazz.getDeclaredField(name);
+ field.setAccessible(true);
+ o = field.get(o);
+ }
+ return o;
+ }
+
+ /**
+ * 得到所有定义字段
+ */
+ private void createExcelField () {
+ this.fields = getFields();
+ this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
+ this.maxHeight = getRowHeight();
+ }
+
+ /**
+ * 获取字段注解信息
+ */
+ public List getFields () {
+ List fields = new ArrayList();
+ List tempFields = new ArrayList<>();
+ tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
+ tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ for (Field field : tempFields) {
+ if (!ArrayUtils.contains(this.excludeFields, field.getName())) {
+ // 单注解
+ if (field.isAnnotationPresent(Excel.class)) {
+ Excel attr = field.getAnnotation(Excel.class);
+ if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) {
+ field.setAccessible(true);
+ fields.add(new Object[]{field, attr});
+ }
+ if (Collection.class.isAssignableFrom(field.getType())) {
+ subMethod = getSubMethod(field.getName(), clazz);
+ ParameterizedType pt = (ParameterizedType) field.getGenericType();
+ Class> subClass = (Class>) pt.getActualTypeArguments()[0];
+ this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
+ }
+ }
+
+ // 多注解
+ if (field.isAnnotationPresent(Excels.class)) {
+ Excels attrs = field.getAnnotation(Excels.class);
+ Excel[] excels = attrs.value();
+ for (Excel attr : excels) {
+ if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr())
+ && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) {
+ field.setAccessible(true);
+ fields.add(new Object[]{field, attr});
+ }
+ }
+ }
+ }
+ }
+ return fields;
+ }
+
+ /**
+ * 根据注解获取最大行高
+ */
+ public short getRowHeight () {
+ double maxHeight = 0;
+ for (Object[] os : this.fields) {
+ Excel excel = (Excel) os[1];
+ maxHeight = Math.max(maxHeight, excel.height());
+ }
+ return (short) (maxHeight * 20);
+ }
+
+ /**
+ * 创建一个工作簿
+ */
+ public void createWorkbook () {
+ this.wb = new SXSSFWorkbook(500);
+ this.sheet = wb.createSheet();
+ wb.setSheetName(0, sheetName);
+ this.styles = createStyles(wb);
+ }
+
+ /**
+ * 创建工作表
+ *
+ * @param sheetNo sheet数量
+ * @param index 序号
+ */
+ public void createSheet (int sheetNo, int index) {
+ // 设置工作表的名称.
+ if (sheetNo > 1 && index > 0) {
+ this.sheet = wb.createSheet();
+ this.createTitle();
+ wb.setSheetName(index, sheetName + index);
+ }
+ }
+
+ /**
+ * 获取单元格值
+ *
+ * @param row 获取的行
+ * @param column 获取单元格列号
+ *
+ * @return 单元格值
+ */
+ public Object getCellValue (Row row, int column) {
+ if (row == null) {
+ return row;
+ }
+ Object val = "";
+ try {
+ Cell cell = row.getCell(column);
+ if (StringUtils.isNotNull(cell)) {
+ if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) {
+ val = cell.getNumericCellValue();
+ if (DateUtil.isCellDateFormatted(cell)) {
+ val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
+ } else {
+ if ((Double) val % 1 != 0) {
+ val = new BigDecimal(val.toString());
+ } else {
+ val = new DecimalFormat("0").format(val);
+ }
+ }
+ } else if (cell.getCellType() == CellType.STRING) {
+ val = cell.getStringCellValue();
+ } else if (cell.getCellType() == CellType.BOOLEAN) {
+ val = cell.getBooleanCellValue();
+ } else if (cell.getCellType() == CellType.ERROR) {
+ val = cell.getErrorCellValue();
+ }
+
+ }
+ } catch (Exception e) {
+ return val;
+ }
+ return val;
+ }
+
+ /**
+ * 判断是否是空行
+ *
+ * @param row 判断的行
+ *
+ * @return
+ */
+ private boolean isRowEmpty (Row row) {
+ if (row == null) {
+ return true;
+ }
+ for (int i = row.getFirstCellNum() ; i < row.getLastCellNum() ; i++) {
+ Cell cell = row.getCell(i);
+ if (cell != null && cell.getCellType() != CellType.BLANK) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 格式化不同类型的日期对象
+ *
+ * @param dateFormat 日期格式
+ * @param val 被格式化的日期对象
+ *
+ * @return 格式化后的日期字符
+ */
+ public String parseDateToStr (String dateFormat, Object val) {
+ if (val == null) {
+ return "";
+ }
+ String str;
+ if (val instanceof Date) {
+ str = DateUtils.parseDateToStr(dateFormat, (Date) val);
+ } else if (val instanceof LocalDateTime) {
+ str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val));
+ } else if (val instanceof LocalDate) {
+ str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val));
+ } else {
+ str = val.toString();
+ }
+ return str;
+ }
+
+ /**
+ * 是否有对象的子列表
+ */
+ public boolean isSubList () {
+ return StringUtils.isNotNull(subFields) && subFields.size() > 0;
+ }
+
+ /**
+ * 是否有对象的子列表,集合不为空
+ */
+ public boolean isSubListValue (T vo) {
+ return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0;
+ }
+
+ /**
+ * 获取集合的值
+ */
+ public Collection> getListCellValue (Object obj) {
+ Object value;
+ try {
+ value = subMethod.invoke(obj, new Object[]{});
+ } catch (Exception e) {
+ return new ArrayList();
+ }
+ return (Collection>) value;
+ }
+
+ /**
+ * 获取对象的子列表方法
+ *
+ * @param name 名称
+ * @param pojoClass 类对象
+ *
+ * @return 子列表方法
+ */
+ public Method getSubMethod (String name, Class> pojoClass) {
+ StringBuffer getMethodName = new StringBuffer("get");
+ getMethodName.append(name.substring(0, 1).toUpperCase());
+ getMethodName.append(name.substring(1));
+ Method method = null;
+ try {
+ method = pojoClass.getMethod(getMethodName.toString(), new Class[]{});
+ } catch (Exception e) {
+ log.error("获取对象异常{}", e.getMessage());
+ }
+ return method;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/reflect/ReflectUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/reflect/ReflectUtils.java
new file mode 100644
index 0000000..2ec7e4f
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/reflect/ReflectUtils.java
@@ -0,0 +1,324 @@
+package com.muyu.common.core.utils.reflect;
+
+import com.muyu.common.core.text.Convert;
+import com.muyu.common.core.utils.DateUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.*;
+import java.util.Date;
+
+/**
+ * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
+ *
+ * @author muyu
+ */
+@SuppressWarnings("rawtypes")
+public class ReflectUtils {
+ private static final String SETTER_PREFIX = "set";
+
+ private static final String GETTER_PREFIX = "get";
+
+ private static final String CGLIB_CLASS_SEPARATOR = "$$";
+
+ private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class);
+
+ /**
+ * 调用Getter方法.
+ * 支持多级,如:对象名.对象名.方法
+ */
+ @SuppressWarnings("unchecked")
+ public static E invokeGetter (Object obj, String propertyName) {
+ Object object = obj;
+ for (String name : StringUtils.split(propertyName, ".")) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
+ object = invokeMethod(object, getterMethodName, new Class[]{}, new Object[]{});
+ }
+ return (E) object;
+ }
+
+ /**
+ * 调用Setter方法, 仅匹配方法名。
+ * 支持多级,如:对象名.对象名.方法
+ */
+ public static void invokeSetter (Object obj, String propertyName, E value) {
+ Object object = obj;
+ String[] names = StringUtils.split(propertyName, ".");
+ for (int i = 0 ; i < names.length ; i++) {
+ if (i < names.length - 1) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
+ object = invokeMethod(object, getterMethodName, new Class[]{}, new Object[]{});
+ } else {
+ String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
+ invokeMethodByName(object, setterMethodName, new Object[]{value});
+ }
+ }
+ }
+
+ /**
+ * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
+ */
+ @SuppressWarnings("unchecked")
+ public static E getFieldValue (final Object obj, final String fieldName) {
+ Field field = getAccessibleField(obj, fieldName);
+ if (field == null) {
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+ return null;
+ }
+ E result = null;
+ try {
+ result = (E) field.get(obj);
+ } catch (IllegalAccessException e) {
+ logger.error("不可能抛出的异常{}", e.getMessage());
+ }
+ return result;
+ }
+
+ /**
+ * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数.
+ */
+ public static void setFieldValue (final Object obj, final String fieldName, final E value) {
+ Field field = getAccessibleField(obj, fieldName);
+ if (field == null) {
+ // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+ return;
+ }
+ try {
+ field.set(obj, value);
+ } catch (IllegalAccessException e) {
+ logger.error("不可能抛出的异常: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 直接调用对象方法, 无视private/protected修饰符.
+ * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用.
+ * 同时匹配方法名+参数类型,
+ */
+ @SuppressWarnings("unchecked")
+ public static E invokeMethod (final Object obj, final String methodName, final Class>[] parameterTypes,
+ final Object[] args) {
+ if (obj == null || methodName == null) {
+ return null;
+ }
+ Method method = getAccessibleMethod(obj, methodName, parameterTypes);
+ if (method == null) {
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
+ return null;
+ }
+ try {
+ return (E) method.invoke(obj, args);
+ } catch (Exception e) {
+ String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
+ throw convertReflectionExceptionToUnchecked(msg, e);
+ }
+ }
+
+ /**
+ * 直接调用对象方法, 无视private/protected修饰符,
+ * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用.
+ * 只匹配函数名,如果有多个同名函数调用第一个。
+ */
+ @SuppressWarnings("unchecked")
+ public static E invokeMethodByName (final Object obj, final String methodName, final Object[] args) {
+ Method method = getAccessibleMethodByName(obj, methodName, args.length);
+ if (method == null) {
+ // 如果为空不报错,直接返回空。
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
+ return null;
+ }
+ try {
+ // 类型转换(将参数数据类型转换为目标方法参数类型)
+ Class>[] cs = method.getParameterTypes();
+ for (int i = 0 ; i < cs.length ; i++) {
+ if (args[i] != null && !args[i].getClass().equals(cs[i])) {
+ if (cs[i] == String.class) {
+ args[i] = Convert.toStr(args[i]);
+ if (StringUtils.endsWith((String) args[i], ".0")) {
+ args[i] = StringUtils.substringBefore((String) args[i], ".0");
+ }
+ } else if (cs[i] == Integer.class) {
+ args[i] = Convert.toInt(args[i]);
+ } else if (cs[i] == Long.class) {
+ args[i] = Convert.toLong(args[i]);
+ } else if (cs[i] == Double.class) {
+ args[i] = Convert.toDouble(args[i]);
+ } else if (cs[i] == Float.class) {
+ args[i] = Convert.toFloat(args[i]);
+ } else if (cs[i] == Date.class) {
+ if (args[i] instanceof String) {
+ args[i] = DateUtils.parseDate(args[i]);
+ } else {
+ args[i] = DateUtil.getJavaDate((Double) args[i]);
+ }
+ } else if (cs[i] == boolean.class || cs[i] == Boolean.class) {
+ args[i] = Convert.toBool(args[i]);
+ }
+ }
+ }
+ return (E) method.invoke(obj, args);
+ } catch (Exception e) {
+ String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
+ throw convertReflectionExceptionToUnchecked(msg, e);
+ }
+ }
+
+ /**
+ * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
+ * 如向上转型到Object仍无法找到, 返回null.
+ */
+ public static Field getAccessibleField (final Object obj, final String fieldName) {
+ // 为空不报错。直接返回 null
+ if (obj == null) {
+ return null;
+ }
+ Validate.notBlank(fieldName, "fieldName can't be blank");
+ for (Class> superClass = obj.getClass() ; superClass != Object.class ; superClass = superClass.getSuperclass()) {
+ try {
+ Field field = superClass.getDeclaredField(fieldName);
+ makeAccessible(field);
+ return field;
+ } catch (NoSuchFieldException e) {
+ continue;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
+ * 如向上转型到Object仍无法找到, 返回null.
+ * 匹配函数名+参数类型。
+ * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
+ */
+ public static Method getAccessibleMethod (final Object obj, final String methodName,
+ final Class>... parameterTypes) {
+ // 为空不报错。直接返回 null
+ if (obj == null) {
+ return null;
+ }
+ Validate.notBlank(methodName, "methodName can't be blank");
+ for (Class> searchType = obj.getClass() ; searchType != Object.class ; searchType = searchType.getSuperclass()) {
+ try {
+ Method method = searchType.getDeclaredMethod(methodName, parameterTypes);
+ makeAccessible(method);
+ return method;
+ } catch (NoSuchMethodException e) {
+ continue;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
+ * 如向上转型到Object仍无法找到, 返回null.
+ * 只匹配函数名。
+ * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
+ */
+ public static Method getAccessibleMethodByName (final Object obj, final String methodName, int argsNum) {
+ // 为空不报错。直接返回 null
+ if (obj == null) {
+ return null;
+ }
+ Validate.notBlank(methodName, "methodName can't be blank");
+ for (Class> searchType = obj.getClass() ; searchType != Object.class ; searchType = searchType.getSuperclass()) {
+ Method[] methods = searchType.getDeclaredMethods();
+ for (Method method : methods) {
+ if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) {
+ makeAccessible(method);
+ return method;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
+ */
+ public static void makeAccessible (Method method) {
+ if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))
+ && !method.isAccessible()) {
+ method.setAccessible(true);
+ }
+ }
+
+ /**
+ * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
+ */
+ public static void makeAccessible (Field field) {
+ if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())
+ || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) {
+ field.setAccessible(true);
+ }
+ }
+
+ /**
+ * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处
+ * 如无法找到, 返回Object.class.
+ */
+ @SuppressWarnings("unchecked")
+ public static Class getClassGenricType (final Class clazz) {
+ return getClassGenricType(clazz, 0);
+ }
+
+ /**
+ * 通过反射, 获得Class定义中声明的父类的泛型参数的类型.
+ * 如无法找到, 返回Object.class.
+ */
+ public static Class getClassGenricType (final Class clazz, final int index) {
+ Type genType = clazz.getGenericSuperclass();
+
+ if (!(genType instanceof ParameterizedType)) {
+ logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType");
+ return Object.class;
+ }
+
+ Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
+
+ if (index >= params.length || index < 0) {
+ logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: "
+ + params.length);
+ return Object.class;
+ }
+ if (!(params[index] instanceof Class)) {
+ logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
+ return Object.class;
+ }
+
+ return (Class) params[index];
+ }
+
+ public static Class> getUserClass (Object instance) {
+ if (instance == null) {
+ throw new RuntimeException("Instance must not be null");
+ }
+ Class clazz = instance.getClass();
+ if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) {
+ Class> superClass = clazz.getSuperclass();
+ if (superClass != null && !Object.class.equals(superClass)) {
+ return superClass;
+ }
+ }
+ return clazz;
+
+ }
+
+ /**
+ * 将反射时的checked exception转换为unchecked exception.
+ */
+ public static RuntimeException convertReflectionExceptionToUnchecked (String msg, Exception e) {
+ if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException
+ || e instanceof NoSuchMethodException) {
+ return new IllegalArgumentException(msg, e);
+ } else if (e instanceof InvocationTargetException) {
+ return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException());
+ }
+ return new RuntimeException(msg, e);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/sign/Base64.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/sign/Base64.java
new file mode 100644
index 0000000..038933f
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/sign/Base64.java
@@ -0,0 +1,256 @@
+package com.muyu.common.core.utils.sign;
+
+/**
+ * Base64工具类
+ *
+ * @author muyu
+ */
+public final class Base64 {
+ static private final int BASELENGTH = 128;
+ static private final int LOOKUPLENGTH = 64;
+ static private final int TWENTYFOURBITGROUP = 24;
+ static private final int EIGHTBIT = 8;
+ static private final int SIXTEENBIT = 16;
+ static private final int FOURBYTE = 4;
+ static private final int SIGN = -128;
+ static private final char PAD = '=';
+ static final private byte[] base64Alphabet = new byte[BASELENGTH];
+ static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
+
+ static {
+ for (int i = 0 ; i < BASELENGTH ; ++i) {
+ base64Alphabet[i] = -1;
+ }
+ for (int i = 'Z' ; i >= 'A' ; i--) {
+ base64Alphabet[i] = (byte) (i - 'A');
+ }
+ for (int i = 'z' ; i >= 'a' ; i--) {
+ base64Alphabet[i] = (byte) (i - 'a' + 26);
+ }
+
+ for (int i = '9' ; i >= '0' ; i--) {
+ base64Alphabet[i] = (byte) (i - '0' + 52);
+ }
+
+ base64Alphabet['+'] = 62;
+ base64Alphabet['/'] = 63;
+
+ for (int i = 0 ; i <= 25 ; i++) {
+ lookUpBase64Alphabet[i] = (char) ('A' + i);
+ }
+
+ for (int i = 26, j = 0 ; i <= 51 ; i++, j++) {
+ lookUpBase64Alphabet[i] = (char) ('a' + j);
+ }
+
+ for (int i = 52, j = 0 ; i <= 61 ; i++, j++) {
+ lookUpBase64Alphabet[i] = (char) ('0' + j);
+ }
+ lookUpBase64Alphabet[62] = (char) '+';
+ lookUpBase64Alphabet[63] = (char) '/';
+ }
+
+ private static boolean isWhiteSpace (char octect) {
+ return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
+ }
+
+ private static boolean isPad (char octect) {
+ return (octect == PAD);
+ }
+
+ private static boolean isData (char octect) {
+ return (octect < BASELENGTH && base64Alphabet[octect] != -1);
+ }
+
+ /**
+ * Encodes hex octects into Base64
+ *
+ * @param binaryData Array containing binaryData
+ *
+ * @return Encoded Base64 array
+ */
+ public static String encode (byte[] binaryData) {
+ if (binaryData == null) {
+ return null;
+ }
+
+ int lengthDataBits = binaryData.length * EIGHTBIT;
+ if (lengthDataBits == 0) {
+ return "";
+ }
+
+ int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
+ int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
+ int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
+ char encodedData[] = null;
+
+ encodedData = new char[numberQuartet * 4];
+
+ byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
+
+ int encodedIndex = 0;
+ int dataIndex = 0;
+
+ for (int i = 0 ; i < numberTriplets ; i++) {
+ b1 = binaryData[dataIndex++];
+ b2 = binaryData[dataIndex++];
+ b3 = binaryData[dataIndex++];
+
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+ byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
+ }
+
+ // form integral number of 6-bit groups
+ if (fewerThan24bits == EIGHTBIT) {
+ b1 = binaryData[dataIndex];
+ k = (byte) (b1 & 0x03);
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
+ encodedData[encodedIndex++] = PAD;
+ encodedData[encodedIndex++] = PAD;
+ } else if (fewerThan24bits == SIXTEENBIT) {
+ b1 = binaryData[dataIndex];
+ b2 = binaryData[dataIndex + 1];
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
+ encodedData[encodedIndex++] = PAD;
+ }
+ return new String(encodedData);
+ }
+
+ /**
+ * Decodes Base64 data into octects
+ *
+ * @param encoded string containing Base64 data
+ *
+ * @return Array containind decoded data.
+ */
+ public static byte[] decode (String encoded) {
+ if (encoded == null) {
+ return null;
+ }
+
+ char[] base64Data = encoded.toCharArray();
+ // remove white spaces
+ int len = removeWhiteSpace(base64Data);
+
+ if (len % FOURBYTE != 0) {
+ return null;// should be divisible by four
+ }
+
+ int numberQuadruple = (len / FOURBYTE);
+
+ if (numberQuadruple == 0) {
+ return new byte[0];
+ }
+
+ byte decodedData[] = null;
+ byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
+ char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
+
+ int i = 0;
+ int encodedIndex = 0;
+ int dataIndex = 0;
+ decodedData = new byte[(numberQuadruple) * 3];
+
+ for ( ; i < numberQuadruple - 1 ; i++) {
+
+ if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
+ || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) {
+ return null;
+ } // if found "no data" just return null
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+ }
+
+ if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) {
+ return null;// if found "no data" just return null
+ }
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+
+ d3 = base64Data[dataIndex++];
+ d4 = base64Data[dataIndex++];
+ if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
+ if (isPad(d3) && isPad(d4)) {
+ if ((b2 & 0xf) != 0)// last 4 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 1];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
+ return tmp;
+ } else if (!isPad(d3) && isPad(d4)) {
+ b3 = base64Alphabet[d3];
+ if ((b3 & 0x3) != 0)// last 2 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 2];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ return tmp;
+ } else {
+ return null;
+ }
+ } else { // No PAD e.g 3cQl
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+
+ }
+ return decodedData;
+ }
+
+ /**
+ * remove WhiteSpace from MIME containing encoded Base64 data.
+ *
+ * @param data the byte array of base64 data (with WS)
+ *
+ * @return the new length
+ */
+ private static int removeWhiteSpace (char[] data) {
+ if (data == null) {
+ return 0;
+ }
+
+ // count characters that's not whitespace
+ int newSize = 0;
+ int len = data.length;
+ for (int i = 0 ; i < len ; i++) {
+ if (!isWhiteSpace(data[i])) {
+ data[newSize++] = data[i];
+ }
+ }
+ return newSize;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/sql/SqlUtil.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/sql/SqlUtil.java
new file mode 100644
index 0000000..3f418e7
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/sql/SqlUtil.java
@@ -0,0 +1,59 @@
+package com.muyu.common.core.utils.sql;
+
+import com.muyu.common.core.exception.UtilException;
+import com.muyu.common.core.utils.StringUtils;
+
+/**
+ * sql操作工具类
+ *
+ * @author muyu
+ */
+public class SqlUtil {
+ /**
+ * 限制orderBy最大长度
+ */
+ private static final int ORDER_BY_MAX_LENGTH = 500;
+ /**
+ * 定义常用的 sql关键字
+ */
+ public static String SQL_REGEX = "and |extractvalue|updatexml|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |+|user()";
+ /**
+ * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
+ */
+ public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
+
+ /**
+ * 检查字符,防止注入绕过
+ */
+ public static String escapeOrderBySql (String value) {
+ if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
+ throw new UtilException("参数不符合规范,不能进行查询");
+ }
+ if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH) {
+ throw new UtilException("参数已超过最大限制,不能进行查询");
+ }
+ return value;
+ }
+
+ /**
+ * 验证 order by 语法是否符合规范
+ */
+ public static boolean isValidOrderBySql (String value) {
+ return value.matches(SQL_PATTERN);
+ }
+
+ /**
+ * SQL关键字检查
+ */
+ public static void filterKeyword (String value) {
+ if (StringUtils.isEmpty(value)) {
+ return;
+ }
+ String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
+ for (String sqlKeyword : sqlKeywords) {
+ if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) {
+ throw new UtilException("参数存在SQL注入风险");
+ }
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/IdUtils.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/IdUtils.java
new file mode 100644
index 0000000..375b034
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/IdUtils.java
@@ -0,0 +1,44 @@
+package com.muyu.common.core.utils.uuid;
+
+/**
+ * ID生成器工具类
+ *
+ * @author muyu
+ */
+public class IdUtils {
+ /**
+ * 获取随机UUID
+ *
+ * @return 随机UUID
+ */
+ public static String randomUUID () {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * 简化的UUID,去掉了横线
+ *
+ * @return 简化的UUID,去掉了横线
+ */
+ public static String simpleUUID () {
+ return UUID.randomUUID().toString(true);
+ }
+
+ /**
+ * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID
+ *
+ * @return 随机UUID
+ */
+ public static String fastUUID () {
+ return UUID.fastUUID().toString();
+ }
+
+ /**
+ * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID
+ *
+ * @return 简化的UUID,去掉了横线
+ */
+ public static String fastSimpleUUID () {
+ return UUID.fastUUID().toString(true);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/Seq.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/Seq.java
new file mode 100644
index 0000000..b72e3dd
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/Seq.java
@@ -0,0 +1,78 @@
+package com.muyu.common.core.utils.uuid;
+
+import com.muyu.common.core.utils.DateUtils;
+import com.muyu.common.core.utils.StringUtils;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author muyu 序列生成类
+ */
+public class Seq {
+ // 通用序列类型
+ public static final String commSeqType = "COMMON";
+
+ // 上传序列类型
+ public static final String uploadSeqType = "UPLOAD";
+ // 机器标识
+ private static final String machineCode = "A";
+ // 通用接口序列数
+ private static AtomicInteger commSeq = new AtomicInteger(1);
+ // 上传接口序列数
+ private static AtomicInteger uploadSeq = new AtomicInteger(1);
+
+ /**
+ * 获取通用序列号
+ *
+ * @return 序列值
+ */
+ public static String getId () {
+ return getId(commSeqType);
+ }
+
+ /**
+ * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串
+ *
+ * @return 序列值
+ */
+ public static String getId (String type) {
+ AtomicInteger atomicInt = commSeq;
+ if (uploadSeqType.equals(type)) {
+ atomicInt = uploadSeq;
+ }
+ return getId(atomicInt, 3);
+ }
+
+ /**
+ * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串
+ *
+ * @param atomicInt 序列数
+ * @param length 数值长度
+ *
+ * @return 序列值
+ */
+ public static String getId (AtomicInteger atomicInt, int length) {
+ String result = DateUtils.dateTimeNow();
+ result += machineCode;
+ result += getSeq(atomicInt, length);
+ return result;
+ }
+
+ /**
+ * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数
+ *
+ * @return 序列值
+ */
+ private synchronized static String getSeq (AtomicInteger atomicInt, int length) {
+ // 先取值再+1
+ int value = atomicInt.getAndIncrement();
+
+ // 如果更新后值>=10 的 (length)幂次方则重置为1
+ int maxSeq = (int) Math.pow(10, length);
+ if (atomicInt.get() >= maxSeq) {
+ atomicInt.set(1);
+ }
+ // 转字符串,用0左补齐
+ return StringUtils.padl(value, length);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/UUID.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/UUID.java
new file mode 100644
index 0000000..37c3c9e
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/utils/uuid/UUID.java
@@ -0,0 +1,450 @@
+package com.muyu.common.core.utils.uuid;
+
+import com.muyu.common.core.exception.UtilException;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 提供通用唯一识别码(universally unique identifier)(UUID)实现
+ *
+ * @author muyu
+ */
+public final class UUID implements java.io.Serializable, Comparable {
+ private static final long serialVersionUID = -1185015143654744140L;
+ /**
+ * 此UUID的最高64有效位
+ */
+ private final long mostSigBits;
+ /**
+ * 此UUID的最低64有效位
+ */
+ private final long leastSigBits;
+
+ /**
+ * 私有构造
+ *
+ * @param data 数据
+ */
+ private UUID (byte[] data) {
+ long msb = 0;
+ long lsb = 0;
+ assert data.length == 16 : "data must be 16 bytes in length";
+ for (int i = 0 ; i < 8 ; i++) {
+ msb = (msb << 8) | (data[i] & 0xff);
+ }
+ for (int i = 8 ; i < 16 ; i++) {
+ lsb = (lsb << 8) | (data[i] & 0xff);
+ }
+ this.mostSigBits = msb;
+ this.leastSigBits = lsb;
+ }
+
+ /**
+ * 使用指定的数据构造新的 UUID。
+ *
+ * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位
+ * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位
+ */
+ public UUID (long mostSigBits, long leastSigBits) {
+ this.mostSigBits = mostSigBits;
+ this.leastSigBits = leastSigBits;
+ }
+
+ /**
+ * 获取类型 4(伪随机生成的)UUID 的静态工厂。
+ *
+ * @return 随机生成的 {@code UUID}
+ */
+ public static UUID fastUUID () {
+ return randomUUID(false);
+ }
+
+ /**
+ * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+ *
+ * @return 随机生成的 {@code UUID}
+ */
+ public static UUID randomUUID () {
+ return randomUUID(true);
+ }
+
+ /**
+ * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+ *
+ * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能
+ *
+ * @return 随机生成的 {@code UUID}
+ */
+ public static UUID randomUUID (boolean isSecure) {
+ final Random ng = isSecure ? Holder.numberGenerator : getRandom();
+
+ byte[] randomBytes = new byte[16];
+ ng.nextBytes(randomBytes);
+ randomBytes[6] &= 0x0f; /* clear version */
+ randomBytes[6] |= 0x40; /* set to version 4 */
+ randomBytes[8] &= 0x3f; /* clear variant */
+ randomBytes[8] |= 0x80; /* set to IETF variant */
+ return new UUID(randomBytes);
+ }
+
+ /**
+ * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。
+ *
+ * @param name 用于构造 UUID 的字节数组。
+ *
+ * @return 根据指定数组生成的 {@code UUID}
+ */
+ public static UUID nameUUIDFromBytes (byte[] name) {
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException nsae) {
+ throw new InternalError("MD5 not supported");
+ }
+ byte[] md5Bytes = md.digest(name);
+ md5Bytes[6] &= 0x0f; /* clear version */
+ md5Bytes[6] |= 0x30; /* set to version 3 */
+ md5Bytes[8] &= 0x3f; /* clear variant */
+ md5Bytes[8] |= 0x80; /* set to IETF variant */
+ return new UUID(md5Bytes);
+ }
+
+ /**
+ * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。
+ *
+ * @param name 指定 {@code UUID} 字符串
+ *
+ * @return 具有指定值的 {@code UUID}
+ *
+ * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常
+ */
+ public static UUID fromString (String name) {
+ String[] components = name.split("-");
+ if (components.length != 5) {
+ throw new IllegalArgumentException("Invalid UUID string: " + name);
+ }
+ for (int i = 0 ; i < 5 ; i++) {
+ components[i] = "0x" + components[i];
+ }
+
+ long mostSigBits = Long.decode(components[0]).longValue();
+ mostSigBits <<= 16;
+ mostSigBits |= Long.decode(components[1]).longValue();
+ mostSigBits <<= 16;
+ mostSigBits |= Long.decode(components[2]).longValue();
+
+ long leastSigBits = Long.decode(components[3]).longValue();
+ leastSigBits <<= 48;
+ leastSigBits |= Long.decode(components[4]).longValue();
+
+ return new UUID(mostSigBits, leastSigBits);
+ }
+
+ /**
+ * 返回指定数字对应的hex值
+ *
+ * @param val 值
+ * @param digits 位
+ *
+ * @return 值
+ */
+ private static String digits (long val, int digits) {
+ long hi = 1L << (digits * 4);
+ return Long.toHexString(hi | (val & (hi - 1))).substring(1);
+ }
+
+ /**
+ * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+ *
+ * @return {@link SecureRandom}
+ */
+ public static SecureRandom getSecureRandom () {
+ try {
+ return SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new UtilException(e);
+ }
+ }
+
+ /**
+ * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。
+ *
+ * @return {@link ThreadLocalRandom}
+ */
+ public static ThreadLocalRandom getRandom () {
+ return ThreadLocalRandom.current();
+ }
+
+ /**
+ * 返回此 UUID 的 128 位值中的最低有效 64 位。
+ *
+ * @return 此 UUID 的 128 位值中的最低有效 64 位。
+ */
+ public long getLeastSignificantBits () {
+ return leastSigBits;
+ }
+
+ /**
+ * 返回此 UUID 的 128 位值中的最高有效 64 位。
+ *
+ * @return 此 UUID 的 128 位值中最高有效 64 位。
+ */
+ public long getMostSignificantBits () {
+ return mostSigBits;
+ }
+
+ /**
+ * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。
+ *
+ * 版本号具有以下含意:
+ *
+ * 1 基于时间的 UUID
+ * 2 DCE 安全 UUID
+ * 3 基于名称的 UUID
+ * 4 随机生成的 UUID
+ *
+ *
+ * @return 此 {@code UUID} 的版本号
+ */
+ public int version () {
+ // Version is bits masked by 0x000000000000F000 in MS long
+ return (int) ((mostSigBits >> 12) & 0x0f);
+ }
+
+ /**
+ * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。
+ *
+ * 变体号具有以下含意:
+ *
+ * 0 为 NCS 向后兼容保留
+ * 2 IETF RFC 4122 (Leach-Salz), 用于此类
+ * 6 保留,微软向后兼容
+ * 7 保留供以后定义使用
+ *
+ *
+ * @return 此 {@code UUID} 相关联的变体号
+ */
+ public int variant () {
+ // This field is composed of a varying number of bits.
+ // 0 - - Reserved for NCS backward compatibility
+ // 1 0 - The IETF aka Leach-Salz variant (used by this class)
+ // 1 1 0 Reserved, Microsoft backward compatibility
+ // 1 1 1 Reserved for future definition.
+ return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63));
+ }
+
+ /**
+ * 与此 UUID 相关联的时间戳值。
+ *
+ *
+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。
+ *
+ *
+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+ *
+ * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。
+ */
+ public long timestamp () throws UnsupportedOperationException {
+ checkTimeBase();
+ return (mostSigBits & 0x0FFFL) << 48//
+ | ((mostSigBits >> 16) & 0x0FFFFL) << 32//
+ | mostSigBits >>> 32;
+ }
+
+ /**
+ * 与此 UUID 相关联的时钟序列值。
+ *
+ *
+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。
+ *
+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出
+ * UnsupportedOperationException。
+ *
+ * @return 此 {@code UUID} 的时钟序列
+ *
+ * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+ */
+ public int clockSequence () throws UnsupportedOperationException {
+ checkTimeBase();
+ return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48);
+ }
+
+ /**
+ * 与此 UUID 相关的节点值。
+ *
+ *
+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。
+ *
+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+ *
+ * @return 此 {@code UUID} 的节点值
+ *
+ * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+ */
+ public long node () throws UnsupportedOperationException {
+ checkTimeBase();
+ return leastSigBits & 0x0000FFFFFFFFFFFFL;
+ }
+
+ /**
+ * 返回此{@code UUID} 的字符串表现形式。
+ *
+ *
+ * UUID 的字符串表示形式由此 BNF 描述:
+ *
+ *
+ * {@code
+ * UUID = ----
+ * time_low = 4*
+ * time_mid = 2*
+ * time_high_and_version = 2*
+ * variant_and_sequence = 2*
+ * node = 6*
+ * hexOctet =
+ * hexDigit = [0-9a-fA-F]
+ * }
+ *
+ *
+ *
+ *
+ * @return 此{@code UUID} 的字符串表现形式
+ *
+ * @see #toString(boolean)
+ */
+ @Override
+ public String toString () {
+ return toString(false);
+ }
+
+ /**
+ * 返回此{@code UUID} 的字符串表现形式。
+ *
+ *
+ * UUID 的字符串表示形式由此 BNF 描述:
+ *
+ *
+ * {@code
+ * UUID = ----
+ * time_low = 4*
+ * time_mid = 2*
+ * time_high_and_version = 2*
+ * variant_and_sequence = 2*
+ * node = 6*
+ * hexOctet =
+ * hexDigit = [0-9a-fA-F]
+ * }
+ *
+ *
+ *
+ *
+ * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串
+ *
+ * @return 此{@code UUID} 的字符串表现形式
+ */
+ public String toString (boolean isSimple) {
+ final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36);
+ // time_low
+ builder.append(digits(mostSigBits >> 32, 8));
+ if (false == isSimple) {
+ builder.append('-');
+ }
+ // time_mid
+ builder.append(digits(mostSigBits >> 16, 4));
+ if (false == isSimple) {
+ builder.append('-');
+ }
+ // time_high_and_version
+ builder.append(digits(mostSigBits, 4));
+ if (false == isSimple) {
+ builder.append('-');
+ }
+ // variant_and_sequence
+ builder.append(digits(leastSigBits >> 48, 4));
+ if (false == isSimple) {
+ builder.append('-');
+ }
+ // node
+ builder.append(digits(leastSigBits, 12));
+
+ return builder.toString();
+ }
+
+ // Comparison Operations
+
+ /**
+ * 返回此 UUID 的哈希码。
+ *
+ * @return UUID 的哈希码值。
+ */
+ @Override
+ public int hashCode () {
+ long hilo = mostSigBits ^ leastSigBits;
+ return ((int) (hilo >> 32)) ^ (int) hilo;
+ }
+
+ // -------------------------------------------------------------------------------------------------------------------
+ // Private method start
+
+ /**
+ * 将此对象与指定对象比较。
+ *
+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。
+ *
+ * @param obj 要与之比较的对象
+ *
+ * @return 如果对象相同,则返回 {@code true};否则返回 {@code false}
+ */
+ @Override
+ public boolean equals (Object obj) {
+ if ((null == obj) || (obj.getClass() != UUID.class)) {
+ return false;
+ }
+ UUID id = (UUID) obj;
+ return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits);
+ }
+
+ /**
+ * 将此 UUID 与指定的 UUID 比较。
+ *
+ *
+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。
+ *
+ * @param val 与此 UUID 比较的 UUID
+ *
+ * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。
+ */
+ @Override
+ public int compareTo (UUID val) {
+ // The ordering is intentionally set up so that the UUIDs
+ // can simply be numerically compared as two numbers
+ return (this.mostSigBits < val.mostSigBits ? -1 : //
+ (this.mostSigBits > val.mostSigBits ? 1 : //
+ (this.leastSigBits < val.leastSigBits ? -1 : //
+ (this.leastSigBits > val.leastSigBits ? 1 : //
+ 0))));
+ }
+
+ /**
+ * 检查是否为time-based版本UUID
+ */
+ private void checkTimeBase () {
+ if (version() != 1) {
+ throw new UnsupportedOperationException("Not a time-based UUID");
+ }
+ }
+
+ /**
+ * SecureRandom 的单例
+ */
+ private static class Holder {
+ static final SecureRandom numberGenerator = getSecureRandom();
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/controller/BaseController.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/controller/BaseController.java
new file mode 100644
index 0000000..3789a82
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/controller/BaseController.java
@@ -0,0 +1,129 @@
+package com.muyu.common.core.web.controller;
+
+import com.github.pagehelper.PageInfo;
+import com.muyu.common.core.utils.DateUtils;
+import com.muyu.common.core.utils.PageUtils;
+import com.muyu.common.core.domain.Result;
+import com.muyu.common.core.web.page.TableDataInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.annotation.InitBinder;
+
+import java.beans.PropertyEditorSupport;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * web层通用数据处理
+ *
+ * @author muyu
+ */
+public class BaseController {
+ protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /**
+ * 将前台传递过来的日期格式的字符串,自动转化为Date类型
+ */
+ @InitBinder
+ public void initBinder (WebDataBinder binder) {
+ // Date 类型转换
+ binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
+ @Override
+ public void setAsText (String text) {
+ setValue(DateUtils.parseDate(text));
+ }
+ });
+ }
+
+ /**
+ * 设置请求分页数据
+ */
+ protected void startPage () {
+ PageUtils.startPage();
+ }
+
+ /**
+ * 清理分页的线程变量
+ */
+ protected void clearPage () {
+ PageUtils.clearPage();
+ }
+
+ /**
+ * 响应请求分页数据
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ protected Result> getDataTable (List list) {
+ return Result.success(
+ TableDataInfo.builder()
+ .total(new PageInfo(list).getTotal())
+ .rows(list)
+ .build()
+ );
+ }
+
+ /**
+ * 返回成功
+ */
+ public Result success () {
+ return Result.success();
+ }
+
+ /**
+ * 返回成功消息
+ */
+ public Result success (String message) {
+ return Result.success(message);
+ }
+
+ /**
+ * 返回成功消息
+ */
+ public Result success (Object data) {
+ return Result.success(data);
+ }
+
+ /**
+ * 返回失败消息
+ */
+ public Result error () {
+ return Result.error();
+ }
+
+ /**
+ * 返回失败消息
+ */
+ public Result error (String message) {
+ return Result.error(message);
+ }
+
+ /**
+ * 返回警告消息
+ */
+ public Result warn (String message) {
+ return Result.warn(message);
+ }
+
+ /**
+ * 响应返回结果
+ *
+ * @param rows 影响行数
+ *
+ * @return 操作结果
+ */
+ protected Result toAjax (int rows) {
+ return rows > 0 ? Result.success() : Result.error();
+ }
+
+ /**
+ * 响应返回结果
+ *
+ * @param result 结果
+ *
+ * @return 操作结果
+ */
+ protected Result toAjax (boolean result) {
+ return result ? success() : error();
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/domain/BaseEntity.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/domain/BaseEntity.java
new file mode 100644
index 0000000..add8e3e
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/domain/BaseEntity.java
@@ -0,0 +1,80 @@
+package com.muyu.common.core.web.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Entity基类
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BaseEntity implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 搜索值
+ */
+ @JsonIgnore
+ @TableField(exist = false)
+ private String searchValue;
+
+ /**
+ * 创建者
+ */
+ private String createBy;
+
+ /**
+ * 创建时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ /**
+ * 更新者
+ */
+ private String updateBy;
+
+ /**
+ * 更新时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date updateTime;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 请求参数
+ */
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @TableField(exist = false)
+ private Map params;
+
+ public Map getParams () {
+ if (params == null) {
+ params = new HashMap<>();
+ }
+ return params;
+ }
+
+ public void setParams (Map params) {
+ this.params = params;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/domain/TreeEntity.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/domain/TreeEntity.java
new file mode 100644
index 0000000..427fcc7
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/domain/TreeEntity.java
@@ -0,0 +1,89 @@
+package com.muyu.common.core.web.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tree基类
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class TreeEntity extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 父菜单名称
+ */
+ private String parentName;
+
+ /**
+ * 父菜单ID
+ */
+ private Long parentId;
+
+ /**
+ * 显示顺序
+ */
+ private Integer orderNum;
+
+ /**
+ * 祖级列表
+ */
+ private String ancestors;
+
+ /**
+ * 子部门
+ */
+ private List> children = new ArrayList<>();
+
+ public String getParentName () {
+ return parentName;
+ }
+
+ public void setParentName (String parentName) {
+ this.parentName = parentName;
+ }
+
+ public Long getParentId () {
+ return parentId;
+ }
+
+ public void setParentId (Long parentId) {
+ this.parentId = parentId;
+ }
+
+ public Integer getOrderNum () {
+ return orderNum;
+ }
+
+ public void setOrderNum (Integer orderNum) {
+ this.orderNum = orderNum;
+ }
+
+ public String getAncestors () {
+ return ancestors;
+ }
+
+ public void setAncestors (String ancestors) {
+ this.ancestors = ancestors;
+ }
+
+ public List> getChildren () {
+ return children;
+ }
+
+ public void setChildren (List> children) {
+ this.children = children;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/PageDomain.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/PageDomain.java
new file mode 100644
index 0000000..b9c5e45
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/PageDomain.java
@@ -0,0 +1,93 @@
+package com.muyu.common.core.web.page;
+
+import com.muyu.common.core.utils.StringUtils;
+
+/**
+ * 分页数据
+ *
+ * @author muyu
+ */
+public class PageDomain {
+ /**
+ * 当前记录起始索引
+ */
+ private Integer pageNum;
+
+ /**
+ * 每页显示记录数
+ */
+ private Integer pageSize;
+
+ /**
+ * 排序列
+ */
+ private String orderByColumn;
+
+ /**
+ * 排序的方向desc或者asc
+ */
+ private String isAsc = "asc";
+
+ /**
+ * 分页参数合理化
+ */
+ private Boolean reasonable = true;
+
+ public String getOrderBy () {
+ if (StringUtils.isEmpty(orderByColumn)) {
+ return "";
+ }
+ return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc;
+ }
+
+ public Integer getPageNum () {
+ return pageNum;
+ }
+
+ public void setPageNum (Integer pageNum) {
+ this.pageNum = pageNum;
+ }
+
+ public Integer getPageSize () {
+ return pageSize;
+ }
+
+ public void setPageSize (Integer pageSize) {
+ this.pageSize = pageSize;
+ }
+
+ public String getOrderByColumn () {
+ return orderByColumn;
+ }
+
+ public void setOrderByColumn (String orderByColumn) {
+ this.orderByColumn = orderByColumn;
+ }
+
+ public String getIsAsc () {
+ return isAsc;
+ }
+
+ public void setIsAsc (String isAsc) {
+ if (StringUtils.isNotEmpty(isAsc)) {
+ // 兼容前端排序类型
+ if ("ascending".equals(isAsc)) {
+ isAsc = "asc";
+ } else if ("descending".equals(isAsc)) {
+ isAsc = "desc";
+ }
+ this.isAsc = isAsc;
+ }
+ }
+
+ public Boolean getReasonable () {
+ if (StringUtils.isNull(reasonable)) {
+ return Boolean.TRUE;
+ }
+ return reasonable;
+ }
+
+ public void setReasonable (Boolean reasonable) {
+ this.reasonable = reasonable;
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/TableDataInfo.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/TableDataInfo.java
new file mode 100644
index 0000000..d677cce
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/TableDataInfo.java
@@ -0,0 +1,45 @@
+package com.muyu.common.core.web.page;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.poi.ss.formula.functions.T;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 表格分页数据对象
+ *
+ * @author muyu
+ */
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TableDataInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 总记录数
+ */
+ private long total;
+
+ /**
+ * 列表数据
+ */
+ private List rows;
+
+ /**
+ * 消息状态码
+ */
+ private int code;
+
+ /**
+ * 消息内容
+ */
+ private String msg;
+
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/TableSupport.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/TableSupport.java
new file mode 100644
index 0000000..ce0328d
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/web/page/TableSupport.java
@@ -0,0 +1,53 @@
+package com.muyu.common.core.web.page;
+
+import com.muyu.common.core.text.Convert;
+import com.muyu.common.core.utils.ServletUtils;
+
+/**
+ * 表格数据处理
+ *
+ * @author muyu
+ */
+public class TableSupport {
+ /**
+ * 当前记录起始索引
+ */
+ public static final String PAGE_NUM = "pageNum";
+
+ /**
+ * 每页显示记录数
+ */
+ public static final String PAGE_SIZE = "pageSize";
+
+ /**
+ * 排序列
+ */
+ public static final String ORDER_BY_COLUMN = "orderByColumn";
+
+ /**
+ * 排序的方向 "desc" 或者 "asc".
+ */
+ public static final String IS_ASC = "isAsc";
+
+ /**
+ * 分页参数合理化
+ */
+ public static final String REASONABLE = "reasonable";
+
+ /**
+ * 封装分页对象
+ */
+ public static PageDomain getPageDomain () {
+ PageDomain pageDomain = new PageDomain();
+ pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));
+ pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));
+ pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
+ pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
+ pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
+ return pageDomain;
+ }
+
+ public static PageDomain buildPageRequest () {
+ return getPageDomain();
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/xss/Xss.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/xss/Xss.java
new file mode 100644
index 0000000..7eb649b
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/xss/Xss.java
@@ -0,0 +1,26 @@
+package com.muyu.common.core.xss;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义xss校验注解
+ *
+ * @author muyu
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
+@Constraint(validatedBy = {XssValidator.class})
+public @interface Xss {
+ String message ()
+
+ default "不允许任何脚本运行";
+
+ Class>[] groups () default {};
+
+ Class extends Payload>[] payload () default {};
+}
diff --git a/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/xss/XssValidator.java b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/xss/XssValidator.java
new file mode 100644
index 0000000..5a59932
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/java/com/muyu/common/core/xss/XssValidator.java
@@ -0,0 +1,31 @@
+package com.muyu.common.core.xss;
+
+import com.muyu.common.core.utils.StringUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 自定义xss校验注解实现
+ *
+ * @author muyu
+ */
+public class XssValidator implements ConstraintValidator {
+ private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />";
+
+ public static boolean containsHtml (String value) {
+ Pattern pattern = Pattern.compile(HTML_PATTERN);
+ Matcher matcher = pattern.matcher(value);
+ return matcher.matches();
+ }
+
+ @Override
+ public boolean isValid (String value, ConstraintValidatorContext constraintValidatorContext) {
+ if (StringUtils.isBlank(value)) {
+ return true;
+ }
+ return !containsHtml(value);
+ }
+}
diff --git a/muyu-common/muyu-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/muyu-common/muyu-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..526ea23
--- /dev/null
+++ b/muyu-common/muyu-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.muyu.common.core.utils.SpringUtils
diff --git a/muyu-common/muyu-common-datascope/pom.xml b/muyu-common/muyu-common-datascope/pom.xml
new file mode 100644
index 0000000..079f752
--- /dev/null
+++ b/muyu-common/muyu-common-datascope/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+ 4.0.0
+
+ muyu-common-datascope
+
+
+ muyu-common-datascope权限范围
+
+
+
+
+
+
+ com.muyu
+ muyu-common-security
+
+
+
+
diff --git a/muyu-common/muyu-common-datascope/src/main/java/com/muyu/common/datascope/annotation/DataScope.java b/muyu-common/muyu-common-datascope/src/main/java/com/muyu/common/datascope/annotation/DataScope.java
new file mode 100644
index 0000000..498f06b
--- /dev/null
+++ b/muyu-common/muyu-common-datascope/src/main/java/com/muyu/common/datascope/annotation/DataScope.java
@@ -0,0 +1,28 @@
+package com.muyu.common.datascope.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限过滤注解
+ *
+ * @author muyu
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataScope {
+ /**
+ * 部门表的别名
+ */
+ public String deptAlias () default "";
+
+ /**
+ * 用户表的别名
+ */
+ public String userAlias () default "";
+
+ /**
+ * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@RequiresPermissions获取,多个权限用逗号分隔开来
+ */
+ public String permission () default "";
+}
diff --git a/muyu-common/muyu-common-datascope/src/main/java/com/muyu/common/datascope/aspect/DataScopeAspect.java b/muyu-common/muyu-common-datascope/src/main/java/com/muyu/common/datascope/aspect/DataScopeAspect.java
new file mode 100644
index 0000000..3d66b0d
--- /dev/null
+++ b/muyu-common/muyu-common-datascope/src/main/java/com/muyu/common/datascope/aspect/DataScopeAspect.java
@@ -0,0 +1,149 @@
+package com.muyu.common.datascope.aspect;
+
+import com.muyu.common.core.context.SecurityContextHolder;
+import com.muyu.common.core.text.Convert;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.core.web.domain.BaseEntity;
+import com.muyu.common.datascope.annotation.DataScope;
+import com.muyu.common.security.utils.SecurityUtils;
+import com.muyu.common.system.domain.SysRole;
+import com.muyu.common.system.domain.SysUser;
+import com.muyu.common.system.domain.LoginUser;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 数据过滤处理
+ *
+ * @author muyu
+ */
+@Aspect
+@Component
+public class DataScopeAspect {
+ /**
+ * 全部数据权限
+ */
+ public static final String DATA_SCOPE_ALL = "1";
+
+ /**
+ * 自定数据权限
+ */
+ public static final String DATA_SCOPE_CUSTOM = "2";
+
+ /**
+ * 部门数据权限
+ */
+ public static final String DATA_SCOPE_DEPT = "3";
+
+ /**
+ * 部门及以下数据权限
+ */
+ public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+ /**
+ * 仅本人数据权限
+ */
+ public static final String DATA_SCOPE_SELF = "5";
+
+ /**
+ * 数据权限过滤关键字
+ */
+ public static final String DATA_SCOPE = "dataScope";
+
+ /**
+ * 数据范围过滤
+ *
+ * @param joinPoint 切点
+ * @param user 用户
+ * @param deptAlias 部门别名
+ * @param userAlias 用户别名
+ * @param permission 权限字符
+ */
+ public static void dataScopeFilter (JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) {
+ StringBuilder sqlString = new StringBuilder();
+ List conditions = new ArrayList();
+
+ for (SysRole role : user.getRoles()) {
+ String dataScope = role.getDataScope();
+ if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) {
+ continue;
+ }
+ if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
+ && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) {
+ continue;
+ }
+ if (DATA_SCOPE_ALL.equals(dataScope)) {
+ sqlString = new StringBuilder();
+ conditions.add(dataScope);
+ break;
+ } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
+ sqlString.append(StringUtils.format(
+ " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
+ role.getRoleId()));
+ } else if (DATA_SCOPE_DEPT.equals(dataScope)) {
+ sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+ } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
+ sqlString.append(StringUtils.format(
+ " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+ deptAlias, user.getDeptId(), user.getDeptId()));
+ } else if (DATA_SCOPE_SELF.equals(dataScope)) {
+ if (StringUtils.isNotBlank(userAlias)) {
+ sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+ } else {
+ // 数据权限为仅本人且没有userAlias别名不查询任何数据
+ sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
+ }
+ }
+ conditions.add(dataScope);
+ }
+
+ // 多角色情况下,所有角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据
+ if (StringUtils.isEmpty(conditions)) {
+ sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
+ }
+
+ if (StringUtils.isNotBlank(sqlString.toString())) {
+ Object params = joinPoint.getArgs()[0];
+ if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
+ BaseEntity baseEntity = (BaseEntity) params;
+ baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+ }
+ }
+ }
+
+ @Before("@annotation(controllerDataScope)")
+ public void doBefore (JoinPoint point, DataScope controllerDataScope) throws Throwable {
+ clearDataScope(point);
+ handleDataScope(point, controllerDataScope);
+ }
+
+ protected void handleDataScope (final JoinPoint joinPoint, DataScope controllerDataScope) {
+ // 获取当前的用户
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNotNull(loginUser)) {
+ SysUser currentUser = loginUser.getSysUser();
+ // 如果是超级管理员,则不过滤数据
+ if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) {
+ String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), SecurityContextHolder.getPermission());
+ dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
+ controllerDataScope.userAlias(), permission);
+ }
+ }
+ }
+
+ /**
+ * 拼接权限sql前先清空params.dataScope参数防止注入
+ */
+ private void clearDataScope (final JoinPoint joinPoint) {
+ Object params = joinPoint.getArgs()[0];
+ if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
+ BaseEntity baseEntity = (BaseEntity) params;
+ baseEntity.getParams().put(DATA_SCOPE, "");
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-datascope/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/muyu-common/muyu-common-datascope/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..8f4967b
--- /dev/null
+++ b/muyu-common/muyu-common-datascope/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.muyu.common.datascope.aspect.DataScopeAspect
diff --git a/muyu-common/muyu-common-datasource/pom.xml b/muyu-common/muyu-common-datasource/pom.xml
new file mode 100644
index 0000000..994cf84
--- /dev/null
+++ b/muyu-common/muyu-common-datasource/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+ 4.0.0
+
+ muyu-common-datasource
+
+
+ muyu-common-datasource多数据源
+
+
+
+
+
+
+ com.alibaba
+ druid-spring-boot-starter
+ ${druid.version}
+
+
+
+
+ com.baomidou
+ dynamic-datasource-spring-boot-starter
+ ${dynamic-ds.version}
+
+
+
+
diff --git a/muyu-common/muyu-common-datasource/src/main/java/com/muyu/common/datasource/annotation/Master.java b/muyu-common/muyu-common-datasource/src/main/java/com/muyu/common/datasource/annotation/Master.java
new file mode 100644
index 0000000..d9aae00
--- /dev/null
+++ b/muyu-common/muyu-common-datasource/src/main/java/com/muyu/common/datasource/annotation/Master.java
@@ -0,0 +1,18 @@
+package com.muyu.common.datasource.annotation;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+
+import java.lang.annotation.*;
+
+/**
+ * 主库数据源
+ *
+ * @author muyu
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@DS("master")
+public @interface Master {
+
+}
diff --git a/muyu-common/muyu-common-datasource/src/main/java/com/muyu/common/datasource/annotation/Slave.java b/muyu-common/muyu-common-datasource/src/main/java/com/muyu/common/datasource/annotation/Slave.java
new file mode 100644
index 0000000..9663cd4
--- /dev/null
+++ b/muyu-common/muyu-common-datasource/src/main/java/com/muyu/common/datasource/annotation/Slave.java
@@ -0,0 +1,18 @@
+package com.muyu.common.datasource.annotation;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+
+import java.lang.annotation.*;
+
+/**
+ * 从库数据源
+ *
+ * @author muyu
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@DS("slave")
+public @interface Slave {
+
+}
diff --git a/muyu-common/muyu-common-log/pom.xml b/muyu-common/muyu-common-log/pom.xml
new file mode 100644
index 0000000..3bf544e
--- /dev/null
+++ b/muyu-common/muyu-common-log/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+ 4.0.0
+
+ muyu-common-log
+
+
+ muyu-common-log日志记录
+
+
+
+
+
+
+ com.muyu
+ muyu-common-security
+
+
+
+
diff --git a/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/annotation/Log.java b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/annotation/Log.java
new file mode 100644
index 0000000..ac6394e
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/annotation/Log.java
@@ -0,0 +1,46 @@
+package com.muyu.common.log.annotation;
+
+import com.muyu.common.log.enums.BusinessType;
+import com.muyu.common.log.enums.OperatorType;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义操作日志记录注解
+ *
+ * @author muyu
+ */
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log {
+ /**
+ * 模块
+ */
+ public String title () default "";
+
+ /**
+ * 功能
+ */
+ public BusinessType businessType () default BusinessType.OTHER;
+
+ /**
+ * 操作人类别
+ */
+ public OperatorType operatorType () default OperatorType.MANAGE;
+
+ /**
+ * 是否保存请求的参数
+ */
+ public boolean isSaveRequestData () default true;
+
+ /**
+ * 是否保存响应的参数
+ */
+ public boolean isSaveResponseData () default true;
+
+ /**
+ * 排除指定的请求参数
+ */
+ public String[] excludeParamNames () default {};
+}
diff --git a/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/aspect/LogAspect.java b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/aspect/LogAspect.java
new file mode 100644
index 0000000..4ecefc5
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/aspect/LogAspect.java
@@ -0,0 +1,220 @@
+package com.muyu.common.log.aspect;
+
+import com.alibaba.fastjson2.JSON;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.core.utils.ip.IpUtils;
+import com.muyu.common.log.annotation.Log;
+import com.muyu.common.log.enums.BusinessStatus;
+import com.muyu.common.log.filter.PropertyPreExcludeFilter;
+import com.muyu.common.log.service.AsyncLogService;
+import com.muyu.common.security.utils.SecurityUtils;
+import com.muyu.common.system.domain.SysOperLog;
+import org.apache.commons.lang3.ArrayUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.NamedThreadLocal;
+import org.springframework.http.HttpMethod;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ *
+ * @author muyu
+ */
+@Aspect
+@Component
+public class LogAspect {
+ /**
+ * 排除敏感属性字段
+ */
+ public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};
+ private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+ /**
+ * 计算操作消耗时间
+ */
+ private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal("Cost Time");
+
+ @Autowired
+ private AsyncLogService asyncLogService;
+
+ /**
+ * 处理请求前执行
+ */
+ @Before(value = "@annotation(controllerLog)")
+ public void boBefore (JoinPoint joinPoint, Log controllerLog) {
+ TIME_THREADLOCAL.set(System.currentTimeMillis());
+ }
+
+ /**
+ * 处理完请求后执行
+ *
+ * @param joinPoint 切点
+ */
+ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
+ public void doAfterReturning (JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
+ handleLog(joinPoint, controllerLog, null, jsonResult);
+ }
+
+ /**
+ * 拦截异常操作
+ *
+ * @param joinPoint 切点
+ * @param e 异常
+ */
+ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
+ public void doAfterThrowing (JoinPoint joinPoint, Log controllerLog, Exception e) {
+ handleLog(joinPoint, controllerLog, e, null);
+ }
+
+ protected void handleLog (final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
+ try {
+ // *========数据库日志=========*//
+ SysOperLog operLog = new SysOperLog();
+ operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+ // 请求的地址
+ String ip = IpUtils.getIpAddr();
+ operLog.setOperIp(ip);
+ operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
+ String username = SecurityUtils.getUsername();
+ if (StringUtils.isNotBlank(username)) {
+ operLog.setOperName(username);
+ }
+
+ if (e != null) {
+ operLog.setStatus(BusinessStatus.FAIL.ordinal());
+ operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+ }
+ // 设置方法名称
+ String className = joinPoint.getTarget().getClass().getName();
+ String methodName = joinPoint.getSignature().getName();
+ operLog.setMethod(className + "." + methodName + "()");
+ // 设置请求方式
+ operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+ // 处理设置注解上的参数
+ getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
+ // 设置消耗时间
+ operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
+ // 保存数据库
+ asyncLogService.saveSysLog(operLog);
+ } catch (Exception exp) {
+ // 记录本地异常日志
+ log.error("异常信息:{}", exp.getMessage());
+ exp.printStackTrace();
+ } finally {
+ TIME_THREADLOCAL.remove();
+ }
+ }
+
+ /**
+ * 获取注解中对方法的描述信息 用于Controller层注解
+ *
+ * @param log 日志
+ * @param operLog 操作日志
+ *
+ * @throws Exception
+ */
+ public void getControllerMethodDescription (JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception {
+ // 设置action动作
+ operLog.setBusinessType(log.businessType().ordinal());
+ // 设置标题
+ operLog.setTitle(log.title());
+ // 设置操作人类别
+ operLog.setOperatorType(log.operatorType().ordinal());
+ // 是否需要保存request,参数和值
+ if (log.isSaveRequestData()) {
+ // 获取参数的信息,传入到数据库中。
+ setRequestValue(joinPoint, operLog, log.excludeParamNames());
+ }
+ // 是否需要保存response,参数和值
+ if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
+ operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
+ }
+ }
+
+ /**
+ * 获取请求的参数,放到log中
+ *
+ * @param operLog 操作日志
+ *
+ * @throws Exception 异常
+ */
+ private void setRequestValue (JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception {
+ String requestMethod = operLog.getRequestMethod();
+ Map, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
+ if (StringUtils.isEmpty(paramsMap)
+ && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) {
+ String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
+ operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+ } else {
+ operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
+ }
+ }
+
+ /**
+ * 参数拼装
+ */
+ private String argsArrayToString (Object[] paramsArray, String[] excludeParamNames) {
+ String params = "";
+ if (paramsArray != null && paramsArray.length > 0) {
+ for (Object o : paramsArray) {
+ if (StringUtils.isNotNull(o) && !isFilterObject(o)) {
+ try {
+ String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
+ params += jsonObj.toString() + " ";
+ } catch (Exception e) {
+ }
+ }
+ }
+ }
+ return params.trim();
+ }
+
+ /**
+ * 忽略敏感属性
+ */
+ public PropertyPreExcludeFilter excludePropertyPreFilter (String[] excludeParamNames) {
+ return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
+ }
+
+ /**
+ * 判断是否需要过滤的对象。
+ *
+ * @param o 对象信息。
+ *
+ * @return 如果是需要过滤的对象,则返回true;否则返回false。
+ */
+ @SuppressWarnings("rawtypes")
+ public boolean isFilterObject (final Object o) {
+ Class> clazz = o.getClass();
+ if (clazz.isArray()) {
+ return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+ } else if (Collection.class.isAssignableFrom(clazz)) {
+ Collection collection = (Collection) o;
+ for (Object value : collection) {
+ return value instanceof MultipartFile;
+ }
+ } else if (Map.class.isAssignableFrom(clazz)) {
+ Map map = (Map) o;
+ for (Object value : map.entrySet()) {
+ Map.Entry entry = (Map.Entry) value;
+ return entry.getValue() instanceof MultipartFile;
+ }
+ }
+ return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+ || o instanceof BindingResult;
+ }
+}
diff --git a/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/BusinessStatus.java b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/BusinessStatus.java
new file mode 100644
index 0000000..45e9713
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/BusinessStatus.java
@@ -0,0 +1,18 @@
+package com.muyu.common.log.enums;
+
+/**
+ * 操作状态
+ *
+ * @author muyu
+ */
+public enum BusinessStatus {
+ /**
+ * 成功
+ */
+ SUCCESS,
+
+ /**
+ * 失败
+ */
+ FAIL,
+}
diff --git a/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/BusinessType.java b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/BusinessType.java
new file mode 100644
index 0000000..2e928c7
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/BusinessType.java
@@ -0,0 +1,58 @@
+package com.muyu.common.log.enums;
+
+/**
+ * 业务操作类型
+ *
+ * @author muyu
+ */
+public enum BusinessType {
+ /**
+ * 其它
+ */
+ OTHER,
+
+ /**
+ * 新增
+ */
+ INSERT,
+
+ /**
+ * 修改
+ */
+ UPDATE,
+
+ /**
+ * 删除
+ */
+ DELETE,
+
+ /**
+ * 授权
+ */
+ GRANT,
+
+ /**
+ * 导出
+ */
+ EXPORT,
+
+ /**
+ * 导入
+ */
+ IMPORT,
+
+ /**
+ * 强退
+ */
+ FORCE,
+
+ /**
+ * 生成代码
+ */
+ GENCODE,
+
+ /**
+ * 清空数据
+ */
+ CLEAN,
+}
diff --git a/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/OperatorType.java b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/OperatorType.java
new file mode 100644
index 0000000..645777f
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/enums/OperatorType.java
@@ -0,0 +1,23 @@
+package com.muyu.common.log.enums;
+
+/**
+ * 操作人类别
+ *
+ * @author muyu
+ */
+public enum OperatorType {
+ /**
+ * 其它
+ */
+ OTHER,
+
+ /**
+ * 后台用户
+ */
+ MANAGE,
+
+ /**
+ * 手机端用户
+ */
+ MOBILE
+}
diff --git a/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/filter/PropertyPreExcludeFilter.java b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/filter/PropertyPreExcludeFilter.java
new file mode 100644
index 0000000..2245256
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/filter/PropertyPreExcludeFilter.java
@@ -0,0 +1,20 @@
+package com.muyu.common.log.filter;
+
+import com.alibaba.fastjson2.filter.SimplePropertyPreFilter;
+
+/**
+ * 排除JSON敏感属性
+ *
+ * @author muyu
+ */
+public class PropertyPreExcludeFilter extends SimplePropertyPreFilter {
+ public PropertyPreExcludeFilter () {
+ }
+
+ public PropertyPreExcludeFilter addExcludes (String... filters) {
+ for (int i = 0 ; i < filters.length ; i++) {
+ this.getExcludes().add(filters[i]);
+ }
+ return this;
+ }
+}
diff --git a/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/service/AsyncLogService.java b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/service/AsyncLogService.java
new file mode 100644
index 0000000..46e1cdf
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/java/com/muyu/common/log/service/AsyncLogService.java
@@ -0,0 +1,27 @@
+package com.muyu.common.log.service;
+
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.system.remote.RemoteLogService;
+import com.muyu.common.system.domain.SysOperLog;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+/**
+ * 异步调用日志服务
+ *
+ * @author muyu
+ */
+@Service
+public class AsyncLogService {
+ @Autowired
+ private RemoteLogService remoteLogService;
+
+ /**
+ * 保存系统日志记录
+ */
+ @Async
+ public void saveSysLog (SysOperLog sysOperLog) throws Exception {
+ remoteLogService.saveLog(sysOperLog, SecurityConstants.INNER);
+ }
+}
diff --git a/muyu-common/muyu-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/muyu-common/muyu-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..e46e773
--- /dev/null
+++ b/muyu-common/muyu-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.muyu.common.log.service.AsyncLogService
+com.muyu.common.log.aspect.LogAspect
diff --git a/muyu-common/muyu-common-redis/pom.xml b/muyu-common/muyu-common-redis/pom.xml
new file mode 100644
index 0000000..912bc01
--- /dev/null
+++ b/muyu-common/muyu-common-redis/pom.xml
@@ -0,0 +1,33 @@
+
+
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+ 4.0.0
+
+ muyu-common-redis
+
+
+ muyu-common-redis缓存服务
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ com.muyu
+ muyu-common-core
+
+
+
+
diff --git a/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/configure/FastJson2JsonRedisSerializer.java b/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/configure/FastJson2JsonRedisSerializer.java
new file mode 100644
index 0000000..5959aad
--- /dev/null
+++ b/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/configure/FastJson2JsonRedisSerializer.java
@@ -0,0 +1,47 @@
+package com.muyu.common.redis.configure;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+import com.alibaba.fastjson2.filter.Filter;
+import com.muyu.common.core.constant.Constants;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+
+import java.nio.charset.Charset;
+
+/**
+ * Redis使用FastJson序列化
+ *
+ * @author muyu
+ */
+public class FastJson2JsonRedisSerializer implements RedisSerializer {
+ public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+ static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
+
+ private Class clazz;
+
+ public FastJson2JsonRedisSerializer (Class clazz) {
+ super();
+ this.clazz = clazz;
+ }
+
+ @Override
+ public byte[] serialize (T t) throws SerializationException {
+ if (t == null) {
+ return new byte[0];
+ }
+ return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
+ }
+
+ @Override
+ public T deserialize (byte[] bytes) throws SerializationException {
+ if (bytes == null || bytes.length <= 0) {
+ return null;
+ }
+ String str = new String(bytes, DEFAULT_CHARSET);
+
+ return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
+ }
+}
diff --git a/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/configure/RedisConfig.java b/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/configure/RedisConfig.java
new file mode 100644
index 0000000..ba8760e
--- /dev/null
+++ b/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/configure/RedisConfig.java
@@ -0,0 +1,41 @@
+package com.muyu.common.redis.configure;
+
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ *
+ * @author muyu
+ */
+@Configuration
+@EnableCaching
+@AutoConfigureBefore(RedisAutoConfiguration.class)
+public class RedisConfig extends CachingConfigurerSupport {
+ @Bean
+ @SuppressWarnings(value = {"unchecked", "rawtypes"})
+ public RedisTemplate redisTemplate (RedisConnectionFactory connectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+
+ FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+ // 使用StringRedisSerializer来序列化和反序列化redis的key值
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(serializer);
+
+ // Hash的key也采用StringRedisSerializer的序列化方式
+ template.setHashKeySerializer(new StringRedisSerializer());
+ template.setHashValueSerializer(serializer);
+
+ template.afterPropertiesSet();
+ return template;
+ }
+}
diff --git a/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/service/RedisService.java b/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/service/RedisService.java
new file mode 100644
index 0000000..db90c1e
--- /dev/null
+++ b/muyu-common/muyu-common-redis/src/main/java/com/muyu/common/redis/service/RedisService.java
@@ -0,0 +1,258 @@
+package com.muyu.common.redis.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * spring redis 工具类
+ *
+ * @author muyu
+ **/
+@SuppressWarnings(value = {"unchecked", "rawtypes"})
+@Component
+public class RedisService {
+ @Autowired
+ public RedisTemplate redisTemplate;
+
+ /**
+ * 缓存基本的对象,Integer、String、实体类等
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ */
+ public void setCacheObject (final String key, final T value) {
+ redisTemplate.opsForValue().set(key, value);
+ }
+
+ /**
+ * 缓存基本的对象,Integer、String、实体类等
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ * @param timeout 时间
+ * @param timeUnit 时间颗粒度
+ */
+ public void setCacheObject (final String key, final T value, final Long timeout, final TimeUnit timeUnit) {
+ redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+ }
+
+ /**
+ * 设置有效时间
+ *
+ * @param key Redis键
+ * @param timeout 超时时间
+ *
+ * @return true=设置成功;false=设置失败
+ */
+ public boolean expire (final String key, final long timeout) {
+ return expire(key, timeout, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 设置有效时间
+ *
+ * @param key Redis键
+ * @param timeout 超时时间
+ * @param unit 时间单位
+ *
+ * @return true=设置成功;false=设置失败
+ */
+ public boolean expire (final String key, final long timeout, final TimeUnit unit) {
+ return redisTemplate.expire(key, timeout, unit);
+ }
+
+ /**
+ * 获取有效时间
+ *
+ * @param key Redis键
+ *
+ * @return 有效时间
+ */
+ public long getExpire (final String key) {
+ return redisTemplate.getExpire(key);
+ }
+
+ /**
+ * 判断 key是否存在
+ *
+ * @param key 键
+ *
+ * @return true 存在 false不存在
+ */
+ public Boolean hasKey (String key) {
+ return redisTemplate.hasKey(key);
+ }
+
+ /**
+ * 获得缓存的基本对象。
+ *
+ * @param key 缓存键值
+ *
+ * @return 缓存键值对应的数据
+ */
+ public T getCacheObject (final String key) {
+ ValueOperations operation = redisTemplate.opsForValue();
+ return operation.get(key);
+ }
+
+ /**
+ * 删除单个对象
+ *
+ * @param key
+ */
+ public boolean deleteObject (final String key) {
+ return redisTemplate.delete(key);
+ }
+
+ /**
+ * 删除集合对象
+ *
+ * @param collection 多个对象
+ *
+ * @return
+ */
+ public boolean deleteObject (final Collection collection) {
+ return redisTemplate.delete(collection) > 0;
+ }
+
+ /**
+ * 缓存List数据
+ *
+ * @param key 缓存的键值
+ * @param dataList 待缓存的List数据
+ *
+ * @return 缓存的对象
+ */
+ public long setCacheList (final String key, final List dataList) {
+ Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
+ return count == null ? 0 : count;
+ }
+
+ /**
+ * 获得缓存的list对象
+ *
+ * @param key 缓存的键值
+ *
+ * @return 缓存键值对应的数据
+ */
+ public List getCacheList (final String key) {
+ return redisTemplate.opsForList().range(key, 0, -1);
+ }
+
+ /**
+ * 缓存Set
+ *
+ * @param key 缓存键值
+ * @param dataSet 缓存的数据
+ *
+ * @return 缓存数据的对象
+ */
+ public BoundSetOperations setCacheSet (final String key, final Set dataSet) {
+ BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
+ Iterator it = dataSet.iterator();
+ while (it.hasNext()) {
+ setOperation.add(it.next());
+ }
+ return setOperation;
+ }
+
+ /**
+ * 获得缓存的set
+ *
+ * @param key
+ *
+ * @return
+ */
+ public Set getCacheSet (final String key) {
+ return redisTemplate.opsForSet().members(key);
+ }
+
+ /**
+ * 缓存Map
+ *
+ * @param key
+ * @param dataMap
+ */
+ public void setCacheMap (final String key, final Map dataMap) {
+ if (dataMap != null) {
+ redisTemplate.opsForHash().putAll(key, dataMap);
+ }
+ }
+
+ /**
+ * 获得缓存的Map
+ *
+ * @param key
+ *
+ * @return
+ */
+ public Map getCacheMap (final String key) {
+ return redisTemplate.opsForHash().entries(key);
+ }
+
+ /**
+ * 往Hash中存入数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @param value 值
+ */
+ public void setCacheMapValue (final String key, final String hKey, final T value) {
+ redisTemplate.opsForHash().put(key, hKey, value);
+ }
+
+ /**
+ * 获取Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ *
+ * @return Hash中的对象
+ */
+ public T getCacheMapValue (final String key, final String hKey) {
+ HashOperations opsForHash = redisTemplate.opsForHash();
+ return opsForHash.get(key, hKey);
+ }
+
+ /**
+ * 获取多个Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKeys Hash键集合
+ *
+ * @return Hash对象集合
+ */
+ public List getMultiCacheMapValue (final String key, final Collection hKeys) {
+ return redisTemplate.opsForHash().multiGet(key, hKeys);
+ }
+
+ /**
+ * 删除Hash中的某条数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ *
+ * @return 是否成功
+ */
+ public boolean deleteCacheMapValue (final String key, final String hKey) {
+ return redisTemplate.opsForHash().delete(key, hKey) > 0;
+ }
+
+ /**
+ * 获得缓存的基本对象列表
+ *
+ * @param pattern 字符串前缀
+ *
+ * @return 对象列表
+ */
+ public Collection keys (final String pattern) {
+ return redisTemplate.keys(pattern);
+ }
+}
diff --git a/muyu-common/muyu-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/muyu-common/muyu-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..27b030e
--- /dev/null
+++ b/muyu-common/muyu-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.muyu.common.redis.configure.RedisConfig
+com.muyu.common.redis.service.RedisService
diff --git a/muyu-common/muyu-common-seata/pom.xml b/muyu-common/muyu-common-seata/pom.xml
new file mode 100644
index 0000000..a8cab12
--- /dev/null
+++ b/muyu-common/muyu-common-seata/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+ 4.0.0
+
+ muyu-common-seata
+
+
+ muyu-common-seata分布式事务
+
+
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-seata
+
+
+
+
diff --git a/muyu-common/muyu-common-security/pom.xml b/muyu-common/muyu-common-security/pom.xml
new file mode 100644
index 0000000..6bb80d3
--- /dev/null
+++ b/muyu-common/muyu-common-security/pom.xml
@@ -0,0 +1,39 @@
+
+
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+ 4.0.0
+
+ muyu-common-security
+
+
+ muyu-common-security安全模块
+
+
+
+
+
+
+ org.springframework
+ spring-webmvc
+
+
+
+
+ com.muyu
+ muyu-common-redis
+
+
+
+
+ com.muyu
+ muyu-common-system
+
+
+
+
+
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/EnableCustomConfig.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/EnableCustomConfig.java
new file mode 100644
index 0000000..ca7a07a
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/EnableCustomConfig.java
@@ -0,0 +1,26 @@
+package com.muyu.common.security.annotation;
+
+import com.muyu.common.security.config.ApplicationConfig;
+import com.muyu.common.security.feign.FeignAutoConfiguration;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.context.annotation.Import;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+import java.lang.annotation.*;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.muyu.**.mapper")
+// 开启线程异步执行
+@EnableAsync
+// 自动加载类
+@Import({ApplicationConfig.class, FeignAutoConfiguration.class})
+public @interface EnableCustomConfig {
+
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/EnableMyFeignClients.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/EnableMyFeignClients.java
new file mode 100644
index 0000000..7a59fa4
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/EnableMyFeignClients.java
@@ -0,0 +1,27 @@
+package com.muyu.common.security.annotation;
+
+import org.springframework.cloud.openfeign.EnableFeignClients;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义feign注解
+ * 添加basePackages路径
+ *
+ * @author muyu
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@EnableFeignClients
+public @interface EnableMyFeignClients {
+ String[] value () default {};
+
+ String[] basePackages () default {"com.muyu"};
+
+ Class>[] basePackageClasses () default {};
+
+ Class>[] defaultConfiguration () default {};
+
+ Class>[] clients () default {};
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/InnerAuth.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/InnerAuth.java
new file mode 100644
index 0000000..092a573
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/InnerAuth.java
@@ -0,0 +1,18 @@
+package com.muyu.common.security.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 内部认证注解
+ *
+ * @author muyu
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface InnerAuth {
+ /**
+ * 是否校验用户信息
+ */
+ boolean isUser () default false;
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/Logical.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/Logical.java
new file mode 100644
index 0000000..0be306a
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/Logical.java
@@ -0,0 +1,18 @@
+package com.muyu.common.security.annotation;
+
+/**
+ * 权限注解的验证模式
+ *
+ * @author muyu
+ */
+public enum Logical {
+ /**
+ * 必须具有所有的元素
+ */
+ AND,
+
+ /**
+ * 只需具有其中一个元素
+ */
+ OR
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresLogin.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresLogin.java
new file mode 100644
index 0000000..4eff911
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresLogin.java
@@ -0,0 +1,16 @@
+package com.muyu.common.security.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 登录认证:只有登录之后才能进入该方法
+ *
+ * @author muyu
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RequiresLogin {
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresPermissions.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresPermissions.java
new file mode 100644
index 0000000..8d95bb4
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresPermissions.java
@@ -0,0 +1,25 @@
+package com.muyu.common.security.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 权限认证:必须具有指定权限才能进入该方法
+ *
+ * @author muyu
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RequiresPermissions {
+ /**
+ * 需要校验的权限码
+ */
+ String[] value () default {};
+
+ /**
+ * 验证模式:AND | OR,默认AND
+ */
+ Logical logical () default Logical.AND;
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresRoles.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresRoles.java
new file mode 100644
index 0000000..78911cc
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/annotation/RequiresRoles.java
@@ -0,0 +1,25 @@
+package com.muyu.common.security.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 角色认证:必须具有指定角色标识才能进入该方法
+ *
+ * @author muyu
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RequiresRoles {
+ /**
+ * 需要校验的角色标识
+ */
+ String[] value () default {};
+
+ /**
+ * 验证逻辑:AND | OR,默认AND
+ */
+ Logical logical () default Logical.AND;
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/aspect/InnerAuthAspect.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/aspect/InnerAuthAspect.java
new file mode 100644
index 0000000..1707742
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/aspect/InnerAuthAspect.java
@@ -0,0 +1,46 @@
+package com.muyu.common.security.aspect;
+
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.core.exception.InnerAuthException;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.security.annotation.InnerAuth;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.core.Ordered;
+import org.springframework.stereotype.Component;
+
+/**
+ * 内部服务调用验证处理
+ *
+ * @author muyu
+ */
+@Aspect
+@Component
+public class InnerAuthAspect implements Ordered {
+ @Around("@annotation(innerAuth)")
+ public Object innerAround (ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable {
+ String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
+ // 内部请求验证
+ if (!StringUtils.equals(SecurityConstants.INNER, source)) {
+ throw new InnerAuthException("没有内部访问权限,不允许访问");
+ }
+
+ String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
+ String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
+ // 用户信息验证
+ if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) {
+ throw new InnerAuthException("没有设置用户信息,不允许访问 ");
+ }
+ return point.proceed();
+ }
+
+ /**
+ * 确保在权限认证aop执行前执行
+ */
+ @Override
+ public int getOrder () {
+ return Ordered.HIGHEST_PRECEDENCE + 1;
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/aspect/PreAuthorizeAspect.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/aspect/PreAuthorizeAspect.java
new file mode 100644
index 0000000..4cdd933
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/aspect/PreAuthorizeAspect.java
@@ -0,0 +1,89 @@
+package com.muyu.common.security.aspect;
+
+import com.muyu.common.security.annotation.RequiresLogin;
+import com.muyu.common.security.annotation.RequiresPermissions;
+import com.muyu.common.security.annotation.RequiresRoles;
+import com.muyu.common.security.auth.AuthUtil;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 基于 Spring Aop 的注解鉴权
+ *
+ * @author kong
+ */
+@Aspect
+@Component
+public class PreAuthorizeAspect {
+ /**
+ * 定义AOP签名 (切入所有使用鉴权注解的方法)
+ */
+ public static final String POINTCUT_SIGN = " @annotation(com.muyu.common.security.annotation.RequiresLogin) || "
+ + "@annotation(com.muyu.common.security.annotation.RequiresPermissions) || "
+ + "@annotation(com.muyu.common.security.annotation.RequiresRoles)";
+
+ /**
+ * 构建
+ */
+ public PreAuthorizeAspect () {
+ }
+
+ /**
+ * 声明AOP签名
+ */
+ @Pointcut(POINTCUT_SIGN)
+ public void pointcut () {
+ }
+
+ /**
+ * 环绕切入
+ *
+ * @param joinPoint 切面对象
+ *
+ * @return 底层方法执行后的返回值
+ *
+ * @throws Throwable 底层方法抛出的异常
+ */
+ @Around("pointcut()")
+ public Object around (ProceedingJoinPoint joinPoint) throws Throwable {
+ // 注解鉴权
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ checkMethodAnnotation(signature.getMethod());
+ try {
+ // 执行原有逻辑
+ Object obj = joinPoint.proceed();
+ return obj;
+ } catch (Throwable e) {
+ throw e;
+ }
+ }
+
+ /**
+ * 对一个Method对象进行注解检查
+ */
+ public void checkMethodAnnotation (Method method) {
+ // 校验 @RequiresLogin 注解
+ RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
+ if (requiresLogin != null) {
+ AuthUtil.checkLogin();
+ }
+
+ // 校验 @RequiresRoles 注解
+ RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
+ if (requiresRoles != null) {
+ AuthUtil.checkRole(requiresRoles);
+ }
+
+ // 校验 @RequiresPermissions 注解
+ RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
+ if (requiresPermissions != null) {
+ AuthUtil.checkPermi(requiresPermissions);
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/auth/AuthLogic.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/auth/AuthLogic.java
new file mode 100644
index 0000000..beb3426
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/auth/AuthLogic.java
@@ -0,0 +1,327 @@
+package com.muyu.common.security.auth;
+
+import com.muyu.common.core.context.SecurityContextHolder;
+import com.muyu.common.core.exception.auth.NotLoginException;
+import com.muyu.common.core.exception.auth.NotPermissionException;
+import com.muyu.common.core.exception.auth.NotRoleException;
+import com.muyu.common.core.utils.SpringUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.security.annotation.Logical;
+import com.muyu.common.security.annotation.RequiresLogin;
+import com.muyu.common.security.annotation.RequiresPermissions;
+import com.muyu.common.security.annotation.RequiresRoles;
+import com.muyu.common.security.service.TokenService;
+import com.muyu.common.security.utils.SecurityUtils;
+import com.muyu.common.system.domain.LoginUser;
+import org.springframework.util.PatternMatchUtils;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Token 权限验证,逻辑实现类
+ *
+ * @author muyu
+ */
+public class AuthLogic {
+ /**
+ * 所有权限标识
+ */
+ private static final String ALL_PERMISSION = "*:*:*";
+
+ /**
+ * 管理员角色权限标识
+ */
+ private static final String SUPER_ADMIN = "admin";
+
+ public TokenService tokenService = SpringUtils.getBean(TokenService.class);
+
+ /**
+ * 会话注销
+ */
+ public void logout () {
+ String token = SecurityUtils.getToken();
+ if (token == null) {
+ return;
+ }
+ logoutByToken(token);
+ }
+
+ /**
+ * 会话注销,根据指定Token
+ */
+ public void logoutByToken (String token) {
+ tokenService.delLoginUser(token);
+ }
+
+ /**
+ * 检验用户是否已经登录,如未登录,则抛出异常
+ */
+ public void checkLogin () {
+ getLoginUser();
+ }
+
+ /**
+ * 获取当前用户缓存信息, 如果未登录,则抛出异常
+ *
+ * @return 用户缓存信息
+ */
+ public LoginUser getLoginUser () {
+ String token = SecurityUtils.getToken();
+ if (token == null) {
+ throw new NotLoginException("未提供token");
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (loginUser == null) {
+ throw new NotLoginException("无效的token");
+ }
+ return loginUser;
+ }
+
+ /**
+ * 获取当前用户缓存信息, 如果未登录,则抛出异常
+ *
+ * @param token 前端传递的认证信息
+ *
+ * @return 用户缓存信息
+ */
+ public LoginUser getLoginUser (String token) {
+ return tokenService.getLoginUser(token);
+ }
+
+ /**
+ * 验证当前用户有效期, 如果相差不足120分钟,自动刷新缓存
+ *
+ * @param loginUser 当前用户信息
+ */
+ public void verifyLoginUserExpire (LoginUser loginUser) {
+ tokenService.verifyToken(loginUser);
+ }
+
+ /**
+ * 验证用户是否具备某权限
+ *
+ * @param permission 权限字符串
+ *
+ * @return 用户是否具备某权限
+ */
+ public boolean hasPermi (String permission) {
+ return hasPermi(getPermiList(), permission);
+ }
+
+ /**
+ * 验证用户是否具备某权限, 如果验证未通过,则抛出异常: NotPermissionException
+ *
+ * @param permission 权限字符串
+ *
+ * @return 用户是否具备某权限
+ */
+ public void checkPermi (String permission) {
+ if (!hasPermi(getPermiList(), permission)) {
+ throw new NotPermissionException(permission);
+ }
+ }
+
+ /**
+ * 根据注解(@RequiresPermissions)鉴权, 如果验证未通过,则抛出异常: NotPermissionException
+ *
+ * @param requiresPermissions 注解对象
+ */
+ public void checkPermi (RequiresPermissions requiresPermissions) {
+ SecurityContextHolder.setPermission(StringUtils.join(requiresPermissions.value(), ","));
+ if (requiresPermissions.logical() == Logical.AND) {
+ checkPermiAnd(requiresPermissions.value());
+ } else {
+ checkPermiOr(requiresPermissions.value());
+ }
+ }
+
+ /**
+ * 验证用户是否含有指定权限,必须全部拥有
+ *
+ * @param permissions 权限列表
+ */
+ public void checkPermiAnd (String... permissions) {
+ Set permissionList = getPermiList();
+ for (String permission : permissions) {
+ if (!hasPermi(permissionList, permission)) {
+ throw new NotPermissionException(permission);
+ }
+ }
+ }
+
+ /**
+ * 验证用户是否含有指定权限,只需包含其中一个
+ *
+ * @param permissions 权限码数组
+ */
+ public void checkPermiOr (String... permissions) {
+ Set permissionList = getPermiList();
+ for (String permission : permissions) {
+ if (hasPermi(permissionList, permission)) {
+ return;
+ }
+ }
+ if (permissions.length > 0) {
+ throw new NotPermissionException(permissions);
+ }
+ }
+
+ /**
+ * 判断用户是否拥有某个角色
+ *
+ * @param role 角色标识
+ *
+ * @return 用户是否具备某角色
+ */
+ public boolean hasRole (String role) {
+ return hasRole(getRoleList(), role);
+ }
+
+ /**
+ * 判断用户是否拥有某个角色, 如果验证未通过,则抛出异常: NotRoleException
+ *
+ * @param role 角色标识
+ */
+ public void checkRole (String role) {
+ if (!hasRole(role)) {
+ throw new NotRoleException(role);
+ }
+ }
+
+ /**
+ * 根据注解(@RequiresRoles)鉴权
+ *
+ * @param requiresRoles 注解对象
+ */
+ public void checkRole (RequiresRoles requiresRoles) {
+ if (requiresRoles.logical() == Logical.AND) {
+ checkRoleAnd(requiresRoles.value());
+ } else {
+ checkRoleOr(requiresRoles.value());
+ }
+ }
+
+ /**
+ * 验证用户是否含有指定角色,必须全部拥有
+ *
+ * @param roles 角色标识数组
+ */
+ public void checkRoleAnd (String... roles) {
+ Set roleList = getRoleList();
+ for (String role : roles) {
+ if (!hasRole(roleList, role)) {
+ throw new NotRoleException(role);
+ }
+ }
+ }
+
+ /**
+ * 验证用户是否含有指定角色,只需包含其中一个
+ *
+ * @param roles 角色标识数组
+ */
+ public void checkRoleOr (String... roles) {
+ Set roleList = getRoleList();
+ for (String role : roles) {
+ if (hasRole(roleList, role)) {
+ return;
+ }
+ }
+ if (roles.length > 0) {
+ throw new NotRoleException(roles);
+ }
+ }
+
+ /**
+ * 根据注解(@RequiresLogin)鉴权
+ *
+ * @param at 注解对象
+ */
+ public void checkByAnnotation (RequiresLogin at) {
+ this.checkLogin();
+ }
+
+ /**
+ * 根据注解(@RequiresRoles)鉴权
+ *
+ * @param at 注解对象
+ */
+ public void checkByAnnotation (RequiresRoles at) {
+ String[] roleArray = at.value();
+ if (at.logical() == Logical.AND) {
+ this.checkRoleAnd(roleArray);
+ } else {
+ this.checkRoleOr(roleArray);
+ }
+ }
+
+ /**
+ * 根据注解(@RequiresPermissions)鉴权
+ *
+ * @param at 注解对象
+ */
+ public void checkByAnnotation (RequiresPermissions at) {
+ String[] permissionArray = at.value();
+ if (at.logical() == Logical.AND) {
+ this.checkPermiAnd(permissionArray);
+ } else {
+ this.checkPermiOr(permissionArray);
+ }
+ }
+
+ /**
+ * 获取当前账号的角色列表
+ *
+ * @return 角色列表
+ */
+ public Set getRoleList () {
+ try {
+ LoginUser loginUser = getLoginUser();
+ return loginUser.getRoles();
+ } catch (Exception e) {
+ return new HashSet<>();
+ }
+ }
+
+ /**
+ * 获取当前账号的权限列表
+ *
+ * @return 权限列表
+ */
+ public Set getPermiList () {
+ try {
+ LoginUser loginUser = getLoginUser();
+ return loginUser.getPermissions();
+ } catch (Exception e) {
+ return new HashSet<>();
+ }
+ }
+
+ /**
+ * 判断是否包含权限
+ *
+ * @param authorities 权限列表
+ * @param permission 权限字符串
+ *
+ * @return 用户是否具备某权限
+ */
+ public boolean hasPermi (Collection authorities, String permission) {
+ return authorities.stream().filter(StringUtils::hasText)
+ .anyMatch(x -> ALL_PERMISSION.equals(x) || PatternMatchUtils.simpleMatch(x, permission));
+ }
+
+ /**
+ * 判断是否包含角色
+ *
+ * @param roles 角色列表
+ * @param role 角色
+ *
+ * @return 用户是否具备某角色权限
+ */
+ public boolean hasRole (Collection roles, String role) {
+ return roles.stream().filter(StringUtils::hasText)
+ .anyMatch(x -> SUPER_ADMIN.equals(x) || PatternMatchUtils.simpleMatch(x, role));
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/auth/AuthUtil.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/auth/AuthUtil.java
new file mode 100644
index 0000000..131d150
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/auth/AuthUtil.java
@@ -0,0 +1,154 @@
+package com.muyu.common.security.auth;
+
+import com.muyu.common.security.annotation.RequiresPermissions;
+import com.muyu.common.security.annotation.RequiresRoles;
+import com.muyu.common.system.domain.LoginUser;
+
+/**
+ * Token 权限验证工具类
+ *
+ * @author muyu
+ */
+public class AuthUtil {
+ /**
+ * 底层的 AuthLogic 对象
+ */
+ public static AuthLogic authLogic = new AuthLogic();
+
+ /**
+ * 会话注销
+ */
+ public static void logout () {
+ authLogic.logout();
+ }
+
+ /**
+ * 会话注销,根据指定Token
+ *
+ * @param token 指定token
+ */
+ public static void logoutByToken (String token) {
+ authLogic.logoutByToken(token);
+ }
+
+ /**
+ * 检验当前会话是否已经登录,如未登录,则抛出异常
+ */
+ public static void checkLogin () {
+ authLogic.checkLogin();
+ }
+
+ /**
+ * 获取当前登录用户信息
+ *
+ * @param token 指定token
+ *
+ * @return 用户信息
+ */
+ public static LoginUser getLoginUser (String token) {
+ return authLogic.getLoginUser(token);
+ }
+
+ /**
+ * 验证当前用户有效期
+ *
+ * @param loginUser 用户信息
+ */
+ public static void verifyLoginUserExpire (LoginUser loginUser) {
+ authLogic.verifyLoginUserExpire(loginUser);
+ }
+
+ /**
+ * 当前账号是否含有指定角色标识, 返回true或false
+ *
+ * @param role 角色标识
+ *
+ * @return 是否含有指定角色标识
+ */
+ public static boolean hasRole (String role) {
+ return authLogic.hasRole(role);
+ }
+
+ /**
+ * 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
+ *
+ * @param role 角色标识
+ */
+ public static void checkRole (String role) {
+ authLogic.checkRole(role);
+ }
+
+ /**
+ * 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotRoleException
+ *
+ * @param requiresRoles 角色权限注解
+ */
+ public static void checkRole (RequiresRoles requiresRoles) {
+ authLogic.checkRole(requiresRoles);
+ }
+
+ /**
+ * 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
+ *
+ * @param roles 角色标识数组
+ */
+ public static void checkRoleAnd (String... roles) {
+ authLogic.checkRoleAnd(roles);
+ }
+
+ /**
+ * 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
+ *
+ * @param roles 角色标识数组
+ */
+ public static void checkRoleOr (String... roles) {
+ authLogic.checkRoleOr(roles);
+ }
+
+ /**
+ * 当前账号是否含有指定权限, 返回true或false
+ *
+ * @param permission 权限码
+ *
+ * @return 是否含有指定权限
+ */
+ public static boolean hasPermi (String permission) {
+ return authLogic.hasPermi(permission);
+ }
+
+ /**
+ * 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
+ *
+ * @param permission 权限码
+ */
+ public static void checkPermi (String permission) {
+ authLogic.checkPermi(permission);
+ }
+
+ /**
+ * 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotPermissionException
+ *
+ * @param requiresPermissions 权限注解
+ */
+ public static void checkPermi (RequiresPermissions requiresPermissions) {
+ authLogic.checkPermi(requiresPermissions);
+ }
+
+ /**
+ * 当前账号是否含有指定权限 [指定多个,必须全部验证通过]
+ *
+ * @param permissions 权限码数组
+ */
+ public static void checkPermiAnd (String... permissions) {
+ authLogic.checkPermiAnd(permissions);
+ }
+
+ /**
+ * 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
+ *
+ * @param permissions 权限码数组
+ */
+ public static void checkPermiOr (String... permissions) {
+ authLogic.checkPermiOr(permissions);
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/config/ApplicationConfig.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/config/ApplicationConfig.java
new file mode 100644
index 0000000..b78abbf
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/config/ApplicationConfig.java
@@ -0,0 +1,21 @@
+package com.muyu.common.security.config;
+
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+
+import java.util.TimeZone;
+
+/**
+ * 系统配置
+ *
+ * @author muyu
+ */
+public class ApplicationConfig {
+ /**
+ * 时区配置
+ */
+ @Bean
+ public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization () {
+ return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/config/WebMvcConfig.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/config/WebMvcConfig.java
new file mode 100644
index 0000000..8acde35
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/config/WebMvcConfig.java
@@ -0,0 +1,32 @@
+package com.muyu.common.security.config;
+
+import com.muyu.common.security.interceptor.HeaderInterceptor;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 拦截器配置
+ *
+ * @author muyu
+ */
+public class WebMvcConfig implements WebMvcConfigurer {
+ /**
+ * 不需要拦截地址
+ */
+ public static final String[] excludeUrls = {"/login", "/logout", "/refresh"};
+
+ @Override
+ public void addInterceptors (InterceptorRegistry registry) {
+ registry.addInterceptor(getHeaderInterceptor())
+ .addPathPatterns("/**")
+ .excludePathPatterns(excludeUrls)
+ .order(-10);
+ }
+
+ /**
+ * 自定义请求头拦截器
+ */
+ public HeaderInterceptor getHeaderInterceptor () {
+ return new HeaderInterceptor();
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/feign/FeignAutoConfiguration.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/feign/FeignAutoConfiguration.java
new file mode 100644
index 0000000..4bfda6d
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/feign/FeignAutoConfiguration.java
@@ -0,0 +1,18 @@
+package com.muyu.common.security.feign;
+
+import feign.RequestInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Feign 配置注册
+ *
+ * @author muyu
+ **/
+@Configuration
+public class FeignAutoConfiguration {
+ @Bean
+ public RequestInterceptor requestInterceptor () {
+ return new FeignRequestInterceptor();
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/feign/FeignRequestInterceptor.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/feign/FeignRequestInterceptor.java
new file mode 100644
index 0000000..196765f
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/feign/FeignRequestInterceptor.java
@@ -0,0 +1,48 @@
+package com.muyu.common.security.feign;
+
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.core.utils.ip.IpUtils;
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+/**
+ * feign 请求拦截器
+ *
+ * @author muyu
+ */
+@Component
+public class FeignRequestInterceptor implements RequestInterceptor {
+ @Override
+ public void apply (RequestTemplate requestTemplate) {
+ HttpServletRequest httpServletRequest = ServletUtils.getRequest();
+ if (StringUtils.isNotNull(httpServletRequest)) {
+ Map headers = ServletUtils.getHeaders(httpServletRequest);
+ // 传递用户信息请求头,防止丢失
+ String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
+ if (StringUtils.isNotEmpty(userId)) {
+ requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
+ }
+ String userKey = headers.get(SecurityConstants.USER_KEY);
+ if (StringUtils.isNotEmpty(userKey)) {
+ requestTemplate.header(SecurityConstants.USER_KEY, userKey);
+ }
+ String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
+ if (StringUtils.isNotEmpty(userName)) {
+ requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
+ }
+ String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
+ if (StringUtils.isNotEmpty(authentication)) {
+ requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
+ }
+
+ // 配置客户端IP
+ requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr());
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/handler/GlobalExceptionHandler.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/handler/GlobalExceptionHandler.java
new file mode 100644
index 0000000..52b43af
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/handler/GlobalExceptionHandler.java
@@ -0,0 +1,147 @@
+package com.muyu.common.security.handler;
+
+import com.muyu.common.core.constant.HttpStatus;
+import com.muyu.common.core.exception.DemoModeException;
+import com.muyu.common.core.exception.InnerAuthException;
+import com.muyu.common.core.exception.ServiceException;
+import com.muyu.common.core.exception.auth.NotPermissionException;
+import com.muyu.common.core.exception.auth.NotRoleException;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.core.domain.Result;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.validation.BindException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 全局异常处理器
+ *
+ * @author muyu
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ /**
+ * 权限码异常
+ */
+ @ExceptionHandler(NotPermissionException.class)
+ public Result handleNotPermissionException (NotPermissionException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage());
+ return Result.error(HttpStatus.FORBIDDEN, "没有访问权限,请联系管理员授权");
+ }
+
+ /**
+ * 角色权限异常
+ */
+ @ExceptionHandler(NotRoleException.class)
+ public Result handleNotRoleException (NotRoleException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage());
+ return Result.error(HttpStatus.FORBIDDEN, "没有访问权限,请联系管理员授权");
+ }
+
+ /**
+ * 请求方式不支持
+ */
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ public Result handleHttpRequestMethodNotSupported (HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
+ return Result.error(e.getMessage());
+ }
+
+ /**
+ * 业务异常
+ */
+ @ExceptionHandler(ServiceException.class)
+ public Result handleServiceException (ServiceException e, HttpServletRequest request) {
+ log.error(e.getMessage(), e);
+ Integer code = e.getCode();
+ return StringUtils.isNotNull(code) ? Result.error(code, e.getMessage()) : Result.error(e.getMessage());
+ }
+
+ /**
+ * 请求路径中缺少必需的路径变量
+ */
+ @ExceptionHandler(MissingPathVariableException.class)
+ public Result handleMissingPathVariableException (MissingPathVariableException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
+ return Result.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
+ }
+
+ /**
+ * 请求参数类型不匹配
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public Result handleMethodArgumentTypeMismatchException (MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
+ return Result.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
+ }
+
+ /**
+ * 拦截未知的运行时异常
+ */
+ @ExceptionHandler(RuntimeException.class)
+ public Result handleRuntimeException (RuntimeException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',发生未知异常.", requestURI, e);
+ return Result.error(e.getMessage());
+ }
+
+ /**
+ * 系统异常
+ */
+ @ExceptionHandler(Exception.class)
+ public Result handleException (Exception e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',发生系统异常.", requestURI, e);
+ return Result.error(e.getMessage());
+ }
+
+ /**
+ * 自定义验证异常
+ */
+ @ExceptionHandler(BindException.class)
+ public Result handleBindException (BindException e) {
+ log.error(e.getMessage(), e);
+ String message = e.getAllErrors().get(0).getDefaultMessage();
+ return Result.error(message);
+ }
+
+ /**
+ * 自定义验证异常
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public Object handleMethodArgumentNotValidException (MethodArgumentNotValidException e) {
+ log.error(e.getMessage(), e);
+ String message = e.getBindingResult().getFieldError().getDefaultMessage();
+ return Result.error(message);
+ }
+
+ /**
+ * 内部认证异常
+ */
+ @ExceptionHandler(InnerAuthException.class)
+ public Result handleInnerAuthException (InnerAuthException e) {
+ return Result.error(e.getMessage());
+ }
+
+ /**
+ * 演示模式异常
+ */
+ @ExceptionHandler(DemoModeException.class)
+ public Result handleDemoModeException (DemoModeException e) {
+ return Result.error("演示模式,不允许操作");
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/interceptor/HeaderInterceptor.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/interceptor/HeaderInterceptor.java
new file mode 100644
index 0000000..fe1774c
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/interceptor/HeaderInterceptor.java
@@ -0,0 +1,49 @@
+package com.muyu.common.security.interceptor;
+
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.core.context.SecurityContextHolder;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.security.auth.AuthUtil;
+import com.muyu.common.security.utils.SecurityUtils;
+import com.muyu.common.system.domain.LoginUser;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.AsyncHandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 自定义请求头拦截器,将Header数据封装到线程变量中方便获取
+ * 注意:此拦截器会同时验证当前用户有效期自动刷新有效期
+ *
+ * @author muyu
+ */
+public class HeaderInterceptor implements AsyncHandlerInterceptor {
+ @Override
+ public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ if (!(handler instanceof HandlerMethod)) {
+ return true;
+ }
+
+ SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
+ SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
+ SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
+
+ String token = SecurityUtils.getToken();
+ if (StringUtils.isNotEmpty(token)) {
+ LoginUser loginUser = AuthUtil.getLoginUser(token);
+ if (StringUtils.isNotNull(loginUser)) {
+ AuthUtil.verifyLoginUserExpire(loginUser);
+ SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
+ throws Exception {
+ SecurityContextHolder.remove();
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/service/TokenService.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/service/TokenService.java
new file mode 100644
index 0000000..b19697e
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/service/TokenService.java
@@ -0,0 +1,153 @@
+package com.muyu.common.security.service;
+
+import com.muyu.common.core.constant.CacheConstants;
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.core.utils.JwtUtils;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.core.utils.ip.IpUtils;
+import com.muyu.common.core.utils.uuid.IdUtils;
+import com.muyu.common.redis.service.RedisService;
+import com.muyu.common.security.utils.SecurityUtils;
+import com.muyu.common.system.domain.LoginUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * token验证处理
+ *
+ * @author muyu
+ */
+@Component
+public class TokenService {
+ protected static final long MILLIS_SECOND = 1000;
+ protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
+ private final static Long MILLIS_MINUTE_TEN = CacheConstants.REFRESH_TIME * MILLIS_MINUTE;
+ private static final Logger log = LoggerFactory.getLogger(TokenService.class);
+ private final static long expireTime = CacheConstants.EXPIRATION;
+
+ private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY;
+ @Autowired
+ private RedisService redisService;
+
+ /**
+ * 创建令牌
+ */
+ public Map createToken (LoginUser loginUser) {
+ String token = IdUtils.fastUUID();
+ Long userId = loginUser.getSysUser().getUserId();
+ String userName = loginUser.getSysUser().getUserName();
+ loginUser.setToken(token);
+ loginUser.setUserid(userId);
+ loginUser.setUsername(userName);
+ loginUser.setIpaddr(IpUtils.getIpAddr());
+ refreshToken(loginUser);
+
+ // Jwt存储信息
+ Map claimsMap = new HashMap();
+ claimsMap.put(SecurityConstants.USER_KEY, token);
+ claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
+ claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);
+
+ // 接口返回信息
+ Map rspMap = new HashMap();
+ rspMap.put("access_token", JwtUtils.createToken(claimsMap));
+ rspMap.put("expires_in", expireTime);
+ return rspMap;
+ }
+
+ /**
+ * 获取用户身份信息
+ *
+ * @return 用户信息
+ */
+ public LoginUser getLoginUser () {
+ return getLoginUser(ServletUtils.getRequest());
+ }
+
+ /**
+ * 设置用户身份信息
+ */
+ public void setLoginUser (LoginUser loginUser) {
+ if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
+ refreshToken(loginUser);
+ }
+ }
+
+ /**
+ * 获取用户身份信息
+ *
+ * @return 用户信息
+ */
+ public LoginUser getLoginUser (HttpServletRequest request) {
+ // 获取请求携带的令牌
+ String token = SecurityUtils.getToken(request);
+ return getLoginUser(token);
+ }
+
+ /**
+ * 获取用户身份信息
+ *
+ * @return 用户信息
+ */
+ public LoginUser getLoginUser (String token) {
+ LoginUser user = null;
+ try {
+ if (StringUtils.isNotEmpty(token)) {
+ String userkey = JwtUtils.getUserKey(token);
+ user = redisService.getCacheObject(getTokenKey(userkey));
+ return user;
+ }
+ } catch (Exception e) {
+ log.error("获取用户信息异常'{}'", e.getMessage());
+ }
+ return user;
+ }
+
+ /**
+ * 删除用户缓存信息
+ */
+ public void delLoginUser (String token) {
+ if (StringUtils.isNotEmpty(token)) {
+ String userkey = JwtUtils.getUserKey(token);
+ redisService.deleteObject(getTokenKey(userkey));
+ }
+ }
+
+ /**
+ * 验证令牌有效期,相差不足120分钟,自动刷新缓存
+ *
+ * @param loginUser
+ */
+ public void verifyToken (LoginUser loginUser) {
+ long expireTime = loginUser.getExpireTime();
+ long currentTime = System.currentTimeMillis();
+ if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
+ refreshToken(loginUser);
+ }
+ }
+
+ /**
+ * 刷新令牌有效期
+ *
+ * @param loginUser 登录信息
+ */
+ public void refreshToken (LoginUser loginUser) {
+ loginUser.setLoginTime(System.currentTimeMillis());
+ loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
+ // 根据uuid将loginUser缓存
+ String userKey = getTokenKey(loginUser.getToken());
+ redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
+ }
+
+ private String getTokenKey (String token) {
+ return ACCESS_TOKEN + token;
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/utils/DictUtils.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/utils/DictUtils.java
new file mode 100644
index 0000000..04ee068
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/utils/DictUtils.java
@@ -0,0 +1,71 @@
+package com.muyu.common.security.utils;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.muyu.common.core.constant.CacheConstants;
+import com.muyu.common.core.utils.SpringUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.redis.service.RedisService;
+import com.muyu.common.system.domain.SysDictData;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 字典工具类
+ *
+ * @author muyu
+ */
+public class DictUtils {
+ /**
+ * 设置字典缓存
+ *
+ * @param key 参数键
+ * @param dictDatas 字典数据列表
+ */
+ public static void setDictCache (String key, List dictDatas) {
+ SpringUtils.getBean(RedisService.class).setCacheObject(getCacheKey(key), dictDatas);
+ }
+
+ /**
+ * 获取字典缓存
+ *
+ * @param key 参数键
+ *
+ * @return dictDatas 字典数据列表
+ */
+ public static List getDictCache (String key) {
+ JSONArray arrayCache = SpringUtils.getBean(RedisService.class).getCacheObject(getCacheKey(key));
+ if (StringUtils.isNotNull(arrayCache)) {
+ return arrayCache.toList(SysDictData.class);
+ }
+ return null;
+ }
+
+ /**
+ * 删除指定字典缓存
+ *
+ * @param key 字典键
+ */
+ public static void removeDictCache (String key) {
+ SpringUtils.getBean(RedisService.class).deleteObject(getCacheKey(key));
+ }
+
+ /**
+ * 清空字典缓存
+ */
+ public static void clearDictCache () {
+ Collection keys = SpringUtils.getBean(RedisService.class).keys(CacheConstants.SYS_DICT_KEY + "*");
+ SpringUtils.getBean(RedisService.class).deleteObject(keys);
+ }
+
+ /**
+ * 设置cache key
+ *
+ * @param configKey 参数键
+ *
+ * @return 缓存键key
+ */
+ public static String getCacheKey (String configKey) {
+ return CacheConstants.SYS_DICT_KEY + configKey;
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/utils/SecurityUtils.java b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/utils/SecurityUtils.java
new file mode 100644
index 0000000..e7f2a2c
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/java/com/muyu/common/security/utils/SecurityUtils.java
@@ -0,0 +1,109 @@
+package com.muyu.common.security.utils;
+
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.core.constant.TokenConstants;
+import com.muyu.common.core.context.SecurityContextHolder;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.system.domain.LoginUser;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 权限获取工具类
+ *
+ * @author muyu
+ */
+public class SecurityUtils {
+ /**
+ * 获取用户ID
+ */
+ public static Long getUserId () {
+ return SecurityContextHolder.getUserId();
+ }
+
+ /**
+ * 获取用户名称
+ */
+ public static String getUsername () {
+ return SecurityContextHolder.getUserName();
+ }
+
+ /**
+ * 获取用户key
+ */
+ public static String getUserKey () {
+ return SecurityContextHolder.getUserKey();
+ }
+
+ /**
+ * 获取登录用户信息
+ */
+ public static LoginUser getLoginUser () {
+ return SecurityContextHolder.get(SecurityConstants.LOGIN_USER, LoginUser.class);
+ }
+
+ /**
+ * 获取请求token
+ */
+ public static String getToken () {
+ return getToken(ServletUtils.getRequest());
+ }
+
+ /**
+ * 根据request获取请求token
+ */
+ public static String getToken (HttpServletRequest request) {
+ // 从header获取token标识
+ String token = request.getHeader(TokenConstants.AUTHENTICATION);
+ return replaceTokenPrefix(token);
+ }
+
+ /**
+ * 裁剪token前缀
+ */
+ public static String replaceTokenPrefix (String token) {
+ // 如果前端设置了令牌前缀,则裁剪掉前缀
+ if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
+ token = token.replaceFirst(TokenConstants.PREFIX, "");
+ }
+ return token;
+ }
+
+ /**
+ * 是否为管理员
+ *
+ * @param userId 用户ID
+ *
+ * @return 结果
+ */
+ public static boolean isAdmin (Long userId) {
+ return userId != null && 1L == userId;
+ }
+
+ /**
+ * 生成BCryptPasswordEncoder密码
+ *
+ * @param password 密码
+ *
+ * @return 加密字符串
+ */
+ public static String encryptPassword (String password) {
+ BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+ return passwordEncoder.encode(password);
+ }
+
+ /**
+ * 判断密码是否相同
+ *
+ * @param rawPassword 真实密码
+ * @param encodedPassword 加密后字符
+ *
+ * @return 结果
+ */
+ public static boolean matchesPassword (String rawPassword, String encodedPassword) {
+ BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+ return passwordEncoder.matches(rawPassword, encodedPassword);
+ }
+}
diff --git a/muyu-common/muyu-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/muyu-common/muyu-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..f4e76b1
--- /dev/null
+++ b/muyu-common/muyu-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,5 @@
+com.muyu.common.security.config.WebMvcConfig
+com.muyu.common.security.service.TokenService
+com.muyu.common.security.aspect.PreAuthorizeAspect
+com.muyu.common.security.aspect.InnerAuthAspect
+com.muyu.common.security.handler.GlobalExceptionHandler
diff --git a/muyu-common/muyu-common-swagger/pom.xml b/muyu-common/muyu-common-swagger/pom.xml
new file mode 100644
index 0000000..750bd3a
--- /dev/null
+++ b/muyu-common/muyu-common-swagger/pom.xml
@@ -0,0 +1,34 @@
+
+
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+ 4.0.0
+
+ muyu-common-swagger
+
+
+ muyu-common-swagger系统接口
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ io.springfox
+ springfox-swagger2
+ ${swagger.fox.version}
+
+
+
+
diff --git a/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/annotation/EnableCustomSwagger2.java b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/annotation/EnableCustomSwagger2.java
new file mode 100644
index 0000000..8b56946
--- /dev/null
+++ b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/annotation/EnableCustomSwagger2.java
@@ -0,0 +1,15 @@
+package com.muyu.common.swagger.annotation;
+
+import com.muyu.common.swagger.config.SwaggerAutoConfiguration;
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import({SwaggerAutoConfiguration.class})
+public @interface EnableCustomSwagger2 {
+
+}
diff --git a/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerAutoConfiguration.java b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerAutoConfiguration.java
new file mode 100644
index 0000000..a58a108
--- /dev/null
+++ b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerAutoConfiguration.java
@@ -0,0 +1,111 @@
+package com.muyu.common.swagger.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.*;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.ApiSelectorBuilder;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+
+@Configuration
+@EnableSwagger2
+@EnableConfigurationProperties(SwaggerProperties.class)
+@ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true)
+@Import({SwaggerBeanPostProcessor.class, SwaggerWebConfiguration.class})
+public class SwaggerAutoConfiguration {
+ /**
+ * 默认的排除路径,排除Spring Boot默认的错误处理路径和端点
+ */
+ private static final List DEFAULT_EXCLUDE_PATH = Arrays.asList("/error", "/actuator/**");
+
+ private static final String BASE_PATH = "/**";
+
+ @Bean
+ public Docket api (SwaggerProperties swaggerProperties) {
+ // base-path处理
+ if (swaggerProperties.getBasePath().isEmpty()) {
+ swaggerProperties.getBasePath().add(BASE_PATH);
+ }
+ // noinspection unchecked
+ List> basePath = new ArrayList>();
+ swaggerProperties.getBasePath().forEach(path -> basePath.add(PathSelectors.ant(path)));
+
+ // exclude-path处理
+ if (swaggerProperties.getExcludePath().isEmpty()) {
+ swaggerProperties.getExcludePath().addAll(DEFAULT_EXCLUDE_PATH);
+ }
+
+ List> excludePath = new ArrayList<>();
+ swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path)));
+
+ ApiSelectorBuilder builder = new Docket(DocumentationType.SWAGGER_2).host(swaggerProperties.getHost())
+ .apiInfo(apiInfo(swaggerProperties)).select()
+ .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()));
+
+ swaggerProperties.getBasePath().forEach(p -> builder.paths(PathSelectors.ant(p)));
+ swaggerProperties.getExcludePath().forEach(p -> builder.paths(PathSelectors.ant(p).negate()));
+
+ return builder.build().securitySchemes(securitySchemes()).securityContexts(securityContexts()).pathMapping("/");
+ }
+
+ /**
+ * 安全模式,这里指定token通过Authorization头请求头传递
+ */
+ private List securitySchemes () {
+ List apiKeyList = new ArrayList();
+ apiKeyList.add(new ApiKey("Authorization", "Authorization", "header"));
+ return apiKeyList;
+ }
+
+ /**
+ * 安全上下文
+ */
+ private List securityContexts () {
+ List securityContexts = new ArrayList<>();
+ securityContexts.add(
+ SecurityContext.builder()
+ .securityReferences(defaultAuth())
+ .operationSelector(o -> o.requestMappingPattern().matches("/.*"))
+ .build());
+ return securityContexts;
+ }
+
+ /**
+ * 默认的全局鉴权策略
+ *
+ * @return
+ */
+ private List defaultAuth () {
+ AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+ AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+ authorizationScopes[0] = authorizationScope;
+ List securityReferences = new ArrayList<>();
+ securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+ return securityReferences;
+ }
+
+ private ApiInfo apiInfo (SwaggerProperties swaggerProperties) {
+ return new ApiInfoBuilder()
+ .title(swaggerProperties.getTitle())
+ .description(swaggerProperties.getDescription())
+ .license(swaggerProperties.getLicense())
+ .licenseUrl(swaggerProperties.getLicenseUrl())
+ .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
+ .contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail()))
+ .version(swaggerProperties.getVersion())
+ .build();
+ }
+}
diff --git a/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerBeanPostProcessor.java b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerBeanPostProcessor.java
new file mode 100644
index 0000000..13d252f
--- /dev/null
+++ b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerBeanPostProcessor.java
@@ -0,0 +1,45 @@
+package com.muyu.common.swagger.config;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
+import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
+import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * swagger 在 springboot 2.6.x 不兼容问题的处理
+ *
+ * @author muyu
+ */
+public class SwaggerBeanPostProcessor implements BeanPostProcessor {
+ @Override
+ public Object postProcessAfterInitialization (Object bean, String beanName) throws BeansException {
+ if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
+ customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
+ }
+ return bean;
+ }
+
+ private void customizeSpringfoxHandlerMappings (List mappings) {
+ List copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null)
+ .collect(Collectors.toList());
+ mappings.clear();
+ mappings.addAll(copy);
+ }
+
+ @SuppressWarnings("unchecked")
+ private List getHandlerMappings (Object bean) {
+ try {
+ Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
+ field.setAccessible(true);
+ return (List) field.get(bean);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerProperties.java b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerProperties.java
new file mode 100644
index 0000000..a0dac64
--- /dev/null
+++ b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerProperties.java
@@ -0,0 +1,296 @@
+package com.muyu.common.swagger.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@ConfigurationProperties("swagger")
+public class SwaggerProperties {
+ /**
+ * 是否开启swagger
+ */
+ private Boolean enabled;
+
+ /**
+ * swagger会解析的包路径
+ **/
+ private String basePackage = "";
+
+ /**
+ * swagger会解析的url规则
+ **/
+ private List basePath = new ArrayList<>();
+
+ /**
+ * 在basePath基础上需要排除的url规则
+ **/
+ private List excludePath = new ArrayList<>();
+
+ /**
+ * 标题
+ **/
+ private String title = "";
+
+ /**
+ * 描述
+ **/
+ private String description = "";
+
+ /**
+ * 版本
+ **/
+ private String version = "";
+
+ /**
+ * 许可证
+ **/
+ private String license = "";
+
+ /**
+ * 许可证URL
+ **/
+ private String licenseUrl = "";
+
+ /**
+ * 服务条款URL
+ **/
+ private String termsOfServiceUrl = "";
+
+ /**
+ * host信息
+ **/
+ private String host = "";
+
+ /**
+ * 联系人信息
+ */
+ private Contact contact = new Contact();
+
+ /**
+ * 全局统一鉴权配置
+ **/
+ private Authorization authorization = new Authorization();
+
+ public Boolean getEnabled () {
+ return enabled;
+ }
+
+ public void setEnabled (Boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getBasePackage () {
+ return basePackage;
+ }
+
+ public void setBasePackage (String basePackage) {
+ this.basePackage = basePackage;
+ }
+
+ public List getBasePath () {
+ return basePath;
+ }
+
+ public void setBasePath (List basePath) {
+ this.basePath = basePath;
+ }
+
+ public List getExcludePath () {
+ return excludePath;
+ }
+
+ public void setExcludePath (List excludePath) {
+ this.excludePath = excludePath;
+ }
+
+ public String getTitle () {
+ return title;
+ }
+
+ public void setTitle (String title) {
+ this.title = title;
+ }
+
+ public String getDescription () {
+ return description;
+ }
+
+ public void setDescription (String description) {
+ this.description = description;
+ }
+
+ public String getVersion () {
+ return version;
+ }
+
+ public void setVersion (String version) {
+ this.version = version;
+ }
+
+ public String getLicense () {
+ return license;
+ }
+
+ public void setLicense (String license) {
+ this.license = license;
+ }
+
+ public String getLicenseUrl () {
+ return licenseUrl;
+ }
+
+ public void setLicenseUrl (String licenseUrl) {
+ this.licenseUrl = licenseUrl;
+ }
+
+ public String getTermsOfServiceUrl () {
+ return termsOfServiceUrl;
+ }
+
+ public void setTermsOfServiceUrl (String termsOfServiceUrl) {
+ this.termsOfServiceUrl = termsOfServiceUrl;
+ }
+
+ public String getHost () {
+ return host;
+ }
+
+ public void setHost (String host) {
+ this.host = host;
+ }
+
+ public Contact getContact () {
+ return contact;
+ }
+
+ public void setContact (Contact contact) {
+ this.contact = contact;
+ }
+
+ public Authorization getAuthorization () {
+ return authorization;
+ }
+
+ public void setAuthorization (Authorization authorization) {
+ this.authorization = authorization;
+ }
+
+ public static class Contact {
+ /**
+ * 联系人
+ **/
+ private String name = "";
+ /**
+ * 联系人url
+ **/
+ private String url = "";
+ /**
+ * 联系人email
+ **/
+ private String email = "";
+
+ public String getName () {
+ return name;
+ }
+
+ public void setName (String name) {
+ this.name = name;
+ }
+
+ public String getUrl () {
+ return url;
+ }
+
+ public void setUrl (String url) {
+ this.url = url;
+ }
+
+ public String getEmail () {
+ return email;
+ }
+
+ public void setEmail (String email) {
+ this.email = email;
+ }
+ }
+
+ public static class Authorization {
+ /**
+ * 鉴权策略ID,需要和SecurityReferences ID保持一致
+ */
+ private String name = "";
+
+ /**
+ * 需要开启鉴权URL的正则
+ */
+ private String authRegex = "^.*$";
+
+ /**
+ * 鉴权作用域列表
+ */
+ private List authorizationScopeList = new ArrayList<>();
+
+ private List tokenUrlList = new ArrayList<>();
+
+ public String getName () {
+ return name;
+ }
+
+ public void setName (String name) {
+ this.name = name;
+ }
+
+ public String getAuthRegex () {
+ return authRegex;
+ }
+
+ public void setAuthRegex (String authRegex) {
+ this.authRegex = authRegex;
+ }
+
+ public List getAuthorizationScopeList () {
+ return authorizationScopeList;
+ }
+
+ public void setAuthorizationScopeList (List authorizationScopeList) {
+ this.authorizationScopeList = authorizationScopeList;
+ }
+
+ public List getTokenUrlList () {
+ return tokenUrlList;
+ }
+
+ public void setTokenUrlList (List tokenUrlList) {
+ this.tokenUrlList = tokenUrlList;
+ }
+ }
+
+ public static class AuthorizationScope {
+ /**
+ * 作用域名称
+ */
+ private String scope = "";
+
+ /**
+ * 作用域描述
+ */
+ private String description = "";
+
+ public String getScope () {
+ return scope;
+ }
+
+ public void setScope (String scope) {
+ this.scope = scope;
+ }
+
+ public String getDescription () {
+ return description;
+ }
+
+ public void setDescription (String description) {
+ this.description = description;
+ }
+ }
+}
diff --git a/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerWebConfiguration.java b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerWebConfiguration.java
new file mode 100644
index 0000000..48d66f6
--- /dev/null
+++ b/muyu-common/muyu-common-swagger/src/main/java/com/muyu/common/swagger/config/SwaggerWebConfiguration.java
@@ -0,0 +1,18 @@
+package com.muyu.common.swagger.config;
+
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * swagger 资源映射路径
+ *
+ * @author muyu
+ */
+public class SwaggerWebConfiguration implements WebMvcConfigurer {
+ @Override
+ public void addResourceHandlers (ResourceHandlerRegistry registry) {
+ /** swagger-ui 地址 */
+ registry.addResourceHandler("/swagger-ui/**")
+ .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
+ }
+}
diff --git a/muyu-common/muyu-common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/muyu-common/muyu-common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..b15b749
--- /dev/null
+++ b/muyu-common/muyu-common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,3 @@
+# com.muyu.common.swagger.config.SwaggerAutoConfiguration
+# com.muyu.common.swagger.config.SwaggerWebConfiguration
+# com.muyu.common.swagger.config.SwaggerBeanPostProcessor
diff --git a/muyu-common/muyu-common-system/pom.xml b/muyu-common/muyu-common-system/pom.xml
new file mode 100644
index 0000000..62fdc74
--- /dev/null
+++ b/muyu-common/muyu-common-system/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+ com.muyu
+ muyu-common
+ 3.6.3
+
+
+ muyu-common-system
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+
+ com.muyu
+ muyu-common-core
+
+
+
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/LoginUser.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/LoginUser.java
new file mode 100644
index 0000000..ce39e96
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/LoginUser.java
@@ -0,0 +1,131 @@
+package com.muyu.common.system.domain;
+
+
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * 用户信息
+ *
+ * @author muyu
+ */
+public class LoginUser implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户唯一标识
+ */
+ private String token;
+
+ /**
+ * 用户名id
+ */
+ private Long userid;
+
+ /**
+ * 用户名
+ */
+ private String username;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+ /**
+ * 过期时间
+ */
+ private Long expireTime;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 权限列表
+ */
+ private Set permissions;
+
+ /**
+ * 角色列表
+ */
+ private Set roles;
+
+ /**
+ * 用户信息
+ */
+ private SysUser sysUser;
+
+ public String getToken () {
+ return token;
+ }
+
+ public void setToken (String token) {
+ this.token = token;
+ }
+
+ public Long getUserid () {
+ return userid;
+ }
+
+ public void setUserid (Long userid) {
+ this.userid = userid;
+ }
+
+ public String getUsername () {
+ return username;
+ }
+
+ public void setUsername (String username) {
+ this.username = username;
+ }
+
+ public Long getLoginTime () {
+ return loginTime;
+ }
+
+ public void setLoginTime (Long loginTime) {
+ this.loginTime = loginTime;
+ }
+
+ public Long getExpireTime () {
+ return expireTime;
+ }
+
+ public void setExpireTime (Long expireTime) {
+ this.expireTime = expireTime;
+ }
+
+ public String getIpaddr () {
+ return ipaddr;
+ }
+
+ public void setIpaddr (String ipaddr) {
+ this.ipaddr = ipaddr;
+ }
+
+ public Set getPermissions () {
+ return permissions;
+ }
+
+ public void setPermissions (Set permissions) {
+ this.permissions = permissions;
+ }
+
+ public Set getRoles () {
+ return roles;
+ }
+
+ public void setRoles (Set roles) {
+ this.roles = roles;
+ }
+
+ public SysUser getSysUser () {
+ return sysUser;
+ }
+
+ public void setSysUser (SysUser sysUser) {
+ this.sysUser = sysUser;
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDept.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDept.java
new file mode 100644
index 0000000..9569d64
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDept.java
@@ -0,0 +1,213 @@
+package com.muyu.common.system.domain;
+
+import com.muyu.common.core.web.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 部门表 sys_dept
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysDept extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 父部门ID
+ */
+ private Long parentId;
+
+ /**
+ * 祖级列表
+ */
+ private String ancestors;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+ /**
+ * 显示顺序
+ */
+ private Integer orderNum;
+
+ /**
+ * 负责人
+ */
+ private String leader;
+
+ /**
+ * 联系电话
+ */
+ private String phone;
+
+ /**
+ * 邮箱
+ */
+ private String email;
+
+ /**
+ * 部门状态:0正常,1停用
+ */
+ private String status;
+
+ /**
+ * 删除标志(0代表存在 2代表删除)
+ */
+ private String delFlag;
+
+ /**
+ * 父部门名称
+ */
+ private String parentName;
+
+ /**
+ * 子部门
+ */
+ private List children = new ArrayList();
+
+ public Long getDeptId () {
+ return deptId;
+ }
+
+ public void setDeptId (Long deptId) {
+ this.deptId = deptId;
+ }
+
+ public Long getParentId () {
+ return parentId;
+ }
+
+ public void setParentId (Long parentId) {
+ this.parentId = parentId;
+ }
+
+ public String getAncestors () {
+ return ancestors;
+ }
+
+ public void setAncestors (String ancestors) {
+ this.ancestors = ancestors;
+ }
+
+ @NotBlank(message = "部门名称不能为空")
+ @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符")
+ public String getDeptName () {
+ return deptName;
+ }
+
+ public void setDeptName (String deptName) {
+ this.deptName = deptName;
+ }
+
+ @NotNull(message = "显示顺序不能为空")
+ public Integer getOrderNum () {
+ return orderNum;
+ }
+
+ public void setOrderNum (Integer orderNum) {
+ this.orderNum = orderNum;
+ }
+
+ public String getLeader () {
+ return leader;
+ }
+
+ public void setLeader (String leader) {
+ this.leader = leader;
+ }
+
+ @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符")
+ public String getPhone () {
+ return phone;
+ }
+
+ public void setPhone (String phone) {
+ this.phone = phone;
+ }
+
+ @Email(message = "邮箱格式不正确")
+ @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
+ public String getEmail () {
+ return email;
+ }
+
+ public void setEmail (String email) {
+ this.email = email;
+ }
+
+ public String getStatus () {
+ return status;
+ }
+
+ public void setStatus (String status) {
+ this.status = status;
+ }
+
+ public String getDelFlag () {
+ return delFlag;
+ }
+
+ public void setDelFlag (String delFlag) {
+ this.delFlag = delFlag;
+ }
+
+ public String getParentName () {
+ return parentName;
+ }
+
+ public void setParentName (String parentName) {
+ this.parentName = parentName;
+ }
+
+ public List getChildren () {
+ return children;
+ }
+
+ public void setChildren (List children) {
+ this.children = children;
+ }
+
+ @Override
+ public String toString () {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("deptId", getDeptId())
+ .append("parentId", getParentId())
+ .append("ancestors", getAncestors())
+ .append("deptName", getDeptName())
+ .append("orderNum", getOrderNum())
+ .append("leader", getLeader())
+ .append("phone", getPhone())
+ .append("email", getEmail())
+ .append("status", getStatus())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .toString();
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDictData.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDictData.java
new file mode 100644
index 0000000..43d28eb
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDictData.java
@@ -0,0 +1,185 @@
+package com.muyu.common.system.domain;
+
+import com.muyu.common.core.annotation.Excel;
+import com.muyu.common.core.annotation.Excel.ColumnType;
+import com.muyu.common.core.constant.UserConstants;
+import com.muyu.common.core.web.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+/**
+ * 字典数据表 sys_dict_data
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysDictData extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字典编码
+ */
+ @Excel(name = "字典编码", cellType = ColumnType.NUMERIC)
+ private Long dictCode;
+
+ /**
+ * 字典排序
+ */
+ @Excel(name = "字典排序", cellType = ColumnType.NUMERIC)
+ private Long dictSort;
+
+ /**
+ * 字典标签
+ */
+ @Excel(name = "字典标签")
+ private String dictLabel;
+
+ /**
+ * 字典键值
+ */
+ @Excel(name = "字典键值")
+ private String dictValue;
+
+ /**
+ * 字典类型
+ */
+ @Excel(name = "字典类型")
+ private String dictType;
+
+ /**
+ * 样式属性(其他样式扩展)
+ */
+ private String cssClass;
+
+ /**
+ * 表格字典样式
+ */
+ private String listClass;
+
+ /**
+ * 是否默认(Y是 N否)
+ */
+ @Excel(name = "是否默认", readConverterExp = "Y=是,N=否")
+ private String isDefault;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ public Long getDictCode () {
+ return dictCode;
+ }
+
+ public void setDictCode (Long dictCode) {
+ this.dictCode = dictCode;
+ }
+
+ public Long getDictSort () {
+ return dictSort;
+ }
+
+ public void setDictSort (Long dictSort) {
+ this.dictSort = dictSort;
+ }
+
+ @NotBlank(message = "字典标签不能为空")
+ @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符")
+ public String getDictLabel () {
+ return dictLabel;
+ }
+
+ public void setDictLabel (String dictLabel) {
+ this.dictLabel = dictLabel;
+ }
+
+ @NotBlank(message = "字典键值不能为空")
+ @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符")
+ public String getDictValue () {
+ return dictValue;
+ }
+
+ public void setDictValue (String dictValue) {
+ this.dictValue = dictValue;
+ }
+
+ @NotBlank(message = "字典类型不能为空")
+ @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符")
+ public String getDictType () {
+ return dictType;
+ }
+
+ public void setDictType (String dictType) {
+ this.dictType = dictType;
+ }
+
+ @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符")
+ public String getCssClass () {
+ return cssClass;
+ }
+
+ public void setCssClass (String cssClass) {
+ this.cssClass = cssClass;
+ }
+
+ public String getListClass () {
+ return listClass;
+ }
+
+ public void setListClass (String listClass) {
+ this.listClass = listClass;
+ }
+
+ public boolean getDefault () {
+ return UserConstants.YES.equals(this.isDefault);
+ }
+
+ public String getIsDefault () {
+ return isDefault;
+ }
+
+ public void setIsDefault (String isDefault) {
+ this.isDefault = isDefault;
+ }
+
+ public String getStatus () {
+ return status;
+ }
+
+ public void setStatus (String status) {
+ this.status = status;
+ }
+
+ @Override
+ public String toString () {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("dictCode", getDictCode())
+ .append("dictSort", getDictSort())
+ .append("dictLabel", getDictLabel())
+ .append("dictValue", getDictValue())
+ .append("dictType", getDictType())
+ .append("cssClass", getCssClass())
+ .append("listClass", getListClass())
+ .append("isDefault", getIsDefault())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDictType.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDictType.java
new file mode 100644
index 0000000..e87832d
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysDictType.java
@@ -0,0 +1,106 @@
+package com.muyu.common.system.domain;
+
+import com.muyu.common.core.annotation.Excel;
+import com.muyu.common.core.annotation.Excel.ColumnType;
+import com.muyu.common.core.web.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * 字典类型表 sys_dict_type
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysDictType extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字典主键
+ */
+ @Excel(name = "字典主键", cellType = ColumnType.NUMERIC)
+ private Long dictId;
+
+ /**
+ * 字典名称
+ */
+ @Excel(name = "字典名称")
+ private String dictName;
+
+ /**
+ * 字典类型
+ */
+ @Excel(name = "字典类型")
+ private String dictType;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ public Long getDictId () {
+ return dictId;
+ }
+
+ public void setDictId (Long dictId) {
+ this.dictId = dictId;
+ }
+
+ @NotBlank(message = "字典名称不能为空")
+ @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符")
+ public String getDictName () {
+ return dictName;
+ }
+
+ public void setDictName (String dictName) {
+ this.dictName = dictName;
+ }
+
+ @NotBlank(message = "字典类型不能为空")
+ @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符")
+ @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)")
+ public String getDictType () {
+ return dictType;
+ }
+
+ public void setDictType (String dictType) {
+ this.dictType = dictType;
+ }
+
+ public String getStatus () {
+ return status;
+ }
+
+ public void setStatus (String status) {
+ this.status = status;
+ }
+
+ @Override
+ public String toString () {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("dictId", getDictId())
+ .append("dictName", getDictName())
+ .append("dictType", getDictType())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysFile.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysFile.java
new file mode 100644
index 0000000..38918d7
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysFile.java
@@ -0,0 +1,45 @@
+package com.muyu.common.system.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 文件信息
+ *
+ * @author muyu
+ */
+public class SysFile {
+ /**
+ * 文件名称
+ */
+ private String name;
+
+ /**
+ * 文件地址
+ */
+ private String url;
+
+ public String getName () {
+ return name;
+ }
+
+ public void setName (String name) {
+ this.name = name;
+ }
+
+ public String getUrl () {
+ return url;
+ }
+
+ public void setUrl (String url) {
+ this.url = url;
+ }
+
+ @Override
+ public String toString () {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("name", getName())
+ .append("url", getUrl())
+ .toString();
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysLogininfor.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysLogininfor.java
new file mode 100644
index 0000000..9ad179f
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysLogininfor.java
@@ -0,0 +1,112 @@
+package com.muyu.common.system.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.muyu.common.core.annotation.Excel;
+import com.muyu.common.core.annotation.Excel.ColumnType;
+import com.muyu.common.core.web.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.util.Date;
+
+/**
+ * 系统访问记录表 sys_logininfor
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysLogininfor extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * ID
+ */
+ @Excel(name = "序号", cellType = ColumnType.NUMERIC)
+ private Long infoId;
+
+ /**
+ * 用户账号
+ */
+ @Excel(name = "用户账号")
+ private String userName;
+
+ /**
+ * 状态 0成功 1失败
+ */
+ @Excel(name = "状态", readConverterExp = "0=成功,1=失败")
+ private String status;
+
+ /**
+ * 地址
+ */
+ @Excel(name = "地址")
+ private String ipaddr;
+
+ /**
+ * 描述
+ */
+ @Excel(name = "描述")
+ private String msg;
+
+ /**
+ * 访问时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date accessTime;
+
+ public Long getInfoId () {
+ return infoId;
+ }
+
+ public void setInfoId (Long infoId) {
+ this.infoId = infoId;
+ }
+
+ public String getUserName () {
+ return userName;
+ }
+
+ public void setUserName (String userName) {
+ this.userName = userName;
+ }
+
+ public String getStatus () {
+ return status;
+ }
+
+ public void setStatus (String status) {
+ this.status = status;
+ }
+
+ public String getIpaddr () {
+ return ipaddr;
+ }
+
+ public void setIpaddr (String ipaddr) {
+ this.ipaddr = ipaddr;
+ }
+
+ public String getMsg () {
+ return msg;
+ }
+
+ public void setMsg (String msg) {
+ this.msg = msg;
+ }
+
+ public Date getAccessTime () {
+ return accessTime;
+ }
+
+ public void setAccessTime (Date accessTime) {
+ this.accessTime = accessTime;
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysOperLog.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysOperLog.java
new file mode 100644
index 0000000..c82fbb6
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysOperLog.java
@@ -0,0 +1,265 @@
+package com.muyu.common.system.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.muyu.common.core.annotation.Excel;
+import com.muyu.common.core.annotation.Excel.ColumnType;
+import com.muyu.common.core.web.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.util.Date;
+
+/**
+ * 操作日志记录表 oper_log
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysOperLog extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 日志主键
+ */
+ @Excel(name = "操作序号", cellType = ColumnType.NUMERIC)
+ private Long operId;
+
+ /**
+ * 操作模块
+ */
+ @Excel(name = "操作模块")
+ private String title;
+
+ /**
+ * 业务类型(0其它 1新增 2修改 3删除)
+ */
+ @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据")
+ private Integer businessType;
+
+ /**
+ * 业务类型数组
+ */
+ private Integer[] businessTypes;
+
+ /**
+ * 请求方法
+ */
+ @Excel(name = "请求方法")
+ private String method;
+
+ /**
+ * 请求方式
+ */
+ @Excel(name = "请求方式")
+ private String requestMethod;
+
+ /**
+ * 操作类别(0其它 1后台用户 2手机端用户)
+ */
+ @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户")
+ private Integer operatorType;
+
+ /**
+ * 操作人员
+ */
+ @Excel(name = "操作人员")
+ private String operName;
+
+ /**
+ * 部门名称
+ */
+ @Excel(name = "部门名称")
+ private String deptName;
+
+ /**
+ * 请求url
+ */
+ @Excel(name = "请求地址")
+ private String operUrl;
+
+ /**
+ * 操作地址
+ */
+ @Excel(name = "操作地址")
+ private String operIp;
+
+ /**
+ * 请求参数
+ */
+ @Excel(name = "请求参数")
+ private String operParam;
+
+ /**
+ * 返回参数
+ */
+ @Excel(name = "返回参数")
+ private String jsonResult;
+
+ /**
+ * 操作状态(0正常 1异常)
+ */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=异常")
+ private Integer status;
+
+ /**
+ * 错误消息
+ */
+ @Excel(name = "错误消息")
+ private String errorMsg;
+
+ /**
+ * 操作时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date operTime;
+
+ /**
+ * 消耗时间
+ */
+ @Excel(name = "消耗时间", suffix = "毫秒")
+ private Long costTime;
+
+ public Long getOperId () {
+ return operId;
+ }
+
+ public void setOperId (Long operId) {
+ this.operId = operId;
+ }
+
+ public String getTitle () {
+ return title;
+ }
+
+ public void setTitle (String title) {
+ this.title = title;
+ }
+
+ public Integer getBusinessType () {
+ return businessType;
+ }
+
+ public void setBusinessType (Integer businessType) {
+ this.businessType = businessType;
+ }
+
+ public Integer[] getBusinessTypes () {
+ return businessTypes;
+ }
+
+ public void setBusinessTypes (Integer[] businessTypes) {
+ this.businessTypes = businessTypes;
+ }
+
+ public String getMethod () {
+ return method;
+ }
+
+ public void setMethod (String method) {
+ this.method = method;
+ }
+
+ public String getRequestMethod () {
+ return requestMethod;
+ }
+
+ public void setRequestMethod (String requestMethod) {
+ this.requestMethod = requestMethod;
+ }
+
+ public Integer getOperatorType () {
+ return operatorType;
+ }
+
+ public void setOperatorType (Integer operatorType) {
+ this.operatorType = operatorType;
+ }
+
+ public String getOperName () {
+ return operName;
+ }
+
+ public void setOperName (String operName) {
+ this.operName = operName;
+ }
+
+ public String getDeptName () {
+ return deptName;
+ }
+
+ public void setDeptName (String deptName) {
+ this.deptName = deptName;
+ }
+
+ public String getOperUrl () {
+ return operUrl;
+ }
+
+ public void setOperUrl (String operUrl) {
+ this.operUrl = operUrl;
+ }
+
+ public String getOperIp () {
+ return operIp;
+ }
+
+ public void setOperIp (String operIp) {
+ this.operIp = operIp;
+ }
+
+ public String getOperParam () {
+ return operParam;
+ }
+
+ public void setOperParam (String operParam) {
+ this.operParam = operParam;
+ }
+
+ public String getJsonResult () {
+ return jsonResult;
+ }
+
+ public void setJsonResult (String jsonResult) {
+ this.jsonResult = jsonResult;
+ }
+
+ public Integer getStatus () {
+ return status;
+ }
+
+ public void setStatus (Integer status) {
+ this.status = status;
+ }
+
+ public String getErrorMsg () {
+ return errorMsg;
+ }
+
+ public void setErrorMsg (String errorMsg) {
+ this.errorMsg = errorMsg;
+ }
+
+ public Date getOperTime () {
+ return operTime;
+ }
+
+ public void setOperTime (Date operTime) {
+ this.operTime = operTime;
+ }
+
+ public Long getCostTime () {
+ return costTime;
+ }
+
+ public void setCostTime (Long costTime) {
+ this.costTime = costTime;
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysRole.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysRole.java
new file mode 100644
index 0000000..09fda22
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysRole.java
@@ -0,0 +1,244 @@
+package com.muyu.common.system.domain;
+
+import com.muyu.common.core.annotation.Excel;
+import com.muyu.common.core.annotation.Excel.ColumnType;
+import com.muyu.common.core.web.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.Set;
+
+/**
+ * 角色表 sys_role
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysRole extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 角色ID
+ */
+ @Excel(name = "角色序号", cellType = ColumnType.NUMERIC)
+ private Long roleId;
+
+ /**
+ * 角色名称
+ */
+ @Excel(name = "角色名称")
+ private String roleName;
+
+ /**
+ * 角色权限
+ */
+ @Excel(name = "角色权限")
+ private String roleKey;
+
+ /**
+ * 角色排序
+ */
+ @Excel(name = "角色排序")
+ private Integer roleSort;
+
+ /**
+ * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+ */
+ @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限")
+ private String dataScope;
+
+ /**
+ * 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示)
+ */
+ private boolean menuCheckStrictly;
+
+ /**
+ * 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 )
+ */
+ private boolean deptCheckStrictly;
+
+ /**
+ * 角色状态(0正常 1停用)
+ */
+ @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ /**
+ * 删除标志(0代表存在 2代表删除)
+ */
+ private String delFlag;
+
+ /**
+ * 用户是否存在此角色标识 默认不存在
+ */
+ private boolean flag = false;
+
+ /**
+ * 菜单组
+ */
+ private Long[] menuIds;
+
+ /**
+ * 部门组(数据权限)
+ */
+ private Long[] deptIds;
+
+ /**
+ * 角色菜单权限
+ */
+ private Set permissions;
+
+
+ public SysRole (Long roleId) {
+ this.roleId = roleId;
+ }
+
+ public static boolean isAdmin (Long roleId) {
+ return roleId != null && 1L == roleId;
+ }
+
+ public Long getRoleId () {
+ return roleId;
+ }
+
+ public void setRoleId (Long roleId) {
+ this.roleId = roleId;
+ }
+
+ public boolean isAdmin () {
+ return isAdmin(this.roleId);
+ }
+
+ @NotBlank(message = "角色名称不能为空")
+ @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符")
+ public String getRoleName () {
+ return roleName;
+ }
+
+ public void setRoleName (String roleName) {
+ this.roleName = roleName;
+ }
+
+ @NotBlank(message = "权限字符不能为空")
+ @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符")
+ public String getRoleKey () {
+ return roleKey;
+ }
+
+ public void setRoleKey (String roleKey) {
+ this.roleKey = roleKey;
+ }
+
+ @NotNull(message = "显示顺序不能为空")
+ public Integer getRoleSort () {
+ return roleSort;
+ }
+
+ public void setRoleSort (Integer roleSort) {
+ this.roleSort = roleSort;
+ }
+
+ public String getDataScope () {
+ return dataScope;
+ }
+
+ public void setDataScope (String dataScope) {
+ this.dataScope = dataScope;
+ }
+
+ public boolean isMenuCheckStrictly () {
+ return menuCheckStrictly;
+ }
+
+ public void setMenuCheckStrictly (boolean menuCheckStrictly) {
+ this.menuCheckStrictly = menuCheckStrictly;
+ }
+
+ public boolean isDeptCheckStrictly () {
+ return deptCheckStrictly;
+ }
+
+ public void setDeptCheckStrictly (boolean deptCheckStrictly) {
+ this.deptCheckStrictly = deptCheckStrictly;
+ }
+
+ public String getStatus () {
+ return status;
+ }
+
+ public void setStatus (String status) {
+ this.status = status;
+ }
+
+ public String getDelFlag () {
+ return delFlag;
+ }
+
+ public void setDelFlag (String delFlag) {
+ this.delFlag = delFlag;
+ }
+
+ public boolean isFlag () {
+ return flag;
+ }
+
+ public void setFlag (boolean flag) {
+ this.flag = flag;
+ }
+
+ public Long[] getMenuIds () {
+ return menuIds;
+ }
+
+ public void setMenuIds (Long[] menuIds) {
+ this.menuIds = menuIds;
+ }
+
+ public Long[] getDeptIds () {
+ return deptIds;
+ }
+
+ public void setDeptIds (Long[] deptIds) {
+ this.deptIds = deptIds;
+ }
+
+ public Set getPermissions () {
+ return permissions;
+ }
+
+ public void setPermissions (Set permissions) {
+ this.permissions = permissions;
+ }
+
+ @Override
+ public String toString () {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("roleId", getRoleId())
+ .append("roleName", getRoleName())
+ .append("roleKey", getRoleKey())
+ .append("roleSort", getRoleSort())
+ .append("dataScope", getDataScope())
+ .append("menuCheckStrictly", isMenuCheckStrictly())
+ .append("deptCheckStrictly", isDeptCheckStrictly())
+ .append("status", getStatus())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysUser.java b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysUser.java
new file mode 100644
index 0000000..6be7436
--- /dev/null
+++ b/muyu-common/muyu-common-system/src/main/java/com/muyu/common/system/domain/SysUser.java
@@ -0,0 +1,328 @@
+package com.muyu.common.system.domain;
+
+import com.muyu.common.core.annotation.Excel;
+import com.muyu.common.core.annotation.Excel.ColumnType;
+import com.muyu.common.core.annotation.Excel.Type;
+import com.muyu.common.core.annotation.Excels;
+import com.muyu.common.core.web.domain.BaseEntity;
+import com.muyu.common.core.xss.Xss;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户对象 sys_user
+ *
+ * @author muyu
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysUser extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户ID
+ */
+ @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号")
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ @Excel(name = "部门编号", type = Type.IMPORT)
+ private Long deptId;
+
+ /**
+ * 用户账号
+ */
+ @Excel(name = "登录名称")
+ private String userName;
+
+ /**
+ * 用户昵称
+ */
+ @Excel(name = "用户名称")
+ private String nickName;
+
+ /**
+ * 用户邮箱
+ */
+ @Excel(name = "用户邮箱")
+ private String email;
+
+ /**
+ * 手机号码
+ */
+ @Excel(name = "手机号码")
+ private String phonenumber;
+
+ /**
+ * 用户性别
+ */
+ @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
+ private String sex;
+
+ /**
+ * 用户头像
+ */
+ private String avatar;
+
+ /**
+ * 密码
+ */
+ private String password;
+
+ /**
+ * 帐号状态(0正常 1停用)
+ */
+ @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ /**
+ * 删除标志(0代表存在 2代表删除)
+ */
+ private String delFlag;
+
+ /**
+ * 最后登录IP
+ */
+ @Excel(name = "最后登录IP", type = Type.EXPORT)
+ private String loginIp;
+
+ /**
+ * 最后登录时间
+ */
+ @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)
+ private Date loginDate;
+
+ /**
+ * 部门对象
+ */
+ @Excels({
+ @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
+ @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
+ })
+ private SysDept dept;
+
+ /**
+ * 角色对象
+ */
+ private List roles;
+
+ /**
+ * 角色组
+ */
+ private Long[] roleIds;
+
+ /**
+ * 岗位组
+ */
+ private Long[] postIds;
+
+ /**
+ * 角色ID
+ */
+ private Long roleId;
+
+ public SysUser (Long userId) {
+ this.userId = userId;
+ }
+
+ public static boolean isAdmin (Long userId) {
+ return userId != null && 1L == userId;
+ }
+
+ public Long getUserId () {
+ return userId;
+ }
+
+ public void setUserId (Long userId) {
+ this.userId = userId;
+ }
+
+ public boolean isAdmin () {
+ return isAdmin(this.userId);
+ }
+
+ public Long getDeptId () {
+ return deptId;
+ }
+
+ public void setDeptId (Long deptId) {
+ this.deptId = deptId;
+ }
+
+ @Xss(message = "用户昵称不能包含脚本字符")
+ @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
+ public String getNickName () {
+ return nickName;
+ }
+
+ public void setNickName (String nickName) {
+ this.nickName = nickName;
+ }
+
+ @Xss(message = "用户账号不能包含脚本字符")
+ @NotBlank(message = "用户账号不能为空")
+ @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")
+ public String getUserName () {
+ return userName;
+ }
+
+ public void setUserName (String userName) {
+ this.userName = userName;
+ }
+
+ @Email(message = "邮箱格式不正确")
+ @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
+ public String getEmail () {
+ return email;
+ }
+
+ public void setEmail (String email) {
+ this.email = email;
+ }
+
+ @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符")
+ public String getPhonenumber () {
+ return phonenumber;
+ }
+
+ public void setPhonenumber (String phonenumber) {
+ this.phonenumber = phonenumber;
+ }
+
+ public String getSex () {
+ return sex;
+ }
+
+ public void setSex (String sex) {
+ this.sex = sex;
+ }
+
+ public String getAvatar () {
+ return avatar;
+ }
+
+ public void setAvatar (String avatar) {
+ this.avatar = avatar;
+ }
+
+ public String getPassword () {
+ return password;
+ }
+
+ public void setPassword (String password) {
+ this.password = password;
+ }
+
+ public String getStatus () {
+ return status;
+ }
+
+ public void setStatus (String status) {
+ this.status = status;
+ }
+
+ public String getDelFlag () {
+ return delFlag;
+ }
+
+ public void setDelFlag (String delFlag) {
+ this.delFlag = delFlag;
+ }
+
+ public String getLoginIp () {
+ return loginIp;
+ }
+
+ public void setLoginIp (String loginIp) {
+ this.loginIp = loginIp;
+ }
+
+ public Date getLoginDate () {
+ return loginDate;
+ }
+
+ public void setLoginDate (Date loginDate) {
+ this.loginDate = loginDate;
+ }
+
+ public SysDept getDept () {
+ return dept;
+ }
+
+ public void setDept (SysDept dept) {
+ this.dept = dept;
+ }
+
+ public List getRoles () {
+ return roles;
+ }
+
+ public void setRoles (List