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
文档版本:v1.0
创建时间:2026-09-09
技术基座:Spring Boot 3.x + Sa-Token + MyBatis-Plus + MySQL 8.0 + Redis 6.0
1. 项目概述
本后端服务于“有色再生资源交易平台”,提供用户认证、权限管理、企业认证、产品目录管理、供应/求购信息发布与审核、即时聊天集成等核心业务 API。系统采用 RBAC(基于角色的访问控制) 权限模型,通过 Sa-Token 实现认证与鉴权,具备高扩展性和可维护性。
1.1 核心业务目标
支持企业注册、认证(营业执照 + 法人信息)、入驻平台;
支持企业发布供应信息与求购信息;
支持管理员审核交易信息与企业认证资料;
支持基于 RBAC 的后台管理权限体系;
支持与 OpenIM 集成,实现买卖双方在线即时沟通;
支持多环境部署(开发、测试、生产)。
2. 技术栈选型
组件 选型 版本 说明
JDK Oracle OpenJDK 17 LTS 长期支持版本,兼容 Spring Boot 3.x
开发框架 Spring Boot 3.2.x 核心基础框架
安全认证 Sa-Token 1.37.x 轻量级权限认证框架,支持 RBAC、分布式会话
ORM 框架 MyBatis-Plus 3.5.x 增强 MyBatis,提供代码生成、分页、逻辑删除等
数据库 MySQL 8.0+ 关系型数据库,支持事务、窗口函数等
缓存 / 会话存储 Redis 6.0+ 用于 Sa-Token 分布式会话、接口防刷、热点数据缓存
API 文档 Knife4j / Swagger 4.x 在线接口调试与文档生成
工具库 Hutool 5.8.x 常用工具类集合(日期、加密、Bean 拷贝等)
JSON 处理 Jackson (Spring Boot 内置) 序列化/反序列化
日志 SLF4J + Logback (Spring Boot 内置) 日志门面 + 实现
连接池 HikariCP (Spring Boot 内置) 高性能数据库连接池
构建工具 Maven 3.6+ 项目构建与依赖管理
3. 系统模块划分(Maven 多模块)
项目采用 Maven 多模块结构,便于后续拆分微服务或模块独立部署。
text
metalloop-backend/
├── pom.xml # 父 POM,统一版本管理
├── metalloop-common/ # 公共模块(无业务依赖)
│ ├── src/main/java/
│ │ └── com.metalloop.common/
│ │ ├── annotation/ # 自定义注解
│ │ ├── constant/ # 常量、枚举类
│ │ ├── exception/ # 自定义异常
│ │ ├── model/ # 统一返回模型(Result、PageResult
│ │ └── utils/ # 工具类(MD5、JWT、DateUtil 等)
│ └── pom.xml
├── metalloop-framework/ # 框架配置模块
│ ├── src/main/java/
│ │ └── com.metalloop.framework/
│ │ ├── config/ # 通用配置(Jackson、线程池、CORS
│ │ ├── satoken/ # Sa-Token 配置、权限拦截器
│ │ ├── mybatis/ # MyBatis-Plus 配置(分页、填充、逻辑删除)
│ │ └── swagger/ # Knife4j 配置
│ └── pom.xml
├── metalloop-dao/ # 数据访问层
│ ├── src/main/java/
│ │ └── com.metalloop.dao/
│ │ ├── entity/ # 数据库实体类(对应每张表)
│ │ ├── mapper/ # MyBatis-Plus Mapper 接口
│ │ ├── dto/ # 数据传输对象(业务层入参/出参)
│ │ └── vo/ # 视图对象(Controller 返回专用)
│ └── pom.xml
├── metalloop-service/ # 业务逻辑层
│ ├── src/main/java/
│ │ └── com.metalloop.service/
│ │ ├── auth/ # 认证服务
│ │ ├── user/ # 用户服务
│ │ ├── enterprise/ # 企业服务(含认证审核)
│ │ ├── product/ # 产品、品种、材质服务
│ │ ├── trade/ # 供应/求购信息服务
│ │ ├── permission/ # 权限与角色服务
│ │ ├── im/ # OpenIM 集成服务
│ │ └── impl/ # 各服务实现类(按模块分包)
│ └── pom.xml
├── metalloop-api/ # API 接口定义(可选,用于 Feign 或微服务)
│ ├── src/main/java/
│ │ └── com.metalloop.api/ # 对外暴露的 API 接口
│ └── pom.xml
└── metalloop-web/ # Web 启动模块
├── src/main/java/
│ └── com.metalloop.web/
│ ├── controller/ # REST Controller(按模块分包)
│ ├── interceptor/ # 全局拦截器
│ ├── filter/ # 过滤器
│ └── advice/ # 全局异常处理器
├── src/main/resources/
│ ├── application.yml # 主配置文件
│ ├── application-dev.yml # 开发环境配置
│ ├── application-prod.yml # 生产环境配置
│ ├── logback-spring.xml # 日志配置
│ └── mapper/ # MyBatis XML 映射文件(可选)
└── pom.xml
4. 包结构设计(单模块方案)
若采用单模块结构,推荐按业务模块分包,清晰隔离各领域:
text
com.metalloop
├── common # 公共组件(可独立为 common 模块)
│ ├── annotation # 自定义注解
│ │ ├── @RequirePermission # 权限校验注解(可选,Sa-Token 自带 @SaCheckPermission
│ │ └── @LogOperation # 操作日志注解
│ ├── constant # 常量与枚举
│ │ ├── ApiCode.java # API 响应码枚举
│ │ ├── AuditStatusEnum.java # 审核状态枚举
│ │ └── UserStatusEnum.java # 用户状态枚举
│ ├── exception # 异常处理
│ │ ├── BusinessException.java # 业务异常
│ │ └── GlobalExceptionHandler.java # 全局异常拦截
│ ├── model # 统一响应模型
│ │ ├── Result.java # 统一返回结构
│ │ └── PageResult.java # 分页返回结构
│ └── utils # 工具类
│ ├── RedisUtil.java # Redis 工具封装
│ ├── SecurityUtil.java # 加密工具(BCrypt、MD5
│ └── BeanCopyUtil.java # 对象拷贝工具
├── framework # 框架集成(可独立为 framework 模块)
│ ├── satoken
│ │ ├── SaTokenConfig.java # Sa-Token 配置类
│ │ ├── StpInterfaceImpl.java # 权限/角色数据源实现
│ │ └── SaTokenInterceptor.java # 全局鉴权拦截器(可选)
│ ├── mybatis
│ │ ├── MybatisPlusConfig.java # 分页、乐观锁配置
│ │ ├── MetaObjectHandler.java # 自动填充(createTime、updateTime
│ │ └── LogicDeleteConfig.java # 逻辑删除配置(若使用)
│ ├── redis
│ │ └── RedisConfig.java # Redis 序列化配置(Jackson2Json
│ └── swagger
│ └── Knife4jConfig.java # API 文档配置
├── modules # 业务模块
│ │
│ ├── auth # 认证模块
│ │ ├── controller
│ │ │ └── AuthController.java # 登录、登出、刷新 Token
│ │ ├── service
│ │ │ ├── AuthService.java
│ │ │ └── impl
│ │ │ └── AuthServiceImpl.java
│ │ ├── model
│ │ │ ├── LoginDTO.java # 登录请求参数
│ │ │ └── LoginVO.java # 登录响应(含 Token)
│ │ └── mapper # (复用 dao 层 Mapper
│ │
│ ├── user # 用户管理模块
│ │ ├── controller
│ │ │ └── UserController.java # 用户 CRUD、禁用/启用
│ │ ├── service
│ │ └── model
│ │
│ ├── enterprise # 企业管理模块
│ │ ├── controller
│ │ │ └── EnterpriseController.java # 企业认证、审核、查询
│ │ ├── service
│ │ ├── model
│ │ │ ├── EnterpriseSubmitDTO.java # 提交认证资料
│ │ │ └── EnterpriseAuditDTO.java # 审核操作
│ │ └── mapper
│ │
│ ├── product # 产品目录模块
│ │ ├── controller
│ │ │ ├── ProductController.java
│ │ │ ├── VarietyController.java
│ │ │ └── MaterialController.java
│ │ ├── service
│ │ └── model
│ │
│ ├── trade # 交易信息模块
│ │ ├── controller
│ │ │ ├── SupplyController.java # 供应信息
│ │ │ └── PurchaseController.java # 求购信息
│ │ ├── service
│ │ └── model
│ │
│ ├── permission # 权限管理模块(后台)
│ │ ├── controller
│ │ │ ├── RoleController.java # 角色 CRUD
│ │ │ └── PermissionController.java # 权限/菜单管理
│ │ ├── service
│ │ └── model
│ │
│ └── im # 即时聊天模块(OpenIM 集成)
│ ├── controller
│ │ └── IMController.java # 获取 IM Token、Webhook 接收
│ ├── service
│ │ ├── IMService.java # OpenIM API 调用封装
│ │ └── WebhookService.java # 处理 OpenIM 回调事件
│ └── model
└── web # Web 层公共配置
├── config
│ └── WebMvcConfig.java # 跨域、静态资源、拦截器注册
├── filter
│ └── RequestLogFilter.java # 请求日志过滤器
└── MainApplication.java # Spring Boot 启动类
5. 数据库表清单
5.1 系统权限模块(sys_*
表名 说明 关键字段
sys_user 用户表 id, username, password, phone, enterprise_id, auth_status, user_status
sys_role 角色表 id, role_name, status, is_system, sort
sys_permission 权限表(菜单 + 按钮) id, name, code, type, parent_id, api_path, route_path
sys_user_role 用户-角色关联表 user_id, role_id
sys_role_permission 角色-权限关联表 role_id, permission_id
5.2 企业模块(sys_enterprise 系列)
注:根据现有数据库,企业表实际前缀为 sys_,与系统模块共用,逻辑上可按业务域区分。
表名 说明 关键字段
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
5.3 业务交易模块(biz_*
表名 说明 关键字段
biz_product 产品基础表 id, product_name, product_code, unit, status
biz_variety 品种表(支持层级) id, variety_name, parent_id, variety_grade
biz_material 材质基础表 id, material_name
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_supply_info 发布信息表(供应) id, create_user_id, title, quantity, unit_price, audit_status, expire_time
有色再生资源交易平台 - 后端项目架构设计文档
项目代号:METALLOOP / metalloop
文档版本:v1.0
创建时间:2026-09-09
技术基座:Spring Boot 3.x + Sa-Token + MyBatis-Plus + MySQL 8.0 + Redis 6.0
1. 项目概述
本后端服务于“有色再生资源交易平台”,提供用户认证、权限管理、企业认证、产品目录管理、供应/求购信息发布与审核、即时聊天集成等核心业务 API。系统采用 RBAC(基于角色的访问控制) 权限模型,通过 Sa-Token 实现认证与鉴权,具备高扩展性和可维护性。
1.1 核心业务目标
支持企业注册、认证(营业执照 + 法人信息)、入驻平台;
支持企业发布供应信息与求购信息;
支持管理员审核交易信息与企业认证资料;
支持基于 RBAC 的后台管理权限体系;
支持与 OpenIM 集成,实现买卖双方在线即时沟通;
支持多环境部署(开发、测试、生产)。
2. 技术栈选型
组件 选型 版本 说明
JDK Oracle OpenJDK 17 LTS 长期支持版本,兼容 Spring Boot 3.x
开发框架 Spring Boot 3.2.x 核心基础框架
安全认证 Sa-Token 1.37.x 轻量级权限认证框架,支持 RBAC、分布式会话
ORM 框架 MyBatis-Plus 3.5.x 增强 MyBatis,提供代码生成、分页、逻辑删除等
数据库 MySQL 8.0+ 关系型数据库,支持事务、窗口函数等
缓存 / 会话存储 Redis 6.0+ 用于 Sa-Token 分布式会话、接口防刷、热点数据缓存
API 文档 Knife4j / Swagger 4.x 在线接口调试与文档生成
工具库 Hutool 5.8.x 常用工具类集合(日期、加密、Bean 拷贝等)
JSON 处理 Jackson (Spring Boot 内置) 序列化/反序列化
日志 SLF4J + Logback (Spring Boot 内置) 日志门面 + 实现
连接池 HikariCP (Spring Boot 内置) 高性能数据库连接池
构建工具 Maven 3.6+ 项目构建与依赖管理
3. 系统模块划分(Maven 多模块)
项目采用 Maven 多模块结构,便于后续拆分微服务或模块独立部署。
text
metalloop-backend/
├── pom.xml # 父 POM,统一版本管理
├── metalloop-common/ # 公共模块(无业务依赖)
│ ├── src/main/java/
│ │ └── com.metalloop.common/
│ │ ├── annotation/ # 自定义注解
│ │ ├── constant/ # 常量、枚举类
│ │ ├── exception/ # 自定义异常
│ │ ├── model/ # 统一返回模型(Result、PageResult
│ │ └── utils/ # 工具类(MD5、JWT、DateUtil 等)
│ └── pom.xml
├── metalloop-framework/ # 框架配置模块
│ ├── src/main/java/
│ │ └── com.metalloop.framework/
│ │ ├── config/ # 通用配置(Jackson、线程池、CORS
│ │ ├── satoken/ # Sa-Token 配置、权限拦截器
│ │ ├── mybatis/ # MyBatis-Plus 配置(分页、填充、逻辑删除)
│ │ └── swagger/ # Knife4j 配置
│ └── pom.xml
├── metalloop-dao/ # 数据访问层
│ ├── src/main/java/
│ │ └── com.metalloop.dao/
│ │ ├── entity/ # 数据库实体类(对应每张表)
│ │ ├── mapper/ # MyBatis-Plus Mapper 接口
│ │ ├── dto/ # 数据传输对象(业务层入参/出参)
│ │ └── vo/ # 视图对象(Controller 返回专用)
│ └── pom.xml
├── metalloop-service/ # 业务逻辑层
│ ├── src/main/java/
│ │ └── com.metalloop.service/
│ │ ├── auth/ # 认证服务
│ │ ├── user/ # 用户服务
│ │ ├── enterprise/ # 企业服务(含认证审核)
│ │ ├── product/ # 产品、品种、材质服务
│ │ ├── trade/ # 供应/求购信息服务
│ │ ├── permission/ # 权限与角色服务
│ │ ├── im/ # OpenIM 集成服务
│ │ └── impl/ # 各服务实现类(按模块分包)
│ └── pom.xml
├── metalloop-api/ # API 接口定义(可选,用于 Feign 或微服务)
│ ├── src/main/java/
│ │ └── com.metalloop.api/ # 对外暴露的 API 接口
│ └── pom.xml
└── metalloop-web/ # Web 启动模块
├── src/main/java/
│ └── com.metalloop.web/
│ ├── controller/ # REST Controller(按模块分包)
│ ├── interceptor/ # 全局拦截器
│ ├── filter/ # 过滤器
│ └── advice/ # 全局异常处理器
├── src/main/resources/
│ ├── application.yml # 主配置文件
│ ├── application-dev.yml # 开发环境配置
│ ├── application-prod.yml # 生产环境配置
│ ├── logback-spring.xml # 日志配置
│ └── mapper/ # MyBatis XML 映射文件(可选)
└── pom.xml
4. 包结构设计(单模块方案)
若采用单模块结构,推荐按业务模块分包,清晰隔离各领域:
text
com.metalloop
├── common # 公共组件(可独立为 common 模块)
│ ├── annotation # 自定义注解
│ │ ├── @RequirePermission # 权限校验注解(可选,Sa-Token 自带 @SaCheckPermission
│ │ └── @LogOperation # 操作日志注解
│ ├── constant # 常量与枚举
│ │ ├── ApiCode.java # API 响应码枚举
│ │ ├── AuditStatusEnum.java # 审核状态枚举
│ │ └── UserStatusEnum.java # 用户状态枚举
│ ├── exception # 异常处理
│ │ ├── BusinessException.java # 业务异常
│ │ └── GlobalExceptionHandler.java # 全局异常拦截
│ ├── model # 统一响应模型
│ │ ├── Result.java # 统一返回结构
│ │ └── PageResult.java # 分页返回结构
│ └── utils # 工具类
│ ├── RedisUtil.java # Redis 工具封装
│ ├── SecurityUtil.java # 加密工具(BCrypt、MD5
│ └── BeanCopyUtil.java # 对象拷贝工具
├── framework # 框架集成(可独立为 framework 模块)
│ ├── satoken
│ │ ├── SaTokenConfig.java # Sa-Token 配置类
│ │ ├── StpInterfaceImpl.java # 权限/角色数据源实现
│ │ └── SaTokenInterceptor.java # 全局鉴权拦截器(可选)
│ ├── mybatis
│ │ ├── MybatisPlusConfig.java # 分页、乐观锁配置
│ │ ├── MetaObjectHandler.java # 自动填充(createTime、updateTime
│ │ └── LogicDeleteConfig.java # 逻辑删除配置(若使用)
│ ├── redis
│ │ └── RedisConfig.java # Redis 序列化配置(Jackson2Json
│ └── swagger
│ └── Knife4jConfig.java # API 文档配置
├── modules # 业务模块
│ │
│ ├── auth # 认证模块
│ │ ├── controller
│ │ │ └── AuthController.java # 登录、登出、刷新 Token
│ │ ├── service
│ │ │ ├── AuthService.java
│ │ │ └── impl
│ │ │ └── AuthServiceImpl.java
│ │ ├── model
│ │ │ ├── LoginDTO.java # 登录请求参数
│ │ │ └── LoginVO.java # 登录响应(含 Token)
│ │ └── mapper # (复用 dao 层 Mapper
│ │
│ ├── user # 用户管理模块
│ │ ├── controller
│ │ │ └── UserController.java # 用户 CRUD、禁用/启用
│ │ ├── service
│ │ └── model
│ │
│ ├── enterprise # 企业管理模块
│ │ ├── controller
│ │ │ └── EnterpriseController.java # 企业认证、审核、查询
│ │ ├── service
│ │ ├── model
│ │ │ ├── EnterpriseSubmitDTO.java # 提交认证资料
│ │ │ └── EnterpriseAuditDTO.java # 审核操作
│ │ └── mapper
│ │
│ ├── product # 产品目录模块
│ │ ├── controller
│ │ │ ├── ProductController.java
│ │ │ ├── VarietyController.java
│ │ │ └── MaterialController.java
│ │ ├── service
│ │ └── model
│ │
│ ├── trade # 交易信息模块
│ │ ├── controller
│ │ │ ├── SupplyController.java # 供应信息
│ │ │ └── PurchaseController.java # 求购信息
│ │ ├── service
│ │ └── model
│ │
│ ├── permission # 权限管理模块(后台)
│ │ ├── controller
│ │ │ ├── RoleController.java # 角色 CRUD
│ │ │ └── PermissionController.java # 权限/菜单管理
│ │ ├── service
│ │ └── model
│ │
│ └── im # 即时聊天模块(OpenIM 集成)
│ ├── controller
│ │ └── IMController.java # 获取 IM Token、Webhook 接收
│ ├── service
│ │ ├── IMService.java # OpenIM API 调用封装
│ │ └── WebhookService.java # 处理 OpenIM 回调事件
│ └── model
└── web # Web 层公共配置
├── config
│ └── WebMvcConfig.java # 跨域、静态资源、拦截器注册
├── filter
│ └── RequestLogFilter.java # 请求日志过滤器
└── MainApplication.java # Spring Boot 启动类
5. 数据库表清单
5.1 系统权限模块(sys_*
表名 说明 关键字段
sys_user 用户表 id, username, password, phone, enterprise_id, auth_status, user_status
sys_role 角色表 id, role_name, status, is_system, sort
sys_permission 权限表(菜单 + 按钮) id, name, code, type, parent_id, api_path, route_path
sys_user_role 用户-角色关联表 user_id, role_id
sys_role_permission 角色-权限关联表 role_id, permission_id
5.2 企业模块(sys_enterprise 系列)
注:根据现有数据库,企业表实际前缀为 sys_,与系统模块共用,逻辑上可按业务域区分。
表名 说明 关键字段
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
5.3 业务交易模块(biz_*
表名 说明 关键字段
biz_product 产品基础表 id, product_name, product_code, unit, status
biz_variety 品种表(支持层级) id, variety_name, parent_id, variety_grade
biz_material 材质基础表 id, material_name
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_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
+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