Initial commit
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Example sysUserDetails template template
|
||||
### Example sysUserDetails template
|
||||
|
||||
# IntelliJ project files
|
||||
.idea
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
target
|
||||
*.log
|
||||
logs
|
||||
.history
|
||||
lib
|
||||
sql
|
||||
|
||||
docker/*/data/
|
||||
docker/minio/config
|
||||
docker/xxljob/logs
|
||||
@@ -0,0 +1,67 @@
|
||||
# AGENTS.md - rnb Backend
|
||||
|
||||
## Build & Run
|
||||
- **Maven profiles**: `dev` (default), `prod`
|
||||
- **Build**: `mvn clean package -DskipTests -Pdev` (or `-Pprod`)
|
||||
- **Run locally**: Execute `RnBApplication.main()` (port 10800)
|
||||
- **Docker**: `docker-compose -f docker/docker-compose.yml -p youlai-boot up -d` (MySQL 3306, Redis 6379, MinIO 9000/9001, XXL-Job 8080)
|
||||
|
||||
## Key Configuration
|
||||
- **Active profile**: Set via `spring.profiles.active` (default: `prod` in application.yml)
|
||||
- **Dev DB**: `localhost:3306/rnb` (user: root, pass: 168soft.xyz)
|
||||
- **Dev Redis**: `localhost:16378` (pass: CZwy16378, db: 2)
|
||||
- **Prod DB**: `localhost:3308/rnb`
|
||||
- **Prod Redis**: `localhost:6379` (no password)
|
||||
- **JWT secret**: Must be ≥32 chars (see `security.session.jwt.secret-key` in profile yml)
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
src/main/java/com/rnb/
|
||||
├── common/ # Shared: annotation, base, constant, enums, exception, model, result, util
|
||||
├── config/ # Auto-configuration, property binding
|
||||
├── core/ # Core: aspect (log, repeat-submit), filter (request-log, rate-limit), handler (data-permission, data-fill), security
|
||||
├── modules/ # Business modules: member, order, product
|
||||
├── shared/ # Shared modules: auth, file, codegen, mail, sms, websocket
|
||||
└── system/ # System module: controller, converter, model (bo/dto/entity/form/query/vo), mapper, service
|
||||
```
|
||||
|
||||
## Code Generation
|
||||
- **MyBatis-Plus Generator**: Run `SystemCodeGenerator.main()` (prompts for table names, uses `sys_` prefix)
|
||||
- **Templates**: `src/test/resources/templates/*.vm` (backend), `codegen.yml` (frontend)
|
||||
- **Output**: `src/main/java/com/rnb/system/...`, `src/main/resources/mapper/...`
|
||||
|
||||
## Testing
|
||||
- **Tests skipped by default**: `maven-surefire-plugin` has `<skip>true</skip>`
|
||||
- **Run single test**: `mvn test -Dtest=ClassName -DfailIfNoTests=false`
|
||||
|
||||
## Common Commands
|
||||
```bash
|
||||
# Build dev
|
||||
mvn clean package -DskipTests -Pdev
|
||||
|
||||
# Build prod
|
||||
mvn clean package -DskipTests -Pprod
|
||||
|
||||
# Run with dev profile (from IDE or)
|
||||
mvn spring-boot:run -Pdev
|
||||
|
||||
# Start infra only
|
||||
cd docker && docker-compose -f docker-compose.yml -p youlai-boot up -d
|
||||
|
||||
# Stop infra
|
||||
cd docker && docker-compose -f docker-compose.yml -p youlai-boot down
|
||||
```
|
||||
|
||||
## Notable Conventions
|
||||
- **Table prefix**: `rb_` (MyBatis-Plus config)
|
||||
- **Logic delete**: field `isDeleted` (1=deleted, 0=active)
|
||||
- **Security modes**: JWT (stateless) or Redis-token (stateful, multi-login control)
|
||||
- **API docs**: Knife4j at `/doc.html` (dev), `/swagger-ui.html` (prod)
|
||||
- **Port**: 10800 (both dev/prod)
|
||||
- **Lombok + MapStruct**: Use `lombok-mapstruct-binding` for compatibility
|
||||
|
||||
## Gotchas
|
||||
- Dockerfile expects `target/youlai-boot.jar` but Maven builds `target/rnb-<version>-dev.jar` → rename or adjust Dockerfile
|
||||
- DB connection validated on startup (app exits if fails)
|
||||
- `application.yml` imports `codegen.yml` for code generator config
|
||||
- Tests require DB/Redis running (use docker-compose)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# 基础镜像
|
||||
FROM openjdk:17-jdk-alpine
|
||||
|
||||
# 维护者信息
|
||||
MAINTAINER youlai <youlaitech@163.com>
|
||||
|
||||
# 设置国内镜像源(中国科技大学镜像源),修改容器时区(alpine镜像需安装tzdata来设置时区),安装字体库(验证码)
|
||||
RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories \
|
||||
&& apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& apk --no-cache add ttf-dejavu fontconfig
|
||||
|
||||
# 在运行时自动挂载 /tmp 目录为匿名卷,提高可移植性。如果 /tmp 目录没有挂载为卷,这些文件会写入容器的可写层,可能导致容器镜像膨胀。
|
||||
VOLUME /tmp
|
||||
|
||||
# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar
|
||||
ADD target/youlai-boot.jar app.jar
|
||||
|
||||
# 指定容器启动时执行的命令
|
||||
CMD java \
|
||||
-Xms512m -Xmx512m \
|
||||
-Djava.security.egd=file:/dev/./urandom \
|
||||
-jar /app.jar
|
||||
|
||||
# 暴露容器的端口
|
||||
EXPOSE 8989
|
||||
@@ -0,0 +1,167 @@
|
||||
|
||||
<div align="center">
|
||||
<img alt="logo" width="100" height="100" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png">
|
||||
<h2>youlai-boot</h2>
|
||||
<img alt="有来技术" src="https://img.shields.io/badge/Java -17-brightgreen.svg"/>
|
||||
<img alt="有来技术" src="https://img.shields.io/badge/SpringBoot-3.3.6-green.svg"/>
|
||||
<a href="https://gitee.com/youlaiorg/youlai-boot" target="_blank">
|
||||
<img alt="有来技术" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
|
||||
</a>
|
||||
<a href="https://github.com/haoxianrui/youlai-boot" target="_blank">
|
||||
<img alt="有来技术" src="https://img.shields.io/github/stars/haoxianrui/youlai-boot.svg?style=social&label=Stars"/>
|
||||
</a>
|
||||
<br/>
|
||||
<img alt="有来技术" src="https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg"/>
|
||||
<a href="https://gitee.com/youlaiorg" target="_blank">
|
||||
<img alt="有来技术" src="https://img.shields.io/badge/Author-有来开源组织-orange.svg"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
<a target="_blank" href="https://vue.youlai.tech/">🖥️ 在线预览</a> | <a target="_blank" href="https://youlai.blog.csdn.net/article/details/145178880">📑 阅读文档</a> | <a target="_blank" href="https://www.youlai.tech/youlai-boot">🌐 官网</a>
|
||||
</div>
|
||||
|
||||
## 📢 项目简介
|
||||
|
||||
基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Vue 3、Element-Plus 构建的前后端分离单体权限管理系统。 [Mybatis-Flex 版本](https://gitee.com/youlaiorg/youlai-boot-flex)
|
||||
|
||||
- **🚀 开发框架**: 使用 Spring Boot 3 和 Vue 3,以及 Element-Plus 等主流技术栈,实时更新。
|
||||
|
||||
- **🔐 安全认证**: 基于 Spring Security 6 原生架构,集成 JWT 令牌自动续期(无状态)和 Redis 会话多端互斥管理(实时强制离线)双重认证机制,构建企业级身份安全中枢。
|
||||
|
||||
- **🔑 权限管理**: 基于 RBAC 模型,实现细粒度的权限控制,涵盖接口方法和按钮级别。
|
||||
|
||||
- **🛠️ 功能模块**: 包括用户管理、角色管理、菜单管理、部门管理、字典管理等功能。
|
||||
|
||||
## 🌈 项目源码
|
||||
|
||||
| 项目类型 | Gitee | Github | GitCode |
|
||||
| --------------| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| ✅ Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot) | [youlai-boot](https://gitcode.com/youlai/youlai-boot) |
|
||||
| vue3 前端 | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) |
|
||||
| uni-app 移动端 | [vue-uniapp-template](https://gitee.com/youlaiorg/vue-uniapp-template) | [vue-uniapp-template](https://github.com/youlaitech/vue-uniapp-template) | [vue-uniapp-template](https://gitcode.com/youlai/vue-uniapp-template) |
|
||||
|
||||
|
||||
|
||||
## 📚 项目文档
|
||||
|
||||
| 文档名称 | 访问地址 |
|
||||
|---------------|-------------------------------------------------------------------------------------------|
|
||||
| 在线接口文档 | [https://www.apifox.cn/apidoc](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) |
|
||||
| 项目介绍与使用指南 | [https://www.youlai.tech/youlai-boot/](https://www.youlai.tech/youlai-boot/) |
|
||||
| 功能详解与操作手册 | [https://youlai.blog.csdn.net/article/details/145178880](https://youlai.blog.csdn.net/article/details/145178880) |
|
||||
| 新手入门指南(项目0到1) | [https://youlai.blog.csdn.net/article/details/145177011](https://youlai.blog.csdn.net/article/details/145177011) |
|
||||
|
||||
|
||||
## 📁 项目目录
|
||||
|
||||
|
||||
<details>
|
||||
<summary> 目录结构 </summary>
|
||||
|
||||
<br>
|
||||
|
||||
```
|
||||
youlai-boot
|
||||
├── docker # Docker 目录
|
||||
│ ├── docker-compose.yml # docker-compose 脚本
|
||||
├── sql # SQL脚本
|
||||
│ ├── mysql # MySQL 脚本
|
||||
├── src # 源码目录
|
||||
│ ├── common # 公共模块
|
||||
│ │ ├── annotation # 注解定义
|
||||
│ │ ├── base # 基础类
|
||||
│ │ ├── constant # 常量
|
||||
│ │ ├── enums # 枚举类型
|
||||
│ │ ├── exception # 异常处理
|
||||
│ │ ├── model # 数据模型
|
||||
│ │ ├── result # 结果封装
|
||||
│ │ └── util # 工具类
|
||||
│ ├── config # 自动装配配置
|
||||
│ │ └── property # 配置属性目录
|
||||
│ ├── core # 核心功能
|
||||
│ │ ├── aspect # 切面(日志、防重提交)
|
||||
│ │ ├── filter # 过滤器(请求日志、限流)
|
||||
│ │ ├── handler # 处理器(数据权限、数据填充)
|
||||
│ │ └── security # Spring Security 安全模块
|
||||
│ ├── modules # 业务模块
|
||||
│ │ ├── member # 会员模块【业务模块演示】
|
||||
│ │ ├── order # 订单模块【业务模块演示】
|
||||
│ │ ├── product # 商品模块【业务模块演示】
|
||||
│ ├── shared # 共享模块
|
||||
│ │ ├── auth # 认证模块
|
||||
│ │ ├── file # 文件模块
|
||||
│ │ ├── codegen # 代码生成模块
|
||||
│ │ ├── mail # 邮件模块
|
||||
│ │ ├── sms # 短信模块
|
||||
│ │ └── websocket # WebSocket 模块
|
||||
│ ├── system # 系统模块
|
||||
│ │ ├── controller # 控制层
|
||||
│ │ ├── converter # MapStruct 转换器
|
||||
│ │ ├── event # 事件处理
|
||||
│ │ ├── handler # 处理器
|
||||
│ │ ├── listener # 监听器
|
||||
│ │ ├── model # 模型层
|
||||
│ │ │ ├── bo # 业务对象
|
||||
│ │ │ ├── dto # 数据传输对象
|
||||
│ │ │ ├── entity # 实体对象
|
||||
│ │ │ ├── form # 表单对象
|
||||
│ │ │ ├── query # 查询参数对象
|
||||
│ │ │ └── vo # 视图对象
|
||||
│ │ ├── mapper # 数据库访问层
|
||||
│ │ └── service # 业务逻辑层
|
||||
│ └── YouLaiBootApplication # 启动类
|
||||
└── end
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
## 🚀 项目启动
|
||||
|
||||
📚 完整流程参考: [项目启动](https://www.youlai.tech/youlai-boot/1.%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8/)
|
||||
|
||||
1. **克隆项目**
|
||||
|
||||
```bash
|
||||
git clone https://gitee.com/youlaiorg/youlai-boot.git
|
||||
```
|
||||
|
||||
2. **数据库初始化**
|
||||
|
||||
执行 [youlai_boot.sql](sql/mysql/youlai_boot.sql) 脚本完成数据库创建、表结构和基础数据的初始化。
|
||||
|
||||
3. **修改配置**
|
||||
|
||||
默认连接`有来`线上 MySQL/Redis(仅读权限),本地开发时请修改 [application-dev.yml](src/main/resources/application-dev.yml) 中的 MySQL 和 Redis 连接信息。
|
||||
|
||||
4. **启动项目**
|
||||
|
||||
执行 [YoulaiBootApplication.java](src/main/java/com/rnb/YoulaiBootApplication.java) 的 main 方法完成后端项目启动;
|
||||
|
||||
访问接口文档地址 [http://localhost:8989/doc.html](http://localhost:8989/doc.html) 验证项目启动是否成功。
|
||||
|
||||
|
||||
## 🚀 项目部署
|
||||
|
||||
参考官方文档: [项目部署指南](https://www.youlai.tech/youlai-boot/5.%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2/)
|
||||
|
||||
|
||||
## ✅ 项目统计
|
||||
|
||||

|
||||
|
||||
Thanks to all the contributors!
|
||||
|
||||
[](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
|
||||
|
||||
|
||||
## 💖 加交流群
|
||||
|
||||
① 关注「有来技术」公众号,点击菜单 **交流群** 获取加群二维码(此举防止广告进群,感谢理解和支持)。
|
||||
|
||||
② 直接添加微信 **`haoxianrui`** 备注「前端/后端/全栈」。
|
||||
|
||||

|
||||
@@ -0,0 +1,69 @@
|
||||
# 创建一个名为 "youlai-boot" 的桥接网络,在同一个网络中的容器可以通过容器名互相访问
|
||||
networks:
|
||||
youlai-boot:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0.29
|
||||
container_name: mysql
|
||||
restart: unless-stopped # 重启策略:除非手动停止容器,否则自动重启
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- LANG= en_US.UTF-8
|
||||
- MYSQL_ROOT_PASSWORD=123456 #设置 root 用户的密码
|
||||
volumes:
|
||||
- ./mysql/conf/my.cnf:/etc/my.cnf # 挂载 my.cnf 文件到容器的指定路径
|
||||
- ./mysql/data:/var/lib/mysql # 持久化 MySQL 数据
|
||||
- ../sql/mysql:/docker-entrypoint-initdb.d # 初始化 SQL 脚本目录
|
||||
ports:
|
||||
- 3306:3306
|
||||
networks:
|
||||
- youlai-boot # 加入 "youlai-boot" 网络
|
||||
|
||||
redis:
|
||||
image: redis:7.2.3
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
command: redis-server /etc/redis/redis.conf --requirepass 123456 --appendonly no # 启动 Redis 服务并添加密码为:123456,默认不开启 Redis AOF 方式持久化配置
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
- ./redis/config/redis.conf:/etc/redis/redis.conf
|
||||
ports:
|
||||
- 6379:6379
|
||||
networks:
|
||||
- youlai-boot
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: minio
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 9001:9001
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- LANG=en_US.UTF-8
|
||||
- MINIO_ROOT_USER=minioadmin
|
||||
- MINIO_ROOT_PASSWORD=minioadmin
|
||||
volumes:
|
||||
- ./minio/data:/data
|
||||
- ./minio/config:/root/.minio
|
||||
networks:
|
||||
- youlai-boot
|
||||
|
||||
xxl-job-admin:
|
||||
image: xuxueli/xxl-job-admin:2.4.0
|
||||
container_name: xxl-job-admin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PARAMS: '--spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=123456 --spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver'
|
||||
volumes:
|
||||
- ./xxljob/logs:/data/applogs
|
||||
ports:
|
||||
- 8080:8080
|
||||
networks:
|
||||
- youlai-boot
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
[mysqld]
|
||||
# 字符集与排序规则
|
||||
character-set-server = utf8mb4 # 服务端默认字符集
|
||||
collation-server = utf8mb4_0900_ai_ci # 服务端默认排序规则
|
||||
|
||||
# 网络与路径
|
||||
datadir = /var/lib/mysql # 数据文件存放的目录
|
||||
bind-address = 0.0.0.0 # 允许远程连接,默认 127.0.0.1 只允许本地连接
|
||||
port = 3306 # 显式指定端口(默认3306可不写)
|
||||
|
||||
# 客户端字符集同步(避免乱码)
|
||||
init_connect = 'SET NAMES utf8mb4' # 连接初始化时设置字符集
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4 # 客户端默认字符集
|
||||
|
||||
[mysql]
|
||||
default-character-set = utf8mb4 # MySQL 命令行工具字符集
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
|
||||
# Docker Compose 安装中间件 MySQL、Redis、Minio、Xxl-Job
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
docker-compose -f ./docker-compose.yml -p youlai-boot up -d
|
||||
```
|
||||
|
||||
- p youlai-boot 指定命名空间,避免与其他容器冲突,这里方便管理,统一管理和卸载
|
||||
|
||||
## 卸载
|
||||
```bash
|
||||
docker-compose -f ./docker-compose.yml -p youlai-boot down
|
||||
```
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.rnb</groupId>
|
||||
<artifactId>rnb</artifactId>
|
||||
<version>1.12</version>
|
||||
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的后台管理系统。</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.6</version> <!-- lookup parent from repository -->
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
|
||||
<hutool.version>5.8.40</hutool.version>
|
||||
|
||||
<mysql-connector-j.version>9.3.0</mysql-connector-j.version>
|
||||
<druid.version>1.2.24</druid.version>
|
||||
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
||||
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
||||
|
||||
<xxl-job.version>3.3.0</xxl-job.version>
|
||||
|
||||
<fastexcel.version>1.1.0</fastexcel.version>
|
||||
|
||||
<!-- 对象存储 -->
|
||||
<minio.version>8.6.0</minio.version>
|
||||
<!-- <okhttp3.version>4.8.1</okhttp3.version>-->
|
||||
|
||||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
||||
|
||||
<!-- redisson 分布式锁 -->
|
||||
<redisson.version>3.40.2</redisson.version>
|
||||
|
||||
<!-- 自动代码生成 -->
|
||||
<mybatis-plus-generator.version>3.5.6</mybatis-plus-generator.version>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
|
||||
<!-- IP 地区转换 -->
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
|
||||
<!-- 阿里云短信 -->
|
||||
<aliyun.java.sdk.core.version>4.6.4</aliyun.java.sdk.core.version>
|
||||
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version>
|
||||
|
||||
<!-- 微信 jdk -->
|
||||
<weixin-java.version>4.5.5.B</weixin-java.version>
|
||||
<caffeine.version>2.9.3</caffeine.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<!--编译测试环境,不打包在lib-->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 允许使用Lombok的Java Bean类中使用MapStruct注解 (Lombok 1.18.20+) -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>${lombok-mapstruct-binding.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>2.0.32</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</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-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>${mysql-connector-j.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- knife4j 接口文档 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct 对象映射 -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- xxl-job 定时任务 -->
|
||||
<dependency>
|
||||
<groupId>com.xuxueli</groupId>
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
<version>${xxl-job.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Excel 工具(EasyExcel-PLus ) -->
|
||||
<dependency>
|
||||
<groupId>cn.idev.excel</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
<version>${fastexcel.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MinIO 对象存储 -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>${minio.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云 OSS 对象存储 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<version>${aliyun-sdk-oss.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- redisson 分布式锁 -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>${redisson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- mybatis-plus 代码生成器 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-generator</artifactId>
|
||||
<version>${mybatis-plus-generator.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- velocity 模板引擎(代码生成) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
<version>${velocity.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- IP 转省市区 -->
|
||||
<dependency>
|
||||
<groupId>org.lionsoul</groupId>
|
||||
<artifactId>ip2region</artifactId>
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-core</artifactId>
|
||||
<version>${aliyun.java.sdk.core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
|
||||
<version>${aliyun.java.sdk.dysmsapi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-miniapp</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 本地缓存 -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>${caffeine.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>dev</id>
|
||||
<properties>
|
||||
<!--自定义的属性-->
|
||||
<spring.profiles.active>dev</spring.profiles.active>
|
||||
</properties>
|
||||
<!-- <activation>-->
|
||||
<!-- <!–如果不指定,则默认使用dev开发环境配置–>-->
|
||||
<!-- <activeByDefault>true</activeByDefault>-->
|
||||
<!-- </activation>-->
|
||||
<build>
|
||||
<finalName>${project.artifactId}-${project.version}-dev</finalName>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>prod</id>
|
||||
<properties>
|
||||
<spring.profiles.active>prod</spring.profiles.active>
|
||||
</properties>
|
||||
<build>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
|
||||
<configuration>
|
||||
<layout>ZIP</layout>
|
||||
<includes>
|
||||
<include>
|
||||
<groupId>nothing</groupId>
|
||||
<artifactId>nothing</artifactId>
|
||||
</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.rnb;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* 应用启动类
|
||||
*
|
||||
* @author Alex.Qu
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan // 开启配置属性绑定
|
||||
@EnableScheduling
|
||||
public class RnBApplication {
|
||||
public static void main(String[] args) {
|
||||
ConfigurableApplicationContext ac = SpringApplication.run(RnBApplication.class, args);
|
||||
try {
|
||||
// ===== 在项目初始化bean后检验数据库连接是否
|
||||
DataSource dataSource = (DataSource) ac.getBean("dataSource");
|
||||
dataSource.getConnection().close();
|
||||
} catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
System.out.println(e.getMessage());
|
||||
// ===== 当检测数据库连接失败时, 停止项目启动
|
||||
System.exit(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.rnb.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限注解
|
||||
*
|
||||
* @author zc
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
public @interface DataPermission {
|
||||
|
||||
/**
|
||||
* 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor}
|
||||
*/
|
||||
String deptAlias() default "";
|
||||
|
||||
String deptIdColumnName() default "dept_id";
|
||||
|
||||
String userAlias() default "";
|
||||
|
||||
String userIdColumnName() default "create_by";
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.rnb.common.annotation;
|
||||
|
||||
import com.rnb.common.enums.LogModuleEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 日志注解
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/6/25
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
public @interface Log {
|
||||
|
||||
/**
|
||||
* 日志描述
|
||||
*
|
||||
* @return 日志描述
|
||||
*/
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* 日志模块
|
||||
*
|
||||
* @return 日志模块
|
||||
*/
|
||||
|
||||
LogModuleEnum module();
|
||||
|
||||
/**
|
||||
* 是否记录请求参数
|
||||
*
|
||||
* @return 是否记录请求参数
|
||||
*/
|
||||
boolean params() default true;
|
||||
|
||||
/**
|
||||
* 是否记录响应结果
|
||||
* <br/>
|
||||
* 响应结果默认不记录,避免日志过大
|
||||
* @return 是否记录响应结果
|
||||
*/
|
||||
boolean result() default false;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.rnb.common.annotation;
|
||||
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 防止重复提交注解
|
||||
* <p>
|
||||
* 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
public @interface RepeatSubmit {
|
||||
|
||||
/**
|
||||
* 锁过期时间(秒)
|
||||
* <p>
|
||||
* 默认5秒内不允许重复提交
|
||||
*/
|
||||
int expire() default 5;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.rnb.common.annotation;
|
||||
|
||||
import com.rnb.core.validator.FieldValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 用于验证字段值是否合法的注解
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.18.0
|
||||
*/
|
||||
@Documented
|
||||
@Constraint(validatedBy = FieldValidator.class)
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidField {
|
||||
|
||||
/**
|
||||
* 验证失败时的错误信息。
|
||||
*/
|
||||
String message() default "非法字段";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
/**
|
||||
* 允许的合法值列表。
|
||||
*/
|
||||
String[] allowedValues();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.rnb.common.base;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 基础实体类
|
||||
*
|
||||
* <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p>
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/6/23
|
||||
*/
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.rnb.common.base;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 基础分页请求对象
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2021/2/28
|
||||
*/
|
||||
@Data
|
||||
@Schema
|
||||
public class BasePageQuery implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "页码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private int pageNum = 1;
|
||||
|
||||
@Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private int pageSize = 10;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.rnb.common.base;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 视图对象基类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/22
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
public class BaseVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.rnb.common.base;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 枚举通用接口
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/3/27 12:06
|
||||
*/
|
||||
public interface IBaseEnum<T> {
|
||||
|
||||
T getValue();
|
||||
|
||||
String getLabel();
|
||||
|
||||
/**
|
||||
* 根据值获取枚举
|
||||
*
|
||||
* @param value
|
||||
* @param clazz
|
||||
* @param <E> 枚举
|
||||
* @return
|
||||
*/
|
||||
static <E extends Enum<E> & IBaseEnum> E getEnumByValue(Object value, Class<E> clazz) {
|
||||
Objects.requireNonNull(value);
|
||||
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
|
||||
E matchEnum = allEnums.stream()
|
||||
.filter(e -> ObjectUtil.equal(e.getValue(), value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
return matchEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文本标签获取值
|
||||
*
|
||||
* @param value
|
||||
* @param clazz
|
||||
* @param <E>
|
||||
* @return
|
||||
*/
|
||||
static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) {
|
||||
Objects.requireNonNull(value);
|
||||
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
|
||||
E matchEnum = allEnums.stream()
|
||||
.filter(e -> ObjectUtil.equal(e.getValue(), value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
String label = null;
|
||||
if (matchEnum != null) {
|
||||
label = matchEnum.getLabel();
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据文本标签获取值
|
||||
*
|
||||
* @param label
|
||||
* @param clazz
|
||||
* @param <E>
|
||||
* @return
|
||||
*/
|
||||
static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) {
|
||||
Objects.requireNonNull(label);
|
||||
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
|
||||
String finalLabel = label;
|
||||
E matchEnum = allEnums.stream()
|
||||
.filter(e -> ObjectUtil.equal(e.getLabel(), finalLabel))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
Object value = null;
|
||||
if (matchEnum != null) {
|
||||
value = matchEnum.getValue();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.rnb.common.constant;
|
||||
|
||||
/**
|
||||
* JWT Claims声明常量
|
||||
* <p>
|
||||
* JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
public interface JwtClaimConstants {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
String USER_ID = "userId";
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
String DEPT_ID = "deptId";
|
||||
|
||||
/**
|
||||
* 数据权限
|
||||
*/
|
||||
String DATA_SCOPE = "dataScope";
|
||||
|
||||
/**
|
||||
* 权限(角色Code)集合
|
||||
*/
|
||||
String AUTHORITIES = "authorities";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.rnb.common.constant;
|
||||
|
||||
/**
|
||||
* Redis 常量
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-7-29 11:46:08
|
||||
*/
|
||||
public interface RedisConstants {
|
||||
|
||||
/**
|
||||
* 限流相关键
|
||||
*/
|
||||
interface RateLimiter {
|
||||
String IP = "rate_limiter:ip:{}"; // IP限流(示例:rate_limiter:ip:192.168.1.1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分布式锁相关键
|
||||
*/
|
||||
interface Lock {
|
||||
String RESUBMIT = "lock:resubmit:{}:{}"; // 防重复提交(示例:lock:resubmit:userIdentifier:requestIdentifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证模块
|
||||
*/
|
||||
interface Auth {
|
||||
// 存储访问令牌对应的用户信息(accessToken -> OnlineUser)
|
||||
String ACCESS_TOKEN_USER = "auth:token:access:{}";
|
||||
// 存储刷新令牌对应的用户信息(refreshToken -> OnlineUser)
|
||||
String REFRESH_TOKEN_USER = "auth:token:refresh:{}";
|
||||
// 用户与访问令牌的映射(userId -> accessToken)
|
||||
String USER_ACCESS_TOKEN = "auth:user:access:{}";
|
||||
// 用户与刷新令牌的映射(userId -> refreshToken
|
||||
String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
|
||||
// 黑名单 Token(用于退出登录或注销)
|
||||
String BLACKLIST_TOKEN = "auth:token:blacklist:{}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码模块
|
||||
*/
|
||||
interface Captcha {
|
||||
String IMAGE_CODE = "captcha:image:{}"; // 图形验证码
|
||||
String SMS_LOGIN_CODE = "captcha:sms_login:{}"; // 登录短信验证码
|
||||
String SMS_REGISTER_CODE = "captcha:sms_register:{}";// 注册短信验证码
|
||||
String MOBILE_CODE = "captcha:mobile:{}"; // 绑定、更换手机验证码
|
||||
String EMAIL_CODE = "captcha:email:{}"; // 邮箱验证码
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统模块
|
||||
*/
|
||||
interface System {
|
||||
String CONFIG = "system:config"; // 系统配置
|
||||
String ROLE_PERMS = "system:role:perms"; // 系统角色和权限映射
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.rnb.common.constant;
|
||||
|
||||
/**
|
||||
* 安全模块常量
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
public interface SecurityConstants {
|
||||
|
||||
/**
|
||||
* 登录路径
|
||||
*/
|
||||
String LOGIN_PATH = "/api/v1/auth/login";
|
||||
|
||||
/**
|
||||
* JWT Token 前缀
|
||||
*/
|
||||
String BEARER_TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
/**
|
||||
* 角色前缀,用于区分 authorities 角色和权限, ROLE_* 角色 、没有前缀的是权限
|
||||
*/
|
||||
String ROLE_PREFIX = "ROLE_";
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.rnb.common.constant;
|
||||
|
||||
/**
|
||||
* 系统常量
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface SystemConstants {
|
||||
|
||||
/**
|
||||
* 根节点ID
|
||||
*/
|
||||
Long ROOT_NODE_ID = 0L;
|
||||
|
||||
/**
|
||||
* 系统默认密码
|
||||
*/
|
||||
String DEFAULT_PASSWORD = "123456";
|
||||
|
||||
/**
|
||||
* 超级管理员角色编码
|
||||
*/
|
||||
String ROOT_ROLE_CODE = "ROOT";
|
||||
|
||||
|
||||
/**
|
||||
* 系统配置 IP的QPS限流的KEY
|
||||
*/
|
||||
String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
|
||||
String SYSTEM_CONFIG_CLIENT_RESP_KEY = "CLIENT_RESP_KEY";
|
||||
int DEFAULT_CLIENT_KEY = 5;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.rnb.common.enums;
|
||||
|
||||
import com.rnb.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据权限枚举
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Getter
|
||||
public enum DataScopeEnum implements IBaseEnum<Integer> {
|
||||
|
||||
/**
|
||||
* value 越小,数据权限范围越大
|
||||
*/
|
||||
ALL(1, "所有数据"),
|
||||
DEPT_AND_SUB(2, "部门及子部门数据"),
|
||||
DEPT(3, "本部门数据"),
|
||||
SELF(4, "本人数据");
|
||||
|
||||
private final Integer value;
|
||||
|
||||
private final String label;
|
||||
|
||||
DataScopeEnum(Integer value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.rnb.common.enums;
|
||||
|
||||
import com.rnb.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 环境枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Getter
|
||||
public enum EnvEnum implements IBaseEnum<String> {
|
||||
|
||||
DEV("dev", "开发环境"),
|
||||
PROD("prod", "生产环境");
|
||||
|
||||
private final String value;
|
||||
|
||||
private final String label;
|
||||
|
||||
EnvEnum(String value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.rnb.common.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 日志模块枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Schema(enumAsRef = true)
|
||||
@Getter
|
||||
public enum LogModuleEnum {
|
||||
|
||||
EXCEPTION("异常"),
|
||||
LOGIN("登录"),
|
||||
USER("用户"),
|
||||
DEPT("部门"),
|
||||
ROLE("角色"),
|
||||
MENU("菜单"),
|
||||
DICT("字典"),
|
||||
SETTING("系统配置"),
|
||||
OTHER("其他");
|
||||
|
||||
@JsonValue
|
||||
private final String moduleName;
|
||||
|
||||
LogModuleEnum(String moduleName) {
|
||||
this.moduleName = moduleName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.rnb.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RequestMethodEnum {
|
||||
/**
|
||||
* 搜寻 @AnonymousGetMapping
|
||||
*/
|
||||
GET("GET"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousPostMapping
|
||||
*/
|
||||
POST("POST"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousPutMapping
|
||||
*/
|
||||
PUT("PUT"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousPatchMapping
|
||||
*/
|
||||
PATCH("PATCH"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousDeleteMapping
|
||||
*/
|
||||
DELETE("DELETE"),
|
||||
|
||||
/**
|
||||
* 否则就是所有 Request 接口都放行
|
||||
*/
|
||||
ALL("All");
|
||||
|
||||
/**
|
||||
* Request 类型
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
public static RequestMethodEnum find(String type) {
|
||||
for (RequestMethodEnum value : RequestMethodEnum.values()) {
|
||||
if (value.getType().equals(type)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return ALL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.rnb.common.enums;
|
||||
|
||||
import com.rnb.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 状态枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/14
|
||||
*/
|
||||
@Getter
|
||||
public enum StatusEnum implements IBaseEnum<Integer> {
|
||||
|
||||
ENABLE(1, "启用"),
|
||||
DISABLE (0, "禁用");
|
||||
|
||||
private final Integer value;
|
||||
|
||||
|
||||
private final String label;
|
||||
|
||||
StatusEnum(Integer value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.rnb.common.exception;
|
||||
|
||||
import com.rnb.common.result.IResultCode;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.helpers.MessageFormatter;
|
||||
|
||||
/**
|
||||
* 自定义业务异常
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2022/7/31
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
public IResultCode resultCode;
|
||||
|
||||
public BusinessException(IResultCode errorCode) {
|
||||
super(errorCode.getMsg());
|
||||
this.resultCode = errorCode;
|
||||
}
|
||||
|
||||
|
||||
public BusinessException(IResultCode errorCode,String message) {
|
||||
super(message);
|
||||
this.resultCode = errorCode;
|
||||
}
|
||||
|
||||
|
||||
public BusinessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public BusinessException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public BusinessException(String message, Object... args) {
|
||||
super(formatMessage(message, args));
|
||||
}
|
||||
|
||||
private static String formatMessage(String message, Object... args) {
|
||||
return MessageFormatter.arrayFormat(message, args).getMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package com.rnb.common.exception;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.rnb.common.result.Result;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.jdbc.BadSqlGrammarException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
|
||||
import java.sql.SQLSyntaxErrorException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局系统异常处理器
|
||||
* <p>
|
||||
* 调整异常处理的HTTP状态码,丰富异常处理类型
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理绑定异常
|
||||
* <p>
|
||||
* 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(BindException e) {
|
||||
log.error("BindException:{}", e.getMessage());
|
||||
String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
|
||||
return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 @RequestParam 参数校验异常
|
||||
* <p>
|
||||
* 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过),
|
||||
* 会捕获到 ConstraintViolationException 异常。
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(ConstraintViolationException e) {
|
||||
log.error("ConstraintViolationException:{}", e.getMessage());
|
||||
String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
|
||||
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方法参数校验异常
|
||||
* <p>
|
||||
* 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败,
|
||||
* 会抛出 MethodArgumentNotValidException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(MethodArgumentNotValidException e) {
|
||||
log.error("MethodArgumentNotValidException:{}", e.getMessage());
|
||||
String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
|
||||
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接口不存在的异常
|
||||
* <p>
|
||||
* 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public <T> Result<T> processException(NoHandlerFoundException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(ResultCode.INTERFACE_NOT_EXIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缺少请求参数的异常
|
||||
* <p>
|
||||
* 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(MissingServletRequestParameterException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方法参数类型不匹配的异常
|
||||
* <p>
|
||||
* 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Servlet 异常
|
||||
* <p>
|
||||
* 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。
|
||||
*/
|
||||
@ExceptionHandler(ServletException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(ServletException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理非法参数异常
|
||||
* <p>
|
||||
* 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 JSON 处理异常
|
||||
* <p>
|
||||
* 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。
|
||||
*/
|
||||
@ExceptionHandler(JsonProcessingException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> handleJsonProcessingException(JsonProcessingException e) {
|
||||
log.error("Json转换异常,异常原因:{}", e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求体不可读的异常
|
||||
* <p>
|
||||
* 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(HttpMessageNotReadableException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
String errorMessage = "请求体不可为空";
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null) {
|
||||
errorMessage = convertMessage(cause);
|
||||
}
|
||||
return Result.failed(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理类型不匹配异常
|
||||
* <p>
|
||||
* 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。
|
||||
*/
|
||||
@ExceptionHandler(TypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(TypeMismatchException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SQL 语法错误异常
|
||||
* <p>
|
||||
* 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BadSqlGrammarException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
String errorMsg = e.getMessage();
|
||||
if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
|
||||
return Result.failed(ResultCode.ACCESS_UNAUTHORIZED);
|
||||
} else {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SQL 语法错误异常
|
||||
* <p>
|
||||
* 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。
|
||||
*/
|
||||
@ExceptionHandler(SQLSyntaxErrorException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
* <p>
|
||||
* 当业务逻辑发生错误时,会抛出 BusinessException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> handleBizException(BusinessException e) {
|
||||
log.error("biz exception", e);
|
||||
if (e.getResultCode() != null) {
|
||||
return Result.failed(e.getResultCode(), e.getMessage());
|
||||
}
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有未捕获的异常
|
||||
* <p>
|
||||
* 当发生未捕获的异常时,会抛出 Exception 异常。
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> handleException(Exception e) throws Exception {
|
||||
// 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
|
||||
if (e instanceof AccessDeniedException
|
||||
|| e instanceof AuthenticationException) {
|
||||
throw e;
|
||||
}
|
||||
log.error("unknown exception", e);
|
||||
return Result.failed(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 传参类型错误时,用于消息转换
|
||||
*
|
||||
* @param throwable 异常
|
||||
* @return 错误信息
|
||||
*/
|
||||
private String convertMessage(Throwable throwable) {
|
||||
String error = throwable.toString();
|
||||
String regulation = "\\[\"(.*?)\"]+";
|
||||
Pattern pattern = Pattern.compile(regulation);
|
||||
Matcher matcher = pattern.matcher(error);
|
||||
String group = "";
|
||||
if (matcher.find()) {
|
||||
String matchString = matcher.group();
|
||||
matchString = matchString.replace("[", "").replace("]", "");
|
||||
matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", ""));
|
||||
group += matchString;
|
||||
}
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rnb.common.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
||||
/**
|
||||
* 键值对
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2024/5/25
|
||||
*/
|
||||
@Schema(description = "键值对")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class KeyValue {
|
||||
|
||||
public KeyValue(String key, String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Schema(description = "选项的值")
|
||||
private String key;
|
||||
|
||||
@Schema(description = "选项的标签")
|
||||
private String value;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.rnb.common.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 下拉选项对象
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/1/22
|
||||
*/
|
||||
@Schema(description ="下拉选项对象")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class Option<T> {
|
||||
|
||||
public Option(T value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Option(T value, String label, List<Option<T>> children) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
this.children= children;
|
||||
}
|
||||
|
||||
public Option(T value, String label, String tag) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
this.tag= tag;
|
||||
}
|
||||
|
||||
|
||||
@Schema(description="选项的值")
|
||||
private T value;
|
||||
|
||||
@Schema(description="选项的标签")
|
||||
private String label;
|
||||
|
||||
@Schema(description = "标签类型")
|
||||
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
|
||||
private String tag;
|
||||
|
||||
@Schema(description="子选项列表")
|
||||
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
|
||||
private List<Option<T>> children;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.rnb.common.result;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Excel导出响应结构体
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2025/1/14 11:46:08
|
||||
*/
|
||||
@Data
|
||||
public class ExcelResult {
|
||||
|
||||
/**
|
||||
* 响应码,来确定是否导入成功
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 有效条数
|
||||
*/
|
||||
private Integer validCount;
|
||||
|
||||
/**
|
||||
* 无效条数
|
||||
*/
|
||||
private Integer invalidCount;
|
||||
|
||||
/**
|
||||
* 错误提示信息
|
||||
*/
|
||||
private List<String> messageList;
|
||||
|
||||
public ExcelResult() {
|
||||
this.code = ResultCode.SUCCESS.getCode();
|
||||
this.validCount = 0;
|
||||
this.invalidCount = 0;
|
||||
this.messageList = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.rnb.common.result;
|
||||
|
||||
/**
|
||||
* 响应码接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 1.0.0
|
||||
**/
|
||||
public interface IResultCode {
|
||||
|
||||
String getCode();
|
||||
|
||||
String getMsg();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.rnb.common.result;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页响应结构体
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2022/2/18
|
||||
*/
|
||||
@Data
|
||||
public class PageResult<T> implements Serializable {
|
||||
|
||||
private String code;
|
||||
|
||||
private Data<T> data;
|
||||
|
||||
private String msg;
|
||||
|
||||
public static <T> PageResult<T> success(IPage<T> page) {
|
||||
PageResult<T> result = new PageResult<>();
|
||||
result.setCode(ResultCode.SUCCESS.getCode());
|
||||
|
||||
Data<T> data = new Data<>();
|
||||
data.setList(page.getRecords());
|
||||
data.setTotal(page.getTotal());
|
||||
|
||||
result.setData(data);
|
||||
result.setMsg(ResultCode.SUCCESS.getMsg());
|
||||
return result;
|
||||
}
|
||||
|
||||
@lombok.Data
|
||||
public static class Data<T> {
|
||||
|
||||
private List<T> list;
|
||||
|
||||
private long total;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.rnb.common.result;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 统一响应结构体
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2022/1/30
|
||||
**/
|
||||
@Data
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
private String code;
|
||||
|
||||
private T data;
|
||||
|
||||
private String msg;
|
||||
|
||||
public static <T> Result<T> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(T data) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(ResultCode.SUCCESS.getCode());
|
||||
result.setMsg(ResultCode.SUCCESS.getMsg());
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed() {
|
||||
return result(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(String msg) {
|
||||
return result(ResultCode.SYSTEM_ERROR.getCode(), msg, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> judge(boolean status) {
|
||||
if (status) {
|
||||
return success();
|
||||
} else {
|
||||
return failed();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(IResultCode resultCode) {
|
||||
return result(resultCode.getCode(), resultCode.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(IResultCode resultCode, String msg) {
|
||||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null);
|
||||
}
|
||||
|
||||
private static <T> Result<T> result(IResultCode resultCode, T data) {
|
||||
return result(resultCode.getCode(), resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
private static <T> Result<T> result(String code, String msg, T data) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(code);
|
||||
result.setData(data);
|
||||
result.setMsg(msg);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean isSuccess(Result<?> result) {
|
||||
return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package com.rnb.common.result;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 响应码枚举
|
||||
* <p>
|
||||
* 参考阿里巴巴开发手册响应码规范
|
||||
* 00000 正常
|
||||
* A**** 用户端错误
|
||||
* B**** 系统执行出错
|
||||
* C**** 调用第三方服务出错
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2020/6/23
|
||||
**/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public enum ResultCode implements IResultCode, Serializable {
|
||||
|
||||
SUCCESS("00000", "一切ok"),
|
||||
|
||||
/** 一级宏观错误码 */
|
||||
USER_ERROR("A0001", "用户端错误"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_REGISTRATION_ERROR("A0100", "用户注册错误"),
|
||||
USER_NOT_AGREE_PRIVACY_AGREEMENT("A0101", "用户未同意隐私协议"),
|
||||
REGISTRATION_COUNTRY_OR_REGION_RESTRICTED("A0102", "注册国家或地区受限"),
|
||||
|
||||
USERNAME_VERIFICATION_FAILED("A0110", "用户名校验失败"),
|
||||
USERNAME_ALREADY_EXISTS("A0111", "用户名已存在"),
|
||||
USERNAME_CONTAINS_SENSITIVE_WORDS("A0112", "用户名包含敏感词"),
|
||||
USERNAME_CONTAINS_SPECIAL_CHARACTERS("A0113", "用户名包含特殊字符"),
|
||||
|
||||
PASSWORD_VERIFICATION_FAILED("A0120", "密码校验失败"),
|
||||
PASSWORD_LENGTH_NOT_ENOUGH("A0121", "密码长度不够"),
|
||||
PASSWORD_STRENGTH_NOT_ENOUGH("A0122", "密码强度不够"),
|
||||
|
||||
VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"),
|
||||
SMS_VERIFICATION_CODE_INPUT_ERROR("A0131", "短信校验码输入错误"),
|
||||
EMAIL_VERIFICATION_CODE_INPUT_ERROR("A0132", "邮件校验码输入错误"),
|
||||
VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"),
|
||||
|
||||
USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"),
|
||||
USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证件类型未选择"),
|
||||
MAINLAND_ID_NUMBER_VERIFICATION_ILLEGAL("A0142", "大陆身份证编号校验非法"),
|
||||
|
||||
USER_BASIC_INFORMATION_VERIFICATION_FAILED("A0150", "用户基本信息校验失败"),
|
||||
PHONE_FORMAT_VERIFICATION_FAILED("A0151", "手机格式校验失败"),
|
||||
ADDRESS_FORMAT_VERIFICATION_FAILED("A0152", "地址格式校验失败"),
|
||||
EMAIL_FORMAT_VERIFICATION_FAILED("A0153", "邮箱格式校验失败"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_LOGIN_EXCEPTION("A0200", "用户登录异常"),
|
||||
USER_ACCOUNT_FROZEN("A0201", "用户账户被冻结"),
|
||||
USER_ACCOUNT_ABOLISHED("A0202", "用户账户已作废"),
|
||||
|
||||
USER_PASSWORD_ERROR("A0210", "用户名或密码错误"),
|
||||
USER_INPUT_PASSWORD_ERROR_LIMIT_EXCEEDED("A0211", "用户输入密码错误次数超限"),
|
||||
USER_NOT_EXIST("A0212", "用户不存在"),
|
||||
|
||||
USER_IDENTITY_VERIFICATION_FAILED("A0220", "用户身份校验失败"),
|
||||
USER_FINGERPRINT_RECOGNITION_FAILED("A0221", "用户指纹识别失败"),
|
||||
USER_FACE_RECOGNITION_FAILED("A0222", "用户面容识别失败"),
|
||||
USER_NOT_AUTHORIZED_THIRD_PARTY_LOGIN("A0223", "用户未获得第三方登录授权"),
|
||||
|
||||
ACCESS_TOKEN_INVALID("A0230", "访问令牌无效或已过期"),
|
||||
REFRESH_TOKEN_INVALID("A0231", "刷新令牌无效或已过期"),
|
||||
|
||||
// 验证码错误
|
||||
USER_VERIFICATION_CODE_ERROR("A0240", "验证码错误"),
|
||||
USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"),
|
||||
USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"),
|
||||
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
|
||||
AUTHORIZATION_IN_PROGRESS("A0302", "正在授权中"),
|
||||
USER_AUTHORIZATION_APPLICATION_REJECTED("A0303", "用户授权申请被拒绝"),
|
||||
|
||||
ACCESS_OBJECT_PRIVACY_SETTINGS_BLOCKED("A0310", "因访问对象隐私设置被拦截"),
|
||||
AUTHORIZATION_EXPIRED("A0311", "授权已过期"),
|
||||
NO_PERMISSION_TO_USE_API("A0312", "无权限使用 API"),
|
||||
|
||||
USER_ACCESS_BLOCKED("A0320", "用户访问被拦截"),
|
||||
BLACKLISTED_USER("A0321", "黑名单用户"),
|
||||
ACCOUNT_FROZEN("A0322", "账号被冻结"),
|
||||
ILLEGAL_IP_ADDRESS("A0323", "非法 IP 地址"),
|
||||
GATEWAY_ACCESS_RESTRICTED("A0324", "网关访问受限"),
|
||||
REGION_BLACKLIST("A0325", "地域黑名单"),
|
||||
|
||||
SERVICE_ARREARS("A0330", "服务已欠费"),
|
||||
|
||||
USER_SIGNATURE_EXCEPTION("A0340", "用户签名异常"),
|
||||
RSA_SIGNATURE_ERROR("A0341", "RSA 签名错误"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_REQUEST_PARAMETER_ERROR("A0400", "用户请求参数错误"),
|
||||
CONTAINS_ILLEGAL_MALICIOUS_REDIRECT_LINK("A0401", "包含非法恶意跳转链接"),
|
||||
INVALID_USER_INPUT("A0402", "无效的用户输入"),
|
||||
|
||||
REQUEST_REQUIRED_PARAMETER_IS_EMPTY("A0410", "请求必填参数为空"),
|
||||
|
||||
REQUEST_PARAMETER_VALUE_EXCEEDS_ALLOWED_RANGE("A0420", "请求参数值超出允许的范围"),
|
||||
PARAMETER_FORMAT_MISMATCH("A0421", "参数格式不匹配"),
|
||||
|
||||
USER_INPUT_CONTENT_ILLEGAL("A0430", "用户输入内容非法"),
|
||||
CONTAINS_PROHIBITED_SENSITIVE_WORDS("A0431", "包含违禁敏感词"),
|
||||
|
||||
USER_OPERATION_EXCEPTION("A0440", "用户操作异常"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_REQUEST_SERVICE_EXCEPTION("A0500", "用户请求服务异常"),
|
||||
REQUEST_LIMIT_EXCEEDED("A0501", "请求次数超出限制"),
|
||||
REQUEST_CONCURRENCY_LIMIT_EXCEEDED("A0502", "请求并发数超出限制"),
|
||||
USER_OPERATION_PLEASE_WAIT("A0503", "用户操作请等待"),
|
||||
WEBSOCKET_CONNECTION_EXCEPTION("A0504", "WebSocket 连接异常"),
|
||||
WEBSOCKET_CONNECTION_DISCONNECTED("A0505", "WebSocket 连接断开"),
|
||||
USER_DUPLICATE_REQUEST("A0506", "请求过于频繁,请稍后再试。"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_RESOURCE_EXCEPTION("A0600", "用户资源异常"),
|
||||
ACCOUNT_BALANCE_INSUFFICIENT("A0601", "账户余额不足"),
|
||||
USER_DISK_SPACE_INSUFFICIENT("A0602", "用户磁盘空间不足"),
|
||||
USER_MEMORY_SPACE_INSUFFICIENT("A0603", "用户内存空间不足"),
|
||||
USER_OSS_CAPACITY_INSUFFICIENT("A0604", "用户 OSS 容量不足"),
|
||||
USER_QUOTA_EXHAUSTED("A0605", "用户配额已用光"),
|
||||
USER_RESOURCE_NOT_FOUND("A0606", "用户资源不存在"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"),
|
||||
UPLOAD_FILE_TYPE_MISMATCH("A0701", "上传文件类型不匹配"),
|
||||
UPLOAD_FILE_TOO_LARGE("A0702", "上传文件太大"),
|
||||
UPLOAD_IMAGE_TOO_LARGE("A0703", "上传图片太大"),
|
||||
UPLOAD_VIDEO_TOO_LARGE("A0704", "上传视频太大"),
|
||||
UPLOAD_COMPRESSED_FILE_TOO_LARGE("A0705", "上传压缩文件太大"),
|
||||
|
||||
DELETE_FILE_EXCEPTION("A0710", "删除文件异常"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_CURRENT_VERSION_EXCEPTION("A0800", "用户当前版本异常"),
|
||||
USER_INSTALLED_VERSION_NOT_MATCH_SYSTEM("A0801", "用户安装版本与系统不匹配"),
|
||||
USER_INSTALLED_VERSION_TOO_LOW("A0802", "用户安装版本过低"),
|
||||
USER_INSTALLED_VERSION_TOO_HIGH("A0803", "用户安装版本过高"),
|
||||
USER_INSTALLED_VERSION_EXPIRED("A0804", "用户安装版本已过期"),
|
||||
USER_API_REQUEST_VERSION_NOT_MATCH("A0805", "用户 API 请求版本不匹配"),
|
||||
USER_API_REQUEST_VERSION_TOO_HIGH("A0806", "用户 API 请求版本过高"),
|
||||
USER_API_REQUEST_VERSION_TOO_LOW("A0807", "用户 API 请求版本过低"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_PRIVACY_NOT_AUTHORIZED("A0900", "用户隐私未授权"),
|
||||
USER_PRIVACY_NOT_SIGNED("A0901", "用户隐私未签署"),
|
||||
USER_CAMERA_NOT_AUTHORIZED("A0903", "用户相机未授权"),
|
||||
USER_PHOTO_LIBRARY_NOT_AUTHORIZED("A0904", "用户图片库未授权"),
|
||||
USER_FILE_NOT_AUTHORIZED("A0905", "用户文件未授权"),
|
||||
USER_LOCATION_INFORMATION_NOT_AUTHORIZED("A0906", "用户位置信息未授权"),
|
||||
USER_CONTACTS_NOT_AUTHORIZED("A0907", "用户通讯录未授权"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_DEVICE_EXCEPTION("A1000", "用户设备异常"),
|
||||
USER_CAMERA_EXCEPTION("A1001", "用户相机异常"),
|
||||
USER_MICROPHONE_EXCEPTION("A1002", "用户麦克风异常"),
|
||||
USER_EARPIECE_EXCEPTION("A1003", "用户听筒异常"),
|
||||
USER_SPEAKER_EXCEPTION("A1004", "用户扬声器异常"),
|
||||
USER_GPS_POSITIONING_EXCEPTION("A1005", "用户 GPS 定位异常"),
|
||||
|
||||
/** 一级宏观错误码 */
|
||||
SYSTEM_ERROR("B0001", "系统执行出错"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
SYSTEM_DISASTER_RECOVERY_FUNCTION_TRIGGERED("B0200", "系统容灾功能被触发"),
|
||||
|
||||
SYSTEM_RATE_LIMITING("B0210", "系统限流"),
|
||||
|
||||
SYSTEM_FUNCTION_DEGRADATION("B0220", "系统功能降级"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
SYSTEM_RESOURCE_EXCEPTION("B0300", "系统资源异常"),
|
||||
SYSTEM_RESOURCE_EXHAUSTED("B0310", "系统资源耗尽"),
|
||||
SYSTEM_DISK_SPACE_EXHAUSTED("B0311", "系统磁盘空间耗尽"),
|
||||
SYSTEM_MEMORY_EXHAUSTED("B0312", "系统内存耗尽"),
|
||||
FILE_HANDLE_EXHAUSTED("B0313", "文件句柄耗尽"),
|
||||
SYSTEM_CONNECTION_POOL_EXHAUSTED("B0314", "系统连接池耗尽"),
|
||||
SYSTEM_THREAD_POOL_EXHAUSTED("B0315", "系统线程池耗尽"),
|
||||
|
||||
SYSTEM_RESOURCE_ACCESS_EXCEPTION("B0320", "系统资源访问异常"),
|
||||
SYSTEM_READ_DISK_FILE_FAILED("B0321", "系统读取磁盘文件失败"),
|
||||
|
||||
|
||||
/** 一级宏观错误码 */
|
||||
THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"),
|
||||
|
||||
RPC_SERVICE_ERROR("C0110", "RPC 服务出错"),
|
||||
RPC_SERVICE_NOT_FOUND("C0111", "RPC 服务未找到"),
|
||||
RPC_SERVICE_NOT_REGISTERED("C0112", "RPC 服务未注册"),
|
||||
INTERFACE_NOT_EXIST("C0113", "接口不存在"),
|
||||
|
||||
MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"),
|
||||
MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"),
|
||||
MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"),
|
||||
MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"),
|
||||
MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"),
|
||||
|
||||
CACHE_SERVICE_ERROR("C0130", "缓存服务出错"),
|
||||
KEY_LENGTH_EXCEEDS_LIMIT("C0131", "key 长度超过限制"),
|
||||
VALUE_LENGTH_EXCEEDS_LIMIT("C0132", "value 长度超过限制"),
|
||||
STORAGE_CAPACITY_FULL("C0133", "存储容量已满"),
|
||||
UNSUPPORTED_DATA_FORMAT("C0134", "不支持的数据格式"),
|
||||
|
||||
CONFIGURATION_SERVICE_ERROR("C0140", "配置服务出错"),
|
||||
|
||||
NETWORK_RESOURCE_SERVICE_ERROR("C0150", "网络资源服务出错"),
|
||||
VPN_SERVICE_ERROR("C0151", "VPN 服务出错"),
|
||||
CDN_SERVICE_ERROR("C0152", "CDN 服务出错"),
|
||||
DOMAIN_NAME_RESOLUTION_SERVICE_ERROR("C0153", "域名解析服务出错"),
|
||||
GATEWAY_SERVICE_ERROR("C0154", "网关服务出错"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
THIRD_PARTY_SYSTEM_EXECUTION_TIMEOUT("C0200", "第三方系统执行超时"),
|
||||
|
||||
RPC_EXECUTION_TIMEOUT("C0210", "RPC 执行超时"),
|
||||
|
||||
MESSAGE_DELIVERY_TIMEOUT("C0220", "消息投递超时"),
|
||||
|
||||
CACHE_SERVICE_TIMEOUT("C0230", "缓存服务超时"),
|
||||
|
||||
CONFIGURATION_SERVICE_TIMEOUT("C0240", "配置服务超时"),
|
||||
|
||||
DATABASE_SERVICE_TIMEOUT("C0250", "数据库服务超时"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"),
|
||||
|
||||
TABLE_NOT_EXIST("C0311", "表不存在"),
|
||||
COLUMN_NOT_EXIST("C0312", "列不存在"),
|
||||
|
||||
MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"),
|
||||
|
||||
DATABASE_DEADLOCK("C0331", "数据库死锁"),
|
||||
|
||||
PRIMARY_KEY_CONFLICT("C0341", "主键冲突"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"),
|
||||
THIRD_PARTY_SYSTEM_RATE_LIMITING("C0401", "第三方系统限流"),
|
||||
THIRD_PARTY_FUNCTION_DEGRADATION("C0402", "第三方功能降级"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
NOTIFICATION_SERVICE_ERROR("C0500", "通知服务出错"),
|
||||
SMS_REMINDER_SERVICE_FAILED("C0501", "短信提醒服务失败"),
|
||||
VOICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"),
|
||||
EMAIL_REMINDER_SERVICE_FAILED("C0503", "邮件提醒服务失败");
|
||||
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
private String code;
|
||||
|
||||
private String msg;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{" +
|
||||
"\"code\":\"" + code + '\"' +
|
||||
", \"msg\":\"" + msg + '\"' +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
public static ResultCode getValue(String code) {
|
||||
for (ResultCode value : values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return SYSTEM_ERROR; // 默认系统执行错误
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
package com.rnb.common.util;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 日期工具类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.4.2
|
||||
*/
|
||||
public class DateUtils {
|
||||
|
||||
/**
|
||||
* 区间日期格式化为数据库日期格式
|
||||
* <p>
|
||||
* eg:2021-01-01 → 2021-01-01 00:00:00
|
||||
*
|
||||
* @param obj 要处理的对象
|
||||
* @param startTimeFieldName 起始时间字段名
|
||||
* @param endTimeFieldName 结束时间字段名
|
||||
*/
|
||||
public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) {
|
||||
Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName);
|
||||
Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName);
|
||||
|
||||
if (startTimeField != null) {
|
||||
processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00");
|
||||
}
|
||||
|
||||
if (endTimeField != null) {
|
||||
processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理日期字段
|
||||
*
|
||||
* @param obj 要处理的对象
|
||||
* @param field 字段
|
||||
* @param fieldName 字段名
|
||||
* @param targetPattern 目标数据库日期格式
|
||||
*/
|
||||
private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) {
|
||||
Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName);
|
||||
if (fieldValue != null) {
|
||||
// 得到原始的日期格式
|
||||
String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd";
|
||||
// 转换为日期对象
|
||||
DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern);
|
||||
// 转换为目标数据库日期格式
|
||||
ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.rnb.common.util;
|
||||
|
||||
import cn.idev.excel.EasyExcel;
|
||||
import cn.idev.excel.event.AnalysisEventListener;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Excel 工具类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/03/01
|
||||
*/
|
||||
public class ExcelUtils {
|
||||
|
||||
public static <T> void importExcel(InputStream is, Class clazz, AnalysisEventListener<T> listener) {
|
||||
EasyExcel.read(is, clazz, listener).sheet().doRead();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.rnb.common.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.lionsoul.ip2region.xdb.Searcher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
/**
|
||||
* IP工具类
|
||||
* <p>
|
||||
* 获取客户端IP地址和IP地址对应的地理位置信息
|
||||
* <p>
|
||||
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
|
||||
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
|
||||
* </p>
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IPUtils {
|
||||
|
||||
private static final String DB_PATH = "/data/ip2region.xdb";
|
||||
private static Searcher searcher;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
// 从类路径加载资源文件
|
||||
InputStream inputStream = getClass().getResourceAsStream(DB_PATH);
|
||||
if (inputStream == null) {
|
||||
throw new FileNotFoundException("Resource not found: " + DB_PATH);
|
||||
}
|
||||
|
||||
// 将资源文件复制到临时文件
|
||||
Path tempDbPath = Files.createTempFile("ip2region", ".xdb");
|
||||
Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// 使用临时文件初始化 Searcher 对象
|
||||
searcher = Searcher.newWithFileOnly(tempDbPath.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("IpRegionUtil initialization ERROR, {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址
|
||||
*
|
||||
* @param request HttpServletRequest对象
|
||||
* @return 客户端IP地址
|
||||
*/
|
||||
public static String getIpAddr(HttpServletRequest request) {
|
||||
String ip = null;
|
||||
try {
|
||||
if (request == null) {
|
||||
return "";
|
||||
}
|
||||
ip = request.getHeader("x-forwarded-for");
|
||||
if (checkIp(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (checkIp(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (checkIp(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (checkIp(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (checkIp(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
|
||||
// 根据网卡取本机配置的IP
|
||||
ip = getLocalAddr();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("IPUtils ERROR, {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 使用代理,则获取第一个IP地址
|
||||
if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) {
|
||||
ip = ip.substring(0, ip.indexOf(","));
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
private static boolean checkIp(String ip) {
|
||||
String unknown = "unknown";
|
||||
return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本机的IP地址
|
||||
*
|
||||
* @return 本机IP地址
|
||||
*/
|
||||
private static String getLocalAddr() {
|
||||
try {
|
||||
return InetAddress.getLocalHost().getHostAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
log.error("InetAddress.getLocalHost()-error, {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IP地址获取地理位置信息
|
||||
*
|
||||
* @param ip IP地址
|
||||
* @return 地理位置信息
|
||||
*/
|
||||
public static String getRegion(String ip) {
|
||||
if (searcher == null) {
|
||||
log.error("Searcher is not initialized");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return searcher.search(ip);
|
||||
} catch (Exception e) {
|
||||
log.error("IpRegionUtil ERROR, {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.rnb.common.util;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.rnb.common.result.Result;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 响应工具类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class ResponseUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 异常消息返回(适用过滤器中处理异常响应)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
*/
|
||||
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
|
||||
int status = getHttpStatus(resultCode);
|
||||
|
||||
response.setStatus(status);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
|
||||
try (PrintWriter writer = response.getWriter()) {
|
||||
String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
|
||||
writer.print(jsonResponse);
|
||||
writer.flush(); // 确保将响应内容写入到输出流
|
||||
} catch (IOException e) {
|
||||
log.error("响应异常处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常消息返回(适用过滤器中处理异常响应)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
*/
|
||||
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) {
|
||||
int status = getHttpStatus(resultCode);
|
||||
|
||||
response.setStatus(status);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
|
||||
try (PrintWriter writer = response.getWriter()) {
|
||||
String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message));
|
||||
writer.print(jsonResponse);
|
||||
writer.flush(); // 确保将响应内容写入到输出流
|
||||
} catch (IOException e) {
|
||||
log.error("响应异常处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据结果码获取HTTP状态码
|
||||
*
|
||||
* @param resultCode 结果码
|
||||
* @return HTTP状态码
|
||||
*/
|
||||
private static int getHttpStatus(ResultCode resultCode) {
|
||||
return switch (resultCode) {
|
||||
case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||
default -> HttpStatus.BAD_REQUEST.value();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* caffeine缓存配置
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2025-01-22 17:40:23
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class CaffeineConfig {
|
||||
|
||||
@Value("${spring.cache.caffeine.spec}")
|
||||
private String caffeineSpec;
|
||||
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @return CacheManager 缓存管理器
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
|
||||
Caffeine<Object, Object> caffeineBuilder = Caffeine.from(caffeineSpec);
|
||||
caffeineCacheManager.setCaffeine(caffeineBuilder);
|
||||
return caffeineCacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import com.rnb.config.property.CaptchaProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 验证码自动装配配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
@Configuration
|
||||
public class CaptchaConfig {
|
||||
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
/**
|
||||
* 验证码文字生成器
|
||||
*
|
||||
* @return CodeGenerator
|
||||
*/
|
||||
@Bean
|
||||
public CodeGenerator codeGenerator() {
|
||||
String codeType = captchaProperties.getCode().getType();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
if ("math".equalsIgnoreCase(codeType)) {
|
||||
return new MathGenerator(codeLength);
|
||||
} else if ("random".equalsIgnoreCase(codeType)) {
|
||||
return new RandomGenerator(codeLength);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码字体
|
||||
*/
|
||||
@Bean
|
||||
public Font captchaFont() {
|
||||
String fontName = captchaProperties.getFont().getName();
|
||||
int fontSize = captchaProperties.getFont().getSize();
|
||||
int fontWight = captchaProperties.getFont().getWeight();
|
||||
return new Font(fontName, fontWight, fontSize);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* CORS 资源共享配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/4/17
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean filterRegistrationBean() {
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
//1.允许任何来源
|
||||
corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
|
||||
//2.允许任何请求头
|
||||
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
|
||||
//3.允许任何方法
|
||||
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
|
||||
//4.允许凭证
|
||||
corsConfiguration.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", corsConfiguration);
|
||||
CorsFilter corsFilter = new CorsFilter(source);
|
||||
|
||||
FilterRegistrationBean<CorsFilter> filterRegistrationBean=new FilterRegistrationBean<>(corsFilter);
|
||||
filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可
|
||||
|
||||
return filterRegistrationBean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import com.rnb.config.property.MailProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* MailConfig 配置类,用于手动配置和注入 JavaMailSender。
|
||||
* 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。
|
||||
* <p>
|
||||
* 手动注入的原因是为了避免在使用 application-dev.yml 或其他非 application.yml 配置文件时,
|
||||
* IDEA 提示无法找到 JavaMailSender 的 bean。
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/8/17
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(MailProperties.class)
|
||||
public class MailConfig {
|
||||
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
public MailConfig(MailProperties mailProperties) {
|
||||
this.mailProperties = mailProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并配置 JavaMailSender bean。
|
||||
*
|
||||
* @return 配置好的 JavaMailSender 实例
|
||||
*/
|
||||
@Bean
|
||||
public JavaMailSender javaMailSender() {
|
||||
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
|
||||
mailSender.setHost(mailProperties.getHost());
|
||||
mailSender.setPort(mailProperties.getPort());
|
||||
mailSender.setUsername(mailProperties.getUsername());
|
||||
mailSender.setPassword(mailProperties.getPassword());
|
||||
|
||||
Properties properties = mailSender.getJavaMailProperties();
|
||||
properties.put("mail.smtp.auth", mailProperties.getProperties().getSmtp().isAuth());
|
||||
properties.put("mail.smtp.starttls.enable", mailProperties.getProperties().getSmtp().getStarttls().isEnable());
|
||||
|
||||
return mailSender;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.rnb.core.handler.MyDataPermissionHandler;
|
||||
import com.rnb.core.handler.MyMetaObjectHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* mybatis-plus 配置类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/7/2
|
||||
*/
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
public class MybatisConfig {
|
||||
|
||||
/**
|
||||
* 分页插件和数据权限插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
//数据权限
|
||||
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
|
||||
//分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充数据库创建人、创建时间、更新人、更新时间
|
||||
*/
|
||||
@Bean
|
||||
public GlobalConfig globalConfig() {
|
||||
GlobalConfig globalConfig = new GlobalConfig();
|
||||
globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
|
||||
return globalConfig;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.rnb.config.property.SecurityProperties;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* OpenAPI 接口文档配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @see <a href="https://doc.xiaominfo.com/docs/quick-start">knife4j 快速开始</a>
|
||||
* @since 2023/2/17
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class OpenApiConfig {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
/**
|
||||
* 接口文档信息
|
||||
*/
|
||||
@Bean
|
||||
public OpenAPI openApi() {
|
||||
|
||||
String appVersion = environment.getProperty("project.version", "1.0.0");
|
||||
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("管理系统 API 文档")
|
||||
.description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。")
|
||||
.version(appVersion)
|
||||
.license(new License()
|
||||
.name("Apache License 2.0")
|
||||
.url("http://www.apache.org/licenses/LICENSE-2.0")
|
||||
)
|
||||
.contact(new Contact()
|
||||
.name("youlai")
|
||||
.email("youlaitech@163.com")
|
||||
.url("https://www.youlai.tech")
|
||||
)
|
||||
)
|
||||
// 配置全局鉴权参数-Authorize
|
||||
.components(new Components()
|
||||
.addSecuritySchemes(HttpHeaders.AUTHORIZATION,
|
||||
new SecurityScheme()
|
||||
.name(HttpHeaders.AUTHORIZATION)
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.scheme("Bearer")
|
||||
.bearerFormat("JWT")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 全局自定义扩展
|
||||
*/
|
||||
@Bean
|
||||
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
|
||||
return openApi -> {
|
||||
// 全局添加Authorization
|
||||
if (openApi.getPaths() != null) {
|
||||
openApi.getPaths().forEach((path, pathItem) -> {
|
||||
|
||||
// 忽略认证的请求无需携带 Authorization
|
||||
String[] ignoreUrls = securityProperties.getIgnoreUrls();
|
||||
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
|
||||
// Ant 匹配忽略的路径,不添加Authorization
|
||||
AntPathMatcher antPathMatcher = new AntPathMatcher();
|
||||
if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 其他接口统一添加Authorization
|
||||
pathItem.readOperations()
|
||||
.forEach(operation ->
|
||||
operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.rnb.config;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/12/3
|
||||
*/
|
||||
@Configuration
|
||||
public class PasswordEncoderConfig {
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 缓存配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2023/12/4
|
||||
*/
|
||||
@EnableCaching
|
||||
@EnableConfigurationProperties(CacheProperties.class)
|
||||
@Configuration
|
||||
@ConditionalOnProperty(name = "spring.cache.enabled") // xxl.job.enabled = true 才会自动装配
|
||||
public class RedisCacheConfig {
|
||||
|
||||
/**
|
||||
* 自定义 RedisCacheManager
|
||||
* <p>
|
||||
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
|
||||
*
|
||||
* @param redisConnectionFactory {@link RedisConnectionFactory}
|
||||
* @param cacheProperties {@link CacheProperties}
|
||||
* @return {@link RedisCacheManager}
|
||||
*/
|
||||
@Bean
|
||||
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties){
|
||||
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
|
||||
.cacheDefaults(redisCacheConfiguration(cacheProperties))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 RedisCacheConfiguration
|
||||
*
|
||||
* @param cacheProperties {@link CacheProperties}
|
||||
* @return {@link RedisCacheConfiguration}
|
||||
*/
|
||||
@Bean
|
||||
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
|
||||
|
||||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
|
||||
|
||||
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()));
|
||||
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
|
||||
|
||||
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
|
||||
|
||||
if (redisProperties.getTimeToLive() != null) {
|
||||
config = config.entryTtl(redisProperties.getTimeToLive());
|
||||
}
|
||||
if (!redisProperties.isCacheNullValues()) {
|
||||
config = config.disableCachingNullValues();
|
||||
}
|
||||
if (!redisProperties.isUseKeyPrefix()) {
|
||||
config = config.disableKeyPrefix();
|
||||
}
|
||||
// 覆盖默认key双冒号 CacheKeyPrefix#prefixed
|
||||
config = config.computePrefixWith(name -> name + ":");
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2023/5/15
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 自定义 RedisTemplate
|
||||
* <p>
|
||||
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
|
||||
*
|
||||
* @param redisConnectionFactory {@link RedisConnectionFactory}
|
||||
* @return {@link RedisTemplate}
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
|
||||
redisTemplate.setKeySerializer(RedisSerializer.string());
|
||||
redisTemplate.setValueSerializer(RedisSerializer.json());
|
||||
|
||||
redisTemplate.setHashKeySerializer(RedisSerializer.string());
|
||||
redisTemplate.setHashValueSerializer(RedisSerializer.json());
|
||||
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory){
|
||||
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
|
||||
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
|
||||
return redisMessageListenerContainer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.rnb.config.property.SecurityProperties;
|
||||
import com.rnb.core.filter.RateLimiterFilter;
|
||||
import com.rnb.core.security.exception.MyAccessDeniedHandler;
|
||||
import com.rnb.core.security.exception.MyAuthenticationEntryPoint;
|
||||
import com.rnb.core.security.extension.sms.SmsAuthenticationProvider;
|
||||
import com.rnb.core.security.extension.wechat.WechatAuthenticationProvider;
|
||||
import com.rnb.core.security.filter.CaptchaValidationFilter;
|
||||
import com.rnb.core.security.filter.TokenAuthenticationFilter;
|
||||
import com.rnb.core.security.token.TokenManager;
|
||||
import com.rnb.core.security.service.SysUserDetailsService;
|
||||
import com.rnb.system.service.ConfigService;
|
||||
import com.rnb.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* Spring Security 配置类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2023/2/17
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
private final WxMaService wxMaService;
|
||||
private final UserService userService;
|
||||
private final SysUserDetailsService userDetailsService;
|
||||
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final ConfigService configService;
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
/**
|
||||
* 配置安全过滤链 SecurityFilterChain
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
return http
|
||||
.authorizeHttpRequests(requestMatcherRegistry -> {
|
||||
// 配置无需登录即可访问的公开接口
|
||||
String[] ignoreUrls = securityProperties.getIgnoreUrls();
|
||||
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
|
||||
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
|
||||
}
|
||||
// 其他所有请求需登录后访问
|
||||
requestMatcherRegistry.anyRequest().authenticated();
|
||||
}
|
||||
)
|
||||
.exceptionHandling(configurer ->
|
||||
configurer
|
||||
.authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证异常处理器
|
||||
.accessDeniedHandler(new MyAccessDeniedHandler()) // 无权限访问异常处理器
|
||||
)
|
||||
|
||||
// 禁用默认的 Spring Security 特性,适用于前后端分离架构
|
||||
.sessionManagement(configurer ->
|
||||
configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态认证,不使用 Session
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF 防护,前后端分离无需此防护机制
|
||||
.formLogin(AbstractHttpConfigurer::disable) // 禁用默认的表单登录功能,前后端分离采用 Token 认证方式
|
||||
.httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录
|
||||
// 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
|
||||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||
// 限流过滤器
|
||||
.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证码校验过滤器
|
||||
.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证和解析过滤器
|
||||
.addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置Web安全自定义器,以忽略特定请求路径的安全性检查。
|
||||
* <p>
|
||||
* 该配置用于指定哪些请求路径不经过Spring Security过滤器链。通常用于静态资源文件。
|
||||
*/
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> {
|
||||
String[] unsecuredUrls = securityProperties.getUnsecuredUrls();
|
||||
if (ArrayUtil.isNotEmpty(unsecuredUrls)) {
|
||||
web.ignoring().requestMatchers(unsecuredUrls);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认密码认证的 Provider
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
|
||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
|
||||
return daoAuthenticationProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信认证 Provider
|
||||
*/
|
||||
@Bean
|
||||
public WechatAuthenticationProvider weChatAuthenticationProvider() {
|
||||
return new WechatAuthenticationProvider(userService, wxMaService);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
*/
|
||||
@Bean
|
||||
public SmsAuthenticationProvider smsAuthenticationProvider() {
|
||||
return new SmsAuthenticationProvider(userService, redisTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
DaoAuthenticationProvider daoAuthenticationProvider,
|
||||
WechatAuthenticationProvider weChatAuthenticationProvider,
|
||||
SmsAuthenticationProvider smsAuthenticationProvider
|
||||
) {
|
||||
return new ProviderManager(
|
||||
daoAuthenticationProvider,
|
||||
weChatAuthenticationProvider,
|
||||
smsAuthenticationProvider
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.hibernate.validator.HibernateValidator;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Web 配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2020/10/16
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 配置消息转换器
|
||||
*
|
||||
* @param converters 消息转换器列表
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer)
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
// 返回指定字符串格式
|
||||
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
|
||||
// 反序列化,接受前端传来的格式
|
||||
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
|
||||
objectMapper.registerModule(javaTimeModule);
|
||||
|
||||
// 配置全局日期格式和时区
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
|
||||
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
||||
|
||||
// 处理 Long/BigInteger 的精度问题
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
|
||||
objectMapper.registerModule(simpleModule);
|
||||
|
||||
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
|
||||
converters.add(1, jackson2HttpMessageConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置校验器
|
||||
*
|
||||
* @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory
|
||||
* @return Validator 实例
|
||||
*/
|
||||
@Bean
|
||||
public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
|
||||
try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
|
||||
.configure()
|
||||
.failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数
|
||||
.constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
|
||||
.buildValidatorFactory()) {
|
||||
|
||||
// 使用 try-with-resources 确保 ValidatorFactory 被正确关闭
|
||||
return validatorFactory.getValidator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.core.security.model.SysUserDetails;
|
||||
import com.rnb.core.security.token.TokenManager;
|
||||
import com.rnb.system.service.WebSocketService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@EnableWebSocketMessageBroker
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
private final WebSocketService webSocketService;
|
||||
|
||||
public WebSocketConfig(TokenManager tokenManager, @Lazy WebSocketService webSocketService) {
|
||||
this.tokenManager = tokenManager;
|
||||
this.webSocketService = webSocketService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个端点,客户端通过这个端点进行连接
|
||||
*/
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
// 注册 /ws 的端点
|
||||
.addEndpoint("/ws")
|
||||
// 允许跨域
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 配置消息代理
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 客户端发送消息的请求前缀
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
|
||||
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
|
||||
// 服务端通知客户端的前缀,可以不设置,默认为user
|
||||
registry.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 配置客户端入站通道拦截器
|
||||
* <p>
|
||||
* 核心功能:
|
||||
* 1. 连接建立时解析令牌并绑定用户身份
|
||||
* 2. 连接关闭时触发下线通知
|
||||
* 3. 异常Token的防御性处理
|
||||
*/
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.interceptors(new ChannelInterceptor() {
|
||||
@Override
|
||||
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
if (accessor == null) {
|
||||
return ChannelInterceptor.super.preSend(message, channel);
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理客户端连接请求
|
||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
/*
|
||||
* 安全校验流程:
|
||||
* 1. 从HEADER中获取Authorization值
|
||||
* 2. 校验Bearer Token格式合法性
|
||||
* 3. 解析并验证JWT有效性
|
||||
* 4. 绑定用户身份到当前会话
|
||||
*/
|
||||
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
// 防御性校验:确保Authorization头存在且格式正确
|
||||
if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) {
|
||||
log.warn("非法连接请求:缺少有效的Authorization头");
|
||||
throw new AuthenticationCredentialsNotFoundException("Missing authorization header");
|
||||
}
|
||||
|
||||
// 提取并处理JWT令牌(移除Bearer前缀)
|
||||
String token = authorization.substring(7);
|
||||
Authentication authentication = tokenManager.parseToken(token);
|
||||
|
||||
// 令牌解析失败处理
|
||||
if (authentication == null) {
|
||||
log.error("令牌解析失败:{}", token);
|
||||
throw new BadCredentialsException("Invalid token");
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||
if (userDetails == null || StrUtil.isBlank(userDetails.getUsername())) {
|
||||
log.error("无效的用户凭证:{}", token);
|
||||
throw new BadCredentialsException("Invalid user credentials");
|
||||
}
|
||||
|
||||
String username = userDetails.getUsername();
|
||||
log.info("WebSocket连接建立:用户[{}]", username);
|
||||
|
||||
// 绑定用户身份到当前会话(重要:用于@SendToUser等注解)
|
||||
accessor.setUser(authentication);
|
||||
|
||||
// 记录用户上线状态
|
||||
webSocketService.userConnected(username, accessor.getSessionId());
|
||||
|
||||
}
|
||||
// 处理客户端断开请求
|
||||
else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
|
||||
/*
|
||||
* 注意:只有成功建立过认证的连接才会触发下线事件
|
||||
* 防止未认证成功的连接产生脏数据
|
||||
*/
|
||||
Authentication authentication = (Authentication) accessor.getUser();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
String username = ((SysUserDetails) authentication.getPrincipal()).getUsername();
|
||||
log.info("WebSocket连接关闭:用户[{}]", username);
|
||||
|
||||
// 记录用户下线状态
|
||||
webSocketService.userDisconnected(username);
|
||||
}
|
||||
}
|
||||
} catch (AuthenticationException ex) {
|
||||
// 认证失败时强制关闭连接
|
||||
log.error("连接认证失败:{}", ex.getMessage());
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
// 捕获其他未知异常
|
||||
log.error("WebSocket连接处理异常:", ex);
|
||||
throw new MessagingException("Connection processing failed");
|
||||
}
|
||||
|
||||
return ChannelInterceptor.super.preSend(message, channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
|
||||
import cn.binarywang.wx.miniapp.config.WxMaConfig;
|
||||
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 配置微信 appId 和 appSecret
|
||||
*
|
||||
* @author wangtao
|
||||
* @since 2024/11/26 17:28
|
||||
*/
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "wx.miniapp")
|
||||
@Configuration
|
||||
public class WxMiniAppConfig {
|
||||
|
||||
private String appId;
|
||||
|
||||
private String appSecret;
|
||||
|
||||
@Bean
|
||||
public WxMaConfig wxMaConfig() {
|
||||
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
|
||||
config.setAppid(appId);
|
||||
config.setSecret(appSecret);
|
||||
return config;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WxMaService wxMaService(WxMaConfig wxMaConfig) {
|
||||
WxMaService service = new WxMaServiceImpl();
|
||||
service.setWxMaConfig(wxMaConfig);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.rnb.config;
|
||||
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* xxl-job config
|
||||
*
|
||||
* @author xuxueli 2017-04-28
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(name = "xxl.job.enabled") // xxl.job.enabled = true 才会自动装配
|
||||
@Slf4j
|
||||
public class XxlJobConfig {
|
||||
|
||||
@Value("${xxl.job.admin.addresses}")
|
||||
private String adminAddresses;
|
||||
|
||||
@Value("${xxl.job.accessToken}")
|
||||
private String accessToken;
|
||||
|
||||
@Value("${xxl.job.executor.appname}")
|
||||
private String appname;
|
||||
|
||||
@Value("${xxl.job.executor.address}")
|
||||
private String address;
|
||||
|
||||
@Value("${xxl.job.executor.ip}")
|
||||
private String ip;
|
||||
|
||||
@Value("${xxl.job.executor.port}")
|
||||
private int port;
|
||||
|
||||
@Value("${xxl.job.executor.logpath}")
|
||||
private String logPath;
|
||||
|
||||
@Value("${xxl.job.executor.logretentiondays}")
|
||||
private int logRetentionDays;
|
||||
|
||||
|
||||
@Bean
|
||||
public XxlJobSpringExecutor xxlJobExecutor() {
|
||||
log.info(">>>>>>>>>>> xxl-job config init.");
|
||||
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
|
||||
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
|
||||
xxlJobSpringExecutor.setAppname(appname);
|
||||
xxlJobSpringExecutor.setAddress(address);
|
||||
xxlJobSpringExecutor.setIp(ip);
|
||||
xxlJobSpringExecutor.setPort(port);
|
||||
xxlJobSpringExecutor.setAccessToken(accessToken);
|
||||
xxlJobSpringExecutor.setLogPath(logPath);
|
||||
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
|
||||
|
||||
return xxlJobSpringExecutor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.rnb.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 阿里云短信配置
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/8/17
|
||||
*/
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "sms.aliyun")
|
||||
@Data
|
||||
public class AliyunSmsProperties {
|
||||
|
||||
/**
|
||||
* 阿里云账户的Access Key ID,用于API请求认证
|
||||
*/
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
*阿里云账户的Access Key Secret,用于API请求认证
|
||||
*/
|
||||
private String accessKeySecret;
|
||||
|
||||
/**
|
||||
* 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 阿里云服务的区域ID,如cn-shanghai
|
||||
*/
|
||||
private String regionId;
|
||||
|
||||
/**
|
||||
* 短信签名,必须是已经在阿里云短信服务中注册并通过审核的
|
||||
*/
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* 短信模板集合
|
||||
*/
|
||||
private Map<String, String> templates;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.rnb.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 验证码 属性配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "captcha")
|
||||
@Data
|
||||
public class CaptchaProperties {
|
||||
|
||||
/**
|
||||
* 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 验证码图片宽度
|
||||
*/
|
||||
private int width;
|
||||
/**
|
||||
* 验证码图片高度
|
||||
*/
|
||||
private int height;
|
||||
|
||||
/**
|
||||
* 干扰线数量
|
||||
*/
|
||||
private int interfereCount;
|
||||
|
||||
/**
|
||||
* 文本透明度
|
||||
*/
|
||||
private Float textAlpha;
|
||||
|
||||
/**
|
||||
* 验证码过期时间,单位:秒
|
||||
*/
|
||||
private Long expireSeconds;
|
||||
|
||||
/**
|
||||
* 验证码字符配置
|
||||
*/
|
||||
private CodeProperties code;
|
||||
|
||||
/**
|
||||
* 验证码字体
|
||||
*/
|
||||
private FontProperties font;
|
||||
|
||||
/**
|
||||
* 验证码字符配置
|
||||
*/
|
||||
@Data
|
||||
public static class CodeProperties {
|
||||
/**
|
||||
* 验证码字符类型 math-算术|random-随机字符串
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数
|
||||
*/
|
||||
private int length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码字体配置
|
||||
*/
|
||||
@Data
|
||||
public static class FontProperties {
|
||||
/**
|
||||
* 字体名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 字体样式 0-普通|1-粗体|2-斜体
|
||||
*/
|
||||
private int weight;
|
||||
/**
|
||||
* 字体大小
|
||||
*/
|
||||
private int size;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.rnb.config.property;
|
||||
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 代码生成配置属性
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.11.0
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "codegen")
|
||||
@Data
|
||||
public class CodegenProperties {
|
||||
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
private DefaultConfig defaultConfig ;
|
||||
|
||||
/**
|
||||
* 模板配置
|
||||
*/
|
||||
private Map<String, TemplateConfig> templateConfigs = MapUtil.newHashMap(true);
|
||||
|
||||
/**
|
||||
* 后端应用名
|
||||
*/
|
||||
private String backendAppName;
|
||||
|
||||
/**
|
||||
* 前端应用名
|
||||
*/
|
||||
private String frontendAppName;
|
||||
|
||||
/**
|
||||
* 下载文件名
|
||||
*/
|
||||
private String downloadFileName;
|
||||
|
||||
/**
|
||||
* 排除数据表
|
||||
*/
|
||||
private List<String> excludeTables;
|
||||
|
||||
/**
|
||||
* 模板配置
|
||||
*/
|
||||
@Data
|
||||
public static class TemplateConfig {
|
||||
|
||||
/**
|
||||
* 模板路径 (e.g. /templates/codegen/controller.java.vm)
|
||||
*/
|
||||
private String templatePath;
|
||||
|
||||
/**
|
||||
* 子包名 (e.g. controller/service/mapper/model)
|
||||
*/
|
||||
private String subpackageName;
|
||||
|
||||
/**
|
||||
* 文件扩展名,如 .java
|
||||
*/
|
||||
private String extension = FileNameUtil.EXT_JAVA;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
@Data
|
||||
public static class DefaultConfig {
|
||||
|
||||
/**
|
||||
* 作者 (e.g. Ray)
|
||||
*/
|
||||
private String author;
|
||||
|
||||
/**
|
||||
* 默认模块名(e.g. system)
|
||||
*/
|
||||
private String moduleName;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.rnb.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 邮件配置类,用于接收和存储邮件相关的配置属性。
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/8/17
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.mail")
|
||||
@Data
|
||||
public class MailProperties {
|
||||
|
||||
/**
|
||||
* 邮件服务器主机名或 IP 地址。
|
||||
* 例如:smtp.example.com
|
||||
*/
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* 邮件服务器端口号。
|
||||
* 例如:587
|
||||
*/
|
||||
private int port;
|
||||
|
||||
/**
|
||||
* 用于连接邮件服务器的用户名。
|
||||
* 例如:your_email@example.com
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用于连接邮件服务器的密码。
|
||||
* 该密码应安全存储,不应在代码中硬编码。
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 邮件发送者地址。
|
||||
*/
|
||||
private String from;
|
||||
|
||||
/**
|
||||
* 邮件服务器的其他属性配置。
|
||||
* 这些配置通常用于进一步定制邮件发送行为。
|
||||
*/
|
||||
private Properties properties = new Properties();
|
||||
|
||||
/**
|
||||
* 内部类,用于封装邮件服务器的详细配置。
|
||||
* 包含 SMTP 相关的配置选项。
|
||||
*/
|
||||
@Data
|
||||
public static class Properties {
|
||||
|
||||
/**
|
||||
* SMTP 配置选项类。
|
||||
* 包含认证、加密等与 SMTP 协议相关的配置。
|
||||
*/
|
||||
private Smtp smtp = new Smtp();
|
||||
|
||||
@Data
|
||||
public static class Smtp {
|
||||
|
||||
/**
|
||||
* 是否启用 SMTP 认证。
|
||||
* 如果为 `true`,则需要提供有效的用户名和密码进行认证。
|
||||
*/
|
||||
private boolean auth;
|
||||
|
||||
/**
|
||||
* STARTTLS 加密配置选项。
|
||||
*/
|
||||
private StartTls starttls = new StartTls();
|
||||
|
||||
@Data
|
||||
public static class StartTls {
|
||||
|
||||
/**
|
||||
* 是否启用 STARTTLS 加密。
|
||||
* 如果为 `true`,在发送邮件时将启用 STARTTLS 协议进行加密传输。
|
||||
*/
|
||||
private boolean enable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.rnb.config.property;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* 安全模块配置属性类
|
||||
*
|
||||
* <p>映射 application.yml 中 security 前缀的安全相关配置</p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/4/18
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "security")
|
||||
public class SecurityProperties {
|
||||
|
||||
/**
|
||||
* 会话管理配置
|
||||
*/
|
||||
private SessionConfig session;
|
||||
|
||||
/**
|
||||
* 安全白名单路径(完全绕过安全过滤器)
|
||||
* <p>示例值:/api/v1/auth/login/**, /ws/**
|
||||
*/
|
||||
@NotEmpty
|
||||
private String[] ignoreUrls;
|
||||
|
||||
/**
|
||||
* 非安全端点路径(允许匿名访问的API)
|
||||
* <p>示例值:/doc.html, /v3/api-docs/**
|
||||
*/
|
||||
@NotEmpty
|
||||
private String[] unsecuredUrls;
|
||||
|
||||
/**
|
||||
* 会话配置嵌套类
|
||||
*/
|
||||
@Data
|
||||
public static class SessionConfig {
|
||||
/**
|
||||
* 认证策略类型
|
||||
* <ul>
|
||||
* <li>jwt - 基于JWT的无状态认证</li>
|
||||
* <li>redis-token - 基于Redis的有状态认证</li>
|
||||
* </ul>
|
||||
*/
|
||||
@NotNull
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 访问令牌有效期(单位:秒)
|
||||
* <p>默认值:3600(1小时)</p>
|
||||
* <p>-1 表示永不过期</p>
|
||||
*/
|
||||
@Min(-1)
|
||||
private Integer accessTokenTimeToLive = 3600;
|
||||
|
||||
/**
|
||||
* 刷新令牌有效期(单位:秒)
|
||||
* <p>默认值:604800(7天)</p>
|
||||
* <p>-1 表示永不过期</p>
|
||||
*/
|
||||
@Min(-1)
|
||||
private Integer refreshTokenTimeToLive = 604800;
|
||||
|
||||
/**
|
||||
* JWT 配置项
|
||||
*/
|
||||
private JwtConfig jwt;
|
||||
|
||||
/**
|
||||
* Redis令牌配置项
|
||||
*/
|
||||
private RedisTokenConfig redisToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 配置嵌套类
|
||||
*/
|
||||
@Data
|
||||
public static class JwtConfig {
|
||||
/**
|
||||
* JWT签名密钥
|
||||
* <p>HS256算法要求至少32个字符</p>
|
||||
* <p>示例:SecretKey012345678901234567890123456789</p>
|
||||
*/
|
||||
@NotNull
|
||||
private String secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis令牌配置嵌套类
|
||||
*/
|
||||
@Data
|
||||
public static class RedisTokenConfig {
|
||||
/**
|
||||
* 是否允许多设备同时登录
|
||||
* <p>true - 允许同一账户多设备登录(默认)</p>
|
||||
* <p>false - 新登录会使旧令牌失效</p>
|
||||
*/
|
||||
private Boolean allowMultiLogin = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.rnb.core.aspect;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.date.TimeInterval;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyun.oss.HttpMethod;
|
||||
import com.rnb.common.enums.LogModuleEnum;
|
||||
import com.rnb.common.util.IPUtils;
|
||||
import com.rnb.core.security.util.SecurityUtils;
|
||||
import com.rnb.system.model.entity.Log;
|
||||
import com.rnb.system.service.LogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 日志切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/6/25
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LogAspect {
|
||||
private final LogService logService;
|
||||
private final HttpServletRequest request;
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* 切点
|
||||
*/
|
||||
@Pointcut("@annotation(com.rnb.common.annotation.Log)")
|
||||
public void logPointcut() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理完请求后执行
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@Around("logPointcut() && @annotation(logAnnotation)")
|
||||
public Object doAround(ProceedingJoinPoint joinPoint, com.rnb.common.annotation.Log logAnnotation) throws Throwable {
|
||||
TimeInterval timer = DateUtil.timer();
|
||||
Object result = null;
|
||||
Exception exception = null;
|
||||
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
} catch (Exception e) {
|
||||
exception = e;
|
||||
throw e;
|
||||
} finally {
|
||||
long executionTime = timer.interval(); // 执行时长
|
||||
this.saveLog(joinPoint, exception, result, logAnnotation, executionTime);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存日志
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param e 异常
|
||||
* @param jsonResult 响应结果
|
||||
* @param logAnnotation 日志注解
|
||||
*/
|
||||
private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, com.rnb.common.annotation.Log logAnnotation, long executionTime) {
|
||||
String requestURI = request.getRequestURI();
|
||||
// 创建日志记录
|
||||
Log log = new Log();
|
||||
log.setExecutionTime(executionTime);
|
||||
if (logAnnotation == null && e != null) {
|
||||
log.setModule(LogModuleEnum.EXCEPTION);
|
||||
log.setContent("系统发生异常");
|
||||
this.setRequestParameters(joinPoint, log);
|
||||
log.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace()));
|
||||
} else {
|
||||
log.setModule(logAnnotation.module());
|
||||
log.setContent(logAnnotation.value());
|
||||
// 请求参数
|
||||
if (logAnnotation.params()) {
|
||||
this.setRequestParameters(joinPoint, log);
|
||||
}
|
||||
// 响应结果
|
||||
if (logAnnotation.result() && jsonResult != null) {
|
||||
log.setResponseContent(JSONUtil.toJsonStr(jsonResult));
|
||||
}
|
||||
}
|
||||
log.setRequestUri(requestURI);
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
log.setCreateBy(userId);
|
||||
String ipAddr = IPUtils.getIpAddr(request);
|
||||
if (StrUtil.isNotBlank(ipAddr)) {
|
||||
log.setIp(ipAddr);
|
||||
String region = IPUtils.getRegion(ipAddr);
|
||||
// 中国|0|四川省|成都市|电信 解析省和市
|
||||
if (StrUtil.isNotBlank(region)) {
|
||||
String[] regionArray = region.split("\\|");
|
||||
if (regionArray.length > 2) {
|
||||
log.setProvince(regionArray[2]);
|
||||
log.setCity(regionArray[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取浏览器和终端系统信息
|
||||
String userAgentString = request.getHeader("User-Agent");
|
||||
UserAgent userAgent = resolveUserAgent(userAgentString);
|
||||
if (Objects.nonNull(userAgent)) {
|
||||
// 系统信息
|
||||
log.setOs(userAgent.getOs().getName());
|
||||
// 浏览器信息
|
||||
log.setBrowser(userAgent.getBrowser().getName());
|
||||
log.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));
|
||||
}
|
||||
// 保存日志到数据库
|
||||
logService.save(log);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求参数到日志对象中
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param log 操作日志
|
||||
*/
|
||||
private void setRequestParameters(JoinPoint joinPoint, Log log) {
|
||||
String requestMethod = request.getMethod();
|
||||
log.setRequestMethod(requestMethod);
|
||||
if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) {
|
||||
String params = convertArgumentsToString(joinPoint.getArgs());
|
||||
log.setRequestParams(StrUtil.sub(params, 0, 65535));
|
||||
} else {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
Map<?, ?> paramsMap = (Map<?, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
|
||||
log.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535));
|
||||
} else {
|
||||
log.setRequestParams("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将参数数组转换为字符串
|
||||
*
|
||||
* @param paramsArray 参数数组
|
||||
* @return 参数字符串
|
||||
*/
|
||||
private String convertArgumentsToString(Object[] paramsArray) {
|
||||
StringBuilder params = new StringBuilder();
|
||||
if (paramsArray != null) {
|
||||
for (Object param : paramsArray) {
|
||||
if (!shouldFilterObject(param)) {
|
||||
params.append(JSONUtil.toJsonStr(param)).append(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
return params.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要过滤的对象。
|
||||
*
|
||||
* @param obj 对象信息。
|
||||
* @return 如果是需要过滤的对象,则返回true;否则返回false。
|
||||
*/
|
||||
private boolean shouldFilterObject(Object obj) {
|
||||
Class<?> clazz = obj.getClass();
|
||||
if (clazz.isArray()) {
|
||||
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
|
||||
} else if (Collection.class.isAssignableFrom(clazz)) {
|
||||
Collection<?> collection = (Collection<?>) obj;
|
||||
return collection.stream().anyMatch(item -> item instanceof MultipartFile);
|
||||
} else if (Map.class.isAssignableFrom(clazz)) {
|
||||
Map<?, ?> map = (Map<?, ?>) obj;
|
||||
return map.values().stream().anyMatch(value -> value instanceof MultipartFile);
|
||||
}
|
||||
return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析UserAgent
|
||||
*
|
||||
* @param userAgentString UserAgent字符串
|
||||
* @return UserAgent
|
||||
*/
|
||||
public UserAgent resolveUserAgent(String userAgentString) {
|
||||
if (StrUtil.isBlank(userAgentString)) {
|
||||
return null;
|
||||
}
|
||||
// 给userAgentStringMD5加密一次防止过长
|
||||
String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString);
|
||||
//判断是否命中缓存
|
||||
UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class);
|
||||
if (userAgent != null) {
|
||||
return userAgent;
|
||||
}
|
||||
userAgent = UserAgentUtil.parse(userAgentString);
|
||||
Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent);
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.rnb.core.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.rnb.common.constant.RedisConstants;
|
||||
import com.rnb.common.constant.SecurityConstants;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.common.exception.BusinessException;
|
||||
import com.rnb.common.annotation.RepeatSubmit;
|
||||
import com.rnb.common.util.IPUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.annotation.Pointcut;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 防重复提交切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class RepeatSubmitAspect {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
* 防重复提交切点
|
||||
*/
|
||||
@Pointcut("@annotation(repeatSubmit)")
|
||||
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕通知:处理防重复提交逻辑
|
||||
*/
|
||||
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
|
||||
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
|
||||
String lockKey = buildLockKey();
|
||||
|
||||
int expire = repeatSubmit.expire();
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
|
||||
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST);
|
||||
}
|
||||
return pjp.proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成防重复提交锁的 key
|
||||
* @return 锁的 key
|
||||
*/
|
||||
private String buildLockKey() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
// 用户唯一标识
|
||||
String userIdentifier = getUserIdentifier(request);
|
||||
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
|
||||
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
|
||||
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户唯一标识
|
||||
* 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return 用户唯一标识
|
||||
*/
|
||||
private String getUserIdentifier(HttpServletRequest request) {
|
||||
// 用户身份唯一标识
|
||||
String userIdentifier;
|
||||
// 从请求头中获取 Token
|
||||
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token
|
||||
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
} else {
|
||||
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识
|
||||
}
|
||||
return userIdentifier;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.rnb.core.filter;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.common.constant.RedisConstants;
|
||||
import com.rnb.common.constant.SystemConstants;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.common.util.IPUtils;
|
||||
import com.rnb.common.util.ResponseUtils;
|
||||
import com.rnb.system.service.ConfigService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IP 限流过滤器
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024/08/10 14:38
|
||||
*/
|
||||
@Slf4j
|
||||
public class RateLimiterFilter extends OncePerRequestFilter {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final ConfigService configService;
|
||||
|
||||
private static final long DEFAULT_IP_LIMIT = 10L; // 默认 IP 限流阈值
|
||||
|
||||
public RateLimiterFilter(RedisTemplate<String, Object> redisTemplate, ConfigService configService) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 IP 是否触发限流
|
||||
* 默认限制同一 IP 每秒最多请求 10 次,可通过系统配置调整。
|
||||
* 如果系统未配置限流阈值,默认跳过限流。
|
||||
*
|
||||
* @param ip IP 地址
|
||||
* @return 是否限流:true 表示限流;false 表示未限流
|
||||
*/
|
||||
public boolean rateLimit(String ip) {
|
||||
// 限流 Redis 键
|
||||
String key = StrUtil.format(RedisConstants.RateLimiter.IP, ip);
|
||||
|
||||
// 自增请求计数
|
||||
Long count = redisTemplate.opsForValue().increment(key);
|
||||
if (count == null || count == 1) {
|
||||
// 第一次访问时设置过期时间为 1 秒
|
||||
redisTemplate.expire(key, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// 获取系统配置的限流阈值
|
||||
Object systemConfig = configService.getSystemConfig(SystemConstants.SYSTEM_CONFIG_IP_QPS_LIMIT_KEY);
|
||||
if (systemConfig == null) {
|
||||
// 系统未配置限流,跳过限流逻辑
|
||||
log.warn("系统未配置限流阈值,跳过限流");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 转换系统配置为限流值,默认为 10
|
||||
long limit = Convert.toLong(systemConfig, DEFAULT_IP_LIMIT);
|
||||
return count != null && count > limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 IP 限流逻辑
|
||||
* 如果 IP 请求超出限制,直接返回限流响应;否则继续执行过滤器链。
|
||||
*
|
||||
* @param request 请求体
|
||||
* @param response 响应体
|
||||
* @param filterChain 过滤器链
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
|
||||
@NotNull FilterChain filterChain) throws ServletException, IOException {
|
||||
// 获取请求的 IP 地址
|
||||
String ip = IPUtils.getIpAddr(request);
|
||||
|
||||
// 判断是否限流
|
||||
if (rateLimit(ip)) {
|
||||
// 返回限流错误信息
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 未触发限流,继续执行过滤器链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.rnb.core.filter;
|
||||
|
||||
import com.rnb.common.util.IPUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.CommonsRequestLoggingFilter;
|
||||
|
||||
/**
|
||||
* 请求日志打印过滤器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/03/03
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class RequestLogFilter extends CommonsRequestLoggingFilter {
|
||||
|
||||
@Override
|
||||
protected boolean shouldLog(HttpServletRequest request) {
|
||||
// 设置日志输出级别,默认debug
|
||||
return this.logger.isInfoEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeRequest(HttpServletRequest request, String message) {
|
||||
String requestURI = request.getRequestURI();
|
||||
String ip = IPUtils.getIpAddr(request);
|
||||
// log.info("request,ip:{}, uri: {}", ip, requestURI);
|
||||
super.beforeRequest(request, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterRequest(HttpServletRequest request, String message) {
|
||||
super.afterRequest(request, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.rnb.core.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
|
||||
import com.rnb.common.annotation.DataPermission;
|
||||
import com.rnb.common.base.IBaseEnum;
|
||||
import com.rnb.common.enums.DataScopeEnum;
|
||||
import com.rnb.core.security.util.SecurityUtils;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 数据权限控制器
|
||||
*
|
||||
* @author zc
|
||||
* @since 2021-12-10 13:28
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyDataPermissionHandler implements DataPermissionHandler {
|
||||
|
||||
/**
|
||||
* 获取数据权限的sql片段
|
||||
* @param where 查询条件
|
||||
* @param mappedStatementId mapper接口方法的全路径
|
||||
* @return sql片段
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Expression getSqlSegment(Expression where, String mappedStatementId) {
|
||||
// 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回
|
||||
if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){
|
||||
return where;
|
||||
}
|
||||
// 获取当前用户的数据权限
|
||||
Integer dataScope = SecurityUtils.getDataScope();
|
||||
DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class);
|
||||
// 如果是全部数据权限,直接返回
|
||||
if (DataScopeEnum.ALL.equals(dataScopeEnum)) {
|
||||
return where;
|
||||
}
|
||||
// 获取当前执行的接口类
|
||||
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT)));
|
||||
// 获取当前执行的方法名称
|
||||
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1);
|
||||
// 获取当前执行的接口类里所有的方法
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
for (Method method : methods) {
|
||||
//找到当前执行的方法
|
||||
if (method.getName().equals(methodName)) {
|
||||
DataPermission annotation = method.getAnnotation(DataPermission.class);
|
||||
// 判断当前执行的方法是否有权限注解,如果没有注解直接返回
|
||||
if (annotation == null ) {
|
||||
return where;
|
||||
}
|
||||
return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where);
|
||||
}
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建过滤条件
|
||||
*
|
||||
* @param where 当前查询条件
|
||||
* @return 构建后查询条件
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) {
|
||||
|
||||
// 获取部门和用户的别名
|
||||
String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName;
|
||||
String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName;
|
||||
|
||||
Long deptId, userId;
|
||||
String appendSqlStr;
|
||||
switch (dataScopeEnum) {
|
||||
case ALL:
|
||||
return where;
|
||||
case DEPT:
|
||||
deptId = SecurityUtils.getDeptId();
|
||||
appendSqlStr = deptColumnName + StringPool.EQUALS + deptId;
|
||||
break;
|
||||
case SELF:
|
||||
userId = SecurityUtils.getUserId();
|
||||
appendSqlStr = userColumnName + StringPool.EQUALS + userId;
|
||||
break;
|
||||
// 默认部门及子部门数据权限
|
||||
default:
|
||||
deptId = SecurityUtils.getDeptId();
|
||||
appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )";
|
||||
break;
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(appendSqlStr)) {
|
||||
return where;
|
||||
}
|
||||
|
||||
Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr);
|
||||
|
||||
if (where == null) {
|
||||
return appendExpression;
|
||||
}
|
||||
|
||||
return new AndExpression(where, appendExpression);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.rnb.core.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* mybatis-plus 字段自动填充
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/14
|
||||
*/
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 新增填充创建时间
|
||||
*
|
||||
* @param metaObject 元数据
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新填充更新时间
|
||||
*
|
||||
* @param metaObject 元数据
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.rnb.core.security.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* 验证码校验异常
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2025/3/1
|
||||
*/
|
||||
public class CaptchaValidationException extends AuthenticationException {
|
||||
public CaptchaValidationException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.rnb.core.security.exception;
|
||||
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.common.util.ResponseUtils;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 无权限访问处理器
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class MyAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.rnb.core.security.exception;
|
||||
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.common.util.ResponseUtils;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 统一处理 Spring Security 认证失败响应
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
/**
|
||||
* 认证失败处理入口方法
|
||||
*
|
||||
* @param request 触发异常的请求对象(可用于获取请求头、参数等)
|
||||
* @param response 响应对象(用于写入错误信息)
|
||||
* @param authException 认证异常对象(包含具体失败原因)
|
||||
*/
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
if (authException instanceof BadCredentialsException) {
|
||||
// 用户名或密码错误
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR);
|
||||
} else if(authException instanceof InsufficientAuthenticationException){
|
||||
// 请求头缺失Authorization、Token格式错误、Token过期、签名验证失败
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
} else {
|
||||
// 其他未明确处理的认证异常(如账户被锁定、账户禁用等)
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.rnb.core.security.extension.sms;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.common.constant.RedisConstants;
|
||||
import com.rnb.core.security.exception.CaptchaValidationException;
|
||||
import com.rnb.core.security.model.SysUserDetails;
|
||||
import com.rnb.core.security.model.UserAuthCredentials;
|
||||
import com.rnb.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.17.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
|
||||
public SmsAuthenticationProvider(UserService userService, RedisTemplate<String, Object> redisTemplate) {
|
||||
this.userService = userService;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码认证逻辑,参考 Spring Security 认证密码校验流程
|
||||
*
|
||||
* @param authentication 认证对象
|
||||
* @return 认证后的 Authentication 对象
|
||||
* @throws AuthenticationException 认证异常
|
||||
* @see org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate(Authentication)
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String mobile = (String) authentication.getPrincipal();
|
||||
String inputVerifyCode = (String) authentication.getCredentials();
|
||||
|
||||
// 根据手机号获取用户信息
|
||||
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(mobile);
|
||||
|
||||
if (userAuthCredentials == null) {
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
// 检查用户状态是否有效
|
||||
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 校验发送短信验证码的手机号是否与当前登录用户一致
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
|
||||
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
|
||||
throw new CaptchaValidationException("验证码错误");
|
||||
} else {
|
||||
// 验证成功后删除验证码
|
||||
redisTemplate.delete(cacheKey);
|
||||
}
|
||||
|
||||
// 构建认证后的用户详情信息
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
|
||||
|
||||
// 创建已认证的 SmsAuthenticationToken
|
||||
return SmsAuthenticationToken.authenticated(
|
||||
userDetails,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.rnb.core.security.extension.sms;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Token
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 621L;
|
||||
|
||||
/**
|
||||
* 认证信息 (手机号)
|
||||
*/
|
||||
private final Object principal;
|
||||
|
||||
/**
|
||||
* 凭证信息 (短信验证码)
|
||||
*/
|
||||
private final Object credentials;
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Token (未认证)
|
||||
*
|
||||
* @param principal 微信用户信息
|
||||
*/
|
||||
public SmsAuthenticationToken(Object principal, Object credentials) {
|
||||
// 没有授权信息时,设置为 null
|
||||
super(null);
|
||||
this.principal = principal;
|
||||
this.credentials = credentials;
|
||||
// 默认未认证
|
||||
this.setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Token (已认证)
|
||||
*
|
||||
* @param principal 用户信息
|
||||
* @param authorities 授权信息
|
||||
*/
|
||||
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
this.credentials = null;
|
||||
// 认证通过
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 认证通过
|
||||
*
|
||||
* @param principal 用户信息
|
||||
* @param authorities 授权信息
|
||||
* @return SmsAuthenticationToken
|
||||
*/
|
||||
public static SmsAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new SmsAuthenticationToken(principal, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.rnb.core.security.extension.wechat;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.core.security.model.SysUserDetails;
|
||||
import com.rnb.core.security.model.UserAuthCredentials;
|
||||
import com.rnb.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
|
||||
/**
|
||||
* 微信认证 Provider
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.17.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class WechatAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
|
||||
|
||||
public WechatAuthenticationProvider(UserService userService, WxMaService wxMaService) {
|
||||
this.userService = userService;
|
||||
this.wxMaService = wxMaService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 微信认证逻辑,参考 Spring Security 认证密码校验流程
|
||||
*
|
||||
* @param authentication 认证对象
|
||||
* @return 认证后的 Authentication 对象
|
||||
* @throws AuthenticationException 认证异常
|
||||
* @see org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate(Authentication)
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String code = (String) authentication.getPrincipal();
|
||||
|
||||
// 通过微信服务端验证 code 并获取用户会话信息
|
||||
WxMaJscode2SessionResult sessionInfo;
|
||||
try {
|
||||
sessionInfo = wxMaService.getUserService().getSessionInfo(code);
|
||||
} catch (WxErrorException e) {
|
||||
throw new CredentialsExpiredException("微信登录 code 无效或已失效,请重新获取");
|
||||
}
|
||||
|
||||
String openId = sessionInfo.getOpenid();
|
||||
if (StrUtil.isBlank(openId)) {
|
||||
throw new UsernameNotFoundException("未能获取到微信 OpenID,请稍后重试");
|
||||
}
|
||||
|
||||
// 根据微信 OpenID 查询用户信息
|
||||
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
|
||||
|
||||
if (userAuthCredentials == null) {
|
||||
// TODO: 用户不存在则注册,这里需要获取用户手机号并与现有用户绑定
|
||||
userService.registerOrBindWechatUser(openId);
|
||||
|
||||
// 再次查询用户信息,确保用户注册成功
|
||||
userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
|
||||
if (userAuthCredentials == null) {
|
||||
throw new UsernameNotFoundException("用户注册失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户状态是否有效
|
||||
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
// 这里因为已经根据 code 从微信小程序获取到 openid 不需要再经过系统认证,所以直接生成
|
||||
|
||||
// 构建认证后的用户详情信息
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
|
||||
|
||||
// 创建已认证的 WeChatAuthenticationToken
|
||||
return WechatAuthenticationToken.authenticated(
|
||||
userDetails,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return WechatAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.rnb.core.security.extension.wechat;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 微信认证 Token
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/12/2
|
||||
*/
|
||||
public class WechatAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 621L;
|
||||
private final Object principal;
|
||||
|
||||
/**
|
||||
* 微信认证 Token (未认证)
|
||||
*
|
||||
* @param principal 微信用户信息
|
||||
*/
|
||||
public WechatAuthenticationToken(Object principal) {
|
||||
// 没有授权信息时,设置为 null
|
||||
super(null);
|
||||
this.principal = principal;
|
||||
// 默认未认证
|
||||
this.setAuthenticated(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 微信认证 Token (已认证)
|
||||
*
|
||||
* @param principal 微信用户信息
|
||||
* @param authorities 授权信息
|
||||
*/
|
||||
public WechatAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
// 认证通过
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 认证通过
|
||||
*
|
||||
* @param principal 微信用户信息
|
||||
* @param authorities 授权信息
|
||||
* @return
|
||||
*/
|
||||
public static WechatAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new WechatAuthenticationToken(principal, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
// 微信认证不需要密码
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.rnb.core.security.filter;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.common.constant.RedisConstants;
|
||||
import com.rnb.common.constant.SecurityConstants;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.common.util.ResponseUtils;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
* 图形验证码校验过滤器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/1
|
||||
*/
|
||||
public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name());
|
||||
|
||||
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
|
||||
public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final CodeGenerator codeGenerator;
|
||||
|
||||
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.codeGenerator = codeGenerator;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
|
||||
// 检验登录接口的验证码
|
||||
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||
// 请求中的验证码
|
||||
String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
|
||||
// TODO 兼容没有验证码的版本(线上请移除这个判断)
|
||||
if (StrUtil.isBlank(captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
// 缓存中的验证码
|
||||
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
|
||||
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey)
|
||||
);
|
||||
if (cacheVerifyCode == null) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||
} else {
|
||||
// 验证码比对
|
||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非登录接口放行
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.rnb.core.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.common.constant.SecurityConstants;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.common.util.ResponseUtils;
|
||||
import com.rnb.core.security.token.TokenManager;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Token 认证校验过滤器
|
||||
*
|
||||
* @author wangtao
|
||||
* @since 2025/3/6 16:50
|
||||
*/
|
||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* Token 管理器
|
||||
*/
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
public TokenAuthenticationFilter(TokenManager tokenManager) {
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Token ,包括验签和是否过期
|
||||
* 如果 Token 有效,将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
try {
|
||||
if (StrUtil.isNotBlank(authorizationHeader)
|
||||
&& authorizationHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
|
||||
// 剥离Bearer前缀获取原始令牌
|
||||
String rawToken = authorizationHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
|
||||
|
||||
// 执行令牌有效性检查(包含密码学验签和过期时间验证)
|
||||
boolean isValidToken = tokenManager.validateToken(rawToken);
|
||||
if (!isValidToken) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
// 将令牌解析为 Spring Security 上下文认证对象
|
||||
Authentication authentication = tokenManager.parseToken(rawToken);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// 安全上下文清除保障(防止上下文残留)
|
||||
SecurityContextHolder.clearContext();
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续后续过滤器链执行
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rnb.core.security.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 认证令牌响应对象
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Schema(description = "认证令牌响应对象")
|
||||
@Data
|
||||
@Builder
|
||||
public class AuthenticationToken {
|
||||
|
||||
@Schema(description = "令牌类型", example = "Bearer")
|
||||
private String tokenType;
|
||||
|
||||
@Schema(description = "访问令牌")
|
||||
private String accessToken;
|
||||
|
||||
@Schema(description = "刷新令牌")
|
||||
private String refreshToken;
|
||||
|
||||
@Schema(description = "过期时间(单位:秒)")
|
||||
private Integer expiresIn;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.rnb.core.security.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 在线用户信息对象
|
||||
*
|
||||
* @author wangtao
|
||||
* @since 2025/2/27 10:31
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OnlineUser {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 数据权限范围
|
||||
* <p>定义用户可访问的数据范围,如全部、本部门或自定义范围</p>
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
/**
|
||||
* 角色权限集合
|
||||
*/
|
||||
private Set<String> roles;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.rnb.core.security.model;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.rnb.common.constant.SecurityConstants;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Spring Security 用户认证对象
|
||||
* <p>
|
||||
* 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。
|
||||
* 实现了 {@link UserDetails} 接口,提供用户的核心信息。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @version 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class SysUserDetails implements UserDetails {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 账号是否启用(true:启用 false:禁用)
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 数据权限范围
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
/**
|
||||
* 用户角色权限集合
|
||||
*/
|
||||
private Collection<SimpleGrantedAuthority> authorities;
|
||||
|
||||
/**
|
||||
* 构造函数:根据用户认证信息初始化用户详情对象
|
||||
*
|
||||
* @param user 用户认证信息对象 {@link UserAuthCredentials}
|
||||
*/
|
||||
public SysUserDetails(UserAuthCredentials user) {
|
||||
this.userId = user.getUserId();
|
||||
this.username = user.getUsername();
|
||||
this.password = user.getPassword();
|
||||
this.enabled = ObjectUtil.equal(user.getStatus(), 1);
|
||||
this.deptId = user.getDeptId();
|
||||
this.dataScope = user.getDataScope();
|
||||
|
||||
// 初始化角色权限集合
|
||||
this.authorities = CollectionUtil.isNotEmpty(user.getRoles())
|
||||
? user.getRoles().stream()
|
||||
// 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add)
|
||||
.map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role))
|
||||
.collect(Collectors.toSet())
|
||||
: Collections.emptySet();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return this.authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.rnb.core.security.model;
|
||||
|
||||
import com.rnb.common.enums.DataScopeEnum;
|
||||
import lombok.Data;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户认证凭证信息
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/22
|
||||
*/
|
||||
@Data
|
||||
public class UserAuthCredentials {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 状态(1:启用;0:禁用)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 用户所属的角色集合
|
||||
*/
|
||||
private Set<String> roles;
|
||||
|
||||
/**
|
||||
* 数据权限范围,用于控制用户可以访问的数据级别
|
||||
*
|
||||
* @see DataScopeEnum
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.rnb.core.security.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.common.constant.RedisConstants;
|
||||
import com.rnb.core.security.util.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* SpringSecurity 权限校验
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/2/22
|
||||
*/
|
||||
@Component("ss")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PermissionService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 判断当前登录用户是否拥有操作权限
|
||||
*
|
||||
* @param requiredPerm 所需权限
|
||||
* @return 是否有权限
|
||||
*/
|
||||
public boolean hasPerm(String requiredPerm) {
|
||||
|
||||
if (StrUtil.isBlank(requiredPerm)) {
|
||||
return false;
|
||||
}
|
||||
// 超级管理员放行
|
||||
if (SecurityUtils.isRoot()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取当前登录用户的角色编码集合
|
||||
Set<String> roleCodes = SecurityUtils.getRoles();
|
||||
if (CollectionUtil.isEmpty(roleCodes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取当前登录用户的所有角色的权限列表
|
||||
Set<String> rolePerms = this.getRolePermsFormCache(roleCodes);
|
||||
if (CollectionUtil.isEmpty(rolePerms)) {
|
||||
return false;
|
||||
}
|
||||
// 判断当前登录用户的所有角色的权限列表中是否包含所需权限
|
||||
boolean hasPermission = rolePerms.stream()
|
||||
.anyMatch(rolePerm ->
|
||||
// 匹配权限,支持通配符(* 等)
|
||||
PatternMatchUtils.simpleMatch(rolePerm, requiredPerm)
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
log.error("用户无操作权限:{}",requiredPerm);
|
||||
}
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从缓存中获取角色权限列表
|
||||
*
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 角色权限列表
|
||||
*/
|
||||
public Set<String> getRolePermsFormCache(Set<String> roleCodes) {
|
||||
// 检查输入是否为空
|
||||
if (CollectionUtil.isEmpty(roleCodes)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<String> perms = new HashSet<>();
|
||||
// 从缓存中一次性获取所有角色的权限
|
||||
Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
|
||||
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects);
|
||||
|
||||
for (Object rolePermsObj : rolePermsList) {
|
||||
if (rolePermsObj instanceof Set) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> rolePerms = (Set<String>) rolePermsObj;
|
||||
perms.addAll(rolePerms);
|
||||
}
|
||||
}
|
||||
|
||||
return perms;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.rnb.core.security.service;
|
||||
|
||||
import com.rnb.core.security.model.SysUserDetails;
|
||||
import com.rnb.core.security.model.UserAuthCredentials;
|
||||
import com.rnb.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 系统用户认证 DetailsService
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2021/10/19
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SysUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 根据用户名获取用户信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
* @throws UsernameNotFoundException 用户名未找到异常
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
try {
|
||||
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByUsername(username);
|
||||
if (userAuthCredentials == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
return new SysUserDetails(userAuthCredentials);
|
||||
} catch (Exception e) {
|
||||
// 记录异常日志
|
||||
log.error("认证异常:{}", e.getMessage());
|
||||
// 抛出异常
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.rnb.core.security.token;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import cn.hutool.jwt.JWTPayload;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import com.rnb.common.constant.JwtClaimConstants;
|
||||
import com.rnb.common.constant.RedisConstants;
|
||||
import com.rnb.common.constant.SecurityConstants;
|
||||
import com.rnb.common.exception.BusinessException;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.config.property.SecurityProperties;
|
||||
import com.rnb.core.security.model.SysUserDetails;
|
||||
import com.rnb.core.security.model.AuthenticationToken;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JWT Token 管理器
|
||||
* <p>
|
||||
* 用于生成、解析、校验、刷新 JWT Token
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/11/15
|
||||
*/
|
||||
@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
|
||||
@Service
|
||||
public class JwtTokenManager implements TokenManager {
|
||||
|
||||
private final SecurityProperties securityProperties;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final byte[] secretKey;
|
||||
|
||||
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
|
||||
this.securityProperties = securityProperties;
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成令牌
|
||||
*
|
||||
* @param authentication 认证信息
|
||||
* @return 令牌响应对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken generateToken(Authentication authentication) {
|
||||
int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||
int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive();
|
||||
|
||||
String accessToken = generateToken(authentication, accessTokenTimeToLive);
|
||||
String refreshToken = generateToken(authentication, refreshTokenTimeToLive);
|
||||
|
||||
return AuthenticationToken.builder()
|
||||
.accessToken(accessToken)
|
||||
.refreshToken(refreshToken)
|
||||
.tokenType("Bearer")
|
||||
.expiresIn(accessTokenTimeToLive)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析令牌
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return Authentication 对象
|
||||
*/
|
||||
@Override
|
||||
public Authentication parseToken(String token) {
|
||||
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
|
||||
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
|
||||
userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
|
||||
|
||||
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
|
||||
// 角色集合
|
||||
Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES)
|
||||
.stream()
|
||||
.map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验令牌
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 是否有效
|
||||
*/
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
// 检查 Token 是否有效(验签 + 是否过期)
|
||||
boolean isValid = jwt.setKey(secretKey).validate(0);
|
||||
|
||||
if (isValid) {
|
||||
// 检查 Token 是否已被加入黑名单(注销、修改密码等场景)
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||
|
||||
// 判断是否在黑名单中,如果在,则返回 false 标识Token无效
|
||||
if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateRefreshToken(String refreshToken) {
|
||||
return this.validateToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将令牌加入黑名单
|
||||
*
|
||||
* @param token JWT Token
|
||||
*/
|
||||
@Override
|
||||
public void invalidateToken(String token) {
|
||||
if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
|
||||
}
|
||||
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
|
||||
Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT);
|
||||
|
||||
// 黑名单Token Key
|
||||
String blacklistTokenKey = StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, payloads.getStr(JWTPayload.JWT_ID));
|
||||
|
||||
if (expirationAt != null) {
|
||||
int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000);
|
||||
if (expirationAt < currentTimeSeconds) {
|
||||
// Token已过期,直接返回
|
||||
return;
|
||||
}
|
||||
// 计算Token剩余时间,将其加入黑名单
|
||||
int expirationIn = expirationAt - currentTimeSeconds;
|
||||
redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS);
|
||||
} else {
|
||||
// 永不过期的Token永久加入黑名单
|
||||
redisTemplate.opsForValue().set(blacklistTokenKey, null);
|
||||
}
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 令牌响应对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
|
||||
boolean isValid = validateToken(refreshToken);
|
||||
if (!isValid) {
|
||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
|
||||
Authentication authentication = parseToken(refreshToken);
|
||||
int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||
String newAccessToken = generateToken(authentication, accessTokenExpiration);
|
||||
|
||||
return AuthenticationToken.builder()
|
||||
.accessToken(newAccessToken)
|
||||
.refreshToken(refreshToken)
|
||||
.tokenType("Bearer")
|
||||
.expiresIn(accessTokenExpiration)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
*
|
||||
* @param authentication 认证信息
|
||||
* @param ttl 过期时间
|
||||
* @return JWT Token
|
||||
*/
|
||||
private String generateToken(Authentication authentication, int ttl) {
|
||||
|
||||
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID
|
||||
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
|
||||
payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围
|
||||
|
||||
// claims 中添加角色信息
|
||||
Set<String> roles = authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet());
|
||||
payload.put(JwtClaimConstants.AUTHORITIES, roles);
|
||||
|
||||
Date now = new Date();
|
||||
payload.put(JWTPayload.ISSUED_AT, now);
|
||||
|
||||
// 设置过期时间 -1 表示永不过期
|
||||
if (ttl != -1) {
|
||||
Date expiresAt = DateUtil.offsetSecond(now, ttl);
|
||||
payload.put(JWTPayload.EXPIRES_AT, expiresAt);
|
||||
}
|
||||
payload.put(JWTPayload.SUBJECT, authentication.getName());
|
||||
payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());
|
||||
|
||||
return JWTUtil.createToken(payload, secretKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package com.rnb.core.security.token;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.common.constant.RedisConstants;
|
||||
import com.rnb.common.exception.BusinessException;
|
||||
import com.rnb.common.result.ResultCode;
|
||||
import com.rnb.config.property.SecurityProperties;
|
||||
import com.rnb.core.security.model.AuthenticationToken;
|
||||
import com.rnb.core.security.model.OnlineUser;
|
||||
import com.rnb.core.security.model.SysUserDetails;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Redis Token 管理器
|
||||
* <p>
|
||||
* 用于生成、解析、校验、刷新 JWT Token
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/11/15
|
||||
*/
|
||||
@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token")
|
||||
@Service
|
||||
public class RedisTokenManager implements TokenManager {
|
||||
|
||||
private final SecurityProperties securityProperties;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public RedisTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
|
||||
this.securityProperties = securityProperties;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Token
|
||||
*
|
||||
* @param authentication 用户认证信息
|
||||
* @return 生成的 AuthenticationToken 对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken generateToken(Authentication authentication) {
|
||||
SysUserDetails user = (SysUserDetails) authentication.getPrincipal();
|
||||
String accessToken = IdUtil.fastSimpleUUID();
|
||||
String refreshToken = IdUtil.fastSimpleUUID();
|
||||
|
||||
// 构建用户在线信息
|
||||
OnlineUser onlineUser = new OnlineUser(
|
||||
user.getUserId(),
|
||||
user.getUsername(),
|
||||
user.getDeptId(),
|
||||
user.getDataScope(),
|
||||
user.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
|
||||
// 存储访问令牌、刷新令牌和刷新令牌映射
|
||||
storeTokensInRedis(accessToken, refreshToken, onlineUser);
|
||||
|
||||
// 单设备登录控制
|
||||
handleSingleDeviceLogin(user.getUserId(), accessToken);
|
||||
|
||||
return AuthenticationToken.builder()
|
||||
.accessToken(accessToken)
|
||||
.refreshToken(refreshToken)
|
||||
.expiresIn(securityProperties.getSession().getAccessTokenTimeToLive())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 token 解析用户信息
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 构建的 Authentication 对象
|
||||
*/
|
||||
@Override
|
||||
public Authentication parseToken(String token) {
|
||||
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||
if (onlineUser == null) return null;
|
||||
|
||||
// 构建用户权限集合
|
||||
Set<SimpleGrantedAuthority> authorities = null;
|
||||
|
||||
Set<String> roles = onlineUser.getRoles();
|
||||
if (CollectionUtil.isNotEmpty(roles)) {
|
||||
authorities = roles.stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
// 构建用户详情对象
|
||||
SysUserDetails userDetails = buildUserDetails(onlineUser, authorities);
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Token 是否有效
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 是否有效
|
||||
*/
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
return redisTemplate.hasKey(formatTokenKey(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 RefreshToken 是否有效
|
||||
*
|
||||
* @param refreshToken 访问令牌
|
||||
* @return 是否有效
|
||||
*/
|
||||
@Override
|
||||
public boolean validateRefreshToken(String refreshToken) {
|
||||
return redisTemplate.hasKey(formatRefreshTokenKey(refreshToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 新生成的 AuthenticationToken 对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
|
||||
if (onlineUser == null) {
|
||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
|
||||
String oldAccessToken = (String) redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()));
|
||||
|
||||
// 删除旧的访问令牌记录
|
||||
if (oldAccessToken != null) {
|
||||
redisTemplate.delete(formatTokenKey(oldAccessToken));
|
||||
}
|
||||
|
||||
// 生成新访问令牌并存储
|
||||
String newAccessToken = IdUtil.fastSimpleUUID();
|
||||
storeAccessToken(newAccessToken, onlineUser);
|
||||
|
||||
int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||
return AuthenticationToken.builder()
|
||||
.accessToken(newAccessToken)
|
||||
.refreshToken(refreshToken)
|
||||
.expiresIn(accessTtl)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使访问令牌失效
|
||||
*
|
||||
* @param token 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public void invalidateToken(String token) {
|
||||
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||
if (onlineUser != null) {
|
||||
Long userId = onlineUser.getUserId();
|
||||
// 1. 删除访问令牌相关
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
|
||||
String accessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
|
||||
if (accessToken != null) {
|
||||
redisTemplate.delete(formatTokenKey(accessToken));
|
||||
redisTemplate.delete(userAccessKey);
|
||||
}
|
||||
|
||||
// 2. 删除刷新令牌相关
|
||||
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
|
||||
String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey);
|
||||
if (refreshToken != null) {
|
||||
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
|
||||
redisTemplate.delete(userRefreshKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将访问令牌和刷新令牌存储至 Redis
|
||||
*
|
||||
* @param accessToken 访问令牌
|
||||
* @param refreshToken 刷新令牌
|
||||
* @param onlineUser 在线用户信息
|
||||
*/
|
||||
private void storeTokensInRedis(String accessToken, String refreshToken, OnlineUser onlineUser) {
|
||||
// 访问令牌 -> 用户信息
|
||||
setRedisValue(formatTokenKey(accessToken), onlineUser, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
|
||||
// 刷新令牌 -> 用户信息
|
||||
String refreshTokenKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
|
||||
setRedisValue(refreshTokenKey, onlineUser, securityProperties.getSession().getRefreshTokenTimeToLive());
|
||||
|
||||
// 用户ID -> 刷新令牌
|
||||
setRedisValue(StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, onlineUser.getUserId()),
|
||||
refreshToken,
|
||||
securityProperties.getSession().getRefreshTokenTimeToLive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单设备登录控制
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param accessToken 新生成的访问令牌
|
||||
*/
|
||||
private void handleSingleDeviceLogin(Long userId, String accessToken) {
|
||||
Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin();
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
|
||||
// 单设备登录控制,删除旧的访问令牌
|
||||
if (!allowMultiLogin) {
|
||||
String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
|
||||
if (oldAccessToken != null) {
|
||||
redisTemplate.delete(formatTokenKey(oldAccessToken));
|
||||
}
|
||||
}
|
||||
// 存储访问令牌映射(用户ID -> 访问令牌),用于单设备登录控制删除旧的访问令牌和刷新令牌时删除旧令牌
|
||||
setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储新的访问令牌
|
||||
*
|
||||
* @param newAccessToken 新访问令牌
|
||||
* @param onlineUser 在线用户信息
|
||||
*/
|
||||
private void storeAccessToken(String newAccessToken, OnlineUser onlineUser) {
|
||||
setRedisValue(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), onlineUser, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId());
|
||||
setRedisValue(userAccessKey, newAccessToken, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户详情对象
|
||||
*
|
||||
* @param onlineUser 在线用户信息
|
||||
* @param authorities 权限集合
|
||||
* @return SysUserDetails 用户详情
|
||||
*/
|
||||
private SysUserDetails buildUserDetails(OnlineUser onlineUser, Set<SimpleGrantedAuthority> authorities) {
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(onlineUser.getUserId());
|
||||
userDetails.setUsername(onlineUser.getUsername());
|
||||
userDetails.setDeptId(onlineUser.getDeptId());
|
||||
userDetails.setDataScope(onlineUser.getDataScope());
|
||||
userDetails.setAuthorities(authorities);
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化访问令牌的 Redis 键
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 格式化后的 Redis 键
|
||||
*/
|
||||
private String formatTokenKey(String token) {
|
||||
return StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化刷新令牌的 Redis 键
|
||||
*
|
||||
* @param refreshToken 访问令牌
|
||||
* @return 格式化后的 Redis 键
|
||||
*/
|
||||
private String formatRefreshTokenKey(String refreshToken) {
|
||||
return StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值存储到 Redis
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 过期时间(秒),-1表示永不过期
|
||||
*/
|
||||
private void setRedisValue(String key, Object value, int ttl) {
|
||||
if (ttl != -1) {
|
||||
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
|
||||
} else {
|
||||
redisTemplate.opsForValue().set(key, value); // ttl=-1时永不过期
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.rnb.core.security.token;
|
||||
|
||||
|
||||
import com.rnb.core.security.model.AuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* Token 管理器
|
||||
* <p>
|
||||
* 用于生成、解析、校验、刷新 Token
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.16.0
|
||||
*/
|
||||
public interface TokenManager {
|
||||
|
||||
/**
|
||||
* 生成认证 Token
|
||||
*
|
||||
* @param authentication 用户认证信息
|
||||
* @return 认证 Token 响应
|
||||
*/
|
||||
AuthenticationToken generateToken(Authentication authentication);
|
||||
|
||||
/**
|
||||
* 解析 Token 获取认证信息
|
||||
*
|
||||
* @param token Token
|
||||
* @return 用户认证信息
|
||||
*/
|
||||
Authentication parseToken(String token);
|
||||
|
||||
/**
|
||||
* 校验 Token 是否有效
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 是否有效
|
||||
*/
|
||||
boolean validateToken(String token);
|
||||
|
||||
/**
|
||||
* 校验 刷新 Token 是否有效
|
||||
*
|
||||
* @param refreshToken JWT Token
|
||||
* @return 是否有效
|
||||
*/
|
||||
boolean validateRefreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*
|
||||
* @param token 刷新令牌
|
||||
* @return 认证 Token 响应
|
||||
*/
|
||||
AuthenticationToken refreshToken(String token);
|
||||
|
||||
/**
|
||||
* 令 Token 失效
|
||||
*
|
||||
* @param token JWT Token
|
||||
*/
|
||||
default void invalidateToken(String token) {
|
||||
// 默认实现可以是空的,或者抛出不支持的操作异常
|
||||
// throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.rnb.core.security.util;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.rnb.common.constant.SecurityConstants;
|
||||
import com.rnb.common.constant.SystemConstants;
|
||||
import com.rnb.core.security.model.SysUserDetails;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Spring Security 工具类
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2021/1/10
|
||||
*/
|
||||
public class SecurityUtils {
|
||||
|
||||
/**
|
||||
* 获取当前登录人信息
|
||||
*
|
||||
* @return Optional<SysUserDetails>
|
||||
*/
|
||||
public static Optional<SysUserDetails> getUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null) {
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof SysUserDetails) {
|
||||
return Optional.of((SysUserDetails) principal);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户ID
|
||||
*
|
||||
* @return Long
|
||||
*/
|
||||
public static Long getUserId() {
|
||||
return getUser().map(SysUserDetails::getUserId).orElse(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户账号
|
||||
*
|
||||
* @return String 用户账号
|
||||
*/
|
||||
public static String getUsername() {
|
||||
return getUser().map(SysUserDetails::getUsername).orElse(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取部门ID
|
||||
*
|
||||
* @return Long
|
||||
*/
|
||||
public static Long getDeptId() {
|
||||
return getUser().map(SysUserDetails::getDeptId).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据权限范围
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public static Integer getDataScope() {
|
||||
return getUser().map(SysUserDetails::getDataScope).orElse(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取角色集合
|
||||
*
|
||||
* @return 角色集合
|
||||
*/
|
||||
public static Set<String> getRoles() {
|
||||
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
|
||||
.map(Authentication::getAuthorities)
|
||||
.filter(CollectionUtil::isNotEmpty)
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
// 筛选角色,authorities 中的角色都是以 ROLE_ 开头
|
||||
.filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX))
|
||||
.map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否超级管理员
|
||||
* <p>
|
||||
* 超级管理员忽视任何权限判断
|
||||
*/
|
||||
public static boolean isRoot() {
|
||||
Set<String> roles = getRoles();
|
||||
return roles.contains(SystemConstants.ROOT_ROLE_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求中的 Token
|
||||
*
|
||||
* @return Token 字符串
|
||||
*/
|
||||
public static String getTokenFromRequest() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
return request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.rnb.core.validator;
|
||||
|
||||
import com.rnb.common.annotation.ValidField;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 字段校验器
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/11/18
|
||||
*/
|
||||
public class FieldValidator implements ConstraintValidator<ValidField, String> {
|
||||
|
||||
private String[] allowedValues;
|
||||
|
||||
@Override
|
||||
public void initialize(ValidField constraintAnnotation) {
|
||||
// 初始化允许的值列表
|
||||
this.allowedValues = constraintAnnotation.allowedValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
return true; // 如果字段允许为空,可以返回 true
|
||||
}
|
||||
// 检查值是否在允许列表中
|
||||
return Arrays.asList(allowedValues).contains(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.rnb.rnb.config;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.rnb.common.constant.SystemConstants;
|
||||
import com.rnb.rnb.service.ApiService;
|
||||
import com.rnb.system.service.ConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Configurable;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ScheduleConfig {
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
|
||||
@Scheduled(cron = "0 0 4 * * ?")
|
||||
// @Scheduled(cron = "0 * * * * ?")
|
||||
public void checkClientKey(){
|
||||
Object systemConfig = configService.getSystemConfig(SystemConstants.SYSTEM_CONFIG_CLIENT_RESP_KEY);
|
||||
Integer clientKey = Convert.toInt(systemConfig, 0);
|
||||
if(clientKey>0) {
|
||||
// clientKey--;
|
||||
configService.updateSystemConfig(SystemConstants.SYSTEM_CONFIG_CLIENT_RESP_KEY, --clientKey);
|
||||
}
|
||||
// log.info("clientKey=" + clientKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.rnb.rnb.controller;
|
||||
|
||||
import com.rnb.rnb.service.ApiService;
|
||||
import com.rnb.rnb.util.ParamDecoder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/Api")
|
||||
public class ApiController {
|
||||
@Autowired
|
||||
private ParamDecoder decoder;
|
||||
|
||||
@Autowired
|
||||
private ApiService apiService;
|
||||
|
||||
@GetMapping("/chkServer")
|
||||
public String chkServer(){
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@GetMapping("/getUpgradeUrl")
|
||||
public String getUpgradeUrl(@RequestParam Integer sid){
|
||||
return apiService.getUpgradePath(sid);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String handleApiRequest(@RequestBody String param) {
|
||||
HashMap<String, String> map = decoder.Decode(param);
|
||||
if(map==null || map.isEmpty() || !map.containsKey("op")) return "err:103";
|
||||
|
||||
String op = map.get("op");
|
||||
switch (op) {
|
||||
case "getInfo": {
|
||||
Integer sid = Integer.parseInt(map.get("sid"));
|
||||
Integer ver = Integer.parseInt(map.get("ver"));
|
||||
|
||||
// System.out.println(String.format("[%s]", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
|
||||
// + map.toString());
|
||||
|
||||
return apiService.getInfo(sid, ver);
|
||||
}
|
||||
case "userLogin": {
|
||||
int ver = Integer.parseInt(map.get("ver"));
|
||||
if(ver<302)
|
||||
return "err:105";
|
||||
|
||||
String uname = map.get("un").trim().toLowerCase();
|
||||
String web = map.get("web");
|
||||
Integer sid = Integer.parseInt(map.get("sid"));
|
||||
String sDid = map.get("did");
|
||||
Long did = sDid!=null ? Long.parseLong(sDid) : null;
|
||||
|
||||
return apiService.userLogin(uname, web, sid, ver, did);
|
||||
}
|
||||
case "rbReport": {
|
||||
Integer uid = Integer.parseInt(map.get("uid"));
|
||||
Integer amo = Integer.parseInt(map.get("amo"));
|
||||
|
||||
return apiService.rbReport(uid, amo);
|
||||
}
|
||||
case "rbLogout": {
|
||||
Integer uid = Integer.parseInt(map.get("uid"));
|
||||
Long did = Long.parseLong(map.get("did"));
|
||||
|
||||
return apiService.rbLogout(uid, did);
|
||||
}
|
||||
}
|
||||
|
||||
return "err:101";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package com.rnb.rnb.controller;
|
||||
|
||||
import cn.hutool.poi.excel.cell.CellSetter;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.rnb.common.annotation.RepeatSubmit;
|
||||
import com.rnb.common.base.BasePageQuery;
|
||||
import com.rnb.common.result.PageResult;
|
||||
import com.rnb.common.result.Result;
|
||||
import com.rnb.core.security.util.SecurityUtils;
|
||||
import com.rnb.rnb.model.entity.*;
|
||||
import com.rnb.rnb.model.query.*;
|
||||
import com.rnb.rnb.model.vo.*;
|
||||
import com.rnb.rnb.service.*;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/v1/rnb")
|
||||
public class ServiceController {
|
||||
@Autowired
|
||||
private final AccountService accountService;
|
||||
@Autowired
|
||||
private final ExtendService extendService;
|
||||
@Autowired
|
||||
private final OplogService oplogService;
|
||||
@Autowired
|
||||
private final AmountService amountService;
|
||||
@Autowired
|
||||
private final AmountSumService amoSumService;
|
||||
@Autowired
|
||||
private final SoftwareService softwareService;
|
||||
|
||||
@GetMapping("/pageAccAdvance")
|
||||
public PageResult<Account> pageAccountAdvance(@ParameterObject AccountPageQuery query) {
|
||||
IPage<Account> page = accountService.pageAdvance(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/pageAccAdmin")
|
||||
public PageResult<Account> pageAccountAdmin(@ParameterObject AccountPageQuery query) {
|
||||
IPage<Account> page = accountService.pageAdmin(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/pageAccBase")
|
||||
public PageResult<AccountBaseVo> pageAccountBase(@ParameterObject AccountPageQuery query) {
|
||||
IPage<AccountBaseVo> page = accountService.pageBase(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@PostMapping("/newAccount")
|
||||
@RepeatSubmit
|
||||
public Result<?> newAccount() {
|
||||
Integer accId = accountService.newAccount();
|
||||
if(accId==0)
|
||||
return Result.failed("数据库操作失败");
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/saveAccount")
|
||||
@RepeatSubmit
|
||||
public Result<?> saveAccount(@RequestBody @Valid AccountBaseVo acc) {
|
||||
Integer accId = accountService.saveOrUpdateAccount(acc);
|
||||
if(accId==0)
|
||||
return Result.failed("数据库操作失败");
|
||||
else if(accId<0)
|
||||
return Result.failed("该百家乐账号已存在");
|
||||
|
||||
if(!"FS".equals(acc.getBacPlatform())) {
|
||||
String username = SecurityUtils.getUsername();
|
||||
int tmNow = (int) (System.currentTimeMillis() / 1000);
|
||||
Oplog log = new Oplog();
|
||||
log.setOpBy(username);
|
||||
log.setOpTime(tmNow);
|
||||
log.setOperation(acc.getId() == null ? 1 : 2); //新增/修改
|
||||
String desc = "ID" + accId + " AG " + acc.getBacAccount();
|
||||
log.setRemark(desc);
|
||||
oplogService.save(log);
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping(value = "/account/{id}")
|
||||
public Result<?> setAccountStatus(@PathVariable Integer id,@RequestParam Integer status ) {
|
||||
boolean result = accountService.setStatus(id, status);
|
||||
|
||||
if(result && status<2){
|
||||
Account acc = accountService.getById(id);
|
||||
if(!"FS".equals(acc.getBacPlatform())) {
|
||||
String username = SecurityUtils.getUsername();
|
||||
int tmNow = (int) (System.currentTimeMillis() / 1000);
|
||||
Oplog log = new Oplog();
|
||||
log.setOpBy(username);
|
||||
log.setOpTime(tmNow);
|
||||
log.setOperation(status == 1 ? 3 : 4); //禁用/启用
|
||||
String desc = "ID" + acc.getId() + " " + acc.getBacPlatform() + " " + acc.getBacAccount();
|
||||
log.setRemark(desc);
|
||||
oplogService.save(log);
|
||||
}
|
||||
}
|
||||
return Result.judge(result);
|
||||
}
|
||||
|
||||
@GetMapping("/account/{id}")
|
||||
public Result<Account> getAccount(@PathVariable Integer id) {
|
||||
Account acc = accountService.getById(id);
|
||||
return Result.success(acc);
|
||||
}
|
||||
|
||||
@DeleteMapping("/account/{id}")
|
||||
public Result<?> delAccount(@PathVariable Integer id) {
|
||||
Account acc = accountService.getById(id);
|
||||
boolean ret = accountService.removeById(id);
|
||||
if(ret && !"FS".equals(acc.getBacPlatform())){
|
||||
String username = SecurityUtils.getUsername();
|
||||
int tmNow = (int) (System.currentTimeMillis()/1000);
|
||||
Oplog log = new Oplog();
|
||||
log.setOpBy(username);
|
||||
log.setOpTime(tmNow);
|
||||
log.setOperation(8); //禁用/启用
|
||||
String desc = "ID"+acc.getId() + " " + acc.getBacPlatform() + " " + acc.getBacAccount();
|
||||
log.setRemark(desc);
|
||||
oplogService.save(log);
|
||||
}
|
||||
return Result.judge(ret);
|
||||
}
|
||||
|
||||
@GetMapping("/pageExtend")
|
||||
public PageResult<Extend> pageExtend(@ParameterObject ExtendPageQuery query) {
|
||||
IPage<Extend> page = extendService.page(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@PostMapping("/saveExtend")
|
||||
@RepeatSubmit
|
||||
public Result<?> saveExtend(@RequestBody @Valid Extend ext) {
|
||||
Account acc = accountService.getById(ext.getAccId());
|
||||
|
||||
String unBac = acc.getBacAccount();
|
||||
if(unBac.startsWith("164agyl")){
|
||||
return Result.failed("彩乐园账号不再延期");
|
||||
}
|
||||
else if(unBac.startsWith("ngjjb")){
|
||||
return Result.failed("竞技宝账号不再延期");
|
||||
}
|
||||
|
||||
int tmNow = (int) (System.currentTimeMillis()/1000);
|
||||
boolean bRemark = true,
|
||||
bFS = "FS".equals(acc.getBacPlatform());
|
||||
if(ext.getId()==null){ //新增延期
|
||||
int extMon = ext.getExtend();
|
||||
int expire = acc.getExpireTime();
|
||||
if(expire<tmNow)
|
||||
expire = tmNow;
|
||||
expire += extMon * 2629800; //按365.25/12算一个月
|
||||
acc.setExpireTime(expire);
|
||||
|
||||
if(!bFS) {
|
||||
ext.setAccId(acc.getId());
|
||||
ext.setBacAccount(acc.getBacAccount());
|
||||
ext.setBacPlatform(acc.getBacPlatform());
|
||||
ext.setLinkPlatform(acc.getLinkPlatform());
|
||||
ext.setLinkAccount(acc.getLinkAccount());
|
||||
bRemark = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(bFS){
|
||||
accountService.updateById(acc);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
boolean result = extendService.saveOrUpdateExtend(ext);
|
||||
if(result){
|
||||
accountService.updateById(acc); //延后更新用户信息
|
||||
|
||||
String username = SecurityUtils.getUsername();
|
||||
Oplog log = new Oplog();
|
||||
log.setOpBy(username);
|
||||
log.setOpTime(tmNow);
|
||||
log.setOperation(ext.getId()==null ? 5 : 6); //授权/备注
|
||||
String desc = "ID"+acc.getId() + " " + acc.getBacPlatform() + " " + acc.getBacAccount()
|
||||
+ " " + ext.getExtend() + "个月 ";
|
||||
if(ext.getType()==0) desc += "免费";
|
||||
else desc += "收费" + ext.getFee();
|
||||
if(bRemark)
|
||||
desc += " " + ext.getRemark();
|
||||
log.setRemark(desc);
|
||||
// log.setAccId(acc.getId());
|
||||
// log.setBacAccount(acc.getBacAccount());
|
||||
// log.setBacPlatform(acc.getBacPlatform());
|
||||
oplogService.save(log);
|
||||
}
|
||||
return Result.judge(result);
|
||||
}
|
||||
|
||||
@PutMapping(value = "/extend/{id}")
|
||||
public Result<?> cancelExtend(@PathVariable Integer id) {
|
||||
Extend ext = extendService.getById(id);
|
||||
Account acc = accountService.getById(ext.getAccId());
|
||||
int expire = acc.getExpireTime();
|
||||
expire -= ext.getExtend() * 2629800;
|
||||
acc.setExpireTime(expire);
|
||||
|
||||
boolean result = extendService.cancel(id);
|
||||
if(result){
|
||||
accountService.updateById(acc);
|
||||
|
||||
int tmNow = (int) (System.currentTimeMillis()/1000);
|
||||
String username = SecurityUtils.getUsername();
|
||||
Oplog log = new Oplog();
|
||||
log.setOpBy(username);
|
||||
log.setOpTime(tmNow);
|
||||
log.setOperation(7); //撤回
|
||||
String desc = "ID"+acc.getId() + " " + acc.getBacPlatform() + " " + acc.getBacAccount()
|
||||
+ " " + ext.getExtend() + "个月 ";
|
||||
if(ext.getType()==0) desc += "免费";
|
||||
else desc += "收费" + ext.getFee();
|
||||
log.setRemark(desc);
|
||||
// log.setAccId(acc.getId());
|
||||
// log.setBacAccount(acc.getBacAccount());
|
||||
// log.setBacPlatform(acc.getBacPlatform());
|
||||
oplogService.save(log);
|
||||
}
|
||||
return Result.judge(result);
|
||||
}
|
||||
|
||||
@GetMapping("/pageOplog")
|
||||
public PageResult<Oplog> pageExtend(@ParameterObject OplogPageQuery query) {
|
||||
IPage<Oplog> page = oplogService.page(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/pageAmount")
|
||||
public PageResult<Amount> pageAmount(@ParameterObject AmountPageQuery query) {
|
||||
IPage<Amount> page = amountService.page(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/pageAmountUser")
|
||||
public PageResult<AmountUserVo> pageAmountUser(@ParameterObject AmountPageQuery query) {
|
||||
IPage<AmountUserVo> page = amountService.pageUser(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/pageAmountSum")
|
||||
public PageResult<AmountSum> pageAmountSum(@ParameterObject AmountSumPageQuery query) {
|
||||
IPage<AmountSum> page = amoSumService.page(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/pageSoftware")
|
||||
public PageResult<Software> pageSoftware(@ParameterObject BasePageQuery query) {
|
||||
IPage<Software> page = softwareService.page(query);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@PostMapping("/saveSoftware")
|
||||
@RepeatSubmit
|
||||
public Result<?> saveSoftware(@RequestBody @Valid Software sw) {
|
||||
Integer done = softwareService.updateOrSave(sw);
|
||||
if(done<0)
|
||||
return Result.failed("软件编号重复");
|
||||
|
||||
return Result.judge(done==1);
|
||||
}
|
||||
|
||||
@PutMapping(value = "/software/{id}")
|
||||
public Result<?> setSoftware(@PathVariable Integer id,@RequestParam Integer status ) {
|
||||
Software sw = softwareService.getById(id);
|
||||
if(sw==null)
|
||||
return Result.failed("无效的软件编号");
|
||||
|
||||
sw.setDeprecated(status);
|
||||
softwareService.saveOrUpdate(sw);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.rnb.rnb.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.rnb.rnb.model.entity.Account;
|
||||
import com.rnb.rnb.model.query.AccountPageQuery;
|
||||
import com.rnb.rnb.model.vo.AccountBaseVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AccountMapper extends BaseMapper<Account> {
|
||||
Page<AccountBaseVo> pageBase(Page<AccountBaseVo> page, AccountPageQuery queryParams);
|
||||
Page<Account> pageAdvance(Page<Account> page, AccountPageQuery queryParams);
|
||||
Page<Account> pageAdmin(Page<Account> page, AccountPageQuery queryParams);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.rnb.rnb.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.rnb.rnb.model.entity.Amount;
|
||||
import com.rnb.rnb.model.query.AmountPageQuery;
|
||||
import com.rnb.rnb.model.vo.AmountUserVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AmountMapper extends BaseMapper<Amount> {
|
||||
IPage<Amount> page(Page<Amount> page, AmountPageQuery queryParams);
|
||||
IPage<AmountUserVo> pageUser(Page<AmountUserVo> page, AmountPageQuery queryParams);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.rnb.rnb.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.rnb.rnb.model.entity.AmountSum;
|
||||
import com.rnb.rnb.model.query.AmountSumPageQuery;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AmountSumMapper extends BaseMapper<AmountSum> {
|
||||
IPage<AmountSum> pageByDay(Page<AmountSum> page, AmountSumPageQuery queryParams);
|
||||
IPage<AmountSum> pageByWeek(Page<AmountSum> page, AmountSumPageQuery queryParams);
|
||||
IPage<AmountSum> pageByMonth(Page<AmountSum> page, AmountSumPageQuery queryParams);
|
||||
IPage<AmountSum> pageByYear(Page<AmountSum> page, AmountSumPageQuery queryParams);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.rnb.rnb.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.rnb.rnb.model.entity.Extend;
|
||||
import com.rnb.rnb.model.query.ExtendPageQuery;
|
||||
import com.rnb.rnb.model.vo.AccountBaseVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ExtendMapper extends BaseMapper<Extend> {
|
||||
IPage<Extend> page(Page<AccountBaseVo> page, ExtendPageQuery queryParams);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.rnb.rnb.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.rnb.rnb.model.entity.Oplog;
|
||||
import com.rnb.rnb.model.query.OplogPageQuery;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface OplogMapper extends BaseMapper<Oplog> {
|
||||
IPage<Oplog> page(Page<Oplog> page, OplogPageQuery queryParams);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.rnb.rnb.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.rnb.common.base.BasePageQuery;
|
||||
import com.rnb.rnb.model.entity.Software;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SoftwareMapper extends BaseMapper<Software> {
|
||||
// IPage<Software> selectPage(Page<Software> page, BasePageQuery queryParams);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.rnb.rnb.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Account {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
private String bacAccount; //百家乐账号
|
||||
private String bacPlatform; //百家乐平台
|
||||
private String linkAccount; //关联账号
|
||||
private String linkPlatform;//关联平台
|
||||
private Integer expireTime; //有效期
|
||||
private Integer addTime; //添加时间
|
||||
private String addBy; //添加人
|
||||
private Integer editTime; //修改时间
|
||||
private String editBy; //修改人
|
||||
private Integer logTime; //登录时间
|
||||
private Integer logCount; //登录次数
|
||||
private Integer appVer; //app版本
|
||||
private String remark; //注释
|
||||
private Integer amount; //实打流水
|
||||
private Integer status; //状态
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Long deviceId; //设备ID
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.rnb.rnb.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Amount {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
private Integer uid; //用户ID
|
||||
private Integer amount; //流水
|
||||
private Integer reportTime; //上报时间
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.rnb.rnb.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AmountSum {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Integer sid;
|
||||
private Integer amountTotal;
|
||||
private Integer amountHy;
|
||||
private Integer amountCly;
|
||||
private Integer amountAbg;
|
||||
private Integer amountHb;
|
||||
private Integer amountDb;
|
||||
private Integer amountOther;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.rnb.rnb.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Extend {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
private Integer accId; //用户ID
|
||||
private String bacAccount; //百家乐账号
|
||||
private String bacPlatform; //百家乐平台
|
||||
private String linkAccount; //关联账号
|
||||
private String linkPlatform;//关联平台
|
||||
private Integer extend; //延期月数
|
||||
private Integer type; //状态 0=免费 1=收费
|
||||
private Integer fee; //收取费用
|
||||
private String reason; //免费原因
|
||||
private String addBy; //操作人
|
||||
private Integer addTime; //操作时间
|
||||
private Integer cancel; //是否撤销 0=否 1=是
|
||||
private String remark; //注释
|
||||
private String editBy; //修改人
|
||||
private Integer editTime; //修改时间
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user