permissions,
+ boolean accountNonExpired, boolean accountNonLocked,
+ boolean credentialsNonExpired, boolean enabled, String lawType) {
+ this.userId = userId;
+ this.phoneNum = phoneNum;
+ this.username = username;
+ this.password = password;
+ this.permissions = permissions;
+ this.accountNonExpired = accountNonExpired;
+ this.accountNonLocked = accountNonLocked;
+ this.credentialsNonExpired = credentialsNonExpired;
+ this.enabled = enabled;
+ this.lawType = lawType;
+ }
+}
diff --git a/src/main/java/com/metalloop/common/auth/userdetails/UserDetails.java b/src/main/java/com/metalloop/common/auth/userdetails/UserDetails.java
new file mode 100644
index 0000000..d1c3dc5
--- /dev/null
+++ b/src/main/java/com/metalloop/common/auth/userdetails/UserDetails.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.auth.userdetails;
+
+
+import com.metalloop.common.auth.permission.Permission;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * 提供核心用户信息,这些信息稍后会封装到Authentication对象中。这允许将与安
+ * 全无关的用户信息(例如电子邮件地址、电话号码等)存储在方便的位置。
+ * 具体的实现必须特别小心,以确保强制执行为每个方法详细说明的非空契约。
+ *
+ * @author zhaowenhao
+ * @see UserDetailsService
+ * @see Permission
+ * @since 2023-03-19
+ */
+public interface UserDetails extends Serializable {
+
+ /**
+ * 返回授予用户的权限。无法返回null 。
+ *
+ * @return 按自然键排序的权限集合。
+ */
+ Collection extends Permission> getPermissions();
+
+ /**
+ * 返回用于对用户进行身份验证的用户Id。无法返回null 。
+ *
+ * @return 用户Id
+ */
+ String getUserId();
+
+ /**
+ * 用户手机号
+ *
+ * @return 用户手机号
+ */
+ String getPhoneNum();
+
+ /**
+ * 返回用于对用户进行身份验证的用户名。无法返回null 。
+ *
+ * @return 用户名
+ */
+ String getUsername();
+
+ /**
+ * 返回用于验证用户身份的密码。
+ *
+ * @return 密码
+ */
+ String getPassword();
+
+ /**
+ * 指示用户的帐户是否已过期。无法验证过期的帐户。
+ *
+ * @return 如果用户的帐户有效(即未过期)则为true ,如果不再有效(即已过期) false
+ */
+ boolean isAccountNonExpired();
+
+ /**
+ * 指示用户是锁定还是解锁。无法对锁定的用户进行身份验证。
+ *
+ * @return 如果用户未被锁定, true ,否则为false
+ */
+ boolean isAccountNonLocked();
+
+ /**
+ * 指示用户的凭据(密码)是否已过期。过期的凭据会阻止身份验证。
+ *
+ * @return 如果用户的凭据有效(即未过期), true ;如果不再有效(即,已过期), false
+ */
+ boolean isCredentialsNonExpired();
+
+ /**
+ * 指示用户是启用还是禁用。无法对禁用的用户进行身份验证。
+ *
+ * @return 如果用户已启用, true ,否则为false
+ */
+ boolean isEnabled();
+
+ /**
+ * 执法类型:0-调度员;1-指挥员;2-办件员
+ * @return 用户执法类型
+ */
+ String getLawType();
+
+}
diff --git a/src/main/java/com/metalloop/common/auth/userdetails/UserDetailsService.java b/src/main/java/com/metalloop/common/auth/userdetails/UserDetailsService.java
new file mode 100644
index 0000000..deb1131
--- /dev/null
+++ b/src/main/java/com/metalloop/common/auth/userdetails/UserDetailsService.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.auth.userdetails;
+//import com.wanji.software.tocc.common.exception.UsernameNotFoundException;
+
+//import com.wanji.software.tocc.common.exception.UsernameNotFoundException;
+
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+/**
+ * 加载用户特定数据的核心接口,它在整个框架中用作用户 DAO,
+ * 该接口只需要一种只读方法,这简化了对新数据访问策略的支持
+ *
+ * @author zhaowenhao
+ * @since 2023-03-19
+ */
+public interface UserDetailsService {
+
+ /**
+ * 根据用户名获取用户详情
+ *
+ * @param username 用户登录名
+ * @return 用户详情
+ * @throws UsernameNotFoundException 用户名不存在
+ */
+ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
+}
diff --git a/src/main/java/com/metalloop/common/constant/CommonConstant.java b/src/main/java/com/metalloop/common/constant/CommonConstant.java
new file mode 100644
index 0000000..c30e58c
--- /dev/null
+++ b/src/main/java/com/metalloop/common/constant/CommonConstant.java
@@ -0,0 +1,134 @@
+package com.metalloop.common.constant;
+
+/**
+ * 全局公共常量
+ *
+ * @author zlt
+ * @since 2018/10/29
+ */
+public interface CommonConstant {
+ /**
+ * 项目版本号(banner使用)
+ */
+ String PROJECT_VERSION = "1.0.0";
+
+ /**
+ * token请求头名称
+ */
+ String TOKEN_HEADER = "Authorization";
+
+ /**
+ * The access token issued by the authorization server. This value is REQUIRED.
+ */
+ String ACCESS_TOKEN = "access_token";
+
+ String BEARER_TYPE = "Bearer";
+
+ /**
+ * 标签 header key
+ */
+ String HEADER_LABEL = "x-label";
+
+ /**
+ * 标签 header 分隔符
+ */
+ String HEADER_LABEL_SPLIT = ",";
+
+ /**
+ * 标签或 名称
+ */
+ String LABEL_OR = "labelOr";
+
+ /**
+ * 标签且 名称
+ */
+ String LABEL_AND = "labelAnd";
+
+ /**
+ * 权重key
+ */
+ String WEIGHT_KEY = "weight";
+
+ /**
+ * 删除
+ */
+ String STATUS_DEL = "1";
+
+ /**
+ * 正常
+ */
+ String STATUS_NORMAL = "0";
+
+ /**
+ * 锁定
+ */
+ String STATUS_LOCK = "9";
+
+ /**
+ * 目录
+ */
+ Integer CATALOG = -1;
+
+ /**
+ * 菜单
+ */
+ Integer MENU = 1;
+
+ /**
+ * 权限
+ */
+ Integer PERMISSION = 2;
+
+ /**
+ * 删除标记
+ */
+ String DEL_FLAG = "is_del";
+
+ /**
+ * 超级管理员用户名
+ */
+ String ADMIN_USER_NAME = "admin";
+
+ /**
+ * 公共日期格式
+ */
+ String MONTH_FORMAT = "yyyy-MM";
+ String DATE_FORMAT = "yyyy-MM-dd";
+ String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+ String SIMPLE_MONTH_FORMAT = "yyyyMM";
+ String SIMPLE_DATE_FORMAT = "yyyyMMdd";
+ String SIMPLE_DATETIME_FORMAT = "yyyyMMddHHmmss";
+ String TIME_ZONE_GMT8 = "GMT+8";
+
+ String DEF_USER_PASSWORD = "123456";
+
+ String LOCK_KEY_PREFIX = "LOCK_KEY";
+
+ /**
+ * 租户id参数
+ */
+ String TENANT_ID_PARAM = "tenantId";
+
+
+ /**
+ * 日志链路追踪id信息头
+ */
+ String TRACE_ID_HEADER = "x-traceId-header";
+ /**
+ * 日志链路追踪id日志标志
+ */
+ String LOG_TRACE_ID = "traceId";
+ /**
+ * 负载均衡策略-版本号 信息头
+ */
+ String Z_L_T_VERSION = "z-l-t-version";
+ /**
+ * 注册中心元数据 版本号
+ */
+ String METADATA_VERSION = "version";
+
+ /**
+ * 文件分隔符
+ */
+ String PATH_SPLIT = "/";
+}
diff --git a/src/main/java/com/metalloop/common/constant/SecurityConstants.java b/src/main/java/com/metalloop/common/constant/SecurityConstants.java
new file mode 100644
index 0000000..37e50af
--- /dev/null
+++ b/src/main/java/com/metalloop/common/constant/SecurityConstants.java
@@ -0,0 +1,21 @@
+package com.metalloop.common.constant;
+/**
+ * Security 相关常量
+ *
+ * @author zhaowenhao
+ * @since 2023-03-19
+ */
+public interface SecurityConstants {
+
+
+ /**
+ * 用户ID请求头
+ */
+ String HEADER_USER_USERID = "x-userid";
+
+ /**
+ * 用户信息在session中的key
+ */
+ String SESSION_KEY_AUTHENTICATION = "AUTHENTICATION_SESSION_KEY";
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/metalloop/common/core/lock/DistributedLock.java b/src/main/java/com/metalloop/common/core/lock/DistributedLock.java
new file mode 100644
index 0000000..27f8d2a
--- /dev/null
+++ b/src/main/java/com/metalloop/common/core/lock/DistributedLock.java
@@ -0,0 +1,80 @@
+package com.metalloop.common.core.lock;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 分布式锁顶级接口
+ *
+ * @author zlt
+ * @date 2018/5/29 14:12
+ *
+ * Blog: https://zlt2000.gitee.io
+ * Github: https://github.com/zlt2000
+ */
+public interface DistributedLock {
+ /**
+ * 获取锁,如果获取不成功则一直等待直到lock被获取
+ *
+ * @param key 锁的key
+ * @param leaseTime 加锁的时间,超过这个时间后锁便自动解锁;
+ * 如果leaseTime为-1,则保持锁定直到显式解锁
+ * @param unit {@code leaseTime} 参数的时间单位
+ * @param isFair 是否公平锁
+ * @return 锁对象
+ */
+ ZLock lock(String key, long leaseTime, TimeUnit unit, boolean isFair) throws Exception;
+
+ default ZLock lock(String key, long leaseTime, TimeUnit unit) throws Exception {
+ return this.lock(key, leaseTime, unit, false);
+ }
+
+ default ZLock lock(String key, boolean isFair) throws Exception {
+ return this.lock(key, -1, null, isFair);
+ }
+
+ default ZLock lock(String key) throws Exception {
+ return this.lock(key, -1, null, false);
+ }
+
+ /**
+ * 尝试获取锁,如果锁不可用则等待最多waitTime时间后放弃
+ *
+ * @param key 锁的key
+ * @param waitTime 获取锁的最大尝试时间(单位 {@code unit})
+ * @param leaseTime 加锁的时间,超过这个时间后锁便自动解锁;
+ * 如果leaseTime为-1,则保持锁定直到显式解锁
+ * @param unit {@code waitTime} 和 {@code leaseTime} 参数的时间单位
+ * @return 锁对象,如果获取锁失败则为null
+ */
+ ZLock tryLock(String key, long waitTime, long leaseTime, TimeUnit unit, boolean isFair) throws Exception;
+
+ default ZLock tryLock(String key, long waitTime, long leaseTime, TimeUnit unit) throws Exception {
+ return this.tryLock(key, waitTime, leaseTime, unit, false);
+ }
+
+ default ZLock tryLock(String key, long waitTime, TimeUnit unit, boolean isFair) throws Exception {
+ return this.tryLock(key, waitTime, -1, unit, isFair);
+ }
+
+ default ZLock tryLock(String key, long waitTime, TimeUnit unit) throws Exception {
+ return this.tryLock(key, waitTime, -1, unit, false);
+ }
+
+ /**
+ * 释放锁
+ *
+ * @param lock 锁对象
+ */
+ void unlock(Object lock) throws Exception;
+
+ /**
+ * 释放锁
+ *
+ * @param zLock 锁抽象对象
+ */
+ default void unlock(ZLock zLock) throws Exception {
+ if (zLock != null) {
+ this.unlock(zLock.getLock());
+ }
+ }
+}
diff --git a/src/main/java/com/metalloop/common/core/lock/Lock.java b/src/main/java/com/metalloop/common/core/lock/Lock.java
new file mode 100644
index 0000000..a4edfca
--- /dev/null
+++ b/src/main/java/com/metalloop/common/core/lock/Lock.java
@@ -0,0 +1,43 @@
+package com.metalloop.common.core.lock;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author zlt
+ * @date 2020/6/6
+ *
+ * Blog: https://zlt2000.gitee.io
+ * Github: https://github.com/zlt2000
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Lock {
+ /**
+ * 锁的key
+ */
+ String key();
+
+ /**
+ * 获取锁的最大尝试时间(单位 {@code unit})
+ * 该值大于0则使用 locker.tryLock 方法加锁,否则使用 locker.lock 方法
+ */
+ long waitTime() default 0;
+
+ /**
+ * 加锁的时间(单位 {@code unit}),超过这个时间后锁便自动解锁;
+ * 如果leaseTime为-1,则保持锁定直到显式解锁
+ */
+ long leaseTime() default -1;
+
+ /**
+ * 参数的时间单位
+ */
+ TimeUnit unit() default TimeUnit.SECONDS;
+
+ /**
+ * 是否公平锁
+ */
+ boolean isFair() default false;
+}
diff --git a/src/main/java/com/metalloop/common/core/lock/LockAspect.java b/src/main/java/com/metalloop/common/core/lock/LockAspect.java
new file mode 100644
index 0000000..ea4a592
--- /dev/null
+++ b/src/main/java/com/metalloop/common/core/lock/LockAspect.java
@@ -0,0 +1,98 @@
+package com.metalloop.common.core.lock;
+
+import cn.hutool.core.util.StrUtil;
+import com.metalloop.common.exception.LockException;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+/**
+ * 分布式锁切面
+ *
+ * @author zlt
+ * @date 2020/6/6
+ *
+ * Blog: https://zlt2000.gitee.io
+ * Github: https://github.com/zlt2000
+ */
+@Slf4j
+@Aspect
+public class LockAspect {
+ @Autowired(required = false)
+ private DistributedLock locker;
+
+ /**
+ * 用于SpEL表达式解析.
+ */
+ private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
+ /**
+ * 用于获取方法参数定义名字.
+ */
+ private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
+
+ @Around("@within(lock) || @annotation(lock)")
+ public Object aroundLock(ProceedingJoinPoint point, Lock lock) throws Throwable {
+ if (lock == null) {
+ // 获取类上的注解
+ lock = point.getTarget().getClass().getDeclaredAnnotation(Lock.class);
+ }
+ String lockKey = lock.key();
+ if (locker == null) {
+ throw new LockException("DistributedLock is null");
+ }
+ if (StrUtil.isEmpty(lockKey)) {
+ throw new LockException("lockKey is null");
+ }
+
+ if (lockKey.contains("#")) {
+ MethodSignature methodSignature = (MethodSignature) point.getSignature();
+ // 获取方法参数值
+ Object[] args = point.getArgs();
+ lockKey = getValBySpEL(lockKey, methodSignature, args);
+ }
+ ZLock lockObj = null;
+ try {
+ // 加锁
+ if (lock.waitTime() > 0) {
+ lockObj = locker.tryLock(lockKey, lock.waitTime(), lock.leaseTime(), lock.unit(), lock.isFair());
+ } else {
+ lockObj = locker.lock(lockKey, lock.leaseTime(), lock.unit(), lock.isFair());
+ }
+
+ if (lockObj != null) {
+ return point.proceed();
+ } else {
+ throw new LockException("锁等待超时");
+ }
+ } finally {
+ locker.unlock(lockObj);
+ }
+ }
+
+ /**
+ * 解析spEL表达式
+ */
+ private String getValBySpEL(String spEL, MethodSignature methodSignature, Object[] args) {
+ // 获取方法形参名数组
+ String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
+ if (paramNames != null && paramNames.length > 0) {
+ Expression expression = spelExpressionParser.parseExpression(spEL);
+ // spring的表达式上下文对象
+ EvaluationContext context = new StandardEvaluationContext();
+ // 给上下文赋值
+ for (int i = 0; i < args.length; i++) {
+ context.setVariable(paramNames[i], args[i]);
+ }
+ return expression.getValue(context).toString();
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/metalloop/common/core/lock/LockCondition.java b/src/main/java/com/metalloop/common/core/lock/LockCondition.java
new file mode 100644
index 0000000..032c6da
--- /dev/null
+++ b/src/main/java/com/metalloop/common/core/lock/LockCondition.java
@@ -0,0 +1,50 @@
+package com.metalloop.common.core.lock;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.Getter;
+
+/**
+ * LockCondition 是一个封装类,它包含了在实现幂等性操作时所需要的关键信息。
+ *
+ * 这个类的主要组成部分包括:
+ *
+ * - 是否开启获取锁及检查(enabled):如果为 true,则执行获取锁及检查;如果为 false,则跳过获取锁及检查。
+ *
+ * - 锁定键(lockKey):用于分布式锁的键,唯一标识了应该被锁定的资源。
+ *
+ * - 查询包装器(queryWrapper):定义了用于检查数据是否已存在的查询条件。
+ *
+ * - 消息(msg):如果已存在符合查询条件的数据,那么这个消息将被用于抛出 IdempotencyException 异常。
+ *
+ * 使用 LockCondition 类可以让代码更加清晰和易于理解。它提供了一种方式,使得在实现多条件幂等性检查时,可以将关键信息组织在一起。
+ *
+ * @author zhaowenhao
+ * @since 2023-05-14
+ */
+@Getter
+public class LockCondition {
+
+ private final boolean enabled;
+
+ private final String lockKey;
+
+ private final QueryWrapper queryWrapper;
+
+ private final String msg;
+
+
+ public LockCondition(boolean enabled, String lockKey, QueryWrapper queryWrapper, String msg) {
+ this.lockKey = lockKey;
+ this.queryWrapper = queryWrapper;
+ this.msg = msg;
+ this.enabled = enabled;
+ }
+
+ public LockCondition(String lockKey, QueryWrapper queryWrapper, String msg) {
+ this.enabled = true;
+ this.lockKey = lockKey;
+ this.queryWrapper = queryWrapper;
+ this.msg = msg;
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/core/lock/LockConditionReqDto.java b/src/main/java/com/metalloop/common/core/lock/LockConditionReqDto.java
new file mode 100644
index 0000000..34cca60
--- /dev/null
+++ b/src/main/java/com/metalloop/common/core/lock/LockConditionReqDto.java
@@ -0,0 +1,56 @@
+package com.metalloop.common.core.lock;
+
+import com.metalloop.common.utils.QueryWrapperBuilder;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.function.Function;
+
+/**
+ * 幂等性请求 DTO
+ *
+ * @author zhaowenhao
+ * @since 2023-09-11
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class LockConditionReqDto implements Serializable {
+ /**
+ * 是否开启获取锁及检查(enabled):如果为 true,则执行获取锁及检查;如果为 false,则跳过获取锁及检查。
+ */
+ @Schema(description = "是否开启获取锁及检查")
+ private Boolean enabled;
+
+ /**
+ * 锁定键(lockKey):用于分布式锁的键,唯一标识了应该被锁定的资源。
+ */
+ @Schema(description = "锁定键")
+ private String lockKey;
+
+ /**
+ * 查询对象:定义了用于检查数据是否已存在的查询条件。
+ */
+ @Schema(description = "查询对象")
+ private Q query;
+
+ /**
+ * 消息(msg):如果已存在符合查询条件的数据,那么这个消息将被用于抛出 IdempotencyException 异常
+ */
+ @Schema(description = "消息")
+ private String msg;
+
+ /**
+ * 转为通用的 LockCondition 对象
+ *
+ * @return LockCondition 对象
+ */
+ public LockCondition toLockCondition(Function queryMapper) {
+ return new LockCondition<>(enabled, lockKey, QueryWrapperBuilder.build(queryMapper.apply(query)), msg);
+ }
+}
diff --git a/src/main/java/com/metalloop/common/core/lock/ZLock.java b/src/main/java/com/metalloop/common/core/lock/ZLock.java
new file mode 100644
index 0000000..3ecc222
--- /dev/null
+++ b/src/main/java/com/metalloop/common/core/lock/ZLock.java
@@ -0,0 +1,26 @@
+package com.metalloop.common.core.lock;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 锁对象抽象
+ *
+ * @author zlt
+ * @date 2020/7/28
+ *
+ * Blog: https://zlt2000.gitee.io
+ * Github: https://github.com/zlt2000
+ */
+@AllArgsConstructor
+public class ZLock implements AutoCloseable {
+ @Getter
+ private final Object lock;
+
+ private final DistributedLock locker;
+
+ @Override
+ public void close() throws Exception {
+ locker.unlock(lock);
+ }
+}
diff --git a/src/main/java/com/metalloop/common/enums/Direction.java b/src/main/java/com/metalloop/common/enums/Direction.java
new file mode 100644
index 0000000..4805383
--- /dev/null
+++ b/src/main/java/com/metalloop/common/enums/Direction.java
@@ -0,0 +1,52 @@
+package com.metalloop.common.enums;
+
+
+import java.util.Locale;
+
+/**
+ * 排序方式枚举 ASC-升序 DESC-降序
+ *
+ * @author yy
+ */
+public enum Direction {
+
+ /**
+ * 升序
+ */
+ ASC,
+
+ /**
+ * 降序
+ */
+ DESC;
+
+ /**
+ * 是否升序
+ */
+ public boolean isAscending() {
+ return this.equals(ASC);
+ }
+
+ /**
+ * 是否降序
+ */
+ public boolean isDescending() {
+ return this.equals(DESC);
+ }
+
+ /**
+ * 通过字符串获取排序枚举
+ *
+ * @param value 字符串
+ * @throws IllegalArgumentException 无法解析错误.
+ */
+ public static Direction fromString(String value) {
+
+ try {
+ return Direction.valueOf(value.toUpperCase(Locale.US));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(String.format(
+ "无效值 '%s'! 必须为 'desc' 或者 'asc' (大小写不敏感).", value), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/metalloop/common/enums/LoginType.java b/src/main/java/com/metalloop/common/enums/LoginType.java
new file mode 100644
index 0000000..30910a0
--- /dev/null
+++ b/src/main/java/com/metalloop/common/enums/LoginType.java
@@ -0,0 +1,74 @@
+package com.metalloop.common.enums;
+
+import cn.dev33.satoken.stp.StpLogic;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 自定义账号类型,多账号体系时以此值区分,sa-token框架默认的账号类型值为login,可以根据需求自定义,但是必须
+ * 在本类同包的下面增加对应的新的权限认证类,比如: StpXxxUtil.java。
+ * 将cn.dev33.satoken.stp.StpUtil.java类的全部代码复制粘贴到 StpXxxUtil.java里。
+ * 更改一下其 stpLogic,从此枚举类中获取。
+ *
+ * 自定义的账号类型要记录到下面:
+ *
+ * admin: 管理员系统使用的账号类型
+ *
+ * app: App 使用的账号类型,和login的区别是会话超时时间,app的会话超时时间长一些
+ *
+ * self: 自助服务终端使用的账号类型,和login的区别是会话超时时间
+ */
+@Getter
+public enum LoginType {
+
+ /**
+ * login
+ */
+ DEFAULT(StpUtil.getLoginType(), StpUtil.getStpLogic()),
+
+ /**
+ * admin
+ */
+ ADMIN("admin", new StpLogic("admin")),
+
+ /**
+ * self
+ */
+ SELF("self", new StpLogic("self")),
+
+ /**
+ * wechat
+ */
+ WECHAT("wechat", new StpLogic("wechat"));
+
+ private final String type;
+ private final StpLogic stpLogic;
+
+ private static final Map TYPE_TO_ENUM = Arrays.stream(LoginType.values())
+ .collect(Collectors.toMap(LoginType::getType, Function.identity()));
+
+ private static final Map STPLOGIC_TO_ENUM = Arrays.stream(LoginType.values())
+ .collect(Collectors.toMap(LoginType::getStpLogic, Function.identity()));
+
+ LoginType(String type, StpLogic stpLogic) {
+ this.type = type;
+ this.stpLogic = stpLogic;
+ }
+
+ public static LoginType of(String type) {
+ return Optional.ofNullable(TYPE_TO_ENUM.get(type))
+ .orElseThrow(() -> new IllegalArgumentException("账号类型错误"));
+ }
+
+ public static LoginType of(StpLogic stpLogic) {
+ return Optional.ofNullable(STPLOGIC_TO_ENUM.get(stpLogic))
+ .orElseThrow(() -> new IllegalArgumentException("账号类型错误"));
+ }
+}
+
diff --git a/src/main/java/com/metalloop/common/exception/AccountExpiredException.java b/src/main/java/com/metalloop/common/exception/AccountExpiredException.java
new file mode 100644
index 0000000..74bd26e
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/AccountExpiredException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.exception;
+
+/**
+ * 如果身份验证请求因为账号已过期而被拒绝,则抛出该异常。
+ *
+ * @author zhaowenhao
+ * @since 2023-03-20
+ */
+public class AccountExpiredException extends AccountStatusException {
+
+ /**
+ * 构造带有指定消息的AccountExpiredException。
+ *
+ * @param msg 详细信息
+ */
+ public AccountExpiredException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * 构造具有指定消息和根本原因的AccountExpiredException。
+ *
+ * @param msg 详细信息
+ * @param cause 根本原因
+ */
+ public AccountExpiredException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/exception/AccountStatusException.java b/src/main/java/com/metalloop/common/exception/AccountStatusException.java
new file mode 100644
index 0000000..d9fe27c
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/AccountStatusException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2002-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.exception;
+
+/**
+ * 由特定账户状态(锁定、禁用等)引起的身份验证异常的基类。
+ *
+ * @author zhaowenhao
+ * @since 2023-03-20
+ */
+public abstract class AccountStatusException extends AuthenticationException {
+
+ public AccountStatusException(String msg) {
+ super(msg);
+ }
+
+ public AccountStatusException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/exception/AuthenticationException.java b/src/main/java/com/metalloop/common/exception/AuthenticationException.java
new file mode 100644
index 0000000..0ea4fc3
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/AuthenticationException.java
@@ -0,0 +1,30 @@
+package com.metalloop.common.exception;
+
+/**
+ * 任何原因无效的身份验证对象相关异常的抽象超类
+ *
+ * @author zhaowenhao
+ * @since 2023-03-19
+ */
+public abstract class AuthenticationException extends RuntimeException {
+
+ /**
+ * 构造具有指定消息和根本原因的AuthenticationException 。
+ *
+ * @param msg 详细信息
+ * @param cause 根本原因
+ */
+ public AuthenticationException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ /**
+ * 构造具有指定消息且无根本原因的AuthenticationException 。
+ *
+ * @param msg 详细信息
+ */
+ public AuthenticationException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/exception/BadCredentialsException.java b/src/main/java/com/metalloop/common/exception/BadCredentialsException.java
new file mode 100644
index 0000000..db1ce4e
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/BadCredentialsException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.exception;
+
+/**
+ * 如果身份验证请求因凭据无效而被拒绝,则抛出该异常。抛出这个异常,说明账号既没有被锁定也没有被禁用.
+ *
+ * @author zhaowenhao
+ * @since 2023-03-19
+ */
+public class BadCredentialsException extends AuthenticationException {
+
+ /**
+ * 构造带有指定消息的BadCredentialsException 。
+ *
+ * @param msg 详细信息
+ */
+ public BadCredentialsException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * 构造具有指定消息和根本原因的BadCredentialsException 。
+ *
+ * @param msg 详细信息
+ * @param cause 根本原因
+ */
+ public BadCredentialsException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/main/java/com/metalloop/common/exception/BusinessException.java b/src/main/java/com/metalloop/common/exception/BusinessException.java
new file mode 100644
index 0000000..2893108
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/BusinessException.java
@@ -0,0 +1,74 @@
+package com.metalloop.common.exception;
+
+
+import com.metalloop.common.exception.code.CodeMsg;
+import com.metalloop.common.exception.code.ResponseCode;
+
+/**
+ * 业务异常
+ *
+ * @author zhaowenhao
+ * @since 2022-12-27
+ */
+public class BusinessException extends RuntimeException {
+ private static final long serialVersionUID = 6610083281801529147L;
+
+ /**
+ * 错误码
+ */
+ private final Integer code;
+
+
+ public BusinessException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ public BusinessException(String message) {
+ this(ResponseCode.FAIL.getCode(), message);
+ }
+
+ public BusinessException(CodeMsg codeMsg) {
+ this(codeMsg.getCode(), codeMsg.getMsg());
+ }
+
+ /**
+ * 获取错误码
+ *
+ * @return 错误码
+ */
+ public Integer getCode() {
+ return code;
+ }
+
+ /**
+ * 根据错误码和错误消息构建业务异常
+ *
+ * @param code 错误码
+ * @param message 错误消息
+ * @return 业务异常
+ */
+ public static BusinessException with(int code, String message) {
+ return new BusinessException(code, message);
+ }
+
+ /**
+ * 根据错误消息构建业务异常
+ *
+ * @param message 错误消息
+ * @return 业务异常
+ */
+ public static BusinessException message(String message) {
+ return new BusinessException(message);
+ }
+
+ /**
+ * 根据错误码与错误消息顶级接口的实现类构建业务异常
+ *
+ * @param codeMsg 错误码与错误消息顶级接口的实现类
+ * @return 业务异常
+ */
+ public static BusinessException with(CodeMsg codeMsg) {
+ return new BusinessException(codeMsg);
+ }
+}
diff --git a/src/main/java/com/metalloop/common/exception/CredentialsExpiredException.java b/src/main/java/com/metalloop/common/exception/CredentialsExpiredException.java
new file mode 100644
index 0000000..fcb22a6
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/CredentialsExpiredException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.exception;
+
+/**
+ * 如果身份验证请求因账户凭据已过期而被拒绝,则抛出该异常。
+ *
+ * @author zhaowenhao
+ * @since 2023-03-20
+ */
+public class CredentialsExpiredException extends AccountStatusException {
+
+ /**
+ * 构造带有指定消息的CredentialsExpiredException 。
+ *
+ * @param msg 详细消息
+ */
+ public CredentialsExpiredException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * 构造具有指定消息和根本原因的CredentialsExpiredException
+ *
+ * @param msg 详细消息
+ * @param cause 根本原因
+ */
+ public CredentialsExpiredException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/exception/DefaultExceptionAdvice.java b/src/main/java/com/metalloop/common/exception/DefaultExceptionAdvice.java
new file mode 100644
index 0000000..82bcbea
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/DefaultExceptionAdvice.java
@@ -0,0 +1,327 @@
+package com.metalloop.common.exception;
+
+import cn.dev33.satoken.exception.*;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.map.MapUtil;
+import com.metalloop.common.exception.code.ResponseCode;
+import com.metalloop.common.exception.model.ApiErrorLog;
+import com.metalloop.common.model.Result;
+import com.metalloop.common.utils.json.JsonUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.util.Assert;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.nio.file.AccessDeniedException;
+import java.sql.SQLException;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 异常通用处理
+ *
+ * @author zhaowenhao
+ * @since 2023-03-25
+ */
+@ResponseBody
+@Slf4j
+public abstract class DefaultExceptionAdvice {
+
+ /**
+ * 自定义消息包装正则表达式,用于提取自定义消息,特别是错误消息由框架接管,拼接了不友好的信息情况下的消息还原,比如参数校验异常
+ */
+ public static final Pattern CUSTOM_MESSAGE_PATTERN = Pattern.compile("\\{CustomMessage\\{([^}]+)}}");
+
+ /**
+ * IllegalArgumentException异常处理返回json
+ */
+ @ExceptionHandler({IllegalArgumentException.class})
+ public Result> badRequestException(IllegalArgumentException e) {
+ return doHandle("参数解析失败", e);
+ }
+
+ /**
+ * AccessDeniedException异常处理返回json
+ */
+ @ExceptionHandler({AccessDeniedException.class})
+ public Result> badMethodExpressException(AccessDeniedException e) {
+ return doHandle("没有权限请求当前方法", e);
+ }
+
+ /**
+ *
+ */
+ @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
+ public Result> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
+ return doHandle("不支持当前请求方法", e);
+ }
+
+ /**
+ *
+ */
+ @ExceptionHandler({HttpMediaTypeNotSupportedException.class})
+ public Result> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
+ return doHandle("不支持当前媒体类型", e);
+ }
+
+ /**
+ * SQLException sql异常处理
+ */
+ @ExceptionHandler({SQLException.class})
+ public Result> handleSqlException(SQLException e) {
+ return doHandle("服务异常", e, true);
+ }
+
+ /**
+ * BusinessException 业务异常处理
+ */
+ @ExceptionHandler(BusinessException.class)
+ public Result> handleException(BusinessException e) {
+ log.error("业务异常", e);
+ return Result.fail(e.getCode(), e.getMessage());
+ }
+
+ /**
+ * ValidationFailedException 参数校验异常
+ */
+ @ExceptionHandler(ValidationFailedException.class)
+ public Result> handleValidationFailedException(ValidationFailedException e) {
+ log.error("参数校验异常", e);
+ return Result.fail(e.getMessage());
+ }
+
+ /**
+ * IdempotencyException 幂等性异常
+ */
+ @ExceptionHandler(IdempotencyException.class)
+ public Result> handleException(IdempotencyException e) {
+ log.error("幂等性异常", e);
+ return doHandle(e.getMessage(), e);
+ }
+
+ /**
+ * 参数校验异常
+ *
+ * @param e MethodArgumentNotValidException
+ * @return Result
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public Result> handlerException(MethodArgumentNotValidException e) {
+ log.error("参数校验异常", e);
+ List fieldErrors = e.getBindingResult().getFieldErrors();
+ String fieldErrorMsg = getFieldErrorMsg(fieldErrors);
+ return doHandle(fieldErrorMsg, e);
+ }
+
+ /**
+ * 参数校验异常
+ *
+ * @param e BindException
+ * @return Result
+ */
+ @ExceptionHandler(BindException.class)
+ public Result> handlerException(BindException e) {
+ log.error("参数校验异常", e);
+ List fieldErrors = e.getBindingResult().getFieldErrors();
+ String fieldErrorMsg = getFieldErrorMsg(fieldErrors);
+ return doHandle(fieldErrorMsg, e);
+ }
+
+ /**
+ * 认证异常
+ *
+ * @param e AuthenticationException
+ * @return Result
+ */
+ @ExceptionHandler(AuthenticationException.class)
+ public Result> handlerException(AuthenticationException e) {
+ log.error("认证异常", e);
+ return doHandle(e.getMessage(), e);
+ }
+
+ // region sa-token 异常处理
+
+ @ExceptionHandler(value = {NotLoginException.class})
+ public Result> handle(NotLoginException e) {
+ log.error("NotLoginException:{}", e.getMessage());
+ return Result.fail(ResponseCode.NO_SESSION);
+ }
+
+ @ExceptionHandler(value = {NotPermissionException.class})
+ public Result> handle(NotPermissionException e) {
+ log.error("NotPermissionException:{}", e.getMessage());
+ return Result.fail(ResponseCode.ACCESS_UNAUTHORIZED);
+ }
+
+ @ExceptionHandler(value = {NotRoleException.class})
+ public Result> handle(NotRoleException e) {
+ log.error("NotRoleException:{}", e.getMessage());
+ return Result.fail(ResponseCode.ACCESS_UNAUTHORIZED);
+ }
+
+ @ExceptionHandler(value = {NotSafeException.class})
+ public Result> handle(NotSafeException e) {
+ log.error("NotSafeException:{}", e.getMessage());
+ return Result.fail(ResponseCode.ACCESS_UNAUTHORIZED);
+ }
+
+ @ExceptionHandler(value = {SameTokenInvalidException.class})
+ public Result> handle(SameTokenInvalidException e) {
+ log.error("SameTokenInvalidException:{}", e.getMessage());
+ return Result.fail(ResponseCode.TOKEN_INVALID);
+ }
+
+ /**
+ * 处理Sa-Token的异常
+ *
+ * @param e Sa-Token异常
+ * @return Result
+ */
+ @ExceptionHandler(value = {SaTokenException.class})
+ public Result> handle(SaTokenException e) {
+ log.error("认证异常", e);
+ return doHandle(e.getMessage(), e);
+ }
+ // endregion sa-token 异常处理
+
+ /**
+ * 所有未知异常统一处理
+ */
+ @ExceptionHandler(Exception.class)
+ public Result> handleException(HttpServletRequest req, Exception ex) {
+ log.error("[defaultExceptionHandler]", ex);
+ // 插入异常日志
+ this.createExceptionLog(req, ex);
+ // 返回 ERROR Result
+ return doHandle("系统繁忙", ex, true);
+ }
+
+ /**
+ * 处理异常,默认不打印异常堆栈,并返回前端异常信息
+ *
+ * @param msg 返给前端的异常信息
+ * @param e 异常
+ * @return 返回前端的响应体
+ */
+ private Result> doHandle(String msg, Exception e) {
+ return doHandle(msg, e, false);
+ }
+
+ /**
+ * 处理异常,并返回前端异常信息
+ *
+ * @param msg 返给前端的异常信息
+ * @param e 异常
+ * @param ifPrintStackTrace 是否打印异常堆栈,系统级或未知异常建议打印
+ * @return 返回前端的响应体
+ */
+ private Result> doHandle(String msg, Exception e, boolean ifPrintStackTrace) {
+ if (ifPrintStackTrace) {
+ e.printStackTrace();
+ }
+ log.error(msg, e);
+ return Result.fail(msg);
+ }
+
+ /**
+ * 获取参数校验错误信息
+ *
+ * @param fieldErrors fieldErrors
+ * @return 参数校验错误信息
+ */
+ private String getFieldErrorMsg(List fieldErrors) {
+ StringBuilder errorMsg = new StringBuilder("参数校验失败:");
+ fieldErrors.forEach(fieldError -> {
+ String defaultMessage = fieldError.getDefaultMessage();
+ String message = extractCustomMessageIfNecessary(defaultMessage);
+ errorMsg.append(fieldError.getField()).append("-").append(message).append(";");
+ });
+ return errorMsg.toString();
+ }
+
+ private void createExceptionLog(HttpServletRequest req, Throwable e) {
+ // 插入错误日志
+ ApiErrorLog errorLog = new ApiErrorLog();
+ try {
+ // 初始化 errorLog
+ initExceptionLog(errorLog, req, e);
+ // 执行插入 errorLog
+ appendErrorLog(errorLog);
+ } catch (Throwable th) {
+ log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
+ }
+ }
+
+ /**
+ * 提取自定义消息如果有的话
+ *
+ * @param message 原始消息,可能包含 {CustomMessage{...}}
+ * @return 提取后的自定义消息
+ */
+ private String extractCustomMessageIfNecessary(String message) {
+ Matcher matcher = CUSTOM_MESSAGE_PATTERN.matcher(message);
+ StringBuilder customMessages = new StringBuilder();
+ boolean found = false;
+
+ while (matcher.find()) {
+ customMessages.append(matcher.group(1)).append(";");
+ found = true;
+ }
+
+ if (found) {
+ // 移除最后的分号
+ if (customMessages.length() > 0 && customMessages.charAt(customMessages.length() - 1) == ';') {
+ customMessages.setLength(customMessages.length() - 1);
+ }
+ return customMessages.toString();
+ }
+
+ // 如果没有匹配到自定义消息,返回原始消息
+ return message;
+ }
+
+ /**
+ * 插入错误日志, 子类实现
+ *
+ * @param errorLog 错误日志
+ */
+ protected void appendErrorLog(ApiErrorLog errorLog) {
+ // 插入错误日志,默认不做任何事情
+ }
+
+ private void initExceptionLog(ApiErrorLog errorLog, HttpServletRequest request, Throwable e) {
+ // 设置异常字段
+ errorLog.setExceptionName(e.getClass().getName());
+ errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
+ errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
+ errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
+ StackTraceElement[] stackTraceElements = e.getStackTrace();
+ Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
+ StackTraceElement stackTraceElement = stackTraceElements[0];
+ errorLog.setExceptionClassName(stackTraceElement.getClassName());
+ errorLog.setExceptionFileName(stackTraceElement.getFileName());
+ errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
+ errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
+ // 设置其它字段
+ errorLog.setTraceId(MDC.get("traceId"));
+ errorLog.setRequestUrl(request.getRequestURI());
+// Map requestParams = MapUtil.builder()
+// .put("query", ServletUtil.getParamMap(request))
+// .put("body", ServletUtil.getBody(request)).build();
+// errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
+// errorLog.setRequestMethod(request.getMethod());
+// errorLog.setUserAgent(ServletUtils.getUserAgent(request));
+// errorLog.setUserIp(ServletUtil.getClientIP(request));
+// errorLog.setExceptionTime(LocalDateTime.now());
+ }
+}
diff --git a/src/main/java/com/metalloop/common/exception/DisabledException.java b/src/main/java/com/metalloop/common/exception/DisabledException.java
new file mode 100644
index 0000000..cbe419e
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/DisabledException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.exception;
+
+/**
+ * 如果身份验证请求因账户被禁用而被拒绝,则抛出该异常。
+ *
+ * @author zhaowenhao
+ * @since 2023-03-20
+ */
+public class DisabledException extends AccountStatusException {
+
+ /**
+ * 构造带有指定消息的DisabledException 。
+ *
+ * @param msg 详细消息
+ */
+ public DisabledException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * 构造具有指定消息和根本原因的DisabledException 。
+ *
+ * @param msg 详细消息
+ * @param cause 根本原因
+ */
+ public DisabledException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/exception/IdempotencyException.java b/src/main/java/com/metalloop/common/exception/IdempotencyException.java
new file mode 100644
index 0000000..d02ed6a
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/IdempotencyException.java
@@ -0,0 +1,14 @@
+package com.metalloop.common.exception;
+
+/**
+ * 幂等性异常
+ *
+ * @author zlt
+ */
+public class IdempotencyException extends RuntimeException {
+ private static final long serialVersionUID = 6610083281801529147L;
+
+ public IdempotencyException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/metalloop/common/exception/LockException.java b/src/main/java/com/metalloop/common/exception/LockException.java
new file mode 100644
index 0000000..ba1cfce
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/LockException.java
@@ -0,0 +1,14 @@
+package com.metalloop.common.exception;
+
+/**
+ * 分布式锁异常
+ *
+ * @author zlt
+ */
+public class LockException extends RuntimeException {
+ private static final long serialVersionUID = 6610083281801529147L;
+
+ public LockException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/metalloop/common/exception/LockedException.java b/src/main/java/com/metalloop/common/exception/LockedException.java
new file mode 100644
index 0000000..ed1ad2e
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/LockedException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.exception;
+
+/**
+ * 如果身份验证请求因账户被锁定而被拒绝,则抛出该异常。
+ *
+ * @author zhaowenhao
+ * @since 2023-03-20
+ */
+public class LockedException extends AccountStatusException {
+
+ /**
+ * 构造一个带有指定消息的LockedException 。
+ *
+ * @param msg 详细信息
+ */
+ public LockedException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * 构造具有指定消息和根本原因的LockedException 。
+ *
+ * @param msg 详细信息
+ * @param cause 根本原因
+ */
+ public LockedException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/exception/UsernameNotFoundException.java b/src/main/java/com/metalloop/common/exception/UsernameNotFoundException.java
new file mode 100644
index 0000000..461b7e2
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/UsernameNotFoundException.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.metalloop.common.exception;
+
+/**
+ * 如果UserDetailsService实现无法通过用户名找到用户,则抛出该异常。
+ *
+ * @author zhaowenhao
+ * @since 2023-03-19
+ */
+public class UsernameNotFoundException extends AuthenticationException {
+
+ /**
+ * 构造带有指定消息的UsernameNotFoundException 。
+ *
+ * @param msg 详细信息。
+ */
+ public UsernameNotFoundException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * 构造具有指定消息和根本原因的UsernameNotFoundException 。
+ * 形参:
+ *
+ * @param msg 详细信息
+ * @param cause 根本原因
+ */
+ public UsernameNotFoundException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/com/metalloop/common/exception/ValidationFailedException.java b/src/main/java/com/metalloop/common/exception/ValidationFailedException.java
new file mode 100644
index 0000000..53fc316
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/ValidationFailedException.java
@@ -0,0 +1,34 @@
+package com.metalloop.common.exception;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 参数校验失败异常
+ *
+ * @author zhaowenhao
+ * @version 2023-05-04
+ */
+public class ValidationFailedException extends RuntimeException {
+ private final Map> linkedEntities;
+
+ public ValidationFailedException(String message) {
+ super(message);
+ this.linkedEntities = Collections.emptyMap();
+ }
+
+ public ValidationFailedException(String message, Map> linkedEntities) {
+ super(message);
+ this.linkedEntities = linkedEntities;
+ }
+
+ public ValidationFailedException(String message, Throwable cause) {
+ super(message, cause);
+ this.linkedEntities = Collections.emptyMap();
+ }
+
+ public Map> getLinkedEntities() {
+ return linkedEntities;
+ }
+}
diff --git a/src/main/java/com/metalloop/common/exception/code/CodeMsg.java b/src/main/java/com/metalloop/common/exception/code/CodeMsg.java
new file mode 100644
index 0000000..2817795
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/code/CodeMsg.java
@@ -0,0 +1,23 @@
+package com.metalloop.common.exception.code;
+
+/**
+ * 错误码与错误消息的顶级接口
+ *
+ * @author zhaowenhao
+ * @since 2022-12-27
+ */
+public interface CodeMsg {
+ /**
+ * 获取错误码
+ *
+ * @return 获取错误码
+ */
+ Integer getCode();
+
+ /**
+ * 获取错误消息
+ *
+ * @return 获取错误消息
+ */
+ String getMsg();
+}
diff --git a/src/main/java/com/metalloop/common/exception/code/ResponseCode.java b/src/main/java/com/metalloop/common/exception/code/ResponseCode.java
new file mode 100644
index 0000000..0be3fb2
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/code/ResponseCode.java
@@ -0,0 +1,124 @@
+package com.metalloop.common.exception.code;
+
+/**
+ * 通用错误码
+ *
+ * @author Lei
+ * @since 2022/10/26 20:12
+ */
+@SuppressWarnings("AlibabaEnumConstantsMustHaveComment")
+public enum ResponseCode implements CodeMsg {
+
+ SUCCESS(0, "操作成功"),
+ FAIL(1, "操作失败"),
+
+ SYSTEM_BUSY(500001, "系统繁忙,请稍候再试"),
+ OPERATION_ERROR(500002, "操作失败"),
+ FIELD_BINDING_ERROR(400001, "请求字段参数校验失败"),
+ FIELD_FORMAT_ERROR(400002, "请求参数格式错误"),
+ FIELD_MISSING_ERROR(400003, "请求参数不完整或不对"),
+ REQUEST_BODY_MISSING(400004, "需要请求体"),
+
+ ACCOUNT_CLIENT_ERROR(400000, "用户端错误"),
+
+ ACCOUNT_REGISTER_ERROR(400100, "用户注册异常"),
+ ACCOUNT_EXISTS(400101, "用户名已存在"),
+ ACCOUNT_ERROR(400102, "用户名校验错误"),
+ ACCOUNT_CONTAINS_SENSITIVE(400103, "用户名包含敏感词 "),
+ ACCOUNT_CONTAINS_SPECIAL(400104, "用户名包含特殊字符"),
+ PASSWORD_ERROR(400105, "密码校验错误"),
+ PASSWORD_TOO_SHORT(400106, "密码长度不够"),
+ PASSWORD_TOO_WEAK(400107, "密码强度不够"),
+ VCODE_ERROR(400108, "校验码错误"),
+ SMS_VCODE_ERROR(400109, "短信校验码错误"),
+ EMAIL_VCODE_ERROR(400110, "邮件校验码错误"),
+ ID_NUMBER_FORMAT_ERROR(400111, "身份证号格式错误"),
+ PHONE_NUMBER_FORMAT_ERROR(400112, "手机号格式错误"),
+ ADDRESS_ERROR(400113, "地址错误"),
+ EMAIL_FORMAT_ERROR(400114, "邮箱格式错误"),
+
+ ACCOUNT_LOGIN_EXCEPTION(400200, "用户登录异常"),
+ NOT_ACCOUNT(400201, "用户名不存在"),
+ ACCOUNT_IS_LOCKED(400202, "用户被锁定,请联系管理人员"),
+ ACCOUNT_HAS_EXPIRED(400203, "用户已作废"),
+ USERNAME_OR_PASSWORD_WRONG(400204, "用户名或密码错误"),
+ WRONG_PASSWORD(400205, "密码错误"),
+ WRONG_PASSWORDS_OVER_LIMIT(400206, "用户输入密码错误次数超限"),
+ ACCOUNT_LOGIN_EXPIRED(400207, "用户登录已过期"),
+ ACCOUNT_VCODE_ERROR(400208, "用户验证码错误"),
+ WRONG_VCODE_OVER_LIMIT(400209, "用户验证码错误次数超限"),
+ NO_AUTHORIZED(400210, "用户未获得第三方登录授权 "),
+ NO_SESSION(400211, "登录信息失效"),
+ CREDENTIALS_EXPIRED(400212, "登录凭证已过期,请重新登录"),
+ CREDENTIALS_EXCEPTION(400213, "登录凭证异常,请重新登录"),
+ ID_NUMBER_ERROR(400214, "身份证号错误"),
+ PHONE_NUMBER_ERROR(400215, "手机号错误"),
+ EMAIL_ERROR(400216, "邮箱错误"),
+
+ ACCOUNT_ACCESS_EXCEPTION(400300, "用户访问权限异常"),
+ ACCESS_UNAUTHORIZED(400301, "访问未授权"),
+ NOT_TOKEN(400302, "未提供token"),
+ TOKEN_INVALID(400303, "token无效"),
+ TOKEN_TIMEOUT(400304, "token已过期"),
+ TOKEN_BE_REPLACED(400305, "token已被顶下线"),
+ TOKEN_KICK_OUT(400306, "token已被踢下线"),
+ TOKEN_OTHER(400307, "token异常"),
+
+ PARAMETER_ERROR(400400, "用户请求参数错误"),
+ NOT_PARAMETER(400401, "请求必填参数为空"),
+ PARAMETER_VERIFICATION_ERROR(400402, "请求参数指定校验失败"),
+ PARAMETER_FORMAT_ERROR(400403, "请求参数格式错误"),
+ INCOMPLETE_PARAMETERS(400404, "请求参数不完整"),
+ NOT_REQUEST_BODY(400405, "无请求体"),
+ NOT_ID(400406, "id为空"),
+ NOT_ORGCODE(400407, "机构编码为空"),
+ NOT_SITECODE(400408, "站点编码为空"),
+
+ PERMISSION_MANAGEMENT_EXCEPTION(400500, "权限管理异常"),
+ NOT_ORG(400501, "机构不存在"),
+ ORG_HAS_SUBORG(400502, "该机构存在下级机构,无法删除"),
+ ORGNAME_EXISTS(400503, "机构名已存在"),
+ ORG_LINKED_USER(400504, "机构关联用户,无法删除"),
+ ROLENAME_EXISTS(400505, "角色名已存在"),
+ ROLE_LINKED_USER(400506, "角色关联用户,无法删除"),
+ NOT_ROLE(400507, "角色为空"),
+ NOT_PERMISSION(400508, "权限为空"),
+
+ REQUEST_SERVICE_EXCEPTION(400600, "用户请求服务异常"),
+ REQUEST_COUNT_OVER_LIMIT(400601, "请求次数超出限制 "),
+ REQUEST_CONCURRENCY_COUNT_OVER_LIMIT(400602, "请求并发数超出限制 "),
+ WEBSOCKET_CONNECTION_EXCEPTION(400603, "WebSocket 连接异常"),
+ WEBSOCKET_CONNECTION_DISCONNECTED(400604, "WebSocket 连接断开"),
+ USER_REPEATED_REQUEST(400605, "用户重复请求"),
+
+ GATEWAY_ERROR(400700, "网关异常"),
+ GATEWAY_NOT_FOUND_SERVICE(400701, "服务未找到"),
+ GATEWAY_CONNECT_TIME_OUT(400702, "网关超时"),
+ UPLOAD_FILE_SIZE_LIMIT(400703, "上传文件大小超过限制"),
+ DUPLICATE_PRIMARY_KEY(400704, "唯一键冲突");
+
+ /**
+ * 错误码
+ */
+ private final Integer code;
+
+ /**
+ * 错误消息
+ */
+ private final String msg;
+
+ ResponseCode(int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ @Override
+ public Integer getCode() {
+ return code;
+ }
+
+ @Override
+ public String getMsg() {
+ return msg;
+ }
+}
diff --git a/src/main/java/com/metalloop/common/exception/model/ApiErrorLog.java b/src/main/java/com/metalloop/common/exception/model/ApiErrorLog.java
new file mode 100644
index 0000000..fd2feac
--- /dev/null
+++ b/src/main/java/com/metalloop/common/exception/model/ApiErrorLog.java
@@ -0,0 +1,107 @@
+package com.metalloop.common.exception.model;
+
+import lombok.Data;
+//
+//import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+/**
+ * API 错误日志
+ *
+ * @author zhaowenhao
+ */
+@Data
+public class ApiErrorLog {
+
+ /**
+ * 链路编号
+ */
+ private String traceId;
+ /**
+ * 账号编号
+ */
+ private Long userId;
+ /**
+ * 用户类型
+ */
+ private Integer userType;
+ /**
+ * 应用名
+ */
+// @NotNull(message = "应用名不能为空")
+ private String applicationName;
+
+ /**
+ * 请求方法名
+ */
+// @NotNull(message = "http 请求方法不能为空")
+ private String requestMethod;
+ /**
+ * 访问地址
+ */
+// @NotNull(message = "访问地址不能为空")
+ private String requestUrl;
+ /**
+ * 请求参数
+ */
+ // @NotNull(message = "请求参数不能为空")
+ private String requestParams;
+ /**
+ * 用户 IP
+ */
+ // @NotNull(message = "ip 不能为空")
+ private String userIp;
+ /**
+ * 浏览器 UA
+ */
+ // @NotNull(message = "User-Agent 不能为空")
+ private String userAgent;
+
+ /**
+ * 异常时间
+ */
+ // @NotNull(message = "异常时间不能为空")
+ private LocalDateTime exceptionTime;
+ /**
+ * 异常名
+ */
+ // @NotNull(message = "异常名不能为空")
+ private String exceptionName;
+ /**
+ * 异常发生的类全名
+ */
+ // @NotNull(message = "异常发生的类全名不能为空")
+ private String exceptionClassName;
+ /**
+ * 异常发生的类文件
+ */
+ // @NotNull(message = "异常发生的类文件不能为空")
+ private String exceptionFileName;
+ /**
+ * 异常发生的方法名
+ */
+ // @NotNull(message = "异常发生的方法名不能为空")
+ private String exceptionMethodName;
+ /**
+ * 异常发生的方法所在行
+ */
+ // @NotNull(message = "异常发生的方法所在行不能为空")
+ private Integer exceptionLineNumber;
+ /**
+ * 异常的栈轨迹异常的栈轨迹
+ */
+ // @NotNull(message = "异常的栈轨迹不能为空")
+ private String exceptionStackTrace;
+ /**
+ * 异常导致的根消息
+ */
+ // @NotNull(message = "异常导致的根消息不能为空")
+ private String exceptionRootCauseMessage;
+ /**
+ * 异常导致的消息
+ */
+ // @NotNull(message = "异常导致的消息不能为空")
+ private String exceptionMessage;
+
+
+}
diff --git a/src/main/java/com/metalloop/common/model/Result.java b/src/main/java/com/metalloop/common/model/Result.java
new file mode 100644
index 0000000..8c5a7a2
--- /dev/null
+++ b/src/main/java/com/metalloop/common/model/Result.java
@@ -0,0 +1,107 @@
+package com.metalloop.common.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.metalloop.common.exception.code.ResponseCode;
+import com.metalloop.common.exception.BusinessException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 响应包装体
+ *
+ * @author zhaowenhao
+ * @since 2022-12-27
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Result implements Serializable {
+
+ private T data;
+ private Integer code;
+ private String msg;
+
+ public static Result ok() {
+ return of(null, ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
+ }
+
+ public static Result ok(T data) {
+ return of(data, ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
+ }
+
+ public static Result ok(T data, String msg) {
+ return of(data, ResponseCode.SUCCESS.getCode(), msg);
+ }
+
+ public static Result fail() {
+ return of(null, ResponseCode.FAIL.getCode(), ResponseCode.FAIL.getMsg());
+ }
+
+ public static Result fail(String msg) {
+ return of(null, ResponseCode.FAIL.getCode(), msg);
+ }
+
+ public static Result fail(int code, String msg) {
+ return of(null, code, msg);
+ }
+
+ public static Result fail(ResponseCode responseCode) {
+ return of(null, responseCode.getCode(), responseCode.getMsg());
+ }
+
+ public static Result of(T data, int code, String msg) {
+ return new Result<>(data, code, msg);
+ }
+
+ /**
+ * 判断是否成功
+ *
+ * @param code 状态码
+ * @return 是否成功
+ */
+ public static boolean isSuccess(Integer code) {
+ return Objects.equals(code, ResponseCode.SUCCESS.getCode());
+ }
+
+ /**
+ * 判断是否成功
+ *
+ * @return 是否成功
+ */
+ @JsonIgnore
+ public boolean isSuccess() {
+ return isSuccess(code);
+ }
+
+ @JsonIgnore
+ public boolean isError() {
+ return !isSuccess();
+ }
+
+ // ========= 和 Exception 异常体系集成 =========
+
+ /**
+ * 判断是否有异常。如果有,则抛出 {@link BusinessException} 异常
+ */
+ public void checkError() throws BusinessException {
+ if (isSuccess()) {
+ return;
+ }
+ // 业务异常
+ throw new BusinessException(code, msg);
+ }
+
+ /**
+ * 判断是否有异常。如果有,则抛出 {@link BusinessException} 异常
+ * 如果没有,则返回 {@link #data} 数据
+ */
+ @JsonIgnore
+ public T getCheckedData() {
+ checkError();
+ return data;
+ }
+}
diff --git a/src/main/java/com/metalloop/common/redis/RedisAutoConfigure.java b/src/main/java/com/metalloop/common/redis/RedisAutoConfigure.java
new file mode 100644
index 0000000..b5a22fc
--- /dev/null
+++ b/src/main/java/com/metalloop/common/redis/RedisAutoConfigure.java
@@ -0,0 +1,140 @@
+package com.metalloop.common.redis;
+
+import com.metalloop.common.redis.properties.CacheManagerProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.interceptor.KeyGenerator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Redis 配置类,键强制为string序列化,值默认使用json序列化,同时支持java序列化、string序列化
+ * 根据需求自行选择
+ * 缓存默认启用
+ *
+ * @author zhaowenhao
+ */
+@SuppressWarnings("DuplicatedCode")
+@EnableConfigurationProperties({RedisProperties.class, CacheManagerProperties.class})
+@EnableCaching
+@Component
+public class RedisAutoConfigure {
+ @Autowired
+ private CacheManagerProperties cacheManagerProperties;
+
+ @Bean
+ public RedisSerializer redisKeyStringSerializer() {
+ return RedisSerializer.string();
+ }
+
+ @Bean
+ public RedisSerializer