Compare commits

..
4 Commits
Author SHA1 Message Date
yy fc9220c345 merge: 合并远程 main 分支 2026-09-11 18:04:20 +08:00
yy a3eb2510ae add 初始化 2026-09-11 17:40:03 +08:00
yy f897270145 add 初始化 2026-09-11 17:38:34 +08:00
yy dfb943919d first commit 2026-09-11 17:34:14 +08:00
252 changed files with 24261 additions and 229 deletions
+83
View File
@@ -0,0 +1,83 @@
# ==================== Maven ====================
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
# ==================== IntelliJ IDEA ====================
.idea/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
# ==================== Eclipse ====================
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
# ==================== NetBeans ====================
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
# ==================== VS Code ====================
.vscode/
# ==================== Kotlin ====================
.kotlin/
# ==================== 日志 ====================
*.log
logs/
log/
# ==================== 编译产物 / 临时文件 ====================
*.class
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar
*.tmp
*.bak
*.swp
*~
# ==================== 数据库相关 ====================
*.db
*.sqlite
# ==================== 系统文件 ====================
.DS_Store
Thumbs.db
Desktop.ini
# ==================== 敏感信息 ====================
# 如果使用本地配置或密钥文件,务必忽略,避免密码泄露
application-local.yml
application-local.properties
application-dev.yml
application-prod.yml
*.pem
*.key
*.p12
*.jks
.env
# ==================== 上传目录 ====================
# 如果有本地上传文件夹,根据实际情况调整
/upload/
/uploads/
+10
View File
@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
</profile>
<profile name="Annotation profile for metaloop" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.6.3/mapstruct-processor-1.6.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.6.3/mapstruct-processor-1.6.3.jar" />
</processorPath>
<module name="metaloop" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="metaloop" options="-parameters" />
</option>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="KubernetesApiProvider"><![CDATA[{}]]></component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="ms-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/metaloop.iml" filepath="$PROJECT_DIR$/metaloop.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+229 -229
View File
@@ -1,230 +1,230 @@
有色再生资源交易平台 - 后端项目架构设计文档 有色再生资源交易平台 - 后端项目架构设计文档
项目代号:METALLOOP / metalloop 项目代号:METALLOOP / metalloop
文档版本:v1.0 文档版本:v1.0
创建时间:2026-09-09 创建时间:2026-09-09
技术基座:Spring Boot 3.x + Sa-Token + MyBatis-Plus + MySQL 8.0 + Redis 6.0 技术基座:Spring Boot 3.x + Sa-Token + MyBatis-Plus + MySQL 8.0 + Redis 6.0
1. 项目概述 1. 项目概述
本后端服务于“有色再生资源交易平台”,提供用户认证、权限管理、企业认证、产品目录管理、供应/求购信息发布与审核、即时聊天集成等核心业务 API。系统采用 RBAC(基于角色的访问控制) 权限模型,通过 Sa-Token 实现认证与鉴权,具备高扩展性和可维护性。 本后端服务于“有色再生资源交易平台”,提供用户认证、权限管理、企业认证、产品目录管理、供应/求购信息发布与审核、即时聊天集成等核心业务 API。系统采用 RBAC(基于角色的访问控制) 权限模型,通过 Sa-Token 实现认证与鉴权,具备高扩展性和可维护性。
1.1 核心业务目标 1.1 核心业务目标
支持企业注册、认证(营业执照 + 法人信息)、入驻平台; 支持企业注册、认证(营业执照 + 法人信息)、入驻平台;
支持企业发布供应信息与求购信息; 支持企业发布供应信息与求购信息;
支持管理员审核交易信息与企业认证资料; 支持管理员审核交易信息与企业认证资料;
支持基于 RBAC 的后台管理权限体系; 支持基于 RBAC 的后台管理权限体系;
支持与 OpenIM 集成,实现买卖双方在线即时沟通; 支持与 OpenIM 集成,实现买卖双方在线即时沟通;
支持多环境部署(开发、测试、生产)。 支持多环境部署(开发、测试、生产)。
2. 技术栈选型 2. 技术栈选型
组件 选型 版本 说明 组件 选型 版本 说明
JDK Oracle OpenJDK 17 LTS 长期支持版本,兼容 Spring Boot 3.x JDK Oracle OpenJDK 17 LTS 长期支持版本,兼容 Spring Boot 3.x
开发框架 Spring Boot 3.2.x 核心基础框架 开发框架 Spring Boot 3.2.x 核心基础框架
安全认证 Sa-Token 1.37.x 轻量级权限认证框架,支持 RBAC、分布式会话 安全认证 Sa-Token 1.37.x 轻量级权限认证框架,支持 RBAC、分布式会话
ORM 框架 MyBatis-Plus 3.5.x 增强 MyBatis,提供代码生成、分页、逻辑删除等 ORM 框架 MyBatis-Plus 3.5.x 增强 MyBatis,提供代码生成、分页、逻辑删除等
数据库 MySQL 8.0+ 关系型数据库,支持事务、窗口函数等 数据库 MySQL 8.0+ 关系型数据库,支持事务、窗口函数等
缓存 / 会话存储 Redis 6.0+ 用于 Sa-Token 分布式会话、接口防刷、热点数据缓存 缓存 / 会话存储 Redis 6.0+ 用于 Sa-Token 分布式会话、接口防刷、热点数据缓存
API 文档 Knife4j / Swagger 4.x 在线接口调试与文档生成 API 文档 Knife4j / Swagger 4.x 在线接口调试与文档生成
工具库 Hutool 5.8.x 常用工具类集合(日期、加密、Bean 拷贝等) 工具库 Hutool 5.8.x 常用工具类集合(日期、加密、Bean 拷贝等)
JSON 处理 Jackson (Spring Boot 内置) 序列化/反序列化 JSON 处理 Jackson (Spring Boot 内置) 序列化/反序列化
日志 SLF4J + Logback (Spring Boot 内置) 日志门面 + 实现 日志 SLF4J + Logback (Spring Boot 内置) 日志门面 + 实现
连接池 HikariCP (Spring Boot 内置) 高性能数据库连接池 连接池 HikariCP (Spring Boot 内置) 高性能数据库连接池
构建工具 Maven 3.6+ 项目构建与依赖管理 构建工具 Maven 3.6+ 项目构建与依赖管理
3. 系统模块划分(Maven 多模块) 3. 系统模块划分(Maven 多模块)
项目采用 Maven 多模块结构,便于后续拆分微服务或模块独立部署。 项目采用 Maven 多模块结构,便于后续拆分微服务或模块独立部署。
text text
metalloop-backend/ metalloop-backend/
├── pom.xml # 父 POM,统一版本管理 ├── pom.xml # 父 POM,统一版本管理
├── metalloop-common/ # 公共模块(无业务依赖) ├── metalloop-common/ # 公共模块(无业务依赖)
│ ├── src/main/java/ │ ├── src/main/java/
│ │ └── com.metalloop.common/ │ │ └── com.metalloop.common/
│ │ ├── annotation/ # 自定义注解 │ │ ├── annotation/ # 自定义注解
│ │ ├── constant/ # 常量、枚举类 │ │ ├── constant/ # 常量、枚举类
│ │ ├── exception/ # 自定义异常 │ │ ├── exception/ # 自定义异常
│ │ ├── model/ # 统一返回模型(Result、PageResult │ │ ├── model/ # 统一返回模型(Result、PageResult
│ │ └── utils/ # 工具类(MD5、JWT、DateUtil 等) │ │ └── utils/ # 工具类(MD5、JWT、DateUtil 等)
│ └── pom.xml │ └── pom.xml
├── metalloop-framework/ # 框架配置模块 ├── metalloop-framework/ # 框架配置模块
│ ├── src/main/java/ │ ├── src/main/java/
│ │ └── com.metalloop.framework/ │ │ └── com.metalloop.framework/
│ │ ├── config/ # 通用配置(Jackson、线程池、CORS │ │ ├── config/ # 通用配置(Jackson、线程池、CORS
│ │ ├── satoken/ # Sa-Token 配置、权限拦截器 │ │ ├── satoken/ # Sa-Token 配置、权限拦截器
│ │ ├── mybatis/ # MyBatis-Plus 配置(分页、填充、逻辑删除) │ │ ├── mybatis/ # MyBatis-Plus 配置(分页、填充、逻辑删除)
│ │ └── swagger/ # Knife4j 配置 │ │ └── swagger/ # Knife4j 配置
│ └── pom.xml │ └── pom.xml
├── metalloop-dao/ # 数据访问层 ├── metalloop-dao/ # 数据访问层
│ ├── src/main/java/ │ ├── src/main/java/
│ │ └── com.metalloop.dao/ │ │ └── com.metalloop.dao/
│ │ ├── entity/ # 数据库实体类(对应每张表) │ │ ├── entity/ # 数据库实体类(对应每张表)
│ │ ├── mapper/ # MyBatis-Plus Mapper 接口 │ │ ├── mapper/ # MyBatis-Plus Mapper 接口
│ │ ├── dto/ # 数据传输对象(业务层入参/出参) │ │ ├── dto/ # 数据传输对象(业务层入参/出参)
│ │ └── vo/ # 视图对象(Controller 返回专用) │ │ └── vo/ # 视图对象(Controller 返回专用)
│ └── pom.xml │ └── pom.xml
├── metalloop-service/ # 业务逻辑层 ├── metalloop-service/ # 业务逻辑层
│ ├── src/main/java/ │ ├── src/main/java/
│ │ └── com.metalloop.service/ │ │ └── com.metalloop.service/
│ │ ├── auth/ # 认证服务 │ │ ├── auth/ # 认证服务
│ │ ├── user/ # 用户服务 │ │ ├── user/ # 用户服务
│ │ ├── enterprise/ # 企业服务(含认证审核) │ │ ├── enterprise/ # 企业服务(含认证审核)
│ │ ├── product/ # 产品、品种、材质服务 │ │ ├── product/ # 产品、品种、材质服务
│ │ ├── trade/ # 供应/求购信息服务 │ │ ├── trade/ # 供应/求购信息服务
│ │ ├── permission/ # 权限与角色服务 │ │ ├── permission/ # 权限与角色服务
│ │ ├── im/ # OpenIM 集成服务 │ │ ├── im/ # OpenIM 集成服务
│ │ └── impl/ # 各服务实现类(按模块分包) │ │ └── impl/ # 各服务实现类(按模块分包)
│ └── pom.xml │ └── pom.xml
├── metalloop-api/ # API 接口定义(可选,用于 Feign 或微服务) ├── metalloop-api/ # API 接口定义(可选,用于 Feign 或微服务)
│ ├── src/main/java/ │ ├── src/main/java/
│ │ └── com.metalloop.api/ # 对外暴露的 API 接口 │ │ └── com.metalloop.api/ # 对外暴露的 API 接口
│ └── pom.xml │ └── pom.xml
└── metalloop-web/ # Web 启动模块 └── metalloop-web/ # Web 启动模块
├── src/main/java/ ├── src/main/java/
│ └── com.metalloop.web/ │ └── com.metalloop.web/
│ ├── controller/ # REST Controller(按模块分包) │ ├── controller/ # REST Controller(按模块分包)
│ ├── interceptor/ # 全局拦截器 │ ├── interceptor/ # 全局拦截器
│ ├── filter/ # 过滤器 │ ├── filter/ # 过滤器
│ └── advice/ # 全局异常处理器 │ └── advice/ # 全局异常处理器
├── src/main/resources/ ├── src/main/resources/
│ ├── application.yml # 主配置文件 │ ├── application.yml # 主配置文件
│ ├── application-dev.yml # 开发环境配置 │ ├── application-dev.yml # 开发环境配置
│ ├── application-prod.yml # 生产环境配置 │ ├── application-prod.yml # 生产环境配置
│ ├── logback-spring.xml # 日志配置 │ ├── logback-spring.xml # 日志配置
│ └── mapper/ # MyBatis XML 映射文件(可选) │ └── mapper/ # MyBatis XML 映射文件(可选)
└── pom.xml └── pom.xml
4. 包结构设计(单模块方案) 4. 包结构设计(单模块方案)
若采用单模块结构,推荐按业务模块分包,清晰隔离各领域: 若采用单模块结构,推荐按业务模块分包,清晰隔离各领域:
text text
com.metalloop com.metalloop
├── common # 公共组件(可独立为 common 模块) ├── common # 公共组件(可独立为 common 模块)
│ ├── annotation # 自定义注解 │ ├── annotation # 自定义注解
│ │ ├── @RequirePermission # 权限校验注解(可选,Sa-Token 自带 @SaCheckPermission │ │ ├── @RequirePermission # 权限校验注解(可选,Sa-Token 自带 @SaCheckPermission
│ │ └── @LogOperation # 操作日志注解 │ │ └── @LogOperation # 操作日志注解
│ ├── constant # 常量与枚举 │ ├── constant # 常量与枚举
│ │ ├── ApiCode.java # API 响应码枚举 │ │ ├── ApiCode.java # API 响应码枚举
│ │ ├── AuditStatusEnum.java # 审核状态枚举 │ │ ├── AuditStatusEnum.java # 审核状态枚举
│ │ └── UserStatusEnum.java # 用户状态枚举 │ │ └── UserStatusEnum.java # 用户状态枚举
│ ├── exception # 异常处理 │ ├── exception # 异常处理
│ │ ├── BusinessException.java # 业务异常 │ │ ├── BusinessException.java # 业务异常
│ │ └── GlobalExceptionHandler.java # 全局异常拦截 │ │ └── GlobalExceptionHandler.java # 全局异常拦截
│ ├── model # 统一响应模型 │ ├── model # 统一响应模型
│ │ ├── Result.java # 统一返回结构 │ │ ├── Result.java # 统一返回结构
│ │ └── PageResult.java # 分页返回结构 │ │ └── PageResult.java # 分页返回结构
│ └── utils # 工具类 │ └── utils # 工具类
│ ├── RedisUtil.java # Redis 工具封装 │ ├── RedisUtil.java # Redis 工具封装
│ ├── SecurityUtil.java # 加密工具(BCrypt、MD5 │ ├── SecurityUtil.java # 加密工具(BCrypt、MD5
│ └── BeanCopyUtil.java # 对象拷贝工具 │ └── BeanCopyUtil.java # 对象拷贝工具
├── framework # 框架集成(可独立为 framework 模块) ├── framework # 框架集成(可独立为 framework 模块)
│ ├── satoken │ ├── satoken
│ │ ├── SaTokenConfig.java # Sa-Token 配置类 │ │ ├── SaTokenConfig.java # Sa-Token 配置类
│ │ ├── StpInterfaceImpl.java # 权限/角色数据源实现 │ │ ├── StpInterfaceImpl.java # 权限/角色数据源实现
│ │ └── SaTokenInterceptor.java # 全局鉴权拦截器(可选) │ │ └── SaTokenInterceptor.java # 全局鉴权拦截器(可选)
│ ├── mybatis │ ├── mybatis
│ │ ├── MybatisPlusConfig.java # 分页、乐观锁配置 │ │ ├── MybatisPlusConfig.java # 分页、乐观锁配置
│ │ ├── MetaObjectHandler.java # 自动填充(createTime、updateTime │ │ ├── MetaObjectHandler.java # 自动填充(createTime、updateTime
│ │ └── LogicDeleteConfig.java # 逻辑删除配置(若使用) │ │ └── LogicDeleteConfig.java # 逻辑删除配置(若使用)
│ ├── redis │ ├── redis
│ │ └── RedisConfig.java # Redis 序列化配置(Jackson2Json │ │ └── RedisConfig.java # Redis 序列化配置(Jackson2Json
│ └── swagger │ └── swagger
│ └── Knife4jConfig.java # API 文档配置 │ └── Knife4jConfig.java # API 文档配置
├── modules # 业务模块 ├── modules # 业务模块
│ │ │ │
│ ├── auth # 认证模块 │ ├── auth # 认证模块
│ │ ├── controller │ │ ├── controller
│ │ │ └── AuthController.java # 登录、登出、刷新 Token │ │ │ └── AuthController.java # 登录、登出、刷新 Token
│ │ ├── service │ │ ├── service
│ │ │ ├── AuthService.java │ │ │ ├── AuthService.java
│ │ │ └── impl │ │ │ └── impl
│ │ │ └── AuthServiceImpl.java │ │ │ └── AuthServiceImpl.java
│ │ ├── model │ │ ├── model
│ │ │ ├── LoginDTO.java # 登录请求参数 │ │ │ ├── LoginDTO.java # 登录请求参数
│ │ │ └── LoginVO.java # 登录响应(含 Token) │ │ │ └── LoginVO.java # 登录响应(含 Token)
│ │ └── mapper # (复用 dao 层 Mapper │ │ └── mapper # (复用 dao 层 Mapper
│ │ │ │
│ ├── user # 用户管理模块 │ ├── user # 用户管理模块
│ │ ├── controller │ │ ├── controller
│ │ │ └── UserController.java # 用户 CRUD、禁用/启用 │ │ │ └── UserController.java # 用户 CRUD、禁用/启用
│ │ ├── service │ │ ├── service
│ │ └── model │ │ └── model
│ │ │ │
│ ├── enterprise # 企业管理模块 │ ├── enterprise # 企业管理模块
│ │ ├── controller │ │ ├── controller
│ │ │ └── EnterpriseController.java # 企业认证、审核、查询 │ │ │ └── EnterpriseController.java # 企业认证、审核、查询
│ │ ├── service │ │ ├── service
│ │ ├── model │ │ ├── model
│ │ │ ├── EnterpriseSubmitDTO.java # 提交认证资料 │ │ │ ├── EnterpriseSubmitDTO.java # 提交认证资料
│ │ │ └── EnterpriseAuditDTO.java # 审核操作 │ │ │ └── EnterpriseAuditDTO.java # 审核操作
│ │ └── mapper │ │ └── mapper
│ │ │ │
│ ├── product # 产品目录模块 │ ├── product # 产品目录模块
│ │ ├── controller │ │ ├── controller
│ │ │ ├── ProductController.java │ │ │ ├── ProductController.java
│ │ │ ├── VarietyController.java │ │ │ ├── VarietyController.java
│ │ │ └── MaterialController.java │ │ │ └── MaterialController.java
│ │ ├── service │ │ ├── service
│ │ └── model │ │ └── model
│ │ │ │
│ ├── trade # 交易信息模块 │ ├── trade # 交易信息模块
│ │ ├── controller │ │ ├── controller
│ │ │ ├── SupplyController.java # 供应信息 │ │ │ ├── SupplyController.java # 供应信息
│ │ │ └── PurchaseController.java # 求购信息 │ │ │ └── PurchaseController.java # 求购信息
│ │ ├── service │ │ ├── service
│ │ └── model │ │ └── model
│ │ │ │
│ ├── permission # 权限管理模块(后台) │ ├── permission # 权限管理模块(后台)
│ │ ├── controller │ │ ├── controller
│ │ │ ├── RoleController.java # 角色 CRUD │ │ │ ├── RoleController.java # 角色 CRUD
│ │ │ └── PermissionController.java # 权限/菜单管理 │ │ │ └── PermissionController.java # 权限/菜单管理
│ │ ├── service │ │ ├── service
│ │ └── model │ │ └── model
│ │ │ │
│ └── im # 即时聊天模块(OpenIM 集成) │ └── im # 即时聊天模块(OpenIM 集成)
│ ├── controller │ ├── controller
│ │ └── IMController.java # 获取 IM Token、Webhook 接收 │ │ └── IMController.java # 获取 IM Token、Webhook 接收
│ ├── service │ ├── service
│ │ ├── IMService.java # OpenIM API 调用封装 │ │ ├── IMService.java # OpenIM API 调用封装
│ │ └── WebhookService.java # 处理 OpenIM 回调事件 │ │ └── WebhookService.java # 处理 OpenIM 回调事件
│ └── model │ └── model
└── web # Web 层公共配置 └── web # Web 层公共配置
├── config ├── config
│ └── WebMvcConfig.java # 跨域、静态资源、拦截器注册 │ └── WebMvcConfig.java # 跨域、静态资源、拦截器注册
├── filter ├── filter
│ └── RequestLogFilter.java # 请求日志过滤器 │ └── RequestLogFilter.java # 请求日志过滤器
└── MainApplication.java # Spring Boot 启动类 └── MainApplication.java # Spring Boot 启动类
5. 数据库表清单 5. 数据库表清单
5.1 系统权限模块(sys_* 5.1 系统权限模块(sys_*
表名 说明 关键字段 表名 说明 关键字段
sys_user 用户表 id, username, password, phone, enterprise_id, auth_status, user_status sys_user 用户表 id, username, password, phone, enterprise_id, auth_status, user_status
sys_role 角色表 id, role_name, status, is_system, sort sys_role 角色表 id, role_name, status, is_system, sort
sys_permission 权限表(菜单 + 按钮) id, name, code, type, parent_id, api_path, route_path sys_permission 权限表(菜单 + 按钮) id, name, code, type, parent_id, api_path, route_path
sys_user_role 用户-角色关联表 user_id, role_id sys_user_role 用户-角色关联表 user_id, role_id
sys_role_permission 角色-权限关联表 role_id, permission_id sys_role_permission 角色-权限关联表 role_id, permission_id
5.2 企业模块(sys_enterprise 系列) 5.2 企业模块(sys_enterprise 系列)
注:根据现有数据库,企业表实际前缀为 sys_,与系统模块共用,逻辑上可按业务域区分。 注:根据现有数据库,企业表实际前缀为 sys_,与系统模块共用,逻辑上可按业务域区分。
表名 说明 关键字段 表名 说明 关键字段
sys_enterprise 企业信息主表 id, enterprise_name, legal_person, unified_social_credit_code, audit_status, credit_limit sys_enterprise 企业信息主表 id, enterprise_name, legal_person, unified_social_credit_code, audit_status, credit_limit
sys_enterprise_image 企业图片附件表 id, enterprise_id, image_type, image_url sys_enterprise_image 企业图片附件表 id, enterprise_id, image_type, image_url
5.3 业务交易模块(biz_* 5.3 业务交易模块(biz_*
表名 说明 关键字段 表名 说明 关键字段
biz_product 产品基础表 id, product_name, product_code, unit, status biz_product 产品基础表 id, product_name, product_code, unit, status
biz_variety 品种表(支持层级) id, variety_name, parent_id, variety_grade biz_variety 品种表(支持层级) id, variety_name, parent_id, variety_grade
biz_material 材质基础表 id, material_name biz_material 材质基础表 id, material_name
biz_enterprise_product 企业经营产品表 id, enterprise_id, variety_id, material_id, unit_price, weight biz_enterprise_product 企业经营产品表 id, enterprise_id, variety_id, material_id, unit_price, weight
biz_purchase_request 求购信息表 id, create_user_id, title, quantity, unit_price, audit_status, expire_time biz_purchase_request 求购信息表 id, create_user_id, title, quantity, unit_price, audit_status, expire_time
biz_supply_info 发布信息表(供应) id, create_user_id, title, quantity, unit_price, audit_status, expire_time biz_supply_info 发布信息表(供应) id, create_user_id, title, quantity, unit_price, audit_status, expire_time
biz_product_element_detail 产品材质详情表 id, product_id, element_name, element_content biz_product_element_detail 产品材质详情表 id, product_id, element_name, element_content
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
</component>
</module>
+158
View File
@@ -0,0 +1,158 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.metalloop</groupId>
<artifactId>metaloop</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.13</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<sa-token.version>1.46.0</sa-token.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<mapstruct.version>1.6.3</mapstruct.version>
<redisson.version>3.40.0</redisson.version>
<hutool.version>5.8.31</hutool.version>
</properties>
<dependencies>
<!-- ==================== Spring Boot Web ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- ==================== Redis ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- ==================== 数据库 ==================== -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MyBatis-Plus 3.5.9+ 需要额外引入 jsqlparser 模块 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- ==================== API 文档 ==================== -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.13</version>
</dependency>
<!-- ==================== Sa-Token ==================== -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- ==================== Redisson(分布式锁等) ==================== -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- ==================== 工具库 ==================== -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- ==================== 测试 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
+15
View File
@@ -0,0 +1,15 @@
//TIP 要<b>运行</b>代码,请按 <shortcut actionId="Run"/> 或
// 点击装订区域中的 <icon src="AllIcons.Actions.Execute"/> 图标。
public class Main {
public static void main(String[] args) {
//TIP 当文本光标位于高亮显示的文本处时按 <shortcut actionId="ShowIntentionActions"/>
// 查看 IntelliJ IDEA 建议如何修正。
System.out.printf("Hello and welcome!");
for (int i = 1; i <= 5; i++) {
//TIP 按 <shortcut actionId="Debug"/> 开始调试代码。我们已经设置了一个 <icon src="AllIcons.Debugger.Db_set_breakpoint"/> 断点
// 但您始终可以通过按 <shortcut actionId="ToggleLineBreakpoint"/> 添加更多断点。
System.out.println("i = " + i);
}
}
}
@@ -0,0 +1,17 @@
package com.metalloop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,142 @@
/*
* 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;
import com.metalloop.common.auth.permission.Permission;
import com.metalloop.common.auth.userdetails.UserDetails;
import java.util.Collection;
/**
* 认证后的用户信息
*
* @author zhaowenhao
* @since 2023-03-21
*/
public class Authentication {
/**
* 权限集合
*/
Collection<? extends Permission> permissions;
/**
* 用户Id
*/
String userId;
/**
* 用户登录名
*/
String username;
/**
* 访问令牌
*/
String accessToken;
/**
* 返回授予用户的权限。无法返回null 。
*
* @return 按自然键排序的权限集合。
*/
public Collection<? extends Permission> getPermissions() {
return this.permissions;
}
/**
* 返回用于对用户进行身份验证的用户Id。无法返回null 。
*
* @return 用户Id
*/
public String getUserId() {
return this.userId;
}
/**
* 返回用于对用户进行身份验证的用户名。无法返回null 。
*
* @return 用户名
*/
public String getUsername() {
return this.username;
}
/**
* 获取访问令牌
*
* @return 访问令牌
*/
public String getAccessToken() {
return accessToken;
}
public void setPermissions(Collection<? extends Permission> permissions) {
this.permissions = permissions;
}
public void setUserId(String userId) {
this.userId = userId;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
/**
* 从UserDetails中构建认证通过的用户
*
* @param userDetails UserDetails
* @return 认证通过的用户
*/
public static Builder withUserDetails(UserDetails userDetails) {
return new Builder(userDetails);
}
public static class Builder {
private final Collection<? extends Permission> permissions;
private final String userId;
private final String username;
private String accessToken;
public Builder(UserDetails userDetails) {
this.permissions = userDetails.getPermissions();
this.userId = userDetails.getUserId();
this.username = userDetails.getUsername();
}
public Builder accessToken(String accessToken) {
this.accessToken = accessToken;
return this;
}
public Authentication build() {
Authentication authentication = new Authentication();
authentication.setPermissions(this.permissions);
authentication.setUserId(this.userId);
authentication.setUsername(this.username);
authentication.setAccessToken(this.accessToken);
return authentication;
}
}
}
@@ -0,0 +1,109 @@
package com.metalloop.common.auth.config;
import com.metalloop.common.enums.LoginType;
import com.metalloop.common.auth.password.PasswordDecryptor;
import com.metalloop.common.auth.password.RsaPasswordDecryptor;
import com.metalloop.modules.auth.controller.AuthController;
import com.metalloop.common.auth.encrypt.KeyProperties;
import com.metalloop.modules.auth.controller.RsaPublicKeyController;
import com.metalloop.modules.auth.model.req.AuthenticationRequest;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.Collections;
import java.util.List;
/**
* 认证服务器配置类
*
* @author zhaowenhao
* @since 2023-03-19
*/
@SuppressWarnings({"SpringJavaAutowiredFieldsWarningInspection", "SpringJavaInjectionPointsAutowiringInspection"})
@Configuration
@EnableConfigurationProperties({KeyProperties.class})
public class AuthServerConfiguration {
@Autowired
private List<AuthServerConfigurer> configurers = Collections.emptyList();
@Autowired
private RequestMappingHandlerMapping handlerMapping;
@Autowired
private KeyProperties keyProperties;
@PostConstruct
public void init() {
// 检查是否有相关的配置
Assert.notEmpty(configurers, "找不到认证服务器的相关配置");
// 遍历每个配置器
for (AuthServerConfigurer configurer : configurers) {
AuthServerEndpointsConfigurer endpointsConfigurer = new AuthServerEndpointsConfigurer();
try {
configurer.configure(endpointsConfigurer);
} catch (Exception e) {
throw new IllegalStateException("无法配置认证端点", e);
}
// 为每个服务创建并注册一个 AuthController
LoginType loginType = endpointsConfigurer.getLoginType();
AuthController authController = new AuthController(loginType, endpointsConfigurer.getUserDetailsService(),
endpointsConfigurer.getLoginSuccessHandler(), endpointsConfigurer.getLogoutSuccessHandler(),
passwordDecryptor(keyProperties), passwordEncoder(), endpointsConfigurer.getVerifyCodeHandler(), endpointsConfigurer.getPasswordCheckFailureHandler());
// 注册登录和登出端点
if ("wechat".equals(loginType.getType())) {
// registerEndpoint(endpointsConfigurer.getLoginProcessingUrl(), "miniAppLogin", authController, RequestMethod.POST, AuthenticationMiniAppRequest.class, HttpServletRequest.class);
// registerEndpoint(endpointsConfigurer.getLogoutProcessingUrl(), "miniAppLogout", authController, RequestMethod.POST);
} else {
registerEndpoint(endpointsConfigurer.getLoginProcessingUrl(), "login", authController, RequestMethod.POST, AuthenticationRequest.class, HttpServletRequest.class);
registerEndpoint(endpointsConfigurer.getLogoutProcessingUrl(), "logout", authController, RequestMethod.POST);
}
// 为每个服务创建并注册一个 RsaPublicKeyController
RsaPublicKeyController rsaPublicKeyController = new RsaPublicKeyController(keyProperties);
// 注册公钥获取端点
registerEndpoint(endpointsConfigurer.getPublicKeyFetchUrl(), "getPublicKey", rsaPublicKeyController, RequestMethod.GET);
// 如果开启了单点登陆
if (Boolean.TRUE.equals(endpointsConfigurer.getEnableSso())) {
authController.initSso(endpointsConfigurer.getSsoCodeService(), endpointsConfigurer.getSsoCodeTimeOut());
// 注册单点登陆相关端点
registerEndpoint(endpointsConfigurer.getSsoCodeFetchUrl(), "getSsoCode", authController, RequestMethod.GET);
registerEndpoint(endpointsConfigurer.getSsoUserinfoFetchUrl(), "getSsoUserInfo", authController, RequestMethod.GET, String.class);
}
}
}
/**
* 注册端点的辅助方法
*/
private void registerEndpoint(String url, String methodName, Object controller, RequestMethod requestMethod, Class<?>... parameterTypes) {
RequestMappingInfo mappingInfo = RequestMappingInfo.paths(url).methods(requestMethod).build();
try {
handlerMapping.registerMapping(mappingInfo, controller, controller.getClass().getMethod(methodName, parameterTypes));
} catch (NoSuchMethodException e) {
throw new IllegalStateException("注册端点出错", e);
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@ConditionalOnMissingBean(PasswordDecryptor.class)
public PasswordDecryptor passwordDecryptor(KeyProperties keyProperties) {
return new RsaPasswordDecryptor(keyProperties);
}
}
@@ -0,0 +1,18 @@
package com.metalloop.common.auth.config;
/**
* 认证服务器配置器
*
* @author zhaowenhao
* @since 2023-03-19
*/
public interface AuthServerConfigurer {
/**
* 配置操作
*
* @param configurer 配置器
* @throws Exception 异常
*/
void configure(AuthServerEndpointsConfigurer configurer) throws Exception;
}
@@ -0,0 +1,16 @@
package com.metalloop.common.auth.config;
import org.springframework.util.Assert;
/**
* 认证服务器配置器适配器
*
* @author zhaowenhao
* @since 2023-03-19
*/
public class AuthServerConfigurerAdapter implements AuthServerConfigurer {
@Override
public void configure(AuthServerEndpointsConfigurer configurer) throws Exception {
Assert.notNull(configurer, "认证服务器端点配置器不能为空");
}
}
@@ -0,0 +1,227 @@
package com.metalloop.common.auth.config;
import com.metalloop.common.auth.handler.*;
import com.metalloop.common.auth.sso.SsoCodeService;
import com.metalloop.common.auth.userdetails.UserDetailsService;
import com.metalloop.common.enums.LoginType;
/**
* 认证服务器端点配置器
*
* @author zhaowenhao
* @since 2023-03-19
*/
public class AuthServerEndpointsConfigurer {
/**
* 账号类型
*/
private LoginType loginType = LoginType.DEFAULT;
/**
* 用户详细信息服务
*/
private UserDetailsService userDetailsService;
/**
* 登录处理Url
*/
private String loginProcessingUrl = "/login";
/**
* 登录处理Url
*/
private String verifyCodeProcessingUrl = "/getVerifyCode";
/**
* 登出处理Url
*/
private String logoutProcessingUrl = "/logout";
/**
* 公钥提取Url
*/
private String publicKeyFetchUrl = "/rsa/public-key";
/**
* 登录成功处理器
*/
private LoginSuccessHandler loginSuccessHandler = new DefaultLoginSuccessHandler();
/**
* 验证码生成处理器
*/
private VerifyCodeHandler verifyCodeHandler = new DefaultVerifyCodeHandler();
/**
* 密码校验失败处理器
*/
private PasswordCheckFailureHandler passwordCheckFailureHandler = new DefaultPasswordCheckFailureHandler();
/**
* 登出成功处理器
*/
private LogoutSuccessHandler logoutSuccessHandler = new DefaultLogoutSuccessHandler();
/**
* 是否启用单点登录
*/
private Boolean enableSso = false;
/**
* 单点登录授权码服务
*/
private SsoCodeService ssoCodeService;
/**
* 单点登录授权码过期时间
*/
private Integer ssoCodeTimeOut = 300;
/**
* 单点登录授权码获取Url
*/
private String ssoCodeFetchUrl = "/sso/code";
/**
* 单点登录授权码用户信息获取Url
*/
private String ssoUserinfoFetchUrl = "/sso/userinfo";
public AuthServerEndpointsConfigurer loginType(LoginType loginType) {
this.loginType = loginType;
return this;
}
public AuthServerEndpointsConfigurer userDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
return this;
}
public AuthServerEndpointsConfigurer loginProcessingUrl(String loginProcessingUrl) {
this.loginProcessingUrl = loginProcessingUrl;
return this;
}
public AuthServerEndpointsConfigurer verifyCodeProcessingUrl(String verifyCodeProcessingUrl) {
this.verifyCodeProcessingUrl = verifyCodeProcessingUrl;
return this;
}
public AuthServerEndpointsConfigurer logoutProcessingUrl(String logoutProcessingUrl) {
this.logoutProcessingUrl = logoutProcessingUrl;
return this;
}
public AuthServerEndpointsConfigurer publicKeyFetchUrl(String publicKeyFetchUrl) {
this.publicKeyFetchUrl = publicKeyFetchUrl;
return this;
}
public AuthServerEndpointsConfigurer loginSuccessHandler(LoginSuccessHandler loginSuccessHandler) {
this.loginSuccessHandler = loginSuccessHandler;
return this;
}
public AuthServerEndpointsConfigurer verifyCodeHandler(VerifyCodeHandler verifyCodeHandler) {
this.verifyCodeHandler = verifyCodeHandler;
return this;
}
public AuthServerEndpointsConfigurer passwordCheckFailureHandler(PasswordCheckFailureHandler passwordCheckFailureHandler) {
this.passwordCheckFailureHandler = passwordCheckFailureHandler;
return this;
}
public AuthServerEndpointsConfigurer logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
this.logoutSuccessHandler = logoutSuccessHandler;
return this;
}
public AuthServerEndpointsConfigurer enableSso(boolean enableSso) {
this.enableSso = enableSso;
return this;
}
public AuthServerEndpointsConfigurer ssoCodeService(SsoCodeService ssoCodeService) {
this.ssoCodeService = ssoCodeService;
return this;
}
public AuthServerEndpointsConfigurer ssoCodeTimeOut(int ssoCodeTimeOut) {
this.ssoCodeTimeOut = ssoCodeTimeOut;
return this;
}
public AuthServerEndpointsConfigurer ssoCodeFetchUrl(String ssoCodeFetchUrl) {
this.ssoCodeFetchUrl = ssoCodeFetchUrl;
return this;
}
public AuthServerEndpointsConfigurer ssoUserinfoFetchUrl(String ssoUserinfoFetchUrl) {
this.ssoUserinfoFetchUrl = ssoUserinfoFetchUrl;
return this;
}
public LoginType getLoginType() {
return loginType;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public String getLoginProcessingUrl() {
return loginProcessingUrl;
}
public String getVerifyCodeProcessingUrl() {
return verifyCodeProcessingUrl;
}
public String getLogoutProcessingUrl() {
return logoutProcessingUrl;
}
public LoginSuccessHandler getLoginSuccessHandler() {
return loginSuccessHandler;
}
public VerifyCodeHandler getVerifyCodeHandler() {
return verifyCodeHandler;
}
public PasswordCheckFailureHandler getPasswordCheckFailureHandler(){
return passwordCheckFailureHandler;
}
public LogoutSuccessHandler getLogoutSuccessHandler() {
return logoutSuccessHandler;
}
public String getPublicKeyFetchUrl() {
return publicKeyFetchUrl;
}
public Boolean getEnableSso() {
return enableSso;
}
public SsoCodeService getSsoCodeService() {
return ssoCodeService;
}
public Integer getSsoCodeTimeOut() {
return ssoCodeTimeOut;
}
public String getSsoCodeFetchUrl() {
return ssoCodeFetchUrl;
}
public String getSsoUserinfoFetchUrl() {
return ssoUserinfoFetchUrl;
}
}
@@ -0,0 +1,22 @@
package com.metalloop.common.auth.config;
import com.metalloop.framework.satoken.admin.config.SaTokenAdminConfiguration;
import com.metalloop.framework.satoken.self.config.SaTokenSelfConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 开启认证服务器
*
* @author zhaowenhao
* @since 2023-03-19
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({AuthServerConfiguration.class, SaTokenAdminConfiguration.class, SaTokenSelfConfiguration.class})
public @interface EnableAuthServer {
}
@@ -0,0 +1,148 @@
package com.metalloop.common.auth.encrypt;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
/**
* 加密解密配置
*
* @author zhaowenhao
* @since 2023-03-19
*/
@ConfigurationProperties(KeyProperties.PREFIX)
public class KeyProperties {
/**
* ConfigurationProperties prefix for KeyProperties.
*/
public static final String PREFIX = "auth-server.encrypt";
/**
* A symmetric key. As a stronger alternative, consider using a keystore.
*/
private String key;
/**
* A salt for the symmetric key, in the form of a hex-encoded byte array. As a
* stronger alternative, consider using a keystore.
*/
private String salt = "deadbeef";
/**
* Flag to say that a process should fail if there is an encryption or decryption
* error.
*/
private boolean failOnError = true;
/**
* The key store properties for locating a key in a Java Key Store (a file in a format
* defined and understood by the JVM).
*/
private KeyStore keyStore = new KeyStore();
public boolean isFailOnError() {
return this.failOnError;
}
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
public String getKey() {
return this.key;
}
public void setKey(String key) {
this.key = key;
}
public String getSalt() {
return this.salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public KeyStore getKeyStore() {
return this.keyStore;
}
public void setKeyStore(KeyStore keyStore) {
this.keyStore = keyStore;
}
/**
* Key store properties.
*/
public static class KeyStore {
/**
* Location of the key store file, e.g. classpath:/keystore.jks.
*/
private Resource location;
/**
* Password that locks the keystore.
*/
private String password;
/**
* Alias for a key in the store.
*/
private String alias;
/**
* Secret protecting the key (defaults to the same as the password).
*/
private String secret;
/**
* The KeyStore type. Defaults to jks.
*/
private String type = "jks";
public String getAlias() {
return this.alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public Resource getLocation() {
return this.location;
}
public void setLocation(Resource location) {
this.location = location;
}
public String getPassword() {
return this.password;
}
public String getType() {
return type;
}
public void setPassword(String password) {
this.password = password;
}
public String getSecret() {
return this.secret == null ? this.password : this.secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public void setType(String type) {
this.type = type;
}
}
}
@@ -0,0 +1,73 @@
package com.metalloop.common.auth.encrypt;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.RSAPublicKeySpec;
/**
* Factory for RSA key pairs from a JKS keystore file. User provides a {@link Resource} location of a keystore file and
* the password to unlock it, and the factory grabs the keypairs from the store by name (and optionally password).
*
* @author Dave Syer
*/
public class KeyStoreKeyFactory {
private static final Log logger = LogFactory.getLog(KeyStoreKeyFactory.class);
private final Resource resource;
private final char[] password;
private KeyStore store;
private final Object lock = new Object();
public KeyStoreKeyFactory(Resource resource, char[] password) {
this.resource = resource;
this.password = password;
}
public KeyPair getKeyPair(String alias) {
return getKeyPair(alias, password);
}
public KeyPair getKeyPair(String alias, char[] password) {
InputStream inputStream = null;
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("jks");
inputStream = resource.getInputStream();
store.load(inputStream, this.password);
}
}
}
RSAPrivateCrtKey key = (RSAPrivateCrtKey) store.getKey(alias, password);
RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
return new KeyPair(publicKey, key);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
logger.warn("Cannot close open stream: ", e);
}
}
}
}
@@ -0,0 +1,23 @@
package com.metalloop.common.auth.handler;
import com.metalloop.common.auth.Authentication;
import com.metalloop.common.model.Result;
import com.metalloop.modules.auth.model.resp.AuthenticationResponse;
/**
* 登录成功处理器默认实现
*
* @author zhaowenhao
* @since 2023-03-19
*/
public class DefaultLoginSuccessHandler implements LoginSuccessHandler {
@Override
public Object onSuccess(Authentication authentication) {
AuthenticationResponse authenticationResponse = new AuthenticationResponse();
authenticationResponse.setAccessToken(authentication.getAccessToken());
authenticationResponse.setUsername(authentication.getUsername());
authenticationResponse.setUserId(authentication.getUserId());
return Result.ok(authenticationResponse);
}
}
@@ -0,0 +1,18 @@
package com.metalloop.common.auth.handler;
import com.metalloop.common.auth.Authentication;
import com.metalloop.common.model.Result;
/**
* 登出成功处理器默认实现
*
* @author zhaowenhao
* @since 2023-03-19
*/
public class DefaultLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public Object onSuccess(Authentication authentication) {
return Result.ok();
}
}
@@ -0,0 +1,27 @@
package com.metalloop.common.auth.handler;
import com.metalloop.common.auth.userdetails.UserDetails;
/**
* 密码校验失败处理器默认实现
*
* @author lifan
* @date 2024/3/26 18:04
*/
public class DefaultPasswordCheckFailureHandler implements PasswordCheckFailureHandler {
@Override
public void onFailure(String ip, UserDetails userDetails) {
}
@Override
public void onSuccess(String ip, UserDetails userDetails) {
}
@Override
public void checkAccountLockStatus(String ip, UserDetails userDetails) {
}
}
@@ -0,0 +1,15 @@
package com.metalloop.common.auth.handler;
public class DefaultVerifyCodeHandler implements VerifyCodeHandler{
@Override
public boolean createVerifyCode(String phoneNum) {
return false;
}
@Override
public boolean checkVerifyCode(String phoneNum, String verifyCode) {
return false;
}
}
@@ -0,0 +1,21 @@
package com.metalloop.common.auth.handler;
import com.metalloop.common.auth.Authentication;
/**
* 登录成功处理器
*
* @author zhaowenhao
* @since 2023-03-19
*/
public interface LoginSuccessHandler {
/**
* 处理登录成功的情况,例如生成令牌、返回用户信息等。
*
* @param authentication 认证信息
* @return 登录成功后的响应对象
*/
Object onSuccess(Authentication authentication);
}
@@ -0,0 +1,21 @@
package com.metalloop.common.auth.handler;
import com.metalloop.common.auth.Authentication;
/**
* 登出成功处理器
*
* @author zhaowenhao
* @since 2023-03-19
*/
public interface LogoutSuccessHandler {
/**
* 处理登出成功的情况,认证信息可能为null
*
* @param authentication 认证信息,可能为null
* @return 登录成功后的响应对象
*/
Object onSuccess(Authentication authentication);
}
@@ -0,0 +1,35 @@
package com.metalloop.common.auth.handler;
import com.metalloop.common.auth.userdetails.UserDetails;
/**
* @author lifan
* @date 2024/3/26 17:58
*/
public interface PasswordCheckFailureHandler {
/**
* 处理密码校验失败的情况
*
* @param ip 用户ip
* @param userDetails 账户信息
*/
void onFailure(String ip, UserDetails userDetails);
/**
* 处理密码校验成功的情况
*
* @param ip 用户ip
* @param userDetails 账户信息
*/
void onSuccess(String ip, UserDetails userDetails);
/**
* 检查账户锁定状态
*
* @param ip 用户ip
* @param userDetails 账户信息
*/
void checkAccountLockStatus(String ip, UserDetails userDetails);
}
@@ -0,0 +1,21 @@
package com.metalloop.common.auth.handler;
public interface VerifyCodeHandler {
/**
* 生成验证码
*
* @param phoneNum 手机号
* @return 是否成功
*/
boolean createVerifyCode(String phoneNum);
/**
* 校验验证码
*
* @param phoneNum 手机号
* @param verifyCode 待校验的验证码
* @return 是否成功
*/
boolean checkVerifyCode(String phoneNum, String verifyCode);
}
@@ -0,0 +1,38 @@
package com.metalloop.common.auth.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 小程序公众号认证请求
*
* @author zhaowenhao
* @since 2023-03-19
*/
@Data
public class AuthenticationMiniAppRequest {
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
/**
* openId
*/
@NotBlank(message = "openId")
private String openId;
/**
* 验证码
*/
private String verifyCode;
}
@@ -0,0 +1,32 @@
package com.metalloop.common.auth.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 认证请求
*
* @author zhaowenhao
* @since 2023-03-19
*/
@Data
public class AuthenticationRequest {
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
/**
* 验证码
*/
private String verifyCode;
}
@@ -0,0 +1,29 @@
package com.metalloop.common.auth.model;
import lombok.Data;
/**
* 认证响应
*
* @author zhaowenhao
* @since 2023-03-19
*/
@Data
public class AuthenticationResponse {
/**
* 访问令牌
*/
private String accessToken;
/**
* 用户登录名
*/
private String username;
/**
* 用户ID
*/
private String userId;
}
@@ -0,0 +1,18 @@
package com.metalloop.common.auth.password;
/**
* 密码解密器
*
* @author zhaowenhao
* @since 2023-02-02
*/
public interface PasswordDecryptor {
/**
* 解密密码密文
*
* @param rawPassword 密码密文
* @return 密码明文
*/
String decrypt(String rawPassword);
}
@@ -0,0 +1,44 @@
package com.metalloop.common.auth.password;
import com.metalloop.common.exception.BadCredentialsException;
import com.metalloop.common.utils.RsaUtils;
import com.metalloop.common.auth.encrypt.KeyProperties;
import com.metalloop.common.auth.encrypt.KeyStoreKeyFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
/**
* RSA 密码解密器
*
* @author zhaowenhao
*/
public class RsaPasswordDecryptor implements PasswordDecryptor {
protected final Log logger = LogFactory.getLog(getClass());
/**
* Key encryption properties.
*/
private final KeyProperties keyProperties;
public RsaPasswordDecryptor(KeyProperties keyProperties) {
this.keyProperties = keyProperties;
}
@Override
public String decrypt(String rawPassword) {
if (keyProperties != null) {
KeyPair keyPair = new KeyStoreKeyFactory(keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray()).getKeyPair(keyProperties.getKeyStore().getAlias());
PrivateKey privateKey = keyPair.getPrivate();
try {
return RsaUtils.decrypt(rawPassword, privateKey);
} catch (Exception e) {
logger.debug("Authentication failed: 解密密码出错: " + e.getMessage());
throw new BadCredentialsException("用户名或密码错误");
}
}
return rawPassword;
}
}
@@ -0,0 +1,42 @@
/*
* 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.permission;
import java.io.Serializable;
/**
* 表示权限,包括权限码和权限对应的URI。
*
* @author zhaowenhao
* @since 2023-03-19
*/
public interface Permission extends Serializable {
/**
* 获取权限码
*
* @return 权限码
*/
String getPermissionCode();
/**
* 获取权限对应的URI
*
* @return 权限对应的URI
*/
String getPermissionUri();
}
@@ -0,0 +1,19 @@
package com.metalloop.common.auth.permission;
import java.util.List;
import java.util.concurrent.Future;
/**
* 权限服务
*
* @author zhaowenhao
*/
public interface PermissionService {
/**
* 加载所有权限,实现者应缓存权限列表,避免每次都查询数据库
*
* @return 权限列表
*/
Future<List<? extends Permission>> loadAllPermissions();
}
@@ -0,0 +1,30 @@
package com.metalloop.common.auth.permission;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 简单的权限实现
*
* @author zhaowenhao
* @since 2023-03-19
*/
@Data
@NoArgsConstructor
public class SimplePermission implements Permission {
/**
* 权限码
*/
private String permissionCode;
/**
* 权限对应的URI
*/
private String permissionUri;
public SimplePermission(String permissionCode, String permissionUri) {
this.permissionCode = permissionCode;
this.permissionUri = permissionUri;
}
}
@@ -0,0 +1,31 @@
package com.metalloop.common.auth.sso;
import com.metalloop.common.auth.sso.model.SsoUserInfo;
import com.metalloop.common.exception.BadCredentialsException;
/**
* 单点登录授权码服务,需要一个服务来生成授权码,并验证授权码的有效性
*
* @author zhaowenhao
* @since 2024-02-28
*/
public interface SsoCodeService {
/**
* 生成指定用户的单点登陆授权码
*
* @param userId 用户ID
* @param timeout 过期时间,单位:秒
* @return 单点登陆授权码
*/
String generateCode(String userId, long timeout);
/**
* 验证单点登陆授权码的有效性,如果授权码有效,返回授权码对应的用户信息
*
* @param code 单点登陆授权码
* @return 单点登陆用户信息
*/
SsoUserInfo validateCode(String code) throws BadCredentialsException;
}
@@ -0,0 +1,298 @@
package com.metalloop.common.auth.sso.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 简单的用户信息实现
*
* @author zhaowenhao
* @since 2023-03-19
*/
@Data
@NoArgsConstructor
public class SimpleSsoUserInfo implements SsoUserInfo {
// region 用户基本信息
/**
* 用户在数据库中的唯一标识符
*/
private String userId;
/**
* 用户登录名
*/
private String username;
/**
* 用户全名
*/
private String fullName;
/**
* 用户昵称
*/
private String nickName;
/**
* 用户类型
*/
private String type;
/**
* 执法类型
*/
private String lawType;
/**
* 职位/职称
*/
private String jobTitle;
/**
* 用户个人资料页面的 URL
*/
private String profile;
/**
* 用户个人资料图片的 URL
*/
private String picture;
/**
* 用户的首选电子邮件地址
*/
private String email;
/**
* 用户的首选电话号码
*/
private String phoneNumber;
/**
* 用户的性别
*/
private String gender;
/**
* 用户的出生日期
*/
private String birthdate;
/**
* 用户的地址
*/
private String address;
/**
* 用户的身份证号
*/
private String idNumber;
/**
* 用户的工作单位
*/
private String workUnit;
// endregion 用户基本信息
// region 用户机构信息
/**
* 用户机构在数据库中的唯一标识符,为了兼容不同的情况,这里使用字符串类型
*/
private String orgId;
/**
* 用户的机构编码
*/
private String orgCode;
/**
* 用户的机构名称
*/
private String orgName;
/**
* 用户的机构级别
*/
private String orgLevel;
/**
* 用户的机构类型
*/
private String orgType;
// endregion 用户机构信息
public SimpleSsoUserInfo(Builder builder) {
this.userId = builder.userId;
this.username = builder.username;
this.fullName = builder.fullName;
this.nickName = builder.nickName;
this.type = builder.type;
this.lawType = builder.lawType;
this.jobTitle = builder.jobTitle;
this.profile = builder.profile;
this.picture = builder.picture;
this.email = builder.email;
this.phoneNumber = builder.phoneNumber;
this.gender = builder.gender;
this.birthdate = builder.birthdate;
this.address = builder.address;
this.idNumber = builder.idNumber;
this.workUnit = builder.workUnit;
this.orgId = builder.orgId;
this.orgCode = builder.orgCode;
this.orgName = builder.orgName;
this.orgLevel = builder.orgLevel;
this.orgType = builder.orgType;
}
public static Builder withUserId(String userId) {
return new Builder(userId);
}
public static class Builder {
private final String userId;
private String username;
private String fullName;
private String nickName;
private String type;
private String lawType;
private String jobTitle;
private String profile;
private String picture;
private String email;
private String phoneNumber;
private String gender;
private String birthdate;
private String address;
private String idNumber;
private String workUnit;
private String orgId;
private String orgCode;
private String orgName;
private String orgLevel;
private String orgType;
private List<String> roleNames;
public Builder(String userId) {
this.userId = userId;
}
public Builder username(String username) {
this.username = username;
return this;
}
public Builder fullName(String fullName) {
this.fullName = fullName;
return this;
}
public Builder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public Builder type(String type) {
this.type = type;
return this;
}
public Builder lawType(String lawType) {
this.lawType = lawType;
return this;
}
public Builder jobTitle(String jobTitle) {
this.jobTitle = jobTitle;
return this;
}
public Builder profile(String profile) {
this.profile = profile;
return this;
}
public Builder picture(String picture) {
this.picture = picture;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
public Builder gender(String gender) {
this.gender = gender;
return this;
}
public Builder birthdate(String birthdate) {
this.birthdate = birthdate;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder idNumber(String idNumber) {
this.idNumber = idNumber;
return this;
}
public Builder workUnit(String workUnit) {
this.workUnit = workUnit;
return this;
}
public Builder orgId(String orgId) {
this.orgId = orgId;
return this;
}
public Builder orgCode(String orgCode) {
this.orgCode = orgCode;
return this;
}
public Builder orgName(String orgName) {
this.orgName = orgName;
return this;
}
public Builder orgLevel(String orgLevel) {
this.orgLevel = orgLevel;
return this;
}
public Builder orgType(String orgType) {
this.orgType = orgType;
return this;
}
public Builder roleNames(List<String> roleNames) {
this.roleNames = roleNames;
return this;
}
public SimpleSsoUserInfo build() {
return new SimpleSsoUserInfo(this);
}
public List<String> getRoleNames() {
return roleNames;
}
}
}
@@ -0,0 +1,203 @@
package com.metalloop.common.auth.sso.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
/**
* 单点登录用户信息接口,相较于UserInfo,该接口只包含用户的非敏感基本信息,不包含用户的角色和权限信息。
*
* @author zhaowenhao
* @since 2024-02-28
*/
public interface SsoUserInfo extends Serializable {
// region 用户基本信息
/**
* 用户在数据库中的唯一标识符,为了兼容不同的情况,这里使用字符串类型。
*
* @return 用户在数据库中的唯一标识符。
*/
String getUserId();
/**
* 获取用户的Long类型唯一标识符,适配用户唯一标识符为 Long 类型的情况,减少转换代码。
*
* @return 用户在数据库中的Long类型唯一标识符
*/
@JsonIgnore
default Long getUserIdAsLong() {
if (getUserId() == null) {
return null;
}
try {
return Long.valueOf(getUserId());
} catch (Exception e) {
return null;
}
}
/**
* 返回用户登录名。
*
* @return 返回用户登录名。
*/
String getUsername();
/**
* 返回用户的全名。
*
* @return 返回用户的全名
*/
String getFullName();
/**
* 返回用户的昵称。
*
* @return 返回用户的昵称
*/
String getNickName();
/**
* 返回用户的类型。
*
* @return 返回用户的类型
*/
String getType();
/**
* 返回执法类型。
*
* @return 返回执法类型
*/
String getLawType();
/**
* 返回职位/职称
*
* @return 返回职位/职称
*/
String getJobTitle();
/**
* 返回用户个人资料页面的URL。
*
* @return 返回用户个人资料页面的URL。
*/
String getProfile();
/**
* 返回用户个人资料图片的URL。
*
* @return 用户个人资料图片的URL。
*/
String getPicture();
/**
* 返回用户的首选电子邮件地址。
*
* @return 户的首选电子邮件地址
*/
String getEmail();
/**
* 返回用户的首选电话号码。
*
* @return 首选电话号码
*/
String getPhoneNumber();
/**
* 返回用户的性别。
*
* @return 返回用户的性别
*/
String getGender();
/**
* 返回用户的出生日期。
*
* @return 用户的出生日期
*/
String getBirthdate();
/**
* 返回用户的地址。
*
* @return 返回用户的地址
*/
String getAddress();
/**
* 返回用户的身份证号。
*
* @return 返回用户的身份证号
*/
String getIdNumber();
/**
* 返回用户的工作单位。
*
* @return 返回用户的工作单位
*/
String getWorkUnit();
// endregion 用户基本信息
// region 用户机构信息
/**
* 用户机构在数据库中的唯一标识符,为了兼容不同的情况,这里使用字符串类型。
*
* @return 用户机构在数据库中的唯一标识符
*/
String getOrgId();
/**
* 获取用户机构的Long类型唯一标识符,适配用户机构唯一标识符为 Long 类型的情况,减少转换代码。
*
* @return 用户机构在数据库中的Long类型唯一标识符
*/
@JsonIgnore
default Long getOrgIdAsLong() {
if (getOrgId() == null) {
return null;
}
try {
return Long.valueOf(getOrgId());
} catch (Exception e) {
return null;
}
}
/**
* 返回用户的机构编码。
*
* @return 返回用户的机构编码
*/
String getOrgCode();
/**
* 返回用户的机构名称。
*
* @return 返回用户的机构名称。
*/
String getOrgName();
/**
* 返回用户的机构级别。
*
* @return 机构级别
*/
String getOrgLevel();
/**
* 返回用户的机构类型。
*
* @return 机构类型
*/
String getOrgType();
// endregion 用户机构信息
}
@@ -0,0 +1,45 @@
package com.metalloop.common.auth.userdetails;
import com.metalloop.common.auth.permission.SimplePermission;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collection;
/**
* 简单的用户核心信息实现
*
* @author zhaowenhao
* @since 2023-03-19
*/
@Data
@NoArgsConstructor
public class SimpleUserDetails implements UserDetails {
private String userId;
private String phoneNum;
private String username;
private String password;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
private String lawType;
private Collection<SimplePermission> permissions;
public SimpleUserDetails(String userId, String phoneNum,String username, String password,
Collection<SimplePermission> 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;
}
}
@@ -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();
}
@@ -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;
}
@@ -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 = "/";
}
@@ -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";
}
@@ -0,0 +1,80 @@
package com.metalloop.common.core.lock;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁顶级接口
*
* @author zlt
* @date 2018/5/29 14:12
* <p>
* 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());
}
}
}
@@ -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
* <p>
* 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;
}
@@ -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
* <p>
* 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;
}
}
@@ -0,0 +1,50 @@
package com.metalloop.common.core.lock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.Getter;
/**
* LockCondition 是一个封装类,它包含了在实现幂等性操作时所需要的关键信息。
* <p>
* 这个类的主要组成部分包括:
* <p>
* - 是否开启获取锁及检查(enabled):如果为 true,则执行获取锁及检查;如果为 false,则跳过获取锁及检查。
* <p>
* - 锁定键(lockKey):用于分布式锁的键,唯一标识了应该被锁定的资源。
* <p>
* - 查询包装器(queryWrapper):定义了用于检查数据是否已存在的查询条件。
* <p>
* - 消息(msg):如果已存在符合查询条件的数据,那么这个消息将被用于抛出 IdempotencyException 异常。
* <p>
* 使用 LockCondition 类可以让代码更加清晰和易于理解。它提供了一种方式,使得在实现多条件幂等性检查时,可以将关键信息组织在一起。
*
* @author zhaowenhao
* @since 2023-05-14
*/
@Getter
public class LockCondition<T> {
private final boolean enabled;
private final String lockKey;
private final QueryWrapper<T> queryWrapper;
private final String msg;
public LockCondition(boolean enabled, String lockKey, QueryWrapper<T> queryWrapper, String msg) {
this.lockKey = lockKey;
this.queryWrapper = queryWrapper;
this.msg = msg;
this.enabled = enabled;
}
public LockCondition(String lockKey, QueryWrapper<T> queryWrapper, String msg) {
this.enabled = true;
this.lockKey = lockKey;
this.queryWrapper = queryWrapper;
this.msg = msg;
}
}
@@ -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<Q extends Serializable> 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 <T> LockCondition<T> toLockCondition(Function<Q, Object> queryMapper) {
return new LockCondition<>(enabled, lockKey, QueryWrapperBuilder.build(queryMapper.apply(query)), msg);
}
}
@@ -0,0 +1,26 @@
package com.metalloop.common.core.lock;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 锁对象抽象
*
* @author zlt
* @date 2020/7/28
* <p>
* 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);
}
}
@@ -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);
}
}
}
@@ -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,从此枚举类中获取。
* <p>
* 自定义的账号类型要记录到下面:
* <p>
* admin: 管理员系统使用的账号类型
* <p>
* app: App 使用的账号类型,和login的区别是会话超时时间,app的会话超时时间长一些
* <p>
* 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<String, LoginType> TYPE_TO_ENUM = Arrays.stream(LoginType.values())
.collect(Collectors.toMap(LoginType::getType, Function.identity()));
private static final Map<StpLogic, LoginType> 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("账号类型错误"));
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<FieldError> 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<FieldError> 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<FieldError> 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<String, Object> requestParams = MapUtil.<String, Object>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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<String, List<String>> linkedEntities;
public ValidationFailedException(String message) {
super(message);
this.linkedEntities = Collections.emptyMap();
}
public ValidationFailedException(String message, Map<String, List<String>> linkedEntities) {
super(message);
this.linkedEntities = linkedEntities;
}
public ValidationFailedException(String message, Throwable cause) {
super(message, cause);
this.linkedEntities = Collections.emptyMap();
}
public Map<String, List<String>> getLinkedEntities() {
return linkedEntities;
}
}
@@ -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();
}
@@ -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;
}
}
@@ -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;
}
@@ -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<T> implements Serializable {
private T data;
private Integer code;
private String msg;
public static <T> Result<T> ok() {
return of(null, ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
}
public static <T> Result<T> ok(T data) {
return of(data, ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
}
public static <T> Result<T> ok(T data, String msg) {
return of(data, ResponseCode.SUCCESS.getCode(), msg);
}
public static <T> Result<T> fail() {
return of(null, ResponseCode.FAIL.getCode(), ResponseCode.FAIL.getMsg());
}
public static <T> Result<T> fail(String msg) {
return of(null, ResponseCode.FAIL.getCode(), msg);
}
public static <T> Result<T> fail(int code, String msg) {
return of(null, code, msg);
}
public static <T> Result<T> fail(ResponseCode responseCode) {
return of(null, responseCode.getCode(), responseCode.getMsg());
}
public static <T> Result<T> 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;
}
}
@@ -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<String> redisKeyStringSerializer() {
return RedisSerializer.string();
}
@Bean
public RedisSerializer<Object> redisValueJsonSerializer() {
return RedisSerializer.json();
}
@Bean
public RedisSerializer<Object> redisValueJavaSerializer() {
return RedisSerializer.java();
}
/**
* string-json RedisTemplate配置,默认使用
*
* @param factory RedisConnectionFactory
* @param redisKeyStringSerializer RedisSerializer
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory, RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJsonSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setDefaultSerializer(redisValueJsonSerializer);
redisTemplate.setKeySerializer(redisKeyStringSerializer);
redisTemplate.setHashKeySerializer(redisKeyStringSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* string-string RedisTemplate配置
*
* @param redisConnectionFactory RedisConnectionFactory
*/
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* string-java RedisTemplate配置
*
* @param factory RedisConnectionFactory
* @param redisKeyStringSerializer RedisSerializer
*/
@Bean
@ConditionalOnMissingBean(name = "javaRedisTemplate")
public RedisTemplate<String, Object> javaRedisTemplate(RedisConnectionFactory factory, RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJavaSerializer) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setDefaultSerializer(redisValueJavaSerializer);
template.setKeySerializer(redisKeyStringSerializer);
template.setHashKeySerializer(redisKeyStringSerializer);
template.afterPropertiesSet();
return template;
}
@Bean(name = "cacheManager")
@Primary
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJsonSerializer) {
RedisCacheConfiguration difConf = getDefConf(redisKeyStringSerializer, redisValueJsonSerializer).entryTtl(Duration.ofHours(1));
// 自定义的缓存过期时间配置
int configSize = cacheManagerProperties.getConfigs() == null ? 0 : cacheManagerProperties.getConfigs().size();
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(configSize);
if (configSize > 0) {
cacheManagerProperties.getConfigs().forEach(e -> {
RedisCacheConfiguration conf = getDefConf(redisKeyStringSerializer, redisValueJsonSerializer).entryTtl(Duration.ofSeconds(e.getSecond()));
redisCacheConfigurationMap.put(e.getKey(), conf);
});
}
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(difConf).withInitialCacheConfigurations(redisCacheConfigurationMap).build();
}
@Bean
public KeyGenerator keyGenerator() {
return (target, method, objects) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(":").append(method.getName()).append(":");
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
};
}
private RedisCacheConfiguration getDefConf(RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJsonSerializer) {
return RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues().computePrefixWith(cacheName -> "cache".concat(":").concat(cacheName).concat(":")).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisKeyStringSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisValueJsonSerializer));
}
}
@@ -0,0 +1,69 @@
package com.metalloop.common.redis.lock;
import com.metalloop.common.constant.CommonConstant;
import com.metalloop.common.core.lock.DistributedLock;
import com.metalloop.common.core.lock.ZLock;
import com.metalloop.common.exception.LockException;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* redisson分布式锁实现,基本锁功能的抽象实现
* 本接口能满足绝大部分的需求,高级的锁功能,请自行扩展或直接使用原生api
*
* @author zhaowenhao
*/
@Component
public class RedissonDistributedLock implements DistributedLock {
@Autowired
private RedissonClient redisson;
private ZLock getLock(String key, boolean isFair) {
RLock lock;
if (isFair) {
lock = redisson.getFairLock(CommonConstant.LOCK_KEY_PREFIX + ":" + key);
} else {
lock = redisson.getLock(CommonConstant.LOCK_KEY_PREFIX + ":" + key);
}
return new ZLock(lock, this);
}
@Override
public ZLock lock(String key, long leaseTime, TimeUnit unit, boolean isFair) {
ZLock zLock = getLock(key, isFair);
RLock lock = (RLock) zLock.getLock();
lock.lock(leaseTime, unit);
return zLock;
}
@Override
public ZLock tryLock(String key, long waitTime, long leaseTime, TimeUnit unit, boolean isFair) throws InterruptedException {
ZLock zLock = getLock(key, isFair);
RLock lock = (RLock) zLock.getLock();
if (lock.tryLock(waitTime, leaseTime, unit)) {
return zLock;
}
return null;
}
@Override
public void unlock(Object lock) {
if (lock != null) {
if (lock instanceof RLock) {
RLock rLock = (RLock) lock;
if (rLock.isLocked()) {
rLock.unlock();
}
} else {
throw new LockException("requires RLock type");
}
}
}
}
@@ -0,0 +1,31 @@
package com.metalloop.common.redis.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* @author zlt
* @date 2019/1/6
*/
@Setter
@Getter
@ConfigurationProperties(prefix = "tocc.cache-manager")
public class CacheManagerProperties {
private List<CacheConfig> configs;
@Setter
@Getter
public static class CacheConfig {
/**
* cache key
*/
private String key;
/**
* 过期时间,sec
*/
private long second = 60;
}
}
@@ -0,0 +1,27 @@
package com.metalloop.common.redis.properties;
import com.metalloop.common.core.lock.DistributedLock;
import com.metalloop.common.redis.lock.RedissonDistributedLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnClass(RedissonClient.class)
@ConditionalOnProperty(
prefix = "tocc.lock",
name = "lockerType",
havingValue = "REDIS",
matchIfMissing = true
)
public class RedissonLockAutoConfiguration {
@Bean
@ConditionalOnMissingBean(DistributedLock.class)
public DistributedLock distributedLock() {
return new RedissonDistributedLock();
}
}
@@ -0,0 +1,577 @@
package com.metalloop.common.redis.template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationUtils;
import org.springframework.util.Assert;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Redis Repository
* redis 基本操作 可扩展,基本够用了
*
* <p>基于 Spring Boot 3.4.xSpring Data Redis 3.4.xAPI 编写。</p>
*
* @author zhaowenhao
*/
@Slf4j
public class RedisRepository {
/**
* Spring Redis Template
*/
private final RedisTemplate<String, Object> redisTemplate;
public RedisRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 获取链接工厂
*/
public RedisConnectionFactory getConnectionFactory() {
return this.redisTemplate.getConnectionFactory();
}
/**
* 获取 RedisTemplate 对象
*/
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
/**
* 清空DB
*
* @param node redis 节点
*/
public void flushDB(RedisClusterNode node) {
this.redisTemplate.opsForCluster().flushDb(node);
}
// ======================== 写入类操作 ========================
/**
* 添加到带有过期时间的缓存(byte[] 版本,直接写原始字节,不做任何序列化包装)
*
* <p>需要写原始字节,无法迁移到 {@code opsForValue()},故保留底层调用。
* Spring Data Redis 3.4.x 中 {@code RedisConnection#set} 已标记废弃,
* 但尚无等价的带自定义序列化器的高层 API,故显式抑制警告。</p>
*
* @param key redis 主键
* @param value 值
* @param time 过期时间(单位秒)
*/
@SuppressWarnings("deprecation")
public void setExpire(final byte[] key, final byte[] value, final long time) {
redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.set(key, value,
Expiration.seconds(time),
RedisStringCommands.SetOption.upsert()));
}
/**
* 添加到带有过期时间的缓存(推荐,使用模板自身的序列化器)
*
* @param key redis 主键
* @param value 值
* @param time 过期时间
* @param timeUnit 过期时间单位
*/
public void setExpire(final String key, final Object value, final long time, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
}
/**
* 添加到带有过期时间的缓存(默认单位为秒)
*
* @param key redis 主键
* @param value 值
* @param time 过期时间(单位秒)
*/
public void setExpire(final String key, final Object value, final long time) {
this.setExpire(key, value, time, TimeUnit.SECONDS);
}
/**
* 添加到带有过期时间的缓存(支持自定义 valueSerializer
*
* <p>因需要独立的 {@code valueSerializer},无法迁移到 {@code opsForValue()} 高层 API
* 故保留底层调用并抑制弃用警告。</p>
*
* @param key redis 主键
* @param value 值
* @param time 过期时间
* @param timeUnit 过期时间单位
* @param valueSerializer 自定义值序列化器
*/
@SuppressWarnings("deprecation")
public void setExpire(final String key, final Object value, final long time,
final TimeUnit timeUnit, RedisSerializer<Object> valueSerializer) {
byte[] rawKey = rawKey(key);
byte[] rawValue = rawValue(value, valueSerializer);
redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.set(rawKey, rawValue,
Expiration.from(time, timeUnit),
RedisStringCommands.SetOption.upsert()));
}
/**
* 一次性添加数组到带过期时间的缓存(使用 Pipeline,减少网络往返)
*
* @param keys redis 主键数组
* @param values 值数组
* @param time 过期时间(单位秒)
*/
@SuppressWarnings("deprecation")
public void setExpire(final String[] keys, final Object[] values, final long time) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
RedisSerializer<Object> ks = keySerializer();
RedisSerializer<Object> vs = valueSerializer();
for (int i = 0; i < keys.length; i++) {
connection.set(Objects.requireNonNull(ks.serialize(keys[i])), Objects.requireNonNull(vs.serialize(values[i])),
Expiration.seconds(time),
RedisStringCommands.SetOption.upsert());
}
return null;
});
}
/**
* 一次性添加数组到缓存(使用 Pipeline,减少网络往返)
*
* @param keys 主键数组
* @param values 值数组
*/
@SuppressWarnings("deprecation")
public void set(final String[] keys, final Object[] values) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
RedisSerializer<Object> ks = keySerializer();
RedisSerializer<Object> vs = valueSerializer();
for (int i = 0; i < keys.length; i++) {
connection.set(Objects.requireNonNull(ks.serialize(keys[i])), Objects.requireNonNull(vs.serialize(values[i])));
}
return null;
});
}
/**
* 添加到缓存(无过期时间)
*
* @param key the key
* @param value the value
*/
public void set(final String key, final Object value) {
redisTemplate.opsForValue().set(key, value);
}
// ======================== 读取类操作 ========================
/**
* 查询在以 keyPattern 开头的所有 key
*
* <p>注意:底层使用 KEYS 命令,生产环境数据量大时会阻塞 Redis,请谨慎使用。</p>
*
* @param keyPattern the key pattern (会自动追加 "*")
* @return the set
*/
public Set<String> keys(final String keyPattern) {
return redisTemplate.keys(keyPattern + "*");
}
/**
* 根据 key 获取原始字节
*
* @param key the key
* @return the byte[]
*/
public byte[] get(final byte[] key) {
return redisTemplate.execute((RedisCallback<byte[]>) connection -> connection.get(key));
}
/**
* 根据 key 获取对象
*
* @param key the key
* @return the object
*/
public Object get(final String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 获取旧值并设置新值(原子操作,对应 Redis 的 SET key value GET 语义)
*
* <p>使用模板的 valueSerializer,请确保其与调用方期望一致。</p>
*
* @param key the key
* @param value the value
* @return 旧值,若不存在返回 null
*/
public String getAndSet(final String key, String value) {
if (StringUtils.isEmpty(key)) {
log.error("非法入参");
return null;
}
try {
Object oldValue = redisTemplate.opsForValue().getAndSet(key, value);
return oldValue == null ? null : oldValue.toString();
} catch (Exception e) {
log.error("redisTemplate操作异常", e);
}
return null;
}
/**
* 根据 key 获取对象(支持自定义 valueSerializer
*
* @param key the key
* @param valueSerializer 序列化器
* @return the object
*/
public Object get(final String key, RedisSerializer<Object> valueSerializer) {
byte[] rawKey = rawKey(key);
return redisTemplate.execute((RedisCallback<Object>) connection ->
deserializeValue(connection.stringCommands().get(rawKey), valueSerializer));
}
// ======================== Set 操作 ========================
/**
* 缓存 Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, Object> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, Object> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的 set
*
* @param key the key
* @return the set
*/
public Set<Object> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
// ======================== Hash 操作 ========================
/**
* Ops for hash hash operations.
*
* @return the hash operations
*/
public HashOperations<String, String, Object> opsForHash() {
return redisTemplate.opsForHash();
}
/**
* 对 HashMap 设置值
*
* @param key the key
* @param hashKey the hash key
* @param hashValue the hash value
*/
public void putHashValue(String key, String hashKey, Object hashValue) {
opsForHash().put(key, hashKey, hashValue);
}
/**
* 获取单个 field 对应的值
*
* @param key the key
* @param hashKey the hash key
* @return the hash values
*/
public Object getHashValues(String key, String hashKey) {
return opsForHash().get(key, hashKey);
}
/**
* 根据 key 值删除 hash 字段
*
* @param key the key
* @param hashKeys the hash keys
*/
public void delHashValues(String key, Object... hashKeys) {
opsForHash().delete(key, hashKeys);
}
/**
* key 对应的整个 map
*
* @param key the key
* @return the hash value
*/
public Map<String, Object> getHashValue(String key) {
return opsForHash().entries(key);
}
/**
* 批量添加 hash
*
* @param key the key
* @param map the map
*/
public void putHashValues(String key, Map<String, Object> map) {
opsForHash().putAll(key, map);
}
// ======================== 通用操作 ========================
/**
* 集合数量
*
* @return the long
*/
public long dbSize() {
Long size = redisTemplate.execute(RedisServerCommands::dbSize);
return size == null ? 0L : size;
}
/**
* 清空 redis 存储的数据
*
* @return the string
*/
public String flushDB() {
return redisTemplate.execute((RedisCallback<String>) connection -> {
connection.flushDb();
return "ok";
});
}
/**
* 判断某个主键是否存在
*
* @param key the key
* @return the boolean
*/
public boolean exists(final String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 批量删除 key
*
* @param keys the keys
* @return 实际删除的数量
*/
public long del(final String... keys) {
if (keys == null || keys.length == 0) {
return 0L;
}
Long count = redisTemplate.delete(Arrays.asList(keys));
return count == null ? 0L : count;
}
/**
* 对某个主键对应的值加一, value 值必须是全数字的字符串
*
* @param key the key
* @return the long
*/
public long incr(final String key) {
Long value = redisTemplate.opsForValue().increment(key);
return value == null ? 0L : value;
}
// ======================== List 操作 ========================
/**
* redis List 引擎
*
* @return the list operations
*/
public ListOperations<String, Object> opsForList() {
return redisTemplate.opsForList();
}
/**
* redis List 数据结构 : 将一个或多个值 value 插入到列表 key 的表头
*
* @param key the key
* @param value the value
* @return the long
*/
public Long leftPush(String key, Object value) {
return opsForList().leftPush(key, value);
}
/**
* redis List 数据结构 : 移除并返回列表 key 的头元素
*
* @param key the key
* @return the object
*/
public Object leftPop(String key) {
return opsForList().leftPop(key);
}
/**
* redis List 数据结构 : 将一个或多个值 value 插入到列表 key 的表尾(最右边)。
*
* @param key the key
* @param value the value
* @return the long
*/
public Long in(String key, Object value) {
return opsForList().rightPush(key, value);
}
/**
* redis List 数据结构 : 移除并返回列表 key 的末尾元素
*
* @param key the key
* @return the object
*/
public Object rightPop(String key) {
return opsForList().rightPop(key);
}
/**
* redis List 数据结构 : 返回列表 key 的长度
*
* @param key the key
* @return the long
*/
public Long length(String key) {
return opsForList().size(key);
}
/**
* redis List 数据结构 : 根据参数 i 的值,移除列表中与参数 value 相等的元素
*
* @param key the key
* @param i the index
* @param value the value
*/
public void remove(String key, long i, Object value) {
opsForList().remove(key, i, value);
}
/**
* redis List 数据结构 : 将列表 key 下标为 index 的元素的值设置为 value
*
* @param key the key
* @param index the index
* @param value the value
*/
public void set(String key, long index, Object value) {
opsForList().set(key, index, value);
}
/**
* redis List 数据结构 : 返回列表 key 中指定区间内的元素
*
* @param key the key
* @param start the start
* @param end the end
* @return the list
*/
public List<Object> getList(String key, int start, int end) {
return opsForList().range(key, start, end);
}
/**
* redis List 数据结构 : 返回列表 key 中指定区间内的元素(自定义 valueSerializer
*
* @param key the key
* @param start the start
* @param end the end
* @param valueSerializer 序列化器
* @return the list
*/
public List<Object> getList(String key, int start, int end, RedisSerializer<Object> valueSerializer) {
byte[] rawKey = rawKey(key);
return redisTemplate.execute((RedisCallback<List<Object>>) connection ->
deserializeValues(connection.listCommands().lRange(rawKey, start, end), valueSerializer));
}
/**
* redis List 数据结构 : 批量存储
*
* @param key the key
* @param list the list
* @return the long
*/
public Long leftPushAll(String key, List<String> list) {
return opsForList().leftPushAll(key, list);
}
/**
* redis List 数据结构 : 将值 value 插入到列表 key 当中,位于值 index 之前或之后,默认之后。
*
* @param key the key
* @param index the index
* @param value the value
*/
public void insert(String key, long index, Object value) {
opsForList().set(key, index, value);
}
// ======================== 私有工具方法 ========================
/**
* 获取 key 序列化器(统一类型转换,避免每次强转告警)
*/
@SuppressWarnings("unchecked")
private RedisSerializer<Object> keySerializer() {
return (RedisSerializer<Object>) redisTemplate.getKeySerializer();
}
/**
* 获取 value 序列化器(统一类型转换,避免每次强转告警)
*/
@SuppressWarnings("unchecked")
private RedisSerializer<Object> valueSerializer() {
return (RedisSerializer<Object>) redisTemplate.getValueSerializer();
}
private byte[] rawKey(Object key) {
Assert.notNull(key, "non null key required");
if (key instanceof byte[]) {
return (byte[]) key;
}
return keySerializer().serialize(key);
}
private byte[] rawValue(Object value, RedisSerializer valueSerializer) {
if (value instanceof byte[]) {
return (byte[]) value;
}
return valueSerializer.serialize(value);
}
private List<Object> deserializeValues(List<byte[]> rawValues, RedisSerializer<Object> valueSerializer) {
if (valueSerializer == null) {
return (List<Object>) (List<?>) rawValues;
}
return SerializationUtils.deserialize(rawValues, valueSerializer);
}
private Object deserializeValue(byte[] value, RedisSerializer<Object> valueSerializer) {
if (valueSerializer == null) {
return value;
}
return valueSerializer.deserialize(value);
}
}
@@ -0,0 +1,186 @@
package com.metalloop.common.service;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.metalloop.common.core.lock.DistributedLock;
import com.metalloop.common.core.lock.LockCondition;
import com.metalloop.common.utils.QueryWrapperBuilder;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* service接口父类
*
* @author zhaowenhao
* @since 2022-12-10
*/
@SuppressWarnings("unchecked")
public interface ISuperService<T> extends IService<T> {
/**
* 使用分布式锁确保保存操作的幂等性。该方法接受一个或多个 LockCondition 对象,对每个条件进行唯一性检查。
* 所有的条件都必须满足(即对应的记录在数据库中不存在)才会保存实体,否则抛出 IdempotencyException 异常。
*
* @param entity 需要保存的实体对象
* @param locker 分布式锁对象
* @param lockConditions 一个或多个锁条件,每个条件对应一个唯一性检查
* @return 如果实体保存成功则返回 true,否则抛出异常
* @throws LockException 如果无法获取锁或锁等待超时则抛出此异常
* @throws IdempotencyException 如果任何唯一性检查失败则抛出此异常
*/
boolean saveIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions);
/**
* 以幂等方式根据实体的ID进行更新。此方法使用分布式锁和锁定条件,以确保操作的幂等性和线程安全性。
*
* @param entity 需要更新的实体。
* @param locker 分布式锁。
* @param lockConditions 在更新之前需要检查的锁定条件。
* 每个条件包括一个锁定键,一个用于检查现有数据的查询包装器,
* 以及在存在重复数据时抛出异常的消息。
* @return 如果更新操作成功,则返回true;否则返回false。
* @throws LockException 如果分布式锁为null或无法获取。
* @throws IdempotencyException 如果存在重复数据或在更新操作中发生其他异常。
*/
boolean updateByIdIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions);
/**
* 这个方法提供了一个并发安全的方式来保存或更新给定的实体。
* 如果实体已经存在,那么它会被更新,否则,它将会被保存。
* <p>
* 这个方法使用了分布式锁来确保并发安全,并且在执行前会检查是否存在满足条件的记录,从而确保操作的幂等性。
* 锁和条件检查的行为可以通过 {@link LockCondition} 对象进行配置。
*
* @param entity 要保存或更新的实体
* @param lock 分布式锁对象,用于确保并发安全
* @param lockConditions 锁条件对象数组,定义了锁和条件检查的行为
* @return 如果操作成功,则返回 true,否则返回 false
* @throws IdempotencyException 如果存在满足条件的记录,或者找不到有效的 {@link TableId} 字段
*/
boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, LockCondition<T>... lockConditions);
/**
* 根据查询对象获取对象
*
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @param <Q> 查询对象类型
* @return 结果对象
*/
default <Q> T get(Q query, Q... orQueryArray) {
return getOne(getWrapper(query, orQueryArray), Boolean.FALSE);
}
/**
* 根据查询对象获取列表
*
* @param <Q> 查询对象类型
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @return 结果列表
*/
default <Q> List<T> list(Q query, Q... orQueryArray){
return list(getWrapper(query, orQueryArray));
}
/**
* 根据查询对象获取列表,并执行分组,返回Map,适用于确定分组后只有一条数据的情况,否则保留最后一条
*
* @param keyMapper 生成键的映射函数
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @param <Q> 查询对象类型
* @param <K> 返回的Map中的key的类型
* @return 结果Map
*/
default <Q, K> Map<K, T> toMap(Function<T, K> keyMapper, Q query, Q... orQueryArray) {
List<T> list = list(getWrapper(query, orQueryArray));
return list.stream()
.collect(Collectors.toMap(keyMapper, Function.identity(), (oldValue, newValue) -> newValue));
}
/**
* 根据查询对象获取列表并执行分组,返回Map,每个键都有一个对象列表
*
* @param keyMapper 生成键的映射函数
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @param <Q> 查询对象类型
* @param <K> 返回的Map中的key的类型
* @return 结果Map,其值是对象列表
*/
default <Q, K> Map<K, List<T>> toGroup(Function<T, K> keyMapper, Q query, Q... orQueryArray) {
List<T> list = list(getWrapper(query, orQueryArray));
return list.stream()
.collect(Collectors.groupingBy(keyMapper));
}
/**
* 计数查询
*
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @param <Q> 查询对象类型
* @return 计数结果
*/
default <Q> long count(Q query, Q... orQueryArray) {
return count(getWrapper(query, orQueryArray));
}
/**
* 分页查询
*
* @param page 分页对象
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @param <Q> 查询对象类型
* @param <E> 分页对象类型
* @return 分页对象
*/
default <Q, E extends IPage<T>> E page(E page, Q query, Q... orQueryArray) {
return page(page, getWrapper(query, orQueryArray));
}
/**
* 根据查询对象修改记录
*
* @param entity 实体对象
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @param <Q> 查询对象类型
* @return 是否成功
*/
default <Q> boolean update(T entity, Q query, Q... orQueryArray) {
return update(entity, getWrapper(query, orQueryArray));
}
/**
* 根据查询对象删除记录
*
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @param <Q> 查询对象类型
* @return 是否成功
*/
default <Q> boolean remove(Q query, Q... orQueryArray) {
return remove(getWrapper(query, orQueryArray));
}
/**
* 内部方法根据查询对象获取查询条件
*
* @param query 查询对象
* @param orQueryArray OR嵌套查询对象数组,如果有的话
* @return 查询条件
*/
default <Q> QueryWrapper<T> getWrapper(Q query, Q... orQueryArray) {
return QueryWrapperBuilder.build(query, orQueryArray);
}
}
@@ -0,0 +1,115 @@
package com.metalloop.common.service.impl;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.metalloop.common.core.lock.DistributedLock;
import com.metalloop.common.core.lock.LockCondition;
import com.metalloop.common.core.lock.ZLock;
import com.metalloop.common.exception.IdempotencyException;
import com.metalloop.common.exception.LockException;
import com.metalloop.common.service.ISuperService;
import com.metalloop.modules.auth.model.po.SysPermission;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* service实现父类
*
* @author zhaowenhao
* @since 2022-12-10
* <p>
*/
public abstract class SuperServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> implements ISuperService<T> {
@SafeVarargs
@Override
public final boolean saveIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions) {
if (locker == null) {
throw new LockException("DistributedLock is null");
}
List<ZLock> locks = new ArrayList<>();
try {
checkRepeat(locker, locks, lockConditions);
return super.save(entity);
} catch (Exception e) {
throw new IdempotencyException(e.getMessage());
} finally {
for (ZLock lock : locks) {
try {
lock.close();
} catch (Exception ignored) {
}
}
}
}
@SafeVarargs
@Override
public final boolean updateByIdIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions) {
if (locker == null) {
throw new LockException("DistributedLock is null");
}
List<ZLock> locks = new ArrayList<>();
try {
checkRepeat(locker, locks, lockConditions);
return super.updateById(entity);
} catch (Exception e) {
throw new IdempotencyException(e.getMessage());
} finally {
for (ZLock lock : locks) {
try {
lock.close();
} catch (Exception ignored) {
}
}
}
}
private void checkRepeat(DistributedLock locker, List<ZLock> locks, LockCondition<T>[] lockConditions) throws Exception {
for (LockCondition<T> condition : lockConditions) {
if (condition.isEnabled()) {
ZLock lock = locker.tryLock(condition.getLockKey(), 10, 60, TimeUnit.SECONDS);
if (lock == null) {
throw new LockException("锁等待超时");
}
locks.add(lock);
long count = super.count(condition.getQueryWrapper());
if (count > 0) {
throw new IdempotencyException(condition.getMsg());
}
}
}
}
@SafeVarargs
@Override
public final boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, LockCondition<T>... lockConditions) {
if (null != entity) {
Class<?> cls = entity.getClass();
TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
if (null != tableInfo && CharSequenceUtil.isNotEmpty(tableInfo.getKeyProperty())) {
Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());
if (StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal))) {
return this.saveIdempotency(entity, lock, lockConditions);
} else {
return updateByIdIdempotency(entity, lock, lockConditions);
}
} else {
throw new IdempotencyException("找不到有效的@TableId字段.");
}
}
return false;
}
}
@@ -0,0 +1,68 @@
package com.metalloop.common.utils;
import jakarta.servlet.http.HttpServletRequest;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author lifan
* @date 2024/3/26 10:26
*/
public class IPHelper {
private static final String UNKNOWN = "unknown";
/**
* 获取用户的真实ip地址 如果有多个就取第一个
*
* @param request http请求
* @return 客户端ip
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return null;
}
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
String[] ips = ip.split(",");
return ips[0].trim();
}
/**
* 将IPV6转化为IPV4 否则采用原来的值
*
* @param ipvAddress ip地址
* @return 转化后的地址
*/
public static String convertIPv6ToIPv4(String ipvAddress) {
try {
InetAddress inet6Address = InetAddress.getByName(ipvAddress);
if (inet6Address instanceof Inet6Address inet6) {
// 如果是IPv6地址
byte[] ipv4Bytes = new byte[4];
System.arraycopy(inet6.getAddress(), 12, ipv4Bytes, 0, 4);
InetAddress ipv4Address = InetAddress.getByAddress(ipv4Bytes);
return ipv4Address.getHostAddress();
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
return ipvAddress;
}
}
@@ -0,0 +1,328 @@
package com.metalloop.common.utils;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.conditions.interfaces.Compare;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.metalloop.common.enums.Direction;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;
import java.beans.PropertyDescriptor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* MyBatis-Plus QueryWrapper 条件构造器,从查询对象中构建查询条件
* 条件构造遵循以下约定:
* <p>
* 1.解析规则
* <p>
* 1.1.查询对象字段值非空(不是null和空字符串)才会加入查询条件
* <p>
* 1.2.查询对象字段对应的数据库字段,通过对应注解 @TableField(value = "client_id") 的value提取
* <p>
* 1.3.查询对象字段值类型是 Direction,用order by片段,如 Direction detectionTimeOrder = Direction.DESC 会转换为 order by detection_time desc
* <p>
* 1.4.查询对象字段值类型是 {@link Collection},用in片段,如果以not开头或结尾,使用not in片段如 List<> notSiteCodes 会转换为 site_code not in (?,?,?),否则使用in片段如 List<> siteCodes 会转换为 site_code in (?,?,?)。另请注意:如果集合不为 null 但元素数量为零,不是不加入查询条件,而是加入一个不可能的值,如 in () 会报错,所以会加入 in ('__UNLIKELY_VALUE_300552__'),这样可以避免报错。同时保持字面的含义,即选择了 0 个值,查询结果也为 0 个
* <p>
* 1.5.查询对象字段名以start|lower|min|from开头或结尾,表示范围的下限,用>=片段,如 detectionTimeStart 会转换为 detection_time >= ?
* <p>
* 1.6.查询对象字段名以end|upper|max|to开头或结尾,表示范围的上限,用<=片段,如 detectionTimeEnd 会转换为 detection_time <= ?
* <p>
* 1.7.查询对象字段名以startEc|lowerEc|minEc|fromEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的下限,但是不包含当前值,用>片段,如 detectionTimeStartEc 会转换为 detection_time > ?
* <p>
* 1.8.查询对象字段名以endEc|upperEc|maxEc|toEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的上限,但是不包含当前值,用<片段,如 detectionTimeEndEc 会转换为 detection_time < ?
* <p>
* 1.9.prefix|suffix开头或结尾,表示前缀或后缀查询,用like片段,如 vehPlatePrefix 会转换为 veh_plate like ?% 或 vehPlateSuffix 会转换为 veh_plate like %?
* <p>
* 1.10.查询对象字段名以notLike|notContains|notMatches|notMatch开头或结尾,表示否定模糊查询,用not like片段,如 vehPlateNotLike 会转换为 veh_plate not like %?%
* <p>
* 1.11.查询对象字段名以like|contains|matches|match开头或结尾,表示模糊查询,用like片段,如 vehPlateLike 会转换为 veh_plate like %?%
* <p>
* 1.12.查询对象字段名以fulltext开头或结尾,表示全文检索查询,用MATCH() AGAINST()片段,如 vehPlateFullText 会转换为 MATCH(veh_plate) AGAINST(?)
* <p>
* 1.13.查询对象字段名以not开头或结尾,表示否定查询,用not片段,如 vehPlateNot 会转换为 veh_plate != ?
* <p>
* 1.14.其他情况表示精确查询,用=片段,如busiNum 会转换为busi_num = ?
* <p>
* 2.OR嵌套查询列表
* <p>
* 需要构建OR嵌套查询列表,解析规则同上,解析结果为嵌套查询,最终将转换为 and (site_code in (?,?,?) or detection_time >= ? or ...)
*
* @author zhaowenhao
* @since 2022-12-27
*/
@SuppressWarnings({"AlibabaMethodTooLong", "DuplicatedCode"})
public class QueryWrapperBuilder {
/**
* Utility classes should not have a public or default constructor.
*/
private QueryWrapperBuilder() {
throw new IllegalStateException("Utility class");
}
/**
* 不可能的值,用于处理集合为空的情况
*/
public static final String UNLIKELY_VALUE = "__UNLIKELY_VALUE_300552__";
/**
* 范围开始(不包含当前值)正则表达式
*/
public static final Pattern EXCLUSIVE_RANGE_START_PATTERN = Pattern.compile("(.+(start|lower|min|from)ec)|((start|lower|min|from)ec.+)", Pattern.CASE_INSENSITIVE);
/**
* 范围结束(不包含当前值)正则表达式
*/
public static final Pattern EXCLUSIVE_RANGE_END_PATTERN = Pattern.compile("(.+(end|upper|max|to)ec)|((end|upper|max|to)ec.+)", Pattern.CASE_INSENSITIVE);
/**
* 范围开始正则表达式
*/
public static final Pattern RANGE_START_PATTERN = Pattern.compile("(.+(start|lower|min|from))|((start|lower|min|from).+)", Pattern.CASE_INSENSITIVE);
/**
* 范围结束正则表达式
*/
public static final Pattern RANGE_END_PATTERN = Pattern.compile("(.+(end|upper|max|to))|((end|upper|max|to).+)", Pattern.CASE_INSENSITIVE);
/**
* 前缀查询
*/
public static final Pattern PREFIX_PATTERN = Pattern.compile("(.+(prefix))|((prefix).+)", Pattern.CASE_INSENSITIVE);
/**
* 后缀查询
*/
public static final Pattern SUFFIX_PATTERN = Pattern.compile("(.+(suffix))|((suffix).+)", Pattern.CASE_INSENSITIVE);
/**
* 模糊查询
*/
public static final Pattern LIKE_PATTERN = Pattern.compile("(.+(like|contains|matches|match))|((like|contains|matches|match).+)", Pattern.CASE_INSENSITIVE);
/**
* 否定模糊查询
*/
public static final Pattern NOT_LIKE_PATTERN = Pattern.compile("(.+(not)(like|contains|matches|match))|((not)(like|contains|matches|match).+)", Pattern.CASE_INSENSITIVE);
/**
* 全文检索查询
*/
public static final Pattern FULL_TEXT_SEARCH_PATTERN = Pattern.compile("(.+(fulltext))|((fulltext).+)", Pattern.CASE_INSENSITIVE);
/**
* 不等于查询
*/
public static final Pattern NOT_PATTERN = Pattern.compile("(.+(not))|((not).+)", Pattern.CASE_INSENSITIVE);
/**
* 特定JSON值查询
*/
public static final Pattern JSON_CONTAINS_PATTERN = Pattern.compile("(.+(jsonContains))|((jsonContains).+)", Pattern.CASE_INSENSITIVE);
@FunctionalInterface
public interface QueryStrategy {
default void apply(QueryWrapper<?> queryWrapper, String column, Object value, boolean isOr) {
if (isOr) {
queryWrapper.or();
}
apply(queryWrapper, column, value);
}
void apply(QueryWrapper<?> wrapper, String column, Object value);
}
// region 封装正则表达式Map和策略Map
private static final Map<String, QueryStrategy> STRATEGY_MAP = new HashMap<>();
private static final Map<String, Pattern> PATTERN_MAP = new HashMap<>();
public static final String RANGE_START_PATTERN_KEY = "RANGE_START_PATTERN";
public static final String RANGE_END_PATTERN_KEY = "RANGE_END_PATTERN";
public static final String EXCLUSIVE_RANGE_START_PATTERN_KEY = "EXCLUSIVE_RANGE_START_PATTERN";
public static final String EXCLUSIVE_RANGE_END_PATTERN_KEY = "EXCLUSIVE_RANGE_END_PATTERN";
public static final String PREFIX_PATTERN_KEY = "PREFIX_PATTERN";
public static final String SUFFIX_PATTERN_KEY = "SUFFIX_PATTERN";
public static final String NOT_LIKE_PATTERN_KEY = "NOT_LIKE_PATTERN";
public static final String LIKE_PATTERN_KEY = "LIKE_PATTERN";
public static final String FULL_TEXT_SEARCH_PATTERN_KEY = "FULL_TEXT_SEARCH_PATTERN";
public static final String NOT_PATTERN_KEY = "NOT_PATTERN";
public static final String JSON_CONTAINS_PATTERN_KEY = "JSON_CONTAINS_PATTERN";
static {
PATTERN_MAP.put(RANGE_START_PATTERN_KEY, RANGE_START_PATTERN);
PATTERN_MAP.put(RANGE_END_PATTERN_KEY, RANGE_END_PATTERN);
PATTERN_MAP.put(EXCLUSIVE_RANGE_START_PATTERN_KEY, EXCLUSIVE_RANGE_START_PATTERN);
PATTERN_MAP.put(EXCLUSIVE_RANGE_END_PATTERN_KEY, EXCLUSIVE_RANGE_END_PATTERN);
PATTERN_MAP.put(PREFIX_PATTERN_KEY, PREFIX_PATTERN);
PATTERN_MAP.put(SUFFIX_PATTERN_KEY, SUFFIX_PATTERN);
PATTERN_MAP.put(NOT_LIKE_PATTERN_KEY, NOT_LIKE_PATTERN);
PATTERN_MAP.put(LIKE_PATTERN_KEY, LIKE_PATTERN);
PATTERN_MAP.put(FULL_TEXT_SEARCH_PATTERN_KEY, FULL_TEXT_SEARCH_PATTERN);
PATTERN_MAP.put(NOT_PATTERN_KEY, NOT_PATTERN);
STRATEGY_MAP.put(RANGE_START_PATTERN_KEY, Compare::ge);
STRATEGY_MAP.put(RANGE_END_PATTERN_KEY, Compare::le);
STRATEGY_MAP.put(EXCLUSIVE_RANGE_START_PATTERN_KEY, Compare::gt);
STRATEGY_MAP.put(EXCLUSIVE_RANGE_END_PATTERN_KEY, Compare::lt);
STRATEGY_MAP.put(PREFIX_PATTERN_KEY, Compare::likeRight);
STRATEGY_MAP.put(SUFFIX_PATTERN_KEY, Compare::likeLeft);
STRATEGY_MAP.put(NOT_LIKE_PATTERN_KEY, Compare::notLike);
STRATEGY_MAP.put(LIKE_PATTERN_KEY, Compare::like);
STRATEGY_MAP.put(FULL_TEXT_SEARCH_PATTERN_KEY, (queryWrapper, column, value) -> queryWrapper.apply("MATCH(" + column + ") AGAINST({0})", value));
STRATEGY_MAP.put(NOT_PATTERN_KEY, Compare::ne);
STRATEGY_MAP.put(JSON_CONTAINS_PATTERN_KEY, (queryWrapper, column, value) ->
queryWrapper.apply("JSON_CONTAINS(" + column + ", CAST({0} AS JSON))", value)
);
}
// endregion
@SafeVarargs
public static <Q, T> QueryWrapper<T> build(Q andQuery, Q... orQueryArray) {
if (andQuery == null) return null;
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
applyQuery(queryWrapper, andQuery, false);
Optional.ofNullable(orQueryArray).ifPresent(array -> Arrays.stream(array)
.filter(Objects::nonNull)
.forEach(orQuery -> queryWrapper.and(qw -> applyQuery(qw, orQuery, true))));
return queryWrapper;
}
private static <Q, T> void applyQuery(QueryWrapper<T> queryWrapper, Q query, boolean isOr) {
Class<?> aClass = query.getClass();
PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(aClass);
final BeanWrapperImpl wrapper = new BeanWrapperImpl(query);
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
String column = getColumn(aClass, propertyDescriptor);
String propertyName = propertyDescriptor.getName();
Object propertyValue = wrapper.getPropertyValue(propertyName);
// 先处理特殊值:排序标志和集合
if (propertyValue instanceof Direction) {
applyOrder(queryWrapper, column, propertyValue);
} else if (propertyValue instanceof Collection<?>) {
// 如果集合为空,需要特殊处理为一个无意义的值,否则会导致 in () 报错
if (((Collection<?>) propertyValue).isEmpty()) {
propertyValue = Collections.singletonList(UNLIKELY_VALUE);
}
applyCollection(queryWrapper, column, propertyName, (Collection<?>) propertyValue);
} else if (propertyValue != null && CharSequenceUtil.isNotBlank(propertyValue.toString())) {
applyStrategy(queryWrapper, column, propertyName, propertyValue, isOr);
}
}
}
private static <T> void applyOrder(QueryWrapper<T> queryWrapper, String column, Object value) {
if (Direction.ASC.equals(value)) {
queryWrapper.orderByAsc(column);
} else {
queryWrapper.orderByDesc(column);
}
}
private static <T> void applyCollection(QueryWrapper<T> queryWrapper, String column, String propertyName, Collection<?> values) {
if (PATTERN_MAP.get(NOT_PATTERN_KEY).matcher(propertyName).matches()) {
queryWrapper.notIn(column, values);
} else {
queryWrapper.in(column, values);
}
}
private static <T> void applyStrategy(QueryWrapper<T> queryWrapper, String column, String propertyName, Object propertyValue, boolean isOr) {
Optional<QueryStrategy> matchedStrategy = findMatchingStrategy(propertyName);
if (matchedStrategy.isPresent() && !propertyName.equalsIgnoreCase(CharSequenceUtil.toCamelCase(column))) {
matchedStrategy.get().apply(queryWrapper, column, propertyValue, isOr);
} else {
applyDefaultStrategy(queryWrapper, column, propertyValue, isOr);
}
}
private static Optional<QueryStrategy> findMatchingStrategy(String propertyName) {
return STRATEGY_MAP.entrySet().stream()
.filter(entry -> {
Pattern pattern = PATTERN_MAP.get(entry.getKey());
return pattern != null && pattern.matcher(propertyName).matches();
})
.findFirst()
.map(Map.Entry::getValue);
}
private static <T> void applyDefaultStrategy(QueryWrapper<T> queryWrapper, String column, Object value, boolean isOr) {
if (isOr) {
queryWrapper.or().eq(column, value);
} else {
queryWrapper.eq(column, value);
}
}
// region 处理缓存的Class的PropertyDescriptor和column的映射关系
static final ConcurrentMap<Class<?>, CachedColumnResults> CLASS_CACHED_COLUMN_RESULTS_MAP =
new ConcurrentHashMap<>(128);
private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {};
public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
return forClass(clazz).getPropertyDescriptors();
}
@Nullable
public static String getColumn(Class<?> clazz, PropertyDescriptor pd) throws BeansException {
return forClass(clazz).getColumn(pd);
}
static CachedColumnResults forClass(Class<?> beanClass) throws BeansException {
CachedColumnResults results = CLASS_CACHED_COLUMN_RESULTS_MAP.get(beanClass);
if (results != null) {
return results;
}
results = new CachedColumnResults(beanClass);
CachedColumnResults existing = CLASS_CACHED_COLUMN_RESULTS_MAP.putIfAbsent(beanClass, results);
return (existing != null ? existing : results);
}
public static final class CachedColumnResults {
private final Map<PropertyDescriptor, String> pdToColumnMap;
private CachedColumnResults(Class<?> beanClass) throws BeansException {
pdToColumnMap = Arrays.stream(beanClass.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(TableField.class))
.collect(Collectors.toMap(field -> BeanUtils.getPropertyDescriptor(beanClass, field.getName()),
field -> field.getAnnotation(TableField.class).value()));
}
@Nullable
String getColumn(PropertyDescriptor pd) {
return pdToColumnMap.get(pd);
}
PropertyDescriptor[] getPropertyDescriptors() {
return this.pdToColumnMap.keySet().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY);
}
}
// endregion
}
@@ -0,0 +1,101 @@
package com.metalloop.common.utils;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* RSA加解密工具类
*
* @author zlt
* @date 2019/7/16
*/
public class RsaUtils {
/**
* 默认"RSA"="RSA/ECB/PKCS1Padding"
*/
private static final String CIPHER_INSTANCE = "RSA/ECB/PKCS1Padding";
/**
* 公钥加密
*
* @param content 要加密的内容
* @param publicKey 公钥
*/
public static String encrypt(String content, PublicKey publicKey) {
try {
return Base64.getEncoder().encodeToString(encrypt(content.getBytes(), publicKey));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 公钥加密
*
* @param content 要加密的内容
* @param publicKey 公钥
*/
public static byte[] encrypt(byte[] content, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_INSTANCE);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(content);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 私钥解密
*
* @param content 要解密的内容
* @param privateKey 私钥
*/
public static String decrypt(String content, PrivateKey privateKey) throws Exception {
return new String(decrypt(Base64.getDecoder().decode(content), privateKey));
}
/**
* 私钥解密
*
* @param content 要解密的内容
* @param privateKey 私钥
*/
public static byte[] decrypt(byte[] content, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(CIPHER_INSTANCE);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(content);
}
/**
* String转公钥PublicKey
*
* @param key 公钥字符
*/
public static RSAPublicKey getPublicKey(String key) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
}
/**
* String转私钥PrivateKey
*
* @param key 私钥字符
*/
public static PrivateKey getPrivateKey(String key) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
}
@@ -0,0 +1,36 @@
package com.metalloop.common.utils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
* spring获取bean工具类
*
* @author 作者 owen E-mail: 624191343@qq.com
*/
@Component
@Order(0)
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
public static <T> T getBean(Class<T> cla) {
return applicationContext.getBean(cla);
}
public static <T> T getBean(String name, Class<T> cal) {
return applicationContext.getBean(name, cal);
}
public static String getProperty(String key) {
return applicationContext.getBean(Environment.class).getProperty(key);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringUtil.applicationContext = applicationContext;
}
}
@@ -0,0 +1,144 @@
package com.metalloop.common.utils.json;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* JSON 工具类
*
* @author zhaowenhao
*/
@UtilityClass
@Slf4j
public class JsonUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
}
/**
* 初始化 objectMapper 属性
* <p>
* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
*
* @param objectMapper ObjectMapper 对象
*/
public static void init(ObjectMapper objectMapper) {
JsonUtils.objectMapper = objectMapper;
}
@SneakyThrows
public static String toJsonString(Object object) {
return objectMapper.writeValueAsString(object);
}
@SneakyThrows
public static byte[] toJsonByte(Object object) {
return objectMapper.writeValueAsBytes(object);
}
@SneakyThrows
public static String toJsonPrettyString(Object object) {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
}
public static <T> T parseObject(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
/**
* 将字符串解析成指定类型的对象
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
* 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
*
* @param text 字符串
* @param clazz 类型
* @return 对象
*/
public static <T> T parseObject2(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
return JSONUtil.toBean(text, clazz);
}
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
if (ArrayUtil.isEmpty(bytes)) {
return null;
}
try {
return objectMapper.readValue(bytes, clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", bytes, e);
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
try {
return objectMapper.readValue(text, typeReference);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static <T> List<T> parseArray(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static JsonNode parseTree(String text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static JsonNode parseTree(byte[] text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static boolean isJson(String text) {
return JSONUtil.isTypeJSON(text);
}
}
@@ -0,0 +1,26 @@
package com.metalloop.framework.satoken;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SaTokenInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 全局拦截器
registry.addInterceptor(new SaInterceptor(handler -> {
// 排除登录、注册、验证码等公开接口
SaRouter.match("/api/**")
.notMatch("/api/auth/login")
.notMatch("/api/auth/register")
.notMatch("/api/auth/captcha")
.notMatch("/api/public/**")
.check(r -> StpUtil.checkLogin());
})).addPathPatterns("/api/**");
}
}
@@ -0,0 +1,37 @@
package com.metalloop.framework.satoken;
import cn.dev33.satoken.stp.StpInterface;
//import com.metalloop.modules.permission.service.PermissionService;
//import com.metalloop.modules.permission.service.RoleService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Sa-Token 权限数据源实现
* 框架会自动调用此接口获取当前登录用户的角色和权限码
*/
@Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {
// private final RoleService roleService;
// private final PermissionService permissionService;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 根据用户 ID 获取该用户所有权限码(code 字段)
Long userId = Long.parseLong(loginId.toString());
// return permissionService.listPermissionCodesByUserId(userId);
return List.of();
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 根据用户 ID 获取该用户所有角色名称
Long userId = Long.parseLong(loginId.toString());
// return roleService.listRoleNamesByUserId(userId);
return null;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
package com.metalloop.framework.satoken.admin.config;
import cn.dev33.satoken.config.SaTokenConfig;
import com.metalloop.framework.satoken.admin.StpAdminUtil;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* Admin Sa-Token 配置类,目标是提供一个支持配置的开箱即用的StpAdminUtil
*
* @author zhaowenhao
* @since 2023-11-17
*/
@Configuration
@EnableConfigurationProperties(SaTokenAdminProperties.class)
public class SaTokenAdminConfiguration {
private final SaTokenAdminProperties saTokenAdminProperties;
public SaTokenAdminConfiguration(SaTokenAdminProperties saTokenAdminProperties) {
this.saTokenAdminProperties = saTokenAdminProperties;
}
// 使用注入的 SaTokenAdminProperties 来配置 StpUtil 或其他组件
@PostConstruct
public void init() {
SaTokenConfig config = new SaTokenConfig();
// 拷贝属性
BeanUtils.copyProperties(saTokenAdminProperties, config);
StpAdminUtil.getStpLogic().setConfig(config);
}
}
@@ -0,0 +1,561 @@
package com.metalloop.framework.satoken.admin.config;
import cn.dev33.satoken.config.SaCookieConfig;
import cn.dev33.satoken.config.SaSignConfig;
import cn.dev33.satoken.util.SaFoxUtil;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
/**
* 复制自 Sa-Token cn.dev33.satoken.config.SaTokenConfig,升级版本时需要注意重新复制,目的是为了使用两套配置,
* 但是又不能影响原框架的注入问题
*
* @author zhaowenhao
* @since 2023-11-20
*/
@Getter
@ConfigurationProperties(prefix = "sa-token.admin")
public class SaTokenAdminProperties implements Serializable {
private static final long serialVersionUID = -6541180061782004705L;
/**
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
* -- GETTER --
* <p>
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
*/
private String tokenName = "satoken";
/**
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
* -- GETTER --
* <p>
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
*/
private long timeout = 60 * 60 * 24 * 30;
/**
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
* -- GETTER --
* <p>
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
*/
private long activeTimeout = -1;
/**
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
* -- GETTER --
* <p>
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
*/
private Boolean dynamicActiveTimeout = false;
/**
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
* -- GETTER --
* <p>
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
*/
private Boolean isConcurrent = true;
/**
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
* -- GETTER --
* <p>
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
*/
private Boolean isShare = true;
/**
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
* -- GETTER --
* <p>
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
*/
private int maxLoginCount = 12;
/**
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
* -- GETTER --
* <p>
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
*/
private int maxTryTimes = 12;
/**
* 是否尝试从请求体里读取 token
* -- GETTER --
* <p>
* 是否尝试从请求体里读取 token
*/
private Boolean isReadBody = true;
/**
* 是否尝试从 header 里读取 token
* -- GETTER --
* <p>
* 是否尝试从 header 里读取 token
*/
private Boolean isReadHeader = true;
/**
* 是否尝试从 cookie 里读取 token
* -- GETTER --
* <p>
* 是否尝试从 cookie 里读取 token
*/
private Boolean isReadCookie = true;
/**
* 是否在登录后将 token 写入到响应头
* -- GETTER --
* <p>
* 是否在登录后将 token 写入到响应头
*/
private Boolean isWriteHeader = false;
/**
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
* -- GETTER --
* <p>
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
*/
private String tokenStyle = "uuid";
/**
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
* -- GETTER --
* <p>
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
*/
private int dataRefreshPeriod = 30;
/**
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
* -- GETTER --
* <p>
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
*/
private Boolean tokenSessionCheckLogin = true;
/**
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
* -- GETTER --
* <p>
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
*/
private Boolean autoRenew = true;
/**
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
* -- GETTER --
* <p>
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
*/
private String tokenPrefix;
/**
* 是否在初始化配置时在控制台打印版本字符画
* -- GETTER --
* <p>
* 是否在初始化配置时在控制台打印版本字符画
*/
private Boolean isPrint = true;
/**
* 是否打印操作日志
* -- GETTER --
* <p>
* 是否打印操作日志
*/
private Boolean isLog = false;
/**
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
* -- GETTER --
* <p>
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
*/
private String logLevel = "trace";
/**
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
* -- GETTER --
* <p>
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
*/
private int logLevelInt = 1;
/**
* 是否打印彩色日志
* -- GETTER --
* 获取:是否打印彩色日志
* <p>
* isColorLog 是否打印彩色日志
*/
private Boolean isColorLog = null;
/**
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
* -- GETTER --
* <p>
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
*/
private String jwtSecretKey;
/**
* Http Basic 认证的默认账号和密码
* -- GETTER --
* <p>
* Http Basic 认证的默认账号和密码
*/
private String basic = "";
/**
* 配置当前项目的网络访问地址
* -- GETTER --
* <p>
* 配置当前项目的网络访问地址
*/
private String currDomain;
/**
* Same-Token 的有效期 (单位: 秒)
* -- GETTER --
* <p>
* Same-Token 的有效期 (单位: 秒)
*/
private long sameTokenTimeout = 60 * 60 * 24;
/**
* 是否校验 Same-Token(部分rpc插件有效)
* -- GETTER --
* <p>
* 是否校验Same-Token(部分rpc插件有效)
*/
private Boolean checkSameToken = false;
/**
* Cookie配置对象
* -- GETTER --
* <p>
* Cookie 全局配置对象
*/
public SaCookieConfig cookie = new SaCookieConfig();
/**
* API 签名配置对象
* -- GETTER --
* <p>
* API 签名全局配置对象
*/
public SaSignConfig sign = new SaSignConfig();
/**
* @param tokenName token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
* 对象自身
*/
public SaTokenAdminProperties setTokenName(String tokenName) {
this.tokenName = tokenName;
return this;
}
/**
* @param timeout token 有效期(单位:秒) 默认30天,-1 代表永久有效
* 对象自身
*/
public SaTokenAdminProperties setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* @param activeTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
* 对象自身
*/
public SaTokenAdminProperties setActiveTimeout(long activeTimeout) {
this.activeTimeout = activeTimeout;
return this;
}
/**
* @param dynamicActiveTimeout 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
* 对象自身
*/
public SaTokenAdminProperties setDynamicActiveTimeout(Boolean dynamicActiveTimeout) {
this.dynamicActiveTimeout = dynamicActiveTimeout;
return this;
}
/**
* @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
* 对象自身
*/
public SaTokenAdminProperties setIsConcurrent(Boolean isConcurrent) {
this.isConcurrent = isConcurrent;
return this;
}
/**
* @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
* 对象自身
*/
public SaTokenAdminProperties setIsShare(Boolean isShare) {
this.isShare = isShare;
return this;
}
/**
* @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
* 对象自身
*/
public SaTokenAdminProperties setMaxLoginCount(int maxLoginCount) {
this.maxLoginCount = maxLoginCount;
return this;
}
/**
* @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
* 对象自身
*/
public SaTokenAdminProperties setMaxTryTimes(int maxTryTimes) {
this.maxTryTimes = maxTryTimes;
return this;
}
/**
* @param isReadBody 是否尝试从请求体里读取 token
* 对象自身
*/
public SaTokenAdminProperties setIsReadBody(Boolean isReadBody) {
this.isReadBody = isReadBody;
return this;
}
/**
* @param isReadHeader 是否尝试从 header 里读取 token
* 对象自身
*/
public SaTokenAdminProperties setIsReadHeader(Boolean isReadHeader) {
this.isReadHeader = isReadHeader;
return this;
}
/**
* @param isReadCookie 是否尝试从 cookie 里读取 token
* 对象自身
*/
public SaTokenAdminProperties setIsReadCookie(Boolean isReadCookie) {
this.isReadCookie = isReadCookie;
return this;
}
/**
* @param isWriteHeader 是否在登录后将 token 写入到响应头
* 对象自身
*/
public SaTokenAdminProperties setIsWriteHeader(Boolean isWriteHeader) {
this.isWriteHeader = isWriteHeader;
return this;
}
/**
* @param tokenStyle token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
* 对象自身
*/
public SaTokenAdminProperties setTokenStyle(String tokenStyle) {
this.tokenStyle = tokenStyle;
return this;
}
/**
* @param dataRefreshPeriod 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
* 对象自身
*/
public SaTokenAdminProperties setDataRefreshPeriod(int dataRefreshPeriod) {
this.dataRefreshPeriod = dataRefreshPeriod;
return this;
}
/**
* @param tokenSessionCheckLogin 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
* 对象自身
*/
public SaTokenAdminProperties setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
this.tokenSessionCheckLogin = tokenSessionCheckLogin;
return this;
}
/**
* @param autoRenew 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
* 对象自身
*/
public SaTokenAdminProperties setAutoRenew(Boolean autoRenew) {
this.autoRenew = autoRenew;
return this;
}
/**
* @param tokenPrefix token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
* 对象自身
*/
public SaTokenAdminProperties setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix;
return this;
}
/**
* @param isPrint 是否在初始化配置时在控制台打印版本字符画
* 对象自身
*/
public SaTokenAdminProperties setIsPrint(Boolean isPrint) {
this.isPrint = isPrint;
return this;
}
/**
* @param isLog 是否打印操作日志
* 对象自身
*/
public SaTokenAdminProperties setIsLog(Boolean isLog) {
this.isLog = isLog;
return this;
}
/**
* @param logLevel 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
* 对象自身
*/
public SaTokenAdminProperties setLogLevel(String logLevel) {
this.logLevel = logLevel;
this.logLevelInt = SaFoxUtil.translateLogLevelToInt(logLevel);
return this;
}
/**
* @param logLevelInt 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
* 对象自身
*/
public SaTokenAdminProperties setLogLevelInt(int logLevelInt) {
this.logLevelInt = logLevelInt;
this.logLevel = SaFoxUtil.translateLogLevelToString(logLevelInt);
return this;
}
/**
* 设置:是否打印彩色日志
*
* @param isColorLog 是否打印彩色日志
* 对象自身
*/
public SaTokenAdminProperties setIsColorLog(Boolean isColorLog) {
this.isColorLog = isColorLog;
return this;
}
/**
* @param jwtSecretKey jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
* 对象自身
*/
public SaTokenAdminProperties setJwtSecretKey(String jwtSecretKey) {
this.jwtSecretKey = jwtSecretKey;
return this;
}
/**
* @param basic Http Basic 认证的默认账号和密码
* 对象自身
*/
public SaTokenAdminProperties setBasic(String basic) {
this.basic = basic;
return this;
}
/**
* @param currDomain 配置当前项目的网络访问地址
* 对象自身
*/
public SaTokenAdminProperties setCurrDomain(String currDomain) {
this.currDomain = currDomain;
return this;
}
/**
* @param sameTokenTimeout Same-Token 的有效期 (单位: 秒)
* 对象自身
*/
public SaTokenAdminProperties setSameTokenTimeout(long sameTokenTimeout) {
this.sameTokenTimeout = sameTokenTimeout;
return this;
}
/**
* @param checkSameToken 是否校验Same-Token(部分rpc插件有效)
* 对象自身
*/
public SaTokenAdminProperties setCheckSameToken(Boolean checkSameToken) {
this.checkSameToken = checkSameToken;
return this;
}
/**
* @param cookie Cookie 全局配置对象
* 对象自身
*/
public SaTokenAdminProperties setCookie(SaCookieConfig cookie) {
this.cookie = cookie;
return this;
}
/**
* @param sign API 签名全局配置对象
* 对象自身
*/
public SaTokenAdminProperties setSign(SaSignConfig sign) {
this.sign = sign;
return this;
}
@Override
public String toString() {
return "SaTokenConfig ["
+ "tokenName=" + tokenName
+ ", timeout=" + timeout
+ ", activeTimeout=" + activeTimeout
+ ", dynamicActiveTimeout=" + dynamicActiveTimeout
+ ", isConcurrent=" + isConcurrent
+ ", isShare=" + isShare
+ ", maxLoginCount=" + maxLoginCount
+ ", maxTryTimes=" + maxTryTimes
+ ", isReadBody=" + isReadBody
+ ", isReadHeader=" + isReadHeader
+ ", isReadCookie=" + isReadCookie
+ ", isWriteHeader=" + isWriteHeader
+ ", tokenStyle=" + tokenStyle
+ ", dataRefreshPeriod=" + dataRefreshPeriod
+ ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
+ ", autoRenew=" + autoRenew
+ ", tokenPrefix=" + tokenPrefix
+ ", isPrint=" + isPrint
+ ", isLog=" + isLog
+ ", logLevel=" + logLevel
+ ", logLevelInt=" + logLevelInt
+ ", isColorLog=" + isColorLog
+ ", jwtSecretKey=" + jwtSecretKey
+ ", basic=" + basic
+ ", currDomain=" + currDomain
+ ", sameTokenTimeout=" + sameTokenTimeout
+ ", checkSameToken=" + checkSameToken
+ ", cookie=" + cookie
+ ", sign=" + sign
+ "]";
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
package com.metalloop.framework.satoken.self.config;
import cn.dev33.satoken.config.SaTokenConfig;
import com.metalloop.framework.satoken.self.StpSelfUtil;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 自助服务终端 Sa-Token 配置类,目标是提供一个支持配置的开箱即用的StpSelfUtil
*
* @author zhaowenhao
* @since 2023-11-17
*/
@Configuration
@EnableConfigurationProperties(SaTokenSelfProperties.class)
public class SaTokenSelfConfiguration {
private final SaTokenSelfProperties saTokenSelfProperties;
public SaTokenSelfConfiguration(SaTokenSelfProperties saTokenSelfProperties) {
this.saTokenSelfProperties = saTokenSelfProperties;
}
// 使用注入的 SaTokenSelfProperties 来配置 StpUtil 或其他组件
@PostConstruct
public void init() {
SaTokenConfig config = new SaTokenConfig();
// 拷贝属性
BeanUtils.copyProperties(saTokenSelfProperties, config);
StpSelfUtil.getStpLogic().setConfig(config);
}
}
@@ -0,0 +1,561 @@
package com.metalloop.framework.satoken.self.config;
import cn.dev33.satoken.config.SaCookieConfig;
import cn.dev33.satoken.config.SaSignConfig;
import cn.dev33.satoken.util.SaFoxUtil;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
/**
* 复制自 Sa-Token cn.dev33.satoken.config.SaTokenConfig,升级版本时需要注意重新复制,目的是为了使用两套配置,
* 但是又不能影响原框架的注入问题
*
* @author zhaowenhao
* @since 2023-11-20
*/
@Getter
@ConfigurationProperties(prefix = "sa-token.self")
public class SaTokenSelfProperties implements Serializable {
private static final long serialVersionUID = -6541180061782004705L;
/**
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
* -- GETTER --
* <p>
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
*/
private String tokenName = "satoken";
/**
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
* -- GETTER --
* <p>
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
*/
private long timeout = 60 * 60 * 24 * 30;
/**
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
* -- GETTER --
* <p>
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
*/
private long activeTimeout = -1;
/**
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
* -- GETTER --
* <p>
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
*/
private Boolean dynamicActiveTimeout = false;
/**
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
* -- GETTER --
* <p>
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
*/
private Boolean isConcurrent = true;
/**
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
* -- GETTER --
* <p>
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
*/
private Boolean isShare = true;
/**
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
* -- GETTER --
* <p>
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
*/
private int maxLoginCount = 12;
/**
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
* -- GETTER --
* <p>
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
*/
private int maxTryTimes = 12;
/**
* 是否尝试从请求体里读取 token
* -- GETTER --
* <p>
* 是否尝试从请求体里读取 token
*/
private Boolean isReadBody = true;
/**
* 是否尝试从 header 里读取 token
* -- GETTER --
* <p>
* 是否尝试从 header 里读取 token
*/
private Boolean isReadHeader = true;
/**
* 是否尝试从 cookie 里读取 token
* -- GETTER --
* <p>
* 是否尝试从 cookie 里读取 token
*/
private Boolean isReadCookie = true;
/**
* 是否在登录后将 token 写入到响应头
* -- GETTER --
* <p>
* 是否在登录后将 token 写入到响应头
*/
private Boolean isWriteHeader = false;
/**
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
* -- GETTER --
* <p>
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
*/
private String tokenStyle = "uuid";
/**
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
* -- GETTER --
* <p>
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
*/
private int dataRefreshPeriod = 30;
/**
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
* -- GETTER --
* <p>
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
*/
private Boolean tokenSessionCheckLogin = true;
/**
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
* -- GETTER --
* <p>
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
*/
private Boolean autoRenew = true;
/**
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
* -- GETTER --
* <p>
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
*/
private String tokenPrefix;
/**
* 是否在初始化配置时在控制台打印版本字符画
* -- GETTER --
* <p>
* 是否在初始化配置时在控制台打印版本字符画
*/
private Boolean isPrint = true;
/**
* 是否打印操作日志
* -- GETTER --
* <p>
* 是否打印操作日志
*/
private Boolean isLog = false;
/**
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
* -- GETTER --
* <p>
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
*/
private String logLevel = "trace";
/**
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
* -- GETTER --
* <p>
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
*/
private int logLevelInt = 1;
/**
* 是否打印彩色日志
* -- GETTER --
* 获取:是否打印彩色日志
* <p>
* isColorLog 是否打印彩色日志
*/
private Boolean isColorLog = null;
/**
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
* -- GETTER --
* <p>
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
*/
private String jwtSecretKey;
/**
* Http Basic 认证的默认账号和密码
* -- GETTER --
* <p>
* Http Basic 认证的默认账号和密码
*/
private String basic = "";
/**
* 配置当前项目的网络访问地址
* -- GETTER --
* <p>
* 配置当前项目的网络访问地址
*/
private String currDomain;
/**
* Same-Token 的有效期 (单位: 秒)
* -- GETTER --
* <p>
* Same-Token 的有效期 (单位: 秒)
*/
private long sameTokenTimeout = 60 * 60 * 24;
/**
* 是否校验 Same-Token(部分rpc插件有效)
* -- GETTER --
* <p>
* 是否校验Same-Token(部分rpc插件有效)
*/
private Boolean checkSameToken = false;
/**
* Cookie配置对象
* -- GETTER --
* <p>
* Cookie 全局配置对象
*/
public SaCookieConfig cookie = new SaCookieConfig();
/**
* API 签名配置对象
* -- GETTER --
* <p>
* API 签名全局配置对象
*/
public SaSignConfig sign = new SaSignConfig();
/**
* @param tokenName token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
* 对象自身
*/
public SaTokenSelfProperties setTokenName(String tokenName) {
this.tokenName = tokenName;
return this;
}
/**
* @param timeout token 有效期(单位:秒) 默认30天,-1 代表永久有效
* 对象自身
*/
public SaTokenSelfProperties setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* @param activeTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
* 对象自身
*/
public SaTokenSelfProperties setActiveTimeout(long activeTimeout) {
this.activeTimeout = activeTimeout;
return this;
}
/**
* @param dynamicActiveTimeout 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
* 对象自身
*/
public SaTokenSelfProperties setDynamicActiveTimeout(Boolean dynamicActiveTimeout) {
this.dynamicActiveTimeout = dynamicActiveTimeout;
return this;
}
/**
* @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
* 对象自身
*/
public SaTokenSelfProperties setIsConcurrent(Boolean isConcurrent) {
this.isConcurrent = isConcurrent;
return this;
}
/**
* @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
* 对象自身
*/
public SaTokenSelfProperties setIsShare(Boolean isShare) {
this.isShare = isShare;
return this;
}
/**
* @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
* 对象自身
*/
public SaTokenSelfProperties setMaxLoginCount(int maxLoginCount) {
this.maxLoginCount = maxLoginCount;
return this;
}
/**
* @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
* 对象自身
*/
public SaTokenSelfProperties setMaxTryTimes(int maxTryTimes) {
this.maxTryTimes = maxTryTimes;
return this;
}
/**
* @param isReadBody 是否尝试从请求体里读取 token
* 对象自身
*/
public SaTokenSelfProperties setIsReadBody(Boolean isReadBody) {
this.isReadBody = isReadBody;
return this;
}
/**
* @param isReadHeader 是否尝试从 header 里读取 token
* 对象自身
*/
public SaTokenSelfProperties setIsReadHeader(Boolean isReadHeader) {
this.isReadHeader = isReadHeader;
return this;
}
/**
* @param isReadCookie 是否尝试从 cookie 里读取 token
* 对象自身
*/
public SaTokenSelfProperties setIsReadCookie(Boolean isReadCookie) {
this.isReadCookie = isReadCookie;
return this;
}
/**
* @param isWriteHeader 是否在登录后将 token 写入到响应头
* 对象自身
*/
public SaTokenSelfProperties setIsWriteHeader(Boolean isWriteHeader) {
this.isWriteHeader = isWriteHeader;
return this;
}
/**
* @param tokenStyle token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
* 对象自身
*/
public SaTokenSelfProperties setTokenStyle(String tokenStyle) {
this.tokenStyle = tokenStyle;
return this;
}
/**
* @param dataRefreshPeriod 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
* 对象自身
*/
public SaTokenSelfProperties setDataRefreshPeriod(int dataRefreshPeriod) {
this.dataRefreshPeriod = dataRefreshPeriod;
return this;
}
/**
* @param tokenSessionCheckLogin 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
* 对象自身
*/
public SaTokenSelfProperties setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
this.tokenSessionCheckLogin = tokenSessionCheckLogin;
return this;
}
/**
* @param autoRenew 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
* 对象自身
*/
public SaTokenSelfProperties setAutoRenew(Boolean autoRenew) {
this.autoRenew = autoRenew;
return this;
}
/**
* @param tokenPrefix token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
* 对象自身
*/
public SaTokenSelfProperties setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix;
return this;
}
/**
* @param isPrint 是否在初始化配置时在控制台打印版本字符画
* 对象自身
*/
public SaTokenSelfProperties setIsPrint(Boolean isPrint) {
this.isPrint = isPrint;
return this;
}
/**
* @param isLog 是否打印操作日志
* 对象自身
*/
public SaTokenSelfProperties setIsLog(Boolean isLog) {
this.isLog = isLog;
return this;
}
/**
* @param logLevel 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
* 对象自身
*/
public SaTokenSelfProperties setLogLevel(String logLevel) {
this.logLevel = logLevel;
this.logLevelInt = SaFoxUtil.translateLogLevelToInt(logLevel);
return this;
}
/**
* @param logLevelInt 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
* 对象自身
*/
public SaTokenSelfProperties setLogLevelInt(int logLevelInt) {
this.logLevelInt = logLevelInt;
this.logLevel = SaFoxUtil.translateLogLevelToString(logLevelInt);
return this;
}
/**
* 设置:是否打印彩色日志
*
* @param isColorLog 是否打印彩色日志
* 对象自身
*/
public SaTokenSelfProperties setIsColorLog(Boolean isColorLog) {
this.isColorLog = isColorLog;
return this;
}
/**
* @param jwtSecretKey jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
* 对象自身
*/
public SaTokenSelfProperties setJwtSecretKey(String jwtSecretKey) {
this.jwtSecretKey = jwtSecretKey;
return this;
}
/**
* @param basic Http Basic 认证的默认账号和密码
* 对象自身
*/
public SaTokenSelfProperties setBasic(String basic) {
this.basic = basic;
return this;
}
/**
* @param currDomain 配置当前项目的网络访问地址
* 对象自身
*/
public SaTokenSelfProperties setCurrDomain(String currDomain) {
this.currDomain = currDomain;
return this;
}
/**
* @param sameTokenTimeout Same-Token 的有效期 (单位: 秒)
* 对象自身
*/
public SaTokenSelfProperties setSameTokenTimeout(long sameTokenTimeout) {
this.sameTokenTimeout = sameTokenTimeout;
return this;
}
/**
* @param checkSameToken 是否校验Same-Token(部分rpc插件有效)
* 对象自身
*/
public SaTokenSelfProperties setCheckSameToken(Boolean checkSameToken) {
this.checkSameToken = checkSameToken;
return this;
}
/**
* @param cookie Cookie 全局配置对象
* 对象自身
*/
public SaTokenSelfProperties setCookie(SaCookieConfig cookie) {
this.cookie = cookie;
return this;
}
/**
* @param sign API 签名全局配置对象
* 对象自身
*/
public SaTokenSelfProperties setSign(SaSignConfig sign) {
this.sign = sign;
return this;
}
@Override
public String toString() {
return "SaTokenConfig ["
+ "tokenName=" + tokenName
+ ", timeout=" + timeout
+ ", activeTimeout=" + activeTimeout
+ ", dynamicActiveTimeout=" + dynamicActiveTimeout
+ ", isConcurrent=" + isConcurrent
+ ", isShare=" + isShare
+ ", maxLoginCount=" + maxLoginCount
+ ", maxTryTimes=" + maxTryTimes
+ ", isReadBody=" + isReadBody
+ ", isReadHeader=" + isReadHeader
+ ", isReadCookie=" + isReadCookie
+ ", isWriteHeader=" + isWriteHeader
+ ", tokenStyle=" + tokenStyle
+ ", dataRefreshPeriod=" + dataRefreshPeriod
+ ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
+ ", autoRenew=" + autoRenew
+ ", tokenPrefix=" + tokenPrefix
+ ", isPrint=" + isPrint
+ ", isLog=" + isLog
+ ", logLevel=" + logLevel
+ ", logLevelInt=" + logLevelInt
+ ", isColorLog=" + isColorLog
+ ", jwtSecretKey=" + jwtSecretKey
+ ", basic=" + basic
+ ", currDomain=" + currDomain
+ ", sameTokenTimeout=" + sameTokenTimeout
+ ", checkSameToken=" + checkSameToken
+ ", cookie=" + cookie
+ ", sign=" + sign
+ "]";
}
}
@@ -0,0 +1,40 @@
package com.metalloop.framework.swagger;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Springdoc OpenAPI 配置类
* 用于定制 API 文档信息及全局安全认证方案
*/
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
// 设置 API 基本信息
.info(new Info()
.title("有色再生资源交易平台 API")
.version("1.0")
.description("METALLOOP 后端接口文档")
)
// 声明全局安全要求(在 Swagger UI 中显示 Authorize 按钮)
.addSecurityItem(new SecurityRequirement().addList("Sa-Token"))
// 定义安全方案(API Key 方式)
.components(new Components()
.addSecuritySchemes("Sa-Token",
new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("metalloop-token") // 与 Sa-Token 配置的 token-name 保持一致
.description("Sa-Token 认证令牌,登录后获取")
)
);
}
}
@@ -0,0 +1,285 @@
package com.metalloop.modules.auth.controller;
import cn.dev33.satoken.session.SaSession;
import com.metalloop.common.auth.Authentication;
import com.metalloop.common.auth.handler.LoginSuccessHandler;
import com.metalloop.common.auth.handler.LogoutSuccessHandler;
import com.metalloop.common.auth.handler.PasswordCheckFailureHandler;
import com.metalloop.common.auth.handler.VerifyCodeHandler;
import com.metalloop.common.auth.model.AuthenticationMiniAppRequest;
import com.metalloop.common.auth.password.PasswordDecryptor;
import com.metalloop.common.auth.sso.SsoCodeService;
import com.metalloop.common.auth.sso.model.SsoUserInfo;
import com.metalloop.common.auth.userdetails.UserDetails;
import com.metalloop.common.auth.userdetails.UserDetailsService;
import com.metalloop.common.constant.SecurityConstants;
import com.metalloop.common.enums.LoginType;
import com.metalloop.common.exception.*;
import com.metalloop.common.model.Result;
import com.metalloop.common.utils.IPHelper;
import com.metalloop.modules.auth.model.req.AuthenticationRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 认证授权端点
*
* @author zhaowenhao
* @since 2023-03-19
*/
public class AuthController {
private final Log logger = LogFactory.getLog(getClass());
private final LoginType loginType;
private final UserDetailsService userDetailsService;
private final LoginSuccessHandler loginSuccessHandler;
private final LogoutSuccessHandler logoutSuccessHandler;
private final PasswordDecryptor passwordDecryptor;
private final PasswordEncoder passwordEncoder;
private final VerifyCodeHandler verifyCodeHandler;
private final PasswordCheckFailureHandler passwordCheckFailureHandler;
/**
* 单点登陆授权码服务
*/
private SsoCodeService ssoCodeService;
/**
* 单点登陆授权码超时时间,单位:秒
*/
private int ssoCodeTimeOut = 300;
public void initSso(SsoCodeService ssoCodeService, int ssoCodeTimeOut) {
this.ssoCodeService = ssoCodeService;
this.ssoCodeTimeOut = ssoCodeTimeOut;
}
public AuthController(LoginType loginType, UserDetailsService userDetailsService, LoginSuccessHandler loginSuccessHandler,
LogoutSuccessHandler logoutSuccessHandler, PasswordDecryptor passwordDecryptor, PasswordEncoder passwordEncoder
, VerifyCodeHandler verifyCodeHandler, PasswordCheckFailureHandler passwordCheckFailureHandler) {
this.loginType = loginType;
this.userDetailsService = userDetailsService;
this.loginSuccessHandler = loginSuccessHandler;
this.logoutSuccessHandler = logoutSuccessHandler;
this.passwordDecryptor = passwordDecryptor;
this.passwordEncoder = passwordEncoder;
this.verifyCodeHandler = verifyCodeHandler;
this.passwordCheckFailureHandler = passwordCheckFailureHandler;
}
@ResponseBody
public Object getVerifyCode(@RequestBody @Validated AuthenticationRequest request, HttpServletRequest httpServletRequest) {
// 用户名、密码校验
UserDetails userDetails = loginVerify(request, httpServletRequest);
if (verifyCodeHandler.createVerifyCode(userDetails.getPhoneNum())) {
return Result.ok("发送验证码成功");
} else {
return Result.fail("发送验证码失败");
}
}
@ResponseBody
public Object login(@RequestBody @Validated AuthenticationRequest request, HttpServletRequest httpServletRequest) {
// 用户名、密码校验
UserDetails userDetails = loginVerify(request, httpServletRequest);
// 在sa-token中进行登录操作
loginType.getStpLogic().login(userDetails.getUserId());
String accessToken = loginType.getStpLogic().getTokenValue();
SaSession session = loginType.getStpLogic().getSession();
// 将登录成功的认证信息封装至session中
Authentication authentication = Authentication.withUserDetails(userDetails).accessToken(accessToken).build();
session.set(SecurityConstants.SESSION_KEY_AUTHENTICATION, authentication);
// 登录成功响应结果处理器
return loginSuccessHandler.onSuccess(authentication);
}
/**
* 用户名、密码校验
*/
private UserDetails loginVerify(AuthenticationRequest request, HttpServletRequest servletRequest) {
String username = request.getUsername();
String password = request.getPassword();
// 获取用户ip
String ip = IPHelper.getIpAddr(servletRequest);
// 从 UserDetailsService 加载用户并验证用户名和密码
UserDetails userDetails;
try {
userDetails = userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException ex) {
this.logger.debug("未找到用户: '" + username + "'");
throw new BadCredentialsException("用户名或密码错误");
}
if (userDetails == null) {
this.logger.debug("未找到用户: '" + username + "'");
throw new BadCredentialsException("用户名或密码错误");
}
// 验证用户账号状态
if (!userDetails.isAccountNonLocked()) {
this.logger.debug("账号被锁定");
passwordCheckFailureHandler.checkAccountLockStatus(ip, userDetails);
}
if (!userDetails.isEnabled()) {
this.logger.debug("账号被禁用");
throw new DisabledException("账号被禁用");
}
if (!userDetails.isAccountNonExpired()) {
this.logger.debug("账号已过期");
throw new AccountExpiredException("账号已过期");
}
if (!userDetails.isCredentialsNonExpired()) {
this.logger.debug("密码已过期");
throw new CredentialsExpiredException("密码已过期");
}
// 验证密码
String presentedPassword = password;
// 密码解密
if (passwordDecryptor != null) {
presentedPassword = passwordDecryptor.decrypt(presentedPassword);
}
// 密码匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("密码错误");
throw new BadCredentialsException("用户名或密码错误");
}
return userDetails;
}
/**
* 用户名、密码校验
*/
private UserDetails loginVerifyForMiniApp(AuthenticationMiniAppRequest request, HttpServletRequest servletRequest) {
String username = request.getUsername();
String password = request.getPassword();
// 获取用户ip
String ip = IPHelper.getIpAddr(servletRequest);
// 从 UserDetailsService 加载用户并验证用户名和密码
UserDetails userDetails;
try {
userDetails = userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException ex) {
this.logger.debug("未找到用户: '" + username + "'");
throw new BadCredentialsException("用户名或密码错误");
}
if (userDetails == null) {
this.logger.debug("未找到用户: '" + username + "'");
throw new BadCredentialsException("用户名或密码错误");
}
// 验证用户账号状态
if (!userDetails.isAccountNonLocked()) {
this.logger.debug("账号被锁定");
passwordCheckFailureHandler.checkAccountLockStatus(ip, userDetails);
}
if (!userDetails.isEnabled()) {
this.logger.debug("账号被禁用");
throw new DisabledException("账号被禁用");
}
if (!userDetails.isAccountNonExpired()) {
this.logger.debug("账号已过期");
throw new AccountExpiredException("账号已过期");
}
if (!userDetails.isCredentialsNonExpired()) {
this.logger.debug("密码已过期");
throw new CredentialsExpiredException("密码已过期");
}
// 验证密码
String presentedPassword = password;
// 密码解密
if (passwordDecryptor != null) {
presentedPassword = passwordDecryptor.decrypt(presentedPassword);
}
// 密码匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("密码错误");
throw new BadCredentialsException("用户名或密码错误");
}
return userDetails;
}
@ResponseBody
public Object logout() {
Object o = loginType.getStpLogic().getSession().get(SecurityConstants.SESSION_KEY_AUTHENTICATION);
Authentication authentication = null;
if (o instanceof Authentication) {
authentication = (Authentication) o;
}
loginType.getStpLogic().logout();
// 登出成功响应结果处理器
return logoutSuccessHandler.onSuccess(authentication);
}
@ResponseBody
public Object miniAppLogout() {
Object o = loginType.getStpLogic().getSession().get(SecurityConstants.SESSION_KEY_AUTHENTICATION);
Authentication authentication = null;
if (o instanceof Authentication) {
authentication = (Authentication) o;
}
loginType.getStpLogic().logout();
// 登出成功响应结果处理器
return logoutSuccessHandler.onSuccess(authentication);
}
@ResponseBody
public Object getSsoCode() {
// 验证当前会话是否有效
if (loginType.getStpLogic().isLogin()) {
Object o = loginType.getStpLogic().getSession().get(SecurityConstants.SESSION_KEY_AUTHENTICATION);
Authentication authentication = null;
if (o instanceof Authentication) {
authentication = (Authentication) o;
}
if (authentication != null) {
// 签发授权码
String authorizationCode = ssoCodeService.generateCode(authentication.getUserId(), ssoCodeTimeOut);
return Result.ok(authorizationCode);
}
}
return Result.fail("当前会话无效");
}
@ResponseBody
public Object getSsoUserInfo(@RequestParam("code") String code) {
// 验证授权码并获取绑定的用户信息
SsoUserInfo userInfo = ssoCodeService.validateCode(code);
// 登录成功响应结果处理器
return Result.ok(userInfo);
}
}
@@ -0,0 +1,37 @@
package com.metalloop.modules.auth.controller;
import com.metalloop.common.auth.encrypt.KeyProperties;
import com.metalloop.common.auth.encrypt.KeyStoreKeyFactory;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
/**
* 获取公钥端点
*
* @author zhaowenhao
* @since 2023-03-19
*/
public class RsaPublicKeyController {
private final KeyProperties keyProperties;
public RsaPublicKeyController(KeyProperties keyProperties) {
this.keyProperties = keyProperties;
}
@ResponseBody
public String getPublicKey() {
if (keyProperties != null) {
KeyPair keyPair = new KeyStoreKeyFactory(keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray()).getKeyPair(keyProperties.getKeyStore().getAlias());
PublicKey publicKey = keyPair.getPublic();
RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
byte[] publicKeyBytes = rsaPublicKey.getEncoded();
return Base64.getEncoder().encodeToString(publicKeyBytes);
}
return "获取失败";
}
}
@@ -0,0 +1,65 @@
package com.metalloop.modules.auth.converter;
import com.metalloop.modules.auth.model.req.SysEnterpriseQueryReqDto;
import com.metalloop.modules.auth.model.req.SysEnterpriseReqDto;
import com.metalloop.modules.auth.model.resp.SysEnterpriseRespDto;
import com.metalloop.modules.auth.model.po.SysEnterprise;
import com.metalloop.modules.auth.model.query.SysEnterpriseQuery;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 企业信息主转换器
*
* @author yy
* @since 2026-09-10
*/
@SuppressWarnings("UnmappedTargetProperties")
@Mapper(builder = @Builder(disableBuilder = true))
public interface SysEnterpriseConverter {
SysEnterpriseConverter INSTANCE = Mappers.getMapper(SysEnterpriseConverter.class);
/**
* 实体转换为响应 DTO
*
* @param sysEnterprise 实体
* @return 转换后的数据
*/
SysEnterpriseRespDto toRespDto(SysEnterprise sysEnterprise);
/**
* 实体列表转换为响应 DTO 列表
*
* @param sysEnterprises 实体列表
* @return 转换后的数据
*/
List<SysEnterpriseRespDto> toRespDto(List<SysEnterprise> sysEnterprises);
/**
* 请求 DTO 转换为实体
*
* @param sysEnterpriseReqDto 请求 DTO
* @return 转换后的数据
*/
SysEnterprise fromReqDto(SysEnterpriseReqDto sysEnterpriseReqDto);
/**
* 请求 DTO 列表转换为实体列表
*
* @param sysEnterpriseReqDtos 请求 DTO 列表
* @return 转换后的数据
*/
List<SysEnterprise> fromReqDto(List<SysEnterpriseReqDto> sysEnterpriseReqDtos);
/**
* 查询请求 DTO 转换为查询对象
*
* @param sysEnterpriseQueryReqDto 查询请求DTO
* @return 转换后的数据
*/
SysEnterpriseQuery fromQueryReqDtoToQuery(SysEnterpriseQueryReqDto sysEnterpriseQueryReqDto);
}
@@ -0,0 +1,65 @@
package com.metalloop.modules.auth.converter;
import com.metalloop.modules.auth.model.req.SysEnterpriseImageQueryReqDto;
import com.metalloop.modules.auth.model.req.SysEnterpriseImageReqDto;
import com.metalloop.modules.auth.model.resp.SysEnterpriseImageRespDto;
import com.metalloop.modules.auth.model.po.SysEnterpriseImage;
import com.metalloop.modules.auth.model.query.SysEnterpriseImageQuery;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 企业图片附件转换器
*
* @author yy
* @since 2026-09-10
*/
@SuppressWarnings("UnmappedTargetProperties")
@Mapper(builder = @Builder(disableBuilder = true))
public interface SysEnterpriseImageConverter {
SysEnterpriseImageConverter INSTANCE = Mappers.getMapper(SysEnterpriseImageConverter.class);
/**
* 实体转换为响应 DTO
*
* @param sysEnterpriseImage 实体
* @return 转换后的数据
*/
SysEnterpriseImageRespDto toRespDto(SysEnterpriseImage sysEnterpriseImage);
/**
* 实体列表转换为响应 DTO 列表
*
* @param sysEnterpriseImages 实体列表
* @return 转换后的数据
*/
List<SysEnterpriseImageRespDto> toRespDto(List<SysEnterpriseImage> sysEnterpriseImages);
/**
* 请求 DTO 转换为实体
*
* @param sysEnterpriseImageReqDto 请求 DTO
* @return 转换后的数据
*/
SysEnterpriseImage fromReqDto(SysEnterpriseImageReqDto sysEnterpriseImageReqDto);
/**
* 请求 DTO 列表转换为实体列表
*
* @param sysEnterpriseImageReqDtos 请求 DTO 列表
* @return 转换后的数据
*/
List<SysEnterpriseImage> fromReqDto(List<SysEnterpriseImageReqDto> sysEnterpriseImageReqDtos);
/**
* 查询请求 DTO 转换为查询对象
*
* @param sysEnterpriseImageQueryReqDto 查询请求DTO
* @return 转换后的数据
*/
SysEnterpriseImageQuery fromQueryReqDtoToQuery(SysEnterpriseImageQueryReqDto sysEnterpriseImageQueryReqDto);
}
@@ -0,0 +1,65 @@
package com.metalloop.modules.auth.converter;
import com.metalloop.modules.auth.model.req.SysPermissionQueryReqDto;
import com.metalloop.modules.auth.model.req.SysPermissionReqDto;
import com.metalloop.modules.auth.model.resp.SysPermissionRespDto;
import com.metalloop.modules.auth.model.po.SysPermission;
import com.metalloop.modules.auth.model.query.SysPermissionQuery;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 权限转换器
*
* @author yy
* @since 2026-09-10
*/
@SuppressWarnings("UnmappedTargetProperties")
@Mapper(builder = @Builder(disableBuilder = true))
public interface SysPermissionConverter {
SysPermissionConverter INSTANCE = Mappers.getMapper(SysPermissionConverter.class);
/**
* 实体转换为响应 DTO
*
* @param sysPermission 实体
* @return 转换后的数据
*/
SysPermissionRespDto toRespDto(SysPermission sysPermission);
/**
* 实体列表转换为响应 DTO 列表
*
* @param sysPermissions 实体列表
* @return 转换后的数据
*/
List<SysPermissionRespDto> toRespDto(List<SysPermission> sysPermissions);
/**
* 请求 DTO 转换为实体
*
* @param sysPermissionReqDto 请求 DTO
* @return 转换后的数据
*/
SysPermission fromReqDto(SysPermissionReqDto sysPermissionReqDto);
/**
* 请求 DTO 列表转换为实体列表
*
* @param sysPermissionReqDtos 请求 DTO 列表
* @return 转换后的数据
*/
List<SysPermission> fromReqDto(List<SysPermissionReqDto> sysPermissionReqDtos);
/**
* 查询请求 DTO 转换为查询对象
*
* @param sysPermissionQueryReqDto 查询请求DTO
* @return 转换后的数据
*/
SysPermissionQuery fromQueryReqDtoToQuery(SysPermissionQueryReqDto sysPermissionQueryReqDto);
}
@@ -0,0 +1,65 @@
package com.metalloop.modules.auth.converter;
import com.metalloop.modules.auth.model.req.SysRoleQueryReqDto;
import com.metalloop.modules.auth.model.req.SysRoleReqDto;
import com.metalloop.modules.auth.model.resp.SysRoleRespDto;
import com.metalloop.modules.auth.model.po.SysRole;
import com.metalloop.modules.auth.model.query.SysRoleQuery;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 角色转换器
*
* @author yy
* @since 2026-09-10
*/
@SuppressWarnings("UnmappedTargetProperties")
@Mapper(builder = @Builder(disableBuilder = true))
public interface SysRoleConverter {
SysRoleConverter INSTANCE = Mappers.getMapper(SysRoleConverter.class);
/**
* 实体转换为响应 DTO
*
* @param sysRole 实体
* @return 转换后的数据
*/
SysRoleRespDto toRespDto(SysRole sysRole);
/**
* 实体列表转换为响应 DTO 列表
*
* @param sysRoles 实体列表
* @return 转换后的数据
*/
List<SysRoleRespDto> toRespDto(List<SysRole> sysRoles);
/**
* 请求 DTO 转换为实体
*
* @param sysRoleReqDto 请求 DTO
* @return 转换后的数据
*/
SysRole fromReqDto(SysRoleReqDto sysRoleReqDto);
/**
* 请求 DTO 列表转换为实体列表
*
* @param sysRoleReqDtos 请求 DTO 列表
* @return 转换后的数据
*/
List<SysRole> fromReqDto(List<SysRoleReqDto> sysRoleReqDtos);
/**
* 查询请求 DTO 转换为查询对象
*
* @param sysRoleQueryReqDto 查询请求DTO
* @return 转换后的数据
*/
SysRoleQuery fromQueryReqDtoToQuery(SysRoleQueryReqDto sysRoleQueryReqDto);
}
@@ -0,0 +1,65 @@
package com.metalloop.modules.auth.converter;
import com.metalloop.modules.auth.model.req.SysRolePermissionQueryReqDto;
import com.metalloop.modules.auth.model.req.SysRolePermissionReqDto;
import com.metalloop.modules.auth.model.resp.SysRolePermissionRespDto;
import com.metalloop.modules.auth.model.po.SysRolePermission;
import com.metalloop.modules.auth.model.query.SysRolePermissionQuery;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 角色-权限关联转换器
*
* @author yy
* @since 2026-09-10
*/
@SuppressWarnings("UnmappedTargetProperties")
@Mapper(builder = @Builder(disableBuilder = true))
public interface SysRolePermissionConverter {
SysRolePermissionConverter INSTANCE = Mappers.getMapper(SysRolePermissionConverter.class);
/**
* 实体转换为响应 DTO
*
* @param sysRolePermission 实体
* @return 转换后的数据
*/
SysRolePermissionRespDto toRespDto(SysRolePermission sysRolePermission);
/**
* 实体列表转换为响应 DTO 列表
*
* @param sysRolePermissions 实体列表
* @return 转换后的数据
*/
List<SysRolePermissionRespDto> toRespDto(List<SysRolePermission> sysRolePermissions);
/**
* 请求 DTO 转换为实体
*
* @param sysRolePermissionReqDto 请求 DTO
* @return 转换后的数据
*/
SysRolePermission fromReqDto(SysRolePermissionReqDto sysRolePermissionReqDto);
/**
* 请求 DTO 列表转换为实体列表
*
* @param sysRolePermissionReqDtos 请求 DTO 列表
* @return 转换后的数据
*/
List<SysRolePermission> fromReqDto(List<SysRolePermissionReqDto> sysRolePermissionReqDtos);
/**
* 查询请求 DTO 转换为查询对象
*
* @param sysRolePermissionQueryReqDto 查询请求DTO
* @return 转换后的数据
*/
SysRolePermissionQuery fromQueryReqDtoToQuery(SysRolePermissionQueryReqDto sysRolePermissionQueryReqDto);
}
@@ -0,0 +1,65 @@
package com.metalloop.modules.auth.converter;
import com.metalloop.modules.auth.model.req.SysUserQueryReqDto;
import com.metalloop.modules.auth.model.req.SysUserReqDto;
import com.metalloop.modules.auth.model.resp.SysUserRespDto;
import com.metalloop.modules.auth.model.po.SysUser;
import com.metalloop.modules.auth.model.query.SysUserQuery;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 用户转换器
*
* @author yy
* @since 2026-09-10
*/
@SuppressWarnings("UnmappedTargetProperties")
@Mapper(builder = @Builder(disableBuilder = true))
public interface SysUserConverter {
SysUserConverter INSTANCE = Mappers.getMapper(SysUserConverter.class);
/**
* 实体转换为响应 DTO
*
* @param sysUser 实体
* @return 转换后的数据
*/
SysUserRespDto toRespDto(SysUser sysUser);
/**
* 实体列表转换为响应 DTO 列表
*
* @param sysUsers 实体列表
* @return 转换后的数据
*/
List<SysUserRespDto> toRespDto(List<SysUser> sysUsers);
/**
* 请求 DTO 转换为实体
*
* @param sysUserReqDto 请求 DTO
* @return 转换后的数据
*/
SysUser fromReqDto(SysUserReqDto sysUserReqDto);
/**
* 请求 DTO 列表转换为实体列表
*
* @param sysUserReqDtos 请求 DTO 列表
* @return 转换后的数据
*/
List<SysUser> fromReqDto(List<SysUserReqDto> sysUserReqDtos);
/**
* 查询请求 DTO 转换为查询对象
*
* @param sysUserQueryReqDto 查询请求DTO
* @return 转换后的数据
*/
SysUserQuery fromQueryReqDtoToQuery(SysUserQueryReqDto sysUserQueryReqDto);
}
@@ -0,0 +1,65 @@
package com.metalloop.modules.auth.converter;
import com.metalloop.modules.auth.model.req.SysUserRoleQueryReqDto;
import com.metalloop.modules.auth.model.req.SysUserRoleReqDto;
import com.metalloop.modules.auth.model.resp.SysUserRoleRespDto;
import com.metalloop.modules.auth.model.po.SysUserRole;
import com.metalloop.modules.auth.model.query.SysUserRoleQuery;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 用户与角色关联转换器
*
* @author yy
* @since 2026-09-10
*/
@SuppressWarnings("UnmappedTargetProperties")
@Mapper(builder = @Builder(disableBuilder = true))
public interface SysUserRoleConverter {
SysUserRoleConverter INSTANCE = Mappers.getMapper(SysUserRoleConverter.class);
/**
* 实体转换为响应 DTO
*
* @param sysUserRole 实体
* @return 转换后的数据
*/
SysUserRoleRespDto toRespDto(SysUserRole sysUserRole);
/**
* 实体列表转换为响应 DTO 列表
*
* @param sysUserRoles 实体列表
* @return 转换后的数据
*/
List<SysUserRoleRespDto> toRespDto(List<SysUserRole> sysUserRoles);
/**
* 请求 DTO 转换为实体
*
* @param sysUserRoleReqDto 请求 DTO
* @return 转换后的数据
*/
SysUserRole fromReqDto(SysUserRoleReqDto sysUserRoleReqDto);
/**
* 请求 DTO 列表转换为实体列表
*
* @param sysUserRoleReqDtos 请求 DTO 列表
* @return 转换后的数据
*/
List<SysUserRole> fromReqDto(List<SysUserRoleReqDto> sysUserRoleReqDtos);
/**
* 查询请求 DTO 转换为查询对象
*
* @param sysUserRoleQueryReqDto 查询请求DTO
* @return 转换后的数据
*/
SysUserRoleQuery fromQueryReqDtoToQuery(SysUserRoleQueryReqDto sysUserRoleQueryReqDto);
}

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