first commit
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
# 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
|
||||
@@ -0,0 +1,111 @@
|
||||
# AGENTS.md
|
||||
|
||||
> 本文件用于帮助后续 OpenCode 会话快速了解本仓库,避免常见错误。
|
||||
|
||||
## 项目概况
|
||||
|
||||
- **技术栈**:Java 17 + Spring Boot 3.3.2 + Spring Security 6 + MyBatis-Plus 3.5.7
|
||||
- **构建工具**:Maven(单模块 POM)
|
||||
- **应用入口**:`com.ichangzuo.iczApplication`
|
||||
- **默认端口**:8989(dev/test)/ 9999(prod)
|
||||
- **接口文档**:启动后访问 `http://localhost:8989/doc.html`(Knife4j)
|
||||
|
||||
## 运行环境依赖
|
||||
|
||||
- **MySQL**:数据库名 `music`,开发配置在 `application-dev.yml`
|
||||
- **Redis**:端口 16378,开发环境密码 `CZwy168`
|
||||
- **运行时 Profile**:
|
||||
- `dev`(默认):本地开发
|
||||
- `test`:测试环境(数据库 `192.168.5.2`)
|
||||
- `prod`:生产环境
|
||||
|
||||
> 注意:`application.yml` 中默认激活的是 `prod`,但 Maven 的 `dev` profile 是默认激活的。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 开发模式启动(使用 dev profile)
|
||||
mvn spring-boot:run -P dev
|
||||
|
||||
# 打包(默认 dev)
|
||||
mvn clean package
|
||||
|
||||
# 测试环境打包
|
||||
mvn clean package -P test
|
||||
|
||||
# 生产环境打包
|
||||
mvn clean package -P prod
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 跳过测试打包
|
||||
mvn clean package -DskipTests
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/main/java/com/ichangzuo/
|
||||
iczApplication.java # 启动类(注意不是 YouLaiApplication)
|
||||
common/ # 公共模块:注解、常量、枚举、异常、工具类
|
||||
config/ # 自动装配配置(CORS、Redis、Security、Swagger 等)
|
||||
core/ # 核心功能:切面(日志、防重提交)、过滤器、Security
|
||||
module/ # 业务模块
|
||||
auth/ # 认证模块
|
||||
codegen/ # 代码生成器
|
||||
file/ # 文件模块
|
||||
mail/ # 邮件模块
|
||||
sms/ # 短信模块
|
||||
websocket/ # WebSocket 模块
|
||||
system/ # 系统管理模块:用户、角色、菜单、部门、字典等
|
||||
controller/
|
||||
converter/ # MapStruct 转换器
|
||||
event/ # 事件处理
|
||||
handler/
|
||||
listener/
|
||||
model/ # bo, dto, entity, form, query, vo
|
||||
mapper/
|
||||
service/
|
||||
```
|
||||
|
||||
## 关键配置与约定
|
||||
|
||||
### 数据库与 ORM
|
||||
|
||||
- **MyBatis-Plus**:
|
||||
- 逻辑删除字段:`is_deleted`(删除=1,未删除=0)
|
||||
- 主键 ID 类型:`none`
|
||||
- 驼峰下划线自动转换已开启
|
||||
- **数据源**:Druid 连接池
|
||||
|
||||
### 安全与认证
|
||||
|
||||
- 基于 Spring Security + JWT 的无状态认证
|
||||
- JWT 密钥配置在 `application.yml` 的 `security.jwt.key`
|
||||
- 部分接口在白名单中(如 `/api/v1/auth/**`、`/client/**` 等),无需认证
|
||||
|
||||
### 文件存储
|
||||
|
||||
- 支持 `minio` 和 `aliyun` 两种类型,通过 `oss.type` 切换
|
||||
- 当前测试/生产环境使用 `minio`
|
||||
|
||||
### 缓存
|
||||
|
||||
- Spring Cache + Redis,但当前 `spring.cache.enabled=false`
|
||||
|
||||
### 定时任务
|
||||
|
||||
- 集成 XXL-JOB,但默认 `enabled: false`
|
||||
|
||||
### 支付
|
||||
|
||||
- 集成支付宝和微信支付,配置在 `pay.ali` 和 `pay.wx` 下
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
1. **启动类名称**:实际启动类是 `iczApplication`,不是 README 中提到的 `YouLaiApplication.java`
|
||||
2. **MapStruct**:项目使用 MapStruct 进行实体转换,配合 Lombok 使用,需确保 `lombok-mapstruct-binding` 依赖存在
|
||||
3. **Undertow 替代 Tomcat**:`spring-boot-starter-tomcat` 被排除,使用 `spring-boot-starter-undertow`
|
||||
4. **打包配置**:`spring-boot-maven-plugin` 配置了 `ZIP` layout,并排除了 Lombok
|
||||
5. **Maven Profile 优先级**:`dev` profile 默认激活,但 Spring 的 `application.yml` 默认激活 `prod`。实际运行时以 Spring 的 `spring.profiles.active` 为准
|
||||
@@ -0,0 +1,60 @@
|
||||
# 2.7.1 (2024/4/18)
|
||||
### 🐛 fix
|
||||
- 修复用户名或者密码错误时,返回的错误信息不正确问题
|
||||
### 🛠️ refactor
|
||||
- JWT 解析和验证代码优化重构
|
||||
- 优化代码结构和完善注释,提高代码可读性
|
||||
|
||||
# 2.7.0 (2024/4/13)
|
||||
### ✨ feat
|
||||
- 集成 Mybatis-Plus generator 代码生成器
|
||||
|
||||
# 2.6.0 (2024/3/6)
|
||||
|
||||
### ✨ feat
|
||||
- 黑名单方式实现 JWT 主动注销过期
|
||||
### 🛠️ refactor
|
||||
- 角色权限重构
|
||||
|
||||
|
||||
# 2.5.0 (2023/12/6)
|
||||
### ✨ feat
|
||||
- [集成 Spring Cache 和 Redis 缓存,路由缓存](https://blog.csdn.net/u013737132/article/details/134789862)
|
||||
### 🛠️ refactor
|
||||
- 权限判断逻辑调整,用户绑定权限调整为角色绑定权限
|
||||
### fix
|
||||
- [接口无请求权限,Spring Security 自定义异常无效问题修复](https://youlai.blog.csdn.net/article/details/134718249)
|
||||
|
||||
|
||||
# 2.4.1 (2023/11/7)
|
||||
### ✂️ refactor
|
||||
- 项目目录结构优化
|
||||
### ⬆️ chore
|
||||
- 升级 SpringBoot 版本 `3.1.4` → `3.1.5`
|
||||
|
||||
|
||||
# 2.2.1 (2023/5/25)
|
||||
|
||||
### 🐛 fix
|
||||
|
||||
- 修复多级路由的组件路径错误导致页面404问题
|
||||
|
||||
# 2.2.0 (2023/5/21)
|
||||
|
||||
### ✨ feat
|
||||
- 菜单、角色、字典、部门添加接口权限控制
|
||||
|
||||
### 🐛 fix
|
||||
|
||||
- 用户登录权限缓存键值不一致导致获取用户数据权限错误问题修复
|
||||
|
||||
### ✂️ refactor
|
||||
|
||||
- 递归获取菜单、部门属性列表代码重构优化
|
||||
|
||||
### ⬆️ chore
|
||||
- 升级 SpringBoot 版本 `3.0.6` → `3.1.0`
|
||||
|
||||
### 📝 docs
|
||||
- SQL 脚本更新,sys_menu 新增 `tree_path` 字段 (升级需更新SQL脚本)
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# 基础镜像
|
||||
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 目录为匿名卷,提高可移植性
|
||||
VOLUME /tmp
|
||||
|
||||
# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar
|
||||
ADD target/youlai-boot.jar app.jar
|
||||
|
||||
# 指定容器启动时执行的命令
|
||||
CMD java \
|
||||
-Djava.security.egd=file:/dev/./urandom \
|
||||
-jar /app.jar
|
||||
|
||||
# 暴露容器的端口
|
||||
EXPOSE 8989
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023 有来开源组织
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
<div align="center">
|
||||
<h2>server-java</h2>
|
||||
<img alt="Java" src="https://img.shields.io/badge/Java-17-brightgreen.svg"/>
|
||||
<img alt="SpringBoot" src="https://img.shields.io/badge/SpringBoot-3.3.2-green.svg"/>
|
||||
<img alt="license" src="https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg"/>
|
||||
</div>
|
||||
|
||||
基于 JDK 17、Spring Boot 3.3.2、Spring Security 6、JWT、Redis、MyBatis-Plus、Knife4j 构建的前后端分离单体业务系统。
|
||||
|
||||
## 项目概况
|
||||
|
||||
- **开发框架**:Spring Boot 3.3.2 + Spring Security 6 + MyBatis-Plus 3.5.7
|
||||
- **安全认证**:Spring Security + JWT 无状态认证,部分接口白名单免登录
|
||||
- **权限管理**:基于 RBAC 模型,细粒度控制到接口方法和按钮级别
|
||||
- **功能模块**:用户管理、角色管理、菜单管理、部门管理、字典管理、代码生成、文件管理、邮件、短信、支付(支付宝/微信)等
|
||||
- **接口文档**:Knife4j 自动生成,支持在线调试
|
||||
|
||||
## 项目目录
|
||||
|
||||
```
|
||||
src/main/java/com/ichangzuo/
|
||||
iczApplication.java # 启动类
|
||||
common/ # 公共模块:注解、常量、枚举、异常、工具类
|
||||
config/ # 自动装配配置(CORS、Redis、Security、Swagger 等)
|
||||
core/ # 核心功能:切面(日志、防重提交)、过滤器、Security
|
||||
module/ # 业务模块
|
||||
auth/ # 认证模块
|
||||
codegen/ # 代码生成器
|
||||
file/ # 文件模块
|
||||
mail/ # 邮件模块
|
||||
sms/ # 短信模块
|
||||
websocket/ # WebSocket 模块
|
||||
system/ # 系统管理模块:用户、角色、菜单、部门、字典等
|
||||
controller/
|
||||
converter/ # MapStruct 转换器
|
||||
event/ # 事件处理
|
||||
handler/
|
||||
listener/
|
||||
model/ # bo, dto, entity, form, query, vo
|
||||
mapper/
|
||||
service/
|
||||
```
|
||||
|
||||
## 技术选型
|
||||
|
||||
| 技术 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| JDK | 17 | |
|
||||
| Spring Boot | 3.3.2 | 开发框架 |
|
||||
| Spring Security | 6 | 安全认证 |
|
||||
| MyBatis-Plus | 3.5.7 | ORM 框架 |
|
||||
| Knife4j | 4.5.0 | 接口文档 |
|
||||
| MapStruct | 1.5.5.Final | 对象映射 |
|
||||
| Druid | 1.2.23 | 数据库连接池 |
|
||||
| XXL-JOB | 2.4.2 | 定时任务 |
|
||||
| Redisson | 3.30.0 | 分布式锁 |
|
||||
|
||||
## 环境依赖
|
||||
|
||||
- **MySQL**:数据库名 `music`,开发配置在 `application-dev.yml`
|
||||
- **Redis**:端口 16378
|
||||
- **运行时 Profile**:
|
||||
- `dev`(默认):本地开发
|
||||
- `test`:测试环境
|
||||
- `prod`:生产环境
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置数据库
|
||||
|
||||
修改 `src/main/resources/application-dev.yml` 中的 MySQL、Redis 连接配置。
|
||||
|
||||
### 2. 启动项目
|
||||
|
||||
```bash
|
||||
# 开发模式启动(使用 dev profile)
|
||||
mvn spring-boot:run -P dev
|
||||
```
|
||||
|
||||
### 3. 验证启动
|
||||
|
||||
访问接口文档地址 [http://localhost:8989/doc.html](http://localhost:8989/doc.html) 验证项目启动是否成功。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 开发模式启动
|
||||
mvn spring-boot:run -P dev
|
||||
|
||||
# 打包(默认 dev)
|
||||
mvn clean package
|
||||
|
||||
# 测试环境打包
|
||||
mvn clean package -P test
|
||||
|
||||
# 生产环境打包
|
||||
mvn clean package -P prod
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 跳过测试打包
|
||||
mvn clean package -DskipTests
|
||||
```
|
||||
|
||||
## 接口文档
|
||||
|
||||
- `knife4j` 接口文档:[http://localhost:8989/doc.html](http://localhost:8989/doc.html)
|
||||
- `swagger` 接口文档:[http://localhost:8989/swagger-ui/index.html](http://localhost:8989/swagger-ui/index.html)
|
||||
@@ -0,0 +1,323 @@
|
||||
<?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.ichangzuo</groupId>
|
||||
<artifactId>server-java</artifactId>
|
||||
<version>1.9</version>
|
||||
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的唱作网系统。</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.2</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.27</hutool.version>
|
||||
|
||||
<mysql.version>8.0.28</mysql.version>
|
||||
<druid.version>1.2.23</druid.version>
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
|
||||
<mapstruct.version>1.5.5.Final</mapstruct.version>
|
||||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
||||
|
||||
<xxl-job.version>2.4.2</xxl-job.version>
|
||||
|
||||
<easyexcel.version>3.2.1</easyexcel.version>
|
||||
|
||||
<!-- 对象存储 -->
|
||||
<minio.version>8.5.10</minio.version>
|
||||
<okhttp3.version>4.8.1</okhttp3.version>
|
||||
|
||||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
||||
|
||||
<!-- redisson 分布式锁 -->
|
||||
<redisson.version>3.30.0</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>
|
||||
</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.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>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>${mysql.version}</version>
|
||||
</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>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<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>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.xuxueli</groupId>
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
<version>${xxl-job.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
<version>${easyexcel.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MinIO 对象存储 -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>io.minio</groupId>-->
|
||||
<!-- <artifactId>minio</artifactId>-->
|
||||
<!-- <version>${minio.version}</version>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!-- <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>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||
<!-- <artifactId>spring-boot-starter-websocket</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.baomidou</groupId>-->
|
||||
<!-- <artifactId>mybatis-plus-generator</artifactId>-->
|
||||
<!-- <version>${mybatis-plus-generator.version}</version>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.apache.velocity</groupId>-->
|
||||
<!-- <artifactId>velocity-engine-core</artifactId>-->
|
||||
<!-- <version>${velocity.version}</version>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.lionsoul</groupId>
|
||||
<artifactId>ip2region</artifactId>
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</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.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.40.8.ALL</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.wechatpay-apiv3</groupId>
|
||||
<artifactId>wechatpay-java</artifactId>
|
||||
<version>0.2.15</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.1</version>
|
||||
</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>
|
||||
<profile>
|
||||
<id>test</id>
|
||||
<properties>
|
||||
<spring.profiles.active>test</spring.profiles.active>
|
||||
</properties>
|
||||
<build>
|
||||
<finalName>${project.artifactId}-${project.version}-test</finalName>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<!-- <finalName>${project.artifactId}-${project.version}</finalName>-->
|
||||
<plugins>
|
||||
<!-- maven 打包时跳过测试 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</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,28 @@
|
||||
package com.ichangzuo.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,23 @@
|
||||
package com.ichangzuo.common.annotation;
|
||||
|
||||
import com.ichangzuo.common.enums.LogModuleEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 日志注解
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/6/25
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
public @interface Log {
|
||||
|
||||
String value() default "";
|
||||
|
||||
LogModuleEnum module() ;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ichangzuo.common.annotation;
|
||||
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 防止重复提交注解
|
||||
* <p>
|
||||
* 该注解用于方法上,防止在指定时间内的重复提交。
|
||||
* 默认时间为5秒。
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
public @interface RepeatSubmit {
|
||||
|
||||
/**
|
||||
* 锁过期时间(秒)
|
||||
* <p>
|
||||
* 默认5秒内不允许重复提交
|
||||
*/
|
||||
int expire() default 5;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ichangzuo.common.base;
|
||||
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
|
||||
/**
|
||||
* 自定义解析结果监听器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/03/01
|
||||
*/
|
||||
public abstract class BaseAnalysisEventListener<T> extends AnalysisEventListener<T> {
|
||||
|
||||
private String msg;
|
||||
public abstract String getMsg();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.ichangzuo.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;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@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,22 @@
|
||||
package com.ichangzuo.common.base;
|
||||
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 基础分页请求对象
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2021/2/28
|
||||
*/
|
||||
@Data
|
||||
@Schema
|
||||
public class BasePageQuery {
|
||||
|
||||
@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.ichangzuo.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.ichangzuo.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.ichangzuo.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,43 @@
|
||||
package com.ichangzuo.common.constant;
|
||||
|
||||
/**
|
||||
* Redis Key常量
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-7-29 11:46:08
|
||||
*/
|
||||
public interface RedisConstants {
|
||||
|
||||
/**
|
||||
* 系统配置Redis-key
|
||||
*/
|
||||
String SYSTEM_CONFIG_KEY = "system:config";
|
||||
|
||||
/**
|
||||
* IP限流Redis-key
|
||||
*/
|
||||
String IP_RATE_LIMITER_KEY = "ip:rate:limiter:";
|
||||
|
||||
/**
|
||||
* 防重复提交Redis-key
|
||||
*/
|
||||
String RESUBMIT_LOCK_PREFIX = "resubmit:lock:";
|
||||
|
||||
/**
|
||||
* 单个IP请求的最大每秒查询数(QPS)阈值Key
|
||||
*/
|
||||
String IP_QPS_THRESHOLD_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
|
||||
|
||||
/**
|
||||
* 手机验证码缓存前缀
|
||||
*/
|
||||
|
||||
String MOBILE_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:MOBILE:";
|
||||
|
||||
|
||||
/**
|
||||
* 邮箱验证码缓存前缀
|
||||
*/
|
||||
String EMAIL_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:EMAIL:";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ichangzuo.common.constant;
|
||||
|
||||
/**
|
||||
* 缓存常量
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
public interface SecurityConstants {
|
||||
|
||||
/**
|
||||
* 验证码缓存前缀
|
||||
*/
|
||||
String CAPTCHA_CODE_PREFIX = "captcha_code:";
|
||||
|
||||
/**
|
||||
* 角色和权限缓存前缀
|
||||
*/
|
||||
String ROLE_PERMS_PREFIX = "role_perms:";
|
||||
|
||||
/**
|
||||
* 黑名单Token缓存前缀
|
||||
*/
|
||||
String BLACKLIST_TOKEN_PREFIX = "token:blacklist:";
|
||||
|
||||
|
||||
/**
|
||||
* 登录路径
|
||||
*/
|
||||
String LOGIN_PATH = "/api/v1/auth/login";
|
||||
/**
|
||||
* JWT Token 前缀
|
||||
*/
|
||||
String JWT_TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
String REGISTER_PATH = "/api/v1/auth/register";
|
||||
String AUTOLOGIN_PATH = "/api/v1/auth/autoLogin";
|
||||
String SEND_REG_CODE = "/api/v1/auth/sendRegCode";
|
||||
String SEND_BIND_CODE = "api/v1/users/sendBindCode";
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.ichangzuo.common.constant;
|
||||
|
||||
/**
|
||||
* 符号和特殊符号常用类
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-7-29 11:46:08
|
||||
*/
|
||||
public interface SymbolConstant {
|
||||
|
||||
/**
|
||||
* 符号:点
|
||||
*/
|
||||
String SPOT = ".";
|
||||
|
||||
/**
|
||||
* 符号:双斜杠
|
||||
*/
|
||||
String DOUBLE_BACKSLASH = "\\";
|
||||
|
||||
/**
|
||||
* 符号:冒号
|
||||
*/
|
||||
String COLON = ":";
|
||||
|
||||
/**
|
||||
* 符号:逗号
|
||||
*/
|
||||
String COMMA = ",";
|
||||
|
||||
/**
|
||||
* 符号:左花括号 {
|
||||
*/
|
||||
String LEFT_CURLY_BRACKET = "{";
|
||||
|
||||
/**
|
||||
* 符号:右花括号 }
|
||||
*/
|
||||
String RIGHT_CURLY_BRACKET = "}";
|
||||
|
||||
/**
|
||||
* 符号:井号 #
|
||||
*/
|
||||
String WELL_NUMBER = "#";
|
||||
|
||||
/**
|
||||
* 符号:单斜杠
|
||||
*/
|
||||
String SINGLE_SLASH = "/";
|
||||
|
||||
/**
|
||||
* 符号:双斜杠
|
||||
*/
|
||||
String DOUBLE_SLASH = "//";
|
||||
|
||||
/**
|
||||
* 符号:感叹号
|
||||
*/
|
||||
String EXCLAMATORY_MARK = "!";
|
||||
|
||||
/**
|
||||
* 符号:下划线
|
||||
*/
|
||||
String UNDERLINE = "_";
|
||||
|
||||
/**
|
||||
* 符号:单引号
|
||||
*/
|
||||
String SINGLE_QUOTATION_MARK = "'";
|
||||
|
||||
/**
|
||||
* 符号:星号
|
||||
*/
|
||||
String ASTERISK = "*";
|
||||
|
||||
/**
|
||||
* 符号:百分号
|
||||
*/
|
||||
String PERCENT_SIGN = "%";
|
||||
|
||||
/**
|
||||
* 符号:美元 $
|
||||
*/
|
||||
String DOLLAR = "$";
|
||||
|
||||
/**
|
||||
* 符号:和 &
|
||||
*/
|
||||
String AND = "&";
|
||||
|
||||
/**
|
||||
* 符号:../
|
||||
*/
|
||||
String SPOT_SINGLE_SLASH = "../";
|
||||
|
||||
/**
|
||||
* 符号:..\\
|
||||
*/
|
||||
String SPOT_DOUBLE_BACKSLASH = "..\\";
|
||||
|
||||
/**
|
||||
* 系统变量前缀 #{
|
||||
*/
|
||||
String SYS_VAR_PREFIX = "#{";
|
||||
|
||||
/**
|
||||
* 符号 {{
|
||||
*/
|
||||
String DOUBLE_LEFT_CURLY_BRACKET = "{{";
|
||||
|
||||
/**
|
||||
* 符号:[
|
||||
*/
|
||||
String SQUARE_BRACKETS_LEFT = "[";
|
||||
|
||||
/**
|
||||
* 符号:]
|
||||
*/
|
||||
String SQUARE_BRACKETS_RIGHT = "]";
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ichangzuo.common.constant;
|
||||
|
||||
/**
|
||||
* 系统常量
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface SystemConstants {
|
||||
|
||||
/**
|
||||
* 根节点ID
|
||||
*/
|
||||
Long ROOT_NODE_ID = 0L;
|
||||
|
||||
/**
|
||||
* 系统默认密码
|
||||
*/
|
||||
String DEFAULT_PASSWORD = "123456";
|
||||
|
||||
/**
|
||||
* 超级管理员角色编码
|
||||
*/
|
||||
String ROOT_ROLE_CODE = "ROOT";
|
||||
|
||||
String REGREX_EMAIL = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
|
||||
String REGREX_PHONE = "^$|^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$";// "^1[3-9]\\d{9}$";
|
||||
String TEMPLATE_MAIL_SENDCODE = "<p>您此次 <b>${sub}</b> 的验证码是:</p><p><h2>${code}</h2></p><p>验证码将于此电子邮件发出 15 分钟后过期。</p>";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
/**
|
||||
* EasyCaptcha 验证码类型枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.5.1
|
||||
*/
|
||||
public enum CaptchaTypeEnum {
|
||||
|
||||
/**
|
||||
* 圆圈干扰验证码
|
||||
*/
|
||||
CIRCLE,
|
||||
/**
|
||||
* GIF验证码
|
||||
*/
|
||||
GIF,
|
||||
/**
|
||||
* 干扰线验证码
|
||||
*/
|
||||
LINE,
|
||||
/**
|
||||
* 扭曲干扰验证码
|
||||
*/
|
||||
SHEAR
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
/**
|
||||
* 联系方式类型
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public enum ContactType {
|
||||
/**
|
||||
* 手机
|
||||
*/
|
||||
MOBILE,
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
EMAIL
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
import com.ichangzuo.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据权限枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Getter
|
||||
public enum DataScopeEnum implements IBaseEnum<Integer> {
|
||||
|
||||
/**
|
||||
* value 越小,数据权限范围越大
|
||||
*/
|
||||
ALL(0, "所有数据"),
|
||||
DEPT_AND_SUB(1, "部门及子部门数据"),
|
||||
DEPT(2, "本部门数据"),
|
||||
SELF(3, "本人数据");
|
||||
|
||||
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.ichangzuo.common.enums;
|
||||
|
||||
import com.ichangzuo.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,84 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.ichangzuo.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 表单类型枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum FormTypeEnum implements IBaseEnum<Integer> {
|
||||
|
||||
/**
|
||||
* 输入框
|
||||
*/
|
||||
INPUT(1, "输入框"),
|
||||
|
||||
/**
|
||||
* 下拉框
|
||||
*/
|
||||
SELECT(2, "下拉框"),
|
||||
|
||||
/**
|
||||
* 单选框
|
||||
*/
|
||||
RADIO(3, "单选框"),
|
||||
|
||||
/**
|
||||
* 复选框
|
||||
*/
|
||||
CHECK_BOX(4, "复选框"),
|
||||
|
||||
/**
|
||||
* 数字输入框
|
||||
*/
|
||||
INPUT_NUMBER(5, "数字输入框"),
|
||||
|
||||
/**
|
||||
* 开关
|
||||
*/
|
||||
SWITCH(6, "开关"),
|
||||
|
||||
/**
|
||||
* 文本域
|
||||
*/
|
||||
TEXT_AREA(7, "文本域"),
|
||||
|
||||
/**
|
||||
* 日期时间框
|
||||
*/
|
||||
DATE(8, "日期框"),
|
||||
|
||||
/**
|
||||
* 日期框
|
||||
*/
|
||||
DATE_TIME(9, "日期时间框");
|
||||
|
||||
|
||||
// Mybatis-Plus 提供注解表示插入数据库时插入该值
|
||||
@EnumValue
|
||||
@JsonValue
|
||||
private final Integer value;
|
||||
|
||||
// @JsonValue // 表示对枚举序列化时返回此字段
|
||||
private final String label;
|
||||
|
||||
|
||||
@JsonCreator
|
||||
public static QueryTypeEnum fromValue(Integer value) {
|
||||
for (QueryTypeEnum type : QueryTypeEnum.values()) {
|
||||
if (type.getValue().equals(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No enum constant with value " + value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
import com.ichangzuo.common.base.IBaseEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 性别枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/14
|
||||
*/
|
||||
@Getter
|
||||
@Schema(enumAsRef = true)
|
||||
public enum GenderEnum implements IBaseEnum<Integer> {
|
||||
|
||||
UNSET(0, ""),
|
||||
MALE(1, "男"),
|
||||
FEMALE (2, "女"),
|
||||
COMB(3,"组合"),
|
||||
OTHER(4,"其他"),
|
||||
UNKNOWN(5,"未知");
|
||||
|
||||
private final Integer value;
|
||||
|
||||
private final String label;
|
||||
|
||||
GenderEnum(Integer value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 表单类型枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Getter
|
||||
public enum JavaTypeEnum {
|
||||
|
||||
VARCHAR("varchar", "String", "string"),
|
||||
CHAR("char", "String", "string"),
|
||||
BLOB("blob", "byte[]", "Uint8Array"),
|
||||
TEXT("text", "String", "string"),
|
||||
JSON("json", "String", "any"),
|
||||
INTEGER("int", "Integer", "number"),
|
||||
TINYINT("tinyint", "Integer", "number"),
|
||||
SMALLINT("smallint", "Integer", "number"),
|
||||
MEDIUMINT("mediumint", "Integer", "number"),
|
||||
BIGINT("bigint", "Long", "bigint"),
|
||||
FLOAT("float", "Float", "number"),
|
||||
DOUBLE("double", "Double", "number"),
|
||||
DECIMAL("decimal", "BigDecimal", "number"),
|
||||
DATE("date", "LocalDate", "Date"),
|
||||
DATETIME("datetime", "LocalDateTime", "Date");
|
||||
|
||||
// 数据库类型
|
||||
private final String dbType;
|
||||
// Java类型
|
||||
private final String javaType;
|
||||
// TypeScript类型
|
||||
private final String tsType;
|
||||
|
||||
// 数据库类型和Java类型的映射
|
||||
private static final Map<String, JavaTypeEnum> typeMap = new HashMap<>();
|
||||
|
||||
// 初始化映射关系
|
||||
static {
|
||||
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
|
||||
typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum);
|
||||
}
|
||||
}
|
||||
|
||||
JavaTypeEnum(String dbType, String javaType, String tsType) {
|
||||
this.dbType = dbType;
|
||||
this.javaType = javaType;
|
||||
this.tsType = tsType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据库类型获取对应的Java类型
|
||||
*
|
||||
* @param columnType 列类型
|
||||
* @return 对应的Java类型
|
||||
*/
|
||||
public static String getJavaTypeByColumnType(String columnType) {
|
||||
JavaTypeEnum javaTypeEnum = typeMap.get(columnType);
|
||||
if (javaTypeEnum != null) {
|
||||
return javaTypeEnum.getJavaType();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Java类型获取对应的TypeScript类型
|
||||
*
|
||||
* @param javaType Java类型
|
||||
* @return 对应的TypeScript类型
|
||||
*/
|
||||
public static String getTsTypeByJavaType(String javaType) {
|
||||
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
|
||||
if (javaTypeEnum.getJavaType().equals(javaType)) {
|
||||
return javaTypeEnum.getTsType();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ichangzuo.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 {
|
||||
|
||||
LOGIN("登录"),
|
||||
USER("用户"),
|
||||
DEPT("部门"),
|
||||
ROLE("角色"),
|
||||
MENU("菜单"),
|
||||
DICT("字典"),
|
||||
OTHER("其他")
|
||||
;
|
||||
|
||||
@JsonValue
|
||||
private final String moduleName;
|
||||
|
||||
LogModuleEnum(String moduleName) {
|
||||
this.moduleName = moduleName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.ichangzuo.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 菜单类型枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/4/23 9:36
|
||||
*/
|
||||
@Getter
|
||||
public enum MenuTypeEnum implements IBaseEnum<Integer> {
|
||||
|
||||
NULL(0, null),
|
||||
MENU(1, "菜单"),
|
||||
CATALOG(2, "目录"),
|
||||
EXTLINK(3, "外链"),
|
||||
BUTTON(4, "按钮");
|
||||
|
||||
// Mybatis-Plus 提供注解表示插入数据库时插入该值
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
|
||||
// @JsonValue // 表示对枚举序列化时返回此字段
|
||||
private final String label;
|
||||
|
||||
MenuTypeEnum(Integer value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.ichangzuo.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 查询类型枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum QueryTypeEnum implements IBaseEnum<Integer> {
|
||||
|
||||
/** 等于 */
|
||||
EQ(1, "="),
|
||||
|
||||
/** 模糊匹配 */
|
||||
LIKE(2, "LIKE '%s%'"),
|
||||
|
||||
/** 包含 */
|
||||
IN(3, "IN"),
|
||||
|
||||
/** 范围 */
|
||||
BETWEEN(4, "BETWEEN"),
|
||||
|
||||
/** 大于 */
|
||||
GT(5, ">"),
|
||||
|
||||
/** 大于等于 */
|
||||
GE(6, ">="),
|
||||
|
||||
/** 小于 */
|
||||
LT(7, "<"),
|
||||
|
||||
/** 小于等于 */
|
||||
LE(8, "<="),
|
||||
|
||||
/** 不等于 */
|
||||
NE(9, "!="),
|
||||
|
||||
/** 左模糊匹配 */
|
||||
LIKE_LEFT(10, "LIKE '%s'"),
|
||||
|
||||
/** 右模糊匹配 */
|
||||
LIKE_RIGHT(11, "LIKE 's%'");
|
||||
|
||||
|
||||
// 存储在数据库中的枚举属性值
|
||||
@EnumValue
|
||||
@JsonValue
|
||||
private final Integer value;
|
||||
|
||||
// 序列化成 JSON 时的属性值
|
||||
private final String label;
|
||||
|
||||
|
||||
@JsonCreator
|
||||
public static QueryTypeEnum fromValue(Integer value) {
|
||||
for (QueryTypeEnum type : QueryTypeEnum.values()) {
|
||||
if (type.getValue().equals(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No enum constant with value " + value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ichangzuo.common.enums;
|
||||
|
||||
import com.ichangzuo.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 状态枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/14
|
||||
*/
|
||||
public enum StatusEnum implements IBaseEnum<Integer> {
|
||||
|
||||
ENABLE(1, "启用"),
|
||||
DISABLE (0, "禁用");
|
||||
|
||||
@Getter
|
||||
private Integer value;
|
||||
|
||||
@Getter
|
||||
private String label;
|
||||
|
||||
StatusEnum(Integer value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ichangzuo.common.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
public class AccountDeletedException extends AuthenticationException {
|
||||
public AccountDeletedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ichangzuo.common.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
public class AccountLockedException extends AuthenticationException {
|
||||
public AccountLockedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.ichangzuo.common.exception;
|
||||
|
||||
import com.ichangzuo.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(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,219 @@
|
||||
package com.ichangzuo.common.exception;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.ichangzuo.common.result.Result;
|
||||
import com.ichangzuo.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状态码,丰富异常处理类型
|
||||
*
|
||||
* @author Gadfly
|
||||
* @since 2020-02-25 13:54
|
||||
**/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@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.PARAM_ERROR, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* RequestParam参数的校验
|
||||
*
|
||||
* @param e
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
@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.PARAM_ERROR, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* RequestBody参数的校验
|
||||
*
|
||||
* @param e
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
@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.PARAM_ERROR, msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public <T> Result<T> processException(NoHandlerFoundException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(ResultCode.RESOURCE_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* MissingServletRequestParameterException
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(MissingServletRequestParameterException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(ResultCode.PARAM_IS_NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* MethodArgumentTypeMismatchException
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(ResultCode.PARAM_ERROR, "类型错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@ExceptionHandler(TypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> processException(TypeMismatchException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
@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.FORBIDDEN_OPERATION);
|
||||
} else {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(SQLSyntaxErrorException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <T> Result<T> handleBizException(BusinessException e) {
|
||||
log.error("biz exception: {}", e.getMessage());
|
||||
if (e.getResultCode() != null) {
|
||||
return Result.failed(e.getResultCode());
|
||||
}
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
@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.getMessage());
|
||||
e.printStackTrace();
|
||||
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,32 @@
|
||||
package com.ichangzuo.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 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,42 @@
|
||||
package com.ichangzuo.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;
|
||||
}
|
||||
|
||||
@Schema(description="选项的值")
|
||||
private T value;
|
||||
|
||||
@Schema(description="选项的标签")
|
||||
private String label;
|
||||
|
||||
@Schema(description="子选项列表")
|
||||
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
|
||||
private List<Option<T>> children;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.ichangzuo.common.result;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 操作消息提醒
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class AjaxResult extends HashMap<String, Object>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 状态码 */
|
||||
public static final String CODE_TAG = "code";
|
||||
|
||||
/** 返回内容 */
|
||||
public static final String MSG_TAG = "msg";
|
||||
|
||||
/** 数据对象 */
|
||||
public static final String DATA_TAG = "data";
|
||||
|
||||
/**
|
||||
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
|
||||
*/
|
||||
public AjaxResult()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化一个新创建的 AjaxResult 对象
|
||||
*
|
||||
* @param code 状态码
|
||||
* @param msg 返回内容
|
||||
*/
|
||||
public AjaxResult(int code, String msg)
|
||||
{
|
||||
super.put(CODE_TAG, code);
|
||||
super.put(MSG_TAG, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化一个新创建的 AjaxResult 对象
|
||||
*
|
||||
* @param code 状态码
|
||||
* @param msg 返回内容
|
||||
* @param data 数据对象
|
||||
*/
|
||||
public AjaxResult(int code, String msg, Object data)
|
||||
{
|
||||
super.put(CODE_TAG, code);
|
||||
super.put(MSG_TAG, msg);
|
||||
if (data!=null)
|
||||
{
|
||||
super.put(DATA_TAG, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回成功消息
|
||||
*
|
||||
* @return 成功消息
|
||||
*/
|
||||
public static AjaxResult success()
|
||||
{
|
||||
return AjaxResult.success("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回成功数据
|
||||
*
|
||||
* @return 成功消息
|
||||
*/
|
||||
public static AjaxResult success(Object data)
|
||||
{
|
||||
return AjaxResult.success("操作成功", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回成功消息
|
||||
*
|
||||
* @param msg 返回内容
|
||||
* @return 成功消息
|
||||
*/
|
||||
public static AjaxResult success(String msg)
|
||||
{
|
||||
return AjaxResult.success(msg, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回成功消息
|
||||
*
|
||||
* @param msg 返回内容
|
||||
* @param data 数据对象
|
||||
* @return 成功消息
|
||||
*/
|
||||
public static AjaxResult success(String msg, Object data)
|
||||
{
|
||||
return new AjaxResult(HttpStatus.OK.value(), msg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误消息
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static AjaxResult error()
|
||||
{
|
||||
return AjaxResult.error("操作失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误消息
|
||||
*
|
||||
* @param msg 返回内容
|
||||
* @return 警告消息
|
||||
*/
|
||||
public static AjaxResult error(String msg)
|
||||
{
|
||||
return AjaxResult.error(msg, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误消息
|
||||
*
|
||||
* @param msg 返回内容
|
||||
* @param data 数据对象
|
||||
* @return 警告消息
|
||||
*/
|
||||
public static AjaxResult error(String msg, Object data)
|
||||
{
|
||||
return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误消息
|
||||
*
|
||||
* @param code 状态码
|
||||
* @param msg 返回内容
|
||||
* @return 警告消息
|
||||
*/
|
||||
public static AjaxResult error(int code, String msg)
|
||||
{
|
||||
return new AjaxResult(code, msg, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 方便链式调用
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return 数据对象
|
||||
*/
|
||||
@Override
|
||||
public AjaxResult put(String key, Object value)
|
||||
{
|
||||
super.put(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ichangzuo.common.result;
|
||||
|
||||
/**
|
||||
* 响应码接口
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2022/2/18
|
||||
**/
|
||||
public interface IResultCode {
|
||||
|
||||
String getCode();
|
||||
|
||||
String getMsg();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ichangzuo.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 data = new Data<T>();
|
||||
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,73 @@
|
||||
package com.ichangzuo.common.result;
|
||||
|
||||
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_EXECUTION_ERROR.getCode(), ResultCode.SYSTEM_EXECUTION_ERROR.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(String msg) {
|
||||
return result(ResultCode.SYSTEM_EXECUTION_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(), msg, 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,125 @@
|
||||
package com.ichangzuo.common.result;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 响应码枚举
|
||||
* <p>
|
||||
* 参考阿里巴巴开发手册响应码规范
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2020/6/23
|
||||
**/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public enum ResultCode implements IResultCode, Serializable {
|
||||
|
||||
SUCCESS("00000", "一切ok"),
|
||||
|
||||
USER_ERROR("A0001", "用户端错误"),
|
||||
REPEAT_SUBMIT_ERROR("A0002", "您的请求已提交,请不要重复提交或等待片刻再尝试。"),
|
||||
|
||||
USER_LOGIN_ERROR("A0200", "用户登录异常"),
|
||||
|
||||
USER_NOT_EXIST("A0201", "用户不存在"),
|
||||
USER_ACCOUNT_LOCKED("A0202", "该账户已被冻结"),
|
||||
USER_ACCOUNT_INVALID("A0203", "账户无效"),
|
||||
USER_ACCOUNT_SHORT("A0204", "用户音符余额不足"),
|
||||
USER_ACCOUNT_NOREALNAME("A0205", "用户未通过实名认证"),
|
||||
USER_NAME_EXISTED("A0206", "该账号已被注册"),
|
||||
USER_ACCOUNT_DELETED("A0207", "该账户已被注销"),
|
||||
|
||||
USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"),
|
||||
PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"),
|
||||
CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"),
|
||||
VERIFY_CODE_TIMEOUT("A0213", "验证码已过期"),
|
||||
VERIFY_CODE_ERROR("A0214", "验证码错误"),
|
||||
|
||||
USER_EMAIL_UNBIND("A0220", "用户邮箱未绑定"),
|
||||
USER_EMAIL_INVALID("A0221", "用户邮箱不正确"),
|
||||
USER_PHONE_UNBIND("A0222", "用户手机未绑定"),
|
||||
USER_PHONE_INVALID("A0223", "用户手机不正确"),
|
||||
|
||||
TOKEN_INVALID("A0230", "token无效或已过期"),
|
||||
TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"),
|
||||
|
||||
AUTHORIZED_ERROR("A0300", "访问权限异常"),
|
||||
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
|
||||
FORBIDDEN_OPERATION("A0302", "演示环境禁止新增、修改和删除数据,请本地部署后测试"),
|
||||
|
||||
PARAM_ERROR("A0400", "用户请求参数错误"),
|
||||
RESOURCE_NOT_FOUND("A0401", "请求资源不存在"),
|
||||
PARAM_IS_NULL("A0410", "请求必填参数为空"),
|
||||
|
||||
PARAM_VERSION_ERROR("A0420", "版本号错误"),
|
||||
|
||||
USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"),
|
||||
USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"),
|
||||
USER_UPLOAD_FILE_SIZE_EXCEEDS("A0702", "用户上传文件太大"),
|
||||
USER_UPLOAD_IMAGE_SIZE_EXCEEDS("A0703", "用户上传图片太大"),
|
||||
|
||||
SYSTEM_EXECUTION_ERROR("B0001", "系统执行出错"),
|
||||
SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"),
|
||||
SYSTEM_ORDER_PROCESSING_TIMEOUT("B0100", "系统订单处理超时"),
|
||||
|
||||
SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被触发"),
|
||||
FLOW_LIMITING("B0210", "系统限流,请稍后再试"),
|
||||
DEGRADATION("B0220", "系统功能降级"),
|
||||
|
||||
SYSTEM_RESOURCE_ERROR("B0300", "系统资源异常"),
|
||||
SYSTEM_RESOURCE_EXHAUSTION("B0310", "系统资源耗尽"),
|
||||
SYSTEM_RESOURCE_ACCESS_ERROR("B0320", "系统资源访问异常"),
|
||||
SYSTEM_READ_DISK_FILE_ERROR("B0321", "系统读取磁盘文件失败"),
|
||||
|
||||
CALL_THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"),
|
||||
MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"),
|
||||
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", "消息分组未查到"),
|
||||
|
||||
DATABASE_ERROR("C0300", "数据库服务出错"),
|
||||
DATABASE_TABLE_NOT_EXIST("C0311", "表不存在"),
|
||||
DATABASE_COLUMN_NOT_EXIST("C0312", "列不存在"),
|
||||
DATABASE_DUPLICATE_COLUMN_NAME("C0321", "多表关联中存在多个相同名称的列"),
|
||||
DATABASE_DEADLOCK("C0331", "数据库死锁"),
|
||||
DATABASE_PRIMARY_KEY_CONFLICT("C0341", "主键冲突");
|
||||
|
||||
@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_EXECUTION_ERROR; // 默认系统执行错误
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
package com.ichangzuo.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,20 @@
|
||||
package com.ichangzuo.common.util;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.ichangzuo.common.base.BaseAnalysisEventListener;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Excel 工具类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/03/01
|
||||
*/
|
||||
public class ExcelUtils {
|
||||
|
||||
public static <T> String importExcel(InputStream is, Class clazz, BaseAnalysisEventListener<T> listener) {
|
||||
EasyExcel.read(is, clazz, listener).sheet().doRead();
|
||||
return listener.getMsg();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.ichangzuo.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,52 @@
|
||||
package com.ichangzuo.common.util;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.ichangzuo.common.result.Result;
|
||||
import com.ichangzuo.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) {
|
||||
// 根据不同的结果码设置HTTP状态
|
||||
int status = switch (resultCode) {
|
||||
case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||
case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value();
|
||||
case VERIFY_CODE_TIMEOUT, VERIFY_CODE_ERROR -> HttpStatus.OK.value();
|
||||
default -> HttpStatus.BAD_REQUEST.value();
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.ichangzuo.config;
|
||||
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayClient;
|
||||
import com.alipay.api.AlipayConstants;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.ichangzuo.config.property.AliPayProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(AliPayProperties.class)
|
||||
public class AlipayConfig {
|
||||
private static AliPayProperties aliPayProperties;
|
||||
|
||||
public AlipayConfig(AliPayProperties properties) {
|
||||
aliPayProperties = properties;
|
||||
}
|
||||
|
||||
public static final String APP_ID_APP = "2016041901311447";
|
||||
public static final String APP_ID_WEB = "2021005104641778";
|
||||
public static final String APP_ID_SANDBOX = "9021000138603610";
|
||||
|
||||
public static final int WEB = 0;
|
||||
public static final int APP = 1;
|
||||
// public static final int WAP = 2;
|
||||
//支付宝私钥
|
||||
private static final String APP_PRIVATE_KEY = """
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDKIIMWunigVf65\
|
||||
5us9PWlMKebBlDwU8mkJHLWqSj35HWugWEq4lQkkCQOegUIHCvVGaQIyha9WpWdi\
|
||||
wxGQZTfry+VxGtA/TiFKD1K5hGQw7Wnf5yMqM/iFZGyORdWdtE1ajESrxHYqp05h\
|
||||
qqfk922qRgKtlFdHxU/52MCXhauhmvARrHHrFpe3QIXpFh5U05WyWqs2VhTGNihM\
|
||||
83sOvu6D8uEru412cMnbe0iu5Rc+nc0Uzy+YaJEVKtDBueu8IMZmTsJenK8eizrU\
|
||||
6gRZrgAB+bCJc4l1ZtA00/ygI6IawJf3S67fwb4+hL4fDyuhyCWU4lRGW8+mM8DP\
|
||||
tFaI9ciPAgMBAAECggEBAJ134MinM2yuMknARgoqusr0ZervwjMLe5r2u+zT9B4M\
|
||||
tplvz56ntTxWrXQh/T+hYN6e1nBnx+b362h80kUtJfjUm4aXPh/jjXm2IFuZcbjQ\
|
||||
bUhbOQEbBgVd7FiWvUokepMNbj5nSywFtcHPAwgVX2FlM5bpV2n5pUnffINPRxwZ\
|
||||
RverEmpcqZf1L5yPG8+axAh3RJdsM5PYp4D8erkD9bp0vU1Tq7hQ+MU9HHM/i9D8\
|
||||
L3kGSz/xah3l68QvI91MiyF3Qg1EMHyz4tqxq0Db63Err6s8v0Y0n8F9SCrzP2d5\
|
||||
d3Xh8MMuVNPtQnIEAG084IpK1HLy2pL9CDb47cRsyWkCgYEA7/E0ga9CFzMt6kAc\
|
||||
2qolXbX1jopwjVFpmfdWjyrrrDgke+eC19bFP/BR7MpLEKanTY8azKKU3VInIHFy\
|
||||
uddp/ZJqrMNHV0BtaiRWqJMw5VnuQTgTo1BNJ128bfLDep6F4I103csQncu9I6/S\
|
||||
Am2GSKPRvKu1CrzOJpige0jQx9UCgYEA16dwtnrw4vKeFvsTC2LOkiQ9YC/EFKkP\
|
||||
EKPhtHuTXeaxBQ1scZgBVTr65pfz3cl3VKbIfxFM5TflSl5K72KtxGPn/2wUh0fV\
|
||||
ud+E5LM76mGSe5pBjoPyFY2i/Qyic+AA8L8vfHQwKqFo1OqfDBEJkmrsXgjQBtyX\
|
||||
1Oi3zj9axNMCgYEA4J7FsMII9P8MdMcgO/QcluXIw3AGfcVBPsm1VsGvbsIAJZ5N\
|
||||
dxGwBnNLvoiCTUw2Qv088WUiRy6pQk3yQNfQeXmgM6t8FcpSo5LxLU7d71eJG7UL\
|
||||
bU+3aqrtw2AIb7oHSngid5+qJo6cudPWnj85/radmiqEiVDHDIrFcaRxDyECgYBW\
|
||||
TUDTFiIegH95rOKzNMh8PZp+Sr9KkVlhDGR/6NBRzMdcwUF7uBwYcrED5R2HzV8+\
|
||||
9jvYdiDyvkq5V0DfyfrGVED8u9D/TmUerG+vYncA1ilb46CGmxEfRP5MDGlau/NE\
|
||||
ZQ5o3MqF1PBx/K7Hkm3lNXsAKsCtbkwovTUJidsWVwKBgQC9BED3+SJIKmGnP107\
|
||||
piz0hCvT/5HkfOXYgP8K0IAATW26h81k5QFEGfWHGSKqam9oB+Jz2YToh7gwuS6E\
|
||||
Zo6zc0xzYPR5QJvbZC/YYOMmfjz0F9Z7uFC2Tq+143bCkg9XlDwY8KKTxAfb80t7\
|
||||
m9zGkwDF59l1GEV9QNmh1q7jKA==""";
|
||||
//支付宝公钥
|
||||
public static final String ALIPAY_PUBLIC_KEY = """
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi2w/yQ9QCkAeKD1mQDkN\
|
||||
tZ5GY83FQ2x+mEVmVMF1c2a4eb8btYd62WOzaCFb0Is8BP2up1B8GElTmoDEO44V\
|
||||
tuC+2elHYezSRXhiBcrnnRjZebFh+k5SG4pgIBXVsSR/2fL94p8reYadTmV18wLd\
|
||||
W7Mqi54S16WNlw+PXkg8o4bnYxvJj6K8/lhgZg5As5cfsWK4WMQ1osnlPkSUb4uj\
|
||||
ye4KB2RWkVla+cZmswu8gWA1M9ygtcjCpvTyT2oVIoFQkWIU3IO9CxVPWhF7l8nP\
|
||||
OJW2fj9qIWWISPNA4PJCeTvGwN7lwE4oASGnag0CSJxpgz8SmKKAFtOm4rB08aCG\
|
||||
4QIDAQAB""";
|
||||
|
||||
public static final String ALIPAY_PUBLIC_KEY_SANDBOX =
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvMfJ4G5X9JtJ5JwIiYpZ8WHKnbBaAHNjdS/Pvy7O51KBuBC/3fiQRKMy8P9HhfikAuoDrAJo3LKc3vQ2oSLcg5P+6/phL/y1fLudu3rX45hfSuB2pm+eImgViylK2oXZxMu4KJCuK+iwt0NDABz5tGKc1veuoLJQKOLPgV0NXOP/QMKa598T98NqlM5qGBvZEykKI6f6E8GOytzY+deTtUPhoNAVmTWz4KgLWN1f9pGRT9qDKPai+TmVgqXPwpsBYeXoC9usCxpc20x+HF+CmL0nG1p7Wb43LDL17pbnL/ZFgfWWoa/mvbLOFP1zmeMsYNU3Rs5V8YjgWf0u5y+rhQIDAQAB";
|
||||
private static final String APP_PRIVATE_KEY_SANDBOX =
|
||||
"MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDhVzjbtde5cTroYrZ2aqVZ85Lwx6M7iVehoOHgW6Hq3mYspA7m4Axs/uCjaeORNsMoG6P0t5XDBkDzQ9XZyzUGdcsXKviIKPMwdzqA3lOGpItRipo4aOqrD0xnAhYknWa2UQSfm4ch0SOEofpD8rO3N1E0wj3RJd7Zlb4+juUfOf8wJ7Dp/QJiEo+LHH3O12to4/YBrFNY8OcJ1uS4Y4u8aAKIkzP9AWH45cKlOY/sjU4cMW3DJ4xAhh6LFIEqo7SkQ7jz6cfvOg8+IrPvcwOz4FiZyjM+qEqF4Pt+fa5iPxInQBkQk2+RhCh5sKVPGSFKFPA4h1OPG3lsqvHCvuU5AgMBAAECggEBANpmDOd1ANXslmlwcuOmTQg3eK0A8IXdgR9XrFQku3PFhUWy4/aEI8bn6JS5JiQx7UfMMkYWQII6Z2zezD9AIe5W8DVOIn7lIX5RsFQCJvYSOC0ZposRtI+CTkxvy0JFg60kLNT8iiYOatw7mTGN4vyqEnyN3hf9GlXMOgyrtglh/MM0xJZF0CEWa1ipA1Modzk349gcUEQTL9Mw6vm2AYhL/tlJTKmrzz7hyjDMeUgeQ7xiHUr6aGj54g9I9RdpV+Uoo+ts3YR9H0oUkz2rjcm/wwPYdF2i1E2yCG5d6Rug7YW/MXQwYSQTGphf3Vibe1xz6/yU3snbvT7FgDp/hJkCgYEA+Npru6u+g61FEtac8gMdcKxr0tKf43oifbp2MaLWWgHNtLhKeZg4lcATvYyHZBhZ9rLUmslg5luufCa/XbwXFdGmS4UdwKIm886uAGE0CKPNLyy7Szc0tHk5FbXk+bOv6aalM49lzINyun40Q1lD6ePrk3Gi5PXGSwnWhpLCPfcCgYEA58/vuYRlFHZnDMcd9IUTQh+Dx+eCWPXSYr0zrgkgQ9DgWt3l5V6VcoUFqykpTNGpy0w5RxgFXN/cEEVRpYe4nEgU287L/hL8kgDnYDDZ+WOT2RIF0jNJ8+wdCQgMjF9EEW4N80AnOSrsp2rBB101RqZy15BLLzTk6k+y3bKm6k8CgYEAsSEHPfXpDWDvoZEQ9VIySljwBofVNt1gX42xQ3Ncj4RpHxFuMU6gODcX1fuJAz4yCt8PZX2hc1YexE/wNpAC/ozTiT2fB9ZjU3bxc2O83cl56vIz/j21TiBa+ZEXAoVb2Tu8qw6OrxuBNi1OlgGbzYdlzvU7tS0bj53ZDMC5cdECgYBlo59piSpEqZGPYbK5pquF/4lpWhGl7cqcLnb9ZNT3xxrH3KlTQ4BlYPvWS0rnerpm//nROTAIw8Kag7pDyNlh9Jzor6hzs2F4ptrMKz83gLivoZ5ZxtEzGSC1+AiAd7jBp66ILGXGRBLVaRJPp0eXvZ129LZycU+5iM5VNGLJMwKBgQCZig65wd33dZdtt/oYdfZqFOZHdVk7QaQygqxWNh8N2TraWW571kfGFqlUJ8+IIuSx8+CAMNRSCB7MruuSVLvkouywcVt3lSsQvhPRriY7UUXI4mYDCSihk7aP7e11OfVH3ex3/mELHPEG0UgfaIa/Co9SlOYDnMyKZz3aL5xV4w==";
|
||||
|
||||
public static final String SIGNTYPE = "RSA2";
|
||||
//支付宝回调的接口
|
||||
public static final String SERVERURL = "https://openapi.alipay.com/gateway.do";
|
||||
public static final String SERVERURL_SANDBOX = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
|
||||
private static AlipayClient alipayAppClient = null;
|
||||
private static AlipayClient alipayWebClient = null;
|
||||
|
||||
//因为支付宝alipayClient本身是线程安全的,因此只用创建一个,创建成单例的模式
|
||||
// public static AlipayClient getAlipayAppClient() {
|
||||
// synchronized (AlipayConfig.class) {
|
||||
// if (null == alipayAppClient) {
|
||||
// alipayAppClient = new DefaultAlipayClient(SERVERURL, APP_ID_APP, APP_PRIVATE_KEY,
|
||||
// AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
|
||||
// ALIPAY_PUBLIC_KEY, SIGNTYPE);
|
||||
// }
|
||||
// }
|
||||
// return alipayAppClient;
|
||||
// }
|
||||
//
|
||||
// public static AlipayClient getAlipayWebClient() {
|
||||
// synchronized (AlipayConfig.class) {
|
||||
// if (null == alipayWebClient) {
|
||||
// alipayWebClient = new DefaultAlipayClient(SERVERURL, APP_ID_WEB, APP_PRIVATE_KEY,
|
||||
// AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
|
||||
// ALIPAY_PUBLIC_KEY, SIGNTYPE);
|
||||
// }
|
||||
// }
|
||||
// return alipayWebClient;
|
||||
// }
|
||||
//
|
||||
// public static AlipayClient getAlipaySandboxClient() {
|
||||
// synchronized (AlipayConfig.class) {
|
||||
// if (null == alipayWebClient) {
|
||||
// alipayWebClient = new DefaultAlipayClient(SERVERURL_SANDBOX, APP_ID_SANDBOX, APP_PRIVATE_KEY_SANDBOX,
|
||||
// AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
|
||||
// ALIPAY_PUBLIC_KEY_SANDBOX, SIGNTYPE);
|
||||
// }
|
||||
// }
|
||||
// return alipayWebClient;
|
||||
// }
|
||||
|
||||
public static String notifyUrl(){
|
||||
return aliPayProperties.getNotifyUrl();
|
||||
}
|
||||
|
||||
public static String returnUrl(){
|
||||
return aliPayProperties.getReturnUrl();
|
||||
}
|
||||
|
||||
//因为支付宝alipayClient本身是线程安全的,因此只用创建一个,创建成单例的模式
|
||||
public static AlipayClient getClient(int type){
|
||||
synchronized (AlipayConfig.class) {
|
||||
AlipayClient client = type==WEB ? alipayWebClient : alipayAppClient;
|
||||
if(null == client) {
|
||||
if (aliPayProperties.isSandBox()) {
|
||||
client = new DefaultAlipayClient(SERVERURL_SANDBOX, APP_ID_SANDBOX, APP_PRIVATE_KEY_SANDBOX,
|
||||
AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
|
||||
ALIPAY_PUBLIC_KEY_SANDBOX, SIGNTYPE);
|
||||
} else if (type == WEB) {
|
||||
client = new DefaultAlipayClient(SERVERURL, APP_ID_WEB, APP_PRIVATE_KEY,
|
||||
AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
|
||||
ALIPAY_PUBLIC_KEY, SIGNTYPE);
|
||||
} else if (type == APP) {
|
||||
client = new DefaultAlipayClient(SERVERURL, APP_ID_APP, APP_PRIVATE_KEY,
|
||||
AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
|
||||
ALIPAY_PUBLIC_KEY, SIGNTYPE);
|
||||
}
|
||||
|
||||
if(type==WEB)
|
||||
alipayWebClient = client;
|
||||
else
|
||||
alipayAppClient = client;
|
||||
}
|
||||
}
|
||||
|
||||
return type==WEB ? alipayWebClient : alipayAppClient;
|
||||
}
|
||||
|
||||
public static Boolean checkSignature(Map<String, String> params) throws AlipayApiException {
|
||||
if(aliPayProperties.isSandBox()){
|
||||
return AlipaySignature.rsaCheckV1(params, AlipayConfig.ALIPAY_PUBLIC_KEY_SANDBOX,
|
||||
AlipayConstants.CHARSET_UTF8,AlipayConfig.SIGNTYPE) ;
|
||||
}
|
||||
else{
|
||||
return AlipaySignature.rsaCheckV1(params, AlipayConfig.ALIPAY_PUBLIC_KEY,
|
||||
AlipayConstants.CHARSET_UTF8,AlipayConfig.SIGNTYPE) ;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ichangzuo.config;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import com.ichangzuo.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 if ("number".equalsIgnoreCase(codeType)) {
|
||||
return new RandomGenerator("0123456789", 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.ichangzuo.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,66 @@
|
||||
package com.ichangzuo.config;
|
||||
|
||||
import com.ichangzuo.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());
|
||||
mailSender.setProtocol(mailProperties.getProtocol());
|
||||
|
||||
Properties properties = mailSender.getJavaMailProperties();
|
||||
properties.put("mail.smtp.auth", mailProperties.getProperties().getMail().getSmtp().isAuth());
|
||||
properties.put("mail.smtp.starttls.enable", mailProperties.getProperties().getMail().getSmtp().getStarttls().isEnable());
|
||||
|
||||
// javaMailSender.setHost("smtp.qq.com"); // 设置邮箱服务器
|
||||
// javaMailSender.setPort(465); // 设置端口
|
||||
// javaMailSender.setUsername("747692844@qq.com"); // 设置用户名
|
||||
// javaMailSender.setPassword("<你的密码/授权码>"); // 设置密码(记得替换为你实际的密码、授权码)
|
||||
// javaMailSender.setProtocol("smtps"); // 设置协议
|
||||
//
|
||||
// Properties properties = new Properties(); // 配置项
|
||||
properties.put("mail.smtp.connectiontimeout", 5000);
|
||||
properties.put("mail.smtp.timeout", 3000);
|
||||
properties.put("mail.smtp.writetimeout", "5000");
|
||||
// properties.put("mail.smtp.auth", true);
|
||||
// properties.put("mail.smtp.starttls.enable", true);
|
||||
// properties.put("mail.smtp.starttls.required", true);
|
||||
|
||||
return mailSender;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ichangzuo.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.ichangzuo.core.handler.MyDataPermissionHandler;
|
||||
import com.ichangzuo.core.handler.MyMetaObjectHandler;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* mybatis-plus 自动配置类
|
||||
*
|
||||
* @author haoxr
|
||||
* @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,74 @@
|
||||
package com.ichangzuo.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
|
||||
* @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,42 @@
|
||||
package com.ichangzuo.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.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置
|
||||
*
|
||||
* @author Ray
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.ichangzuo.config;
|
||||
|
||||
import com.ichangzuo.core.security.service.SysUserDetailsService;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.ichangzuo.config.property.SecurityProperties;
|
||||
import com.ichangzuo.common.constant.SecurityConstants;
|
||||
import com.ichangzuo.core.filter.RateLimiterFilter;
|
||||
//import com.ichangzuo.core.security.authentication.PreAuthenticationProvider;
|
||||
import com.ichangzuo.core.security.exception.MyAccessDeniedHandler;
|
||||
import com.ichangzuo.core.security.exception.MyAuthenticationEntryPoint;
|
||||
import com.ichangzuo.core.security.filter.JwtValidationFilter;
|
||||
import com.ichangzuo.core.security.filter.CaptchaValidationFilter;
|
||||
import com.ichangzuo.system.service.ConfigService;
|
||||
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.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
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.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/**
|
||||
* Spring Security 权限配置
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2023/2/17
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final MyAuthenticationEntryPoint authenticationEntryPoint;
|
||||
private final MyAccessDeniedHandler accessDeniedHandler;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final SecurityProperties securityProperties;
|
||||
private final ConfigService configService;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
|
||||
.authorizeHttpRequests(requestMatcherRegistry ->
|
||||
requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll()
|
||||
.requestMatchers(SecurityConstants.REGISTER_PATH).permitAll()
|
||||
.requestMatchers(SecurityConstants.AUTOLOGIN_PATH).permitAll()
|
||||
.requestMatchers(SecurityConstants.SEND_REG_CODE).permitAll()
|
||||
.requestMatchers(SecurityConstants.SEND_BIND_CODE).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
|
||||
httpSecurityExceptionHandlingConfigurer
|
||||
.authenticationEntryPoint(authenticationEntryPoint)
|
||||
.accessDeniedHandler(accessDeniedHandler)
|
||||
)
|
||||
// .authenticationProvider(preAuthenticationProvider)
|
||||
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||
|
||||
;
|
||||
// 限流过滤器
|
||||
http.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class);
|
||||
// 验证码校验过滤器
|
||||
http.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class);
|
||||
// JWT 校验过滤器
|
||||
http.addFilterBefore(new JwtValidationFilter(redisTemplate,securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 不走过滤器链的放行配置
|
||||
*/
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> {
|
||||
if (CollectionUtil.isNotEmpty(securityProperties.getIgnoreUrls())) {
|
||||
web.ignoring().requestMatchers(securityProperties.getIgnoreUrls().toArray(new String[0]));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
public static class MyPasswordEncoder implements PasswordEncoder {
|
||||
@Override
|
||||
public String encode(CharSequence charSequence) {
|
||||
if(charSequence.length()>20)
|
||||
return charSequence.toString();
|
||||
return DigestUtil.sha256Hex(DigestUtil.sha256Hex((String)charSequence));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CharSequence charSequence, String s) {
|
||||
return s.equals(encode(charSequence));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new MyPasswordEncoder(); //new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthenticationManager 手动注入
|
||||
*
|
||||
* @param authenticationConfiguration 认证配置
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.ichangzuo.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import 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;
|
||||
|
||||
/**
|
||||
* Swagger 配置
|
||||
* <p>
|
||||
*
|
||||
* @author Ray
|
||||
* @see <a href="https://doc.xiaominfo.com/docs/quick-start">knife4j 快速开始</a>
|
||||
* @since 2023/2/17
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class SwaggerConfig {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
/**
|
||||
* 接口信息
|
||||
*/
|
||||
@Bean
|
||||
public OpenAPI openApi() {
|
||||
|
||||
String appVersion = environment.getProperty("project.version", "1.0.0");
|
||||
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("系统接口文档")
|
||||
.version(appVersion)
|
||||
)
|
||||
// 配置全局鉴权参数-Authorize
|
||||
.components(new Components()
|
||||
.addSecuritySchemes(HttpHeaders.AUTHORIZATION,
|
||||
new SecurityScheme()
|
||||
.name(HttpHeaders.AUTHORIZATION)
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.scheme("Bearer")
|
||||
.bearerFormat("JWT")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 全局自定义扩展
|
||||
* <p>
|
||||
* 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。
|
||||
* 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。
|
||||
*/
|
||||
@Bean
|
||||
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
|
||||
return openApi -> {
|
||||
// 全局添加鉴权参数
|
||||
if (openApi.getPaths() != null) {
|
||||
openApi.getPaths().forEach((s, pathItem) -> {
|
||||
// 登录接口/验证码不需要添加鉴权参数
|
||||
if ("/api/v1/auth/login".equals(s) || "/api/v1/auth/captcha".equals(s)) {
|
||||
return;
|
||||
}
|
||||
// 接口添加鉴权参数
|
||||
pathItem.readOperations()
|
||||
.forEach(operation ->
|
||||
operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ichangzuo.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
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 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.StringHttpMessageConverter;
|
||||
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.util.List;
|
||||
|
||||
/**
|
||||
* WebMvc 自动装配配置
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2020/10/16
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
converters.add(new StringHttpMessageConverter());
|
||||
|
||||
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
|
||||
ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
|
||||
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
|
||||
objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
|
||||
|
||||
// 后台Long值传递给前端精度丢失问题(JS最大精度整数是Math.pow(2,53))
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
|
||||
objectMapper.registerModule(simpleModule);
|
||||
|
||||
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
|
||||
converters.add(jackson2HttpMessageConverter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
|
||||
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
|
||||
.configure()
|
||||
.failFast(true) // failFast=true 不校验所有参数,只要出现校验失败情况直接返回,不再进行后续参数校验
|
||||
.constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
|
||||
.buildValidatorFactory();
|
||||
|
||||
return validatorFactory.getValidator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
//package com.ichangzuo.config;
|
||||
//
|
||||
//import cn.hutool.core.util.StrUtil;
|
||||
//import cn.hutool.jwt.JWTPayload;
|
||||
//import cn.hutool.jwt.JWTUtil;
|
||||
//import com.ichangzuo.common.constant.SecurityConstants;
|
||||
//import com.ichangzuo.system.event.UserConnectionEvent;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.jetbrains.annotations.NotNull;
|
||||
//import org.springframework.context.ApplicationEventPublisher;
|
||||
//import org.springframework.context.annotation.Configuration;
|
||||
//import org.springframework.http.HttpHeaders;
|
||||
//import org.springframework.messaging.Message;
|
||||
//import org.springframework.messaging.MessageChannel;
|
||||
//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.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
//import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
//import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
//
|
||||
///**
|
||||
// * WebSocket 自动配置类
|
||||
// *
|
||||
// * @author haoxr
|
||||
// * @since 2.4.0
|
||||
// */
|
||||
//// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递
|
||||
//@EnableWebSocketMessageBroker
|
||||
//@Configuration
|
||||
//@Slf4j
|
||||
//public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
//
|
||||
// private final ApplicationEventPublisher eventPublisher;
|
||||
//
|
||||
// public WebSocketConfig(ApplicationEventPublisher eventPublisher) {
|
||||
// this.eventPublisher = eventPublisher;
|
||||
// }
|
||||
// /**
|
||||
// * 注册一个端点,客户端通过这个端点进行连接
|
||||
// */
|
||||
// @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>
|
||||
// * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户
|
||||
// *
|
||||
// * @param registration 通道注册器
|
||||
// */
|
||||
// @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) {
|
||||
// if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
// String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
|
||||
// if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
// bearerToken = bearerToken.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
|
||||
// String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT);
|
||||
// if (StrUtil.isNotBlank(username)) {
|
||||
// accessor.setUser(() -> username);
|
||||
// eventPublisher.publishEvent(new UserConnectionEvent(this, username, true));
|
||||
// }
|
||||
// }
|
||||
// } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
|
||||
// if (accessor.getUser() != null) {
|
||||
// String username = accessor.getUser().getName();
|
||||
// eventPublisher.publishEvent(new UserConnectionEvent(this, username, false));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return ChannelInterceptor.super.preSend(message, channel);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
//}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.ichangzuo.config;
|
||||
|
||||
import com.ichangzuo.config.property.AliPayProperties;
|
||||
import com.ichangzuo.config.property.WxPayProperties;
|
||||
import com.wechat.pay.java.core.Config;
|
||||
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(WxPayProperties.class)
|
||||
public class WxpayConfig {
|
||||
private static WxPayProperties wxPayProperties;
|
||||
|
||||
public WxpayConfig(WxPayProperties properties) {
|
||||
wxPayProperties = properties;
|
||||
}
|
||||
|
||||
public static final int WX_APP = 0;
|
||||
public static final int WX_WEB = 1;
|
||||
/** App号 */
|
||||
public static String appAppId = "wx8f83a7f750d63431";
|
||||
/** 商户号 */
|
||||
public static String appMerchantId = "1266239101";
|
||||
/** 商户API私钥路径 */
|
||||
/** 商户证书序列号 */
|
||||
public static String appMerchantSerialNumber = "18650196CADB6AB05753AFF90B5C60EBFDDB9858";
|
||||
|
||||
/** App号 */
|
||||
public static String webAppId = "wx0243718acff3873f";//
|
||||
/** 商户号 */
|
||||
public static String webMerchantId = "1250765101";//
|
||||
/** 商户API私钥路径 */
|
||||
/** 商户证书序列号 */
|
||||
public static String webMerchantSerialNumber = "6D24B656FFCD5DFADAC7CDFFDFEC60FDA07B8654";//
|
||||
|
||||
/** 商户APIV3密钥 */
|
||||
public static String apiV3Key = "aS1yC4sS3xM8hI3oF1qD2uZ7kH8mO3uB";//
|
||||
//回调地址
|
||||
//Wxpayconfig
|
||||
private static Config wxpayAppConfig = null;
|
||||
private static Config wxpayWebConfig = null;
|
||||
|
||||
public static Config getWxpayConfig(int type) {
|
||||
Config config = type == WX_APP ? wxpayAppConfig : wxpayWebConfig;
|
||||
synchronized (WxpayConfig.class) {
|
||||
|
||||
if (null == config) {
|
||||
if(type==WX_APP) {
|
||||
config = new RSAAutoCertificateConfig.Builder()
|
||||
.merchantId(appMerchantId)
|
||||
.privateKeyFromPath(wxPayProperties.getAppPrivateKeyPath() + "apiclient_key.pem")
|
||||
.merchantSerialNumber(appMerchantSerialNumber)
|
||||
.apiV3Key(apiV3Key)
|
||||
.build();
|
||||
}
|
||||
else if(type==WX_WEB){
|
||||
config = new RSAAutoCertificateConfig.Builder()
|
||||
.merchantId(webMerchantId)
|
||||
.privateKeyFromPath(wxPayProperties.getWebPrivateKeyPath() + "apiclient_key.pem")
|
||||
.merchantSerialNumber(webMerchantSerialNumber)
|
||||
.apiV3Key(apiV3Key)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public static String notifyUrl(int type){
|
||||
if( type==WX_APP )
|
||||
return wxPayProperties.getAppNotifyUrl();
|
||||
else if( type==WX_WEB )
|
||||
return wxPayProperties.getWebNotifyUrl();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ichangzuo.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,12 @@
|
||||
package com.ichangzuo.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "pay.ali")
|
||||
@Data
|
||||
public class AliPayProperties {
|
||||
private boolean sandBox;
|
||||
private String notifyUrl;
|
||||
private String returnUrl;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ichangzuo.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> templateCodes;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ichangzuo.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.ichangzuo.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,99 @@
|
||||
package com.ichangzuo.config.property;
|
||||
|
||||
import cn.hutool.extra.mail.Mail;
|
||||
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 protocol;
|
||||
|
||||
/**
|
||||
* 邮件发送者地址。
|
||||
*/
|
||||
private String from;
|
||||
|
||||
/**
|
||||
* 邮件服务器的其他属性配置。
|
||||
* 这些配置通常用于进一步定制邮件发送行为。
|
||||
*/
|
||||
private Properties properties = new Properties();
|
||||
|
||||
/**
|
||||
* 内部类,用于封装邮件服务器的详细配置。
|
||||
* 包含 SMTP 相关的配置选项。
|
||||
*/
|
||||
@Data
|
||||
public static class Properties {
|
||||
private Mail mail = new Mail();
|
||||
|
||||
@Data
|
||||
public static class Mail {
|
||||
/**
|
||||
* 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,44 @@
|
||||
package com.ichangzuo.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author haoxr
|
||||
* @since 2024/4/18
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "security")
|
||||
public class SecurityProperties {
|
||||
|
||||
/**
|
||||
* 白名单 URL 集合
|
||||
*/
|
||||
private List<String> ignoreUrls;
|
||||
|
||||
/**
|
||||
* JWT 配置
|
||||
*/
|
||||
private JwtProperty jwt;
|
||||
|
||||
|
||||
/**
|
||||
* JWT 配置
|
||||
*/
|
||||
@Data
|
||||
public static class JwtProperty {
|
||||
|
||||
/**
|
||||
* JWT 密钥
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* JWT 过期时间
|
||||
*/
|
||||
private Long ttl;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ichangzuo.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "pay.wx")
|
||||
@Data
|
||||
public class WxPayProperties {
|
||||
private String appNotifyUrl;
|
||||
private String webNotifyUrl;
|
||||
private String appPrivateKeyPath;
|
||||
private String webPrivateKeyPath;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.ichangzuo.core.aspect;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.date.TimeInterval;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import com.ichangzuo.common.annotation.Log;
|
||||
import com.ichangzuo.common.constant.SecurityConstants;
|
||||
import com.ichangzuo.common.util.IPUtils;
|
||||
import com.ichangzuo.core.security.util.SecurityUtils;
|
||||
import com.ichangzuo.system.service.LogService;
|
||||
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.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 日志切面
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/6/25
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LogAspect {
|
||||
|
||||
private final LogService logService;
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@Pointcut("@annotation(com.ichangzuo.common.annotation.Log)")
|
||||
public void logPointcut() {
|
||||
}
|
||||
|
||||
@Around("logPointcut() && @annotation(logAnnotation)")
|
||||
public Object logExecutionTime(ProceedingJoinPoint joinPoint, Log logAnnotation) throws Throwable {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
Long userId = null;
|
||||
// 非登录请求获取用户ID,登录请求在登录成功后(joinPoint.proceed())获取用户ID
|
||||
if (!SecurityConstants.LOGIN_PATH.equals(requestURI)) {
|
||||
userId = SecurityUtils.getUserId();
|
||||
}
|
||||
|
||||
TimeInterval timer = DateUtil.timer();
|
||||
// 执行方法
|
||||
Object proceed = joinPoint.proceed();
|
||||
long executionTime = timer.interval();
|
||||
|
||||
// 创建日志记录
|
||||
com.ichangzuo.system.model.entity.Log log = new com.ichangzuo.system.model.entity.Log();
|
||||
log.setModule(logAnnotation.module());
|
||||
log.setContent(logAnnotation.value());
|
||||
log.setRequestUri(requestURI);
|
||||
// 登录方法需要在登录成功后获取用户ID
|
||||
if (userId == null) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.setExecutionTime(executionTime);
|
||||
// 获取浏览器和终端系统信息
|
||||
String userAgentString = request.getHeader("User-Agent");
|
||||
UserAgent userAgent = UserAgentUtil.parse(userAgentString);
|
||||
// 系统信息
|
||||
log.setOs(userAgent.getOs().getName());
|
||||
// 浏览器信息
|
||||
log.setBrowser(userAgent.getBrowser().getName());
|
||||
log.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));
|
||||
// 保存日志到数据库
|
||||
logService.save(log);
|
||||
|
||||
return proceed;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ichangzuo.core.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import cn.hutool.jwt.RegisteredPayload;
|
||||
import com.ichangzuo.common.annotation.RepeatSubmit;
|
||||
import com.ichangzuo.common.constant.RedisConstants;
|
||||
import com.ichangzuo.common.constant.SecurityConstants;
|
||||
import com.ichangzuo.common.exception.BusinessException;
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
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 haoxr
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RepeatSubmitAspect {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
* 防重复提交切点
|
||||
*/
|
||||
@Pointcut("@annotation(repeatSubmit)")
|
||||
public void preventDuplicateSubmitPointCut(RepeatSubmit repeatSubmit) {
|
||||
log.info("定义防重复提交切点");
|
||||
}
|
||||
|
||||
@Around("preventDuplicateSubmitPointCut(repeatSubmit)")
|
||||
public Object doAround(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
|
||||
|
||||
String resubmitLockKey = generateResubmitLockKey();
|
||||
if (resubmitLockKey != null) {
|
||||
int expire = repeatSubmit.expire(); // 防重提交锁过期时间
|
||||
RLock lock = redissonClient.getLock(resubmitLockKey);
|
||||
boolean lockResult = lock.tryLock(0, expire, TimeUnit.SECONDS); // 获取锁失败,直接返回 false
|
||||
if (!lockResult) {
|
||||
throw new BusinessException(ResultCode.REPEAT_SUBMIT_ERROR); // 抛出重复提交提示信息
|
||||
}
|
||||
}
|
||||
return pjp.proceed();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取重复提交锁的 key
|
||||
*/
|
||||
private String generateResubmitLockKey() {
|
||||
String resubmitLockKey = null;
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
|
||||
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
|
||||
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
|
||||
// 从 JWT Token 中获取 jti
|
||||
String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID);
|
||||
resubmitLockKey = RedisConstants.RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI();
|
||||
}
|
||||
return resubmitLockKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ichangzuo.core.filter;
|
||||
|
||||
import com.ichangzuo.common.constant.RedisConstants;
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
import com.ichangzuo.common.util.IPUtils;
|
||||
import com.ichangzuo.common.util.ResponseUtils;
|
||||
import com.ichangzuo.system.service.ConfigService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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;
|
||||
|
||||
public RateLimiterFilter(RedisTemplate<String, Object> redisTemplate, ConfigService configService) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认是否限流方法
|
||||
* 默认情况下:限制同一个IP的QPS最大为10,可以通过修改系统配置进行调整
|
||||
* 这里也可以进行扩展,比如redis记录同一个ip每天出发限流的上限次数,记录在redis中,达到某个阈值后,进行永久封禁这个ip
|
||||
*
|
||||
* @param ip ip地址
|
||||
* @return 是否限流
|
||||
*/
|
||||
public boolean rateLimit(String ip) {
|
||||
String key = RedisConstants.IP_RATE_LIMITER_KEY + ip;
|
||||
Long count = redisTemplate.opsForValue().increment(key);
|
||||
if (count == null || count == 1) {
|
||||
redisTemplate.expire(key,1, TimeUnit.SECONDS);
|
||||
}
|
||||
Object systemConfig = configService.getSystemConfig(RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY);
|
||||
long limit = 10;
|
||||
if(systemConfig != null){
|
||||
limit = Long.parseLong(systemConfig.toString());
|
||||
}else{
|
||||
// log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP请求限制QPS阈值配置,使用默认值:{},请检查配置项:{}",
|
||||
// limit,RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY);
|
||||
}
|
||||
return count != null && count > limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* IP限流过滤器
|
||||
* 默认情况下:限制同一个IP在一分钟内只能访问10次,可以通过修改系统配置进行调整
|
||||
*
|
||||
* @param request 请求体
|
||||
* @param response 响应体
|
||||
* @param filterChain 过滤器链
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
|
||||
@NotNull FilterChain filterChain) throws ServletException, IOException {
|
||||
String ip = IPUtils.getIpAddr(request);
|
||||
if (rateLimit(ip)) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.FLOW_LIMITING);
|
||||
return;
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ichangzuo.core.filter;
|
||||
|
||||
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();
|
||||
log.info("request uri: {}", requestURI);
|
||||
super.beforeRequest(request, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterRequest(HttpServletRequest request, String message) {
|
||||
super.afterRequest(request, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.ichangzuo.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.ichangzuo.common.annotation.DataPermission;
|
||||
import com.ichangzuo.common.base.IBaseEnum;
|
||||
import com.ichangzuo.common.enums.DataScopeEnum;
|
||||
import com.ichangzuo.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 {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Expression getSqlSegment(Expression where, String mappedStatementId) {
|
||||
|
||||
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 || SecurityUtils.isRoot() ) {
|
||||
return where;
|
||||
}
|
||||
return dataScopeFilter(/*annotation.deptAlias(), annotation.deptIdColumnName(), */annotation.userAlias(), annotation.userIdColumnName(), where);
|
||||
}
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建过滤条件
|
||||
*
|
||||
* @param where 当前查询条件
|
||||
* @return 构建后查询条件
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static Expression dataScopeFilter(/*String deptAlias, String deptIdColumnName,*/ String userAlias, String userIdColumnName, Expression where) {
|
||||
|
||||
|
||||
// String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName;
|
||||
String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName;
|
||||
|
||||
// 获取当前用户的数据权限
|
||||
Integer dataScope = SecurityUtils.getDataScope();
|
||||
|
||||
DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class);
|
||||
|
||||
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;
|
||||
return where;
|
||||
}
|
||||
|
||||
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.ichangzuo.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,25 @@
|
||||
package com.ichangzuo.core.security.exception;
|
||||
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
import com.ichangzuo.common.util.ResponseUtils;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Spring Security访问异常处理器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/18
|
||||
*/
|
||||
@Component
|
||||
public class MyAccessDeniedHandler implements AccessDeniedHandler {
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ichangzuo.core.security.exception;
|
||||
|
||||
import com.ichangzuo.common.exception.AccountDeletedException;
|
||||
import com.ichangzuo.common.exception.AccountLockedException;
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
import com.ichangzuo.common.util.ResponseUtils;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 认证异常处理
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
int status = response.getStatus();
|
||||
if (status == HttpServletResponse.SC_NOT_FOUND) {
|
||||
// 资源不存在
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.RESOURCE_NOT_FOUND);
|
||||
} else {
|
||||
if(authException instanceof BadCredentialsException){
|
||||
// 用户名或密码错误
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.USERNAME_OR_PASSWORD_ERROR);
|
||||
}
|
||||
else if(authException.getCause() instanceof AccountLockedException){
|
||||
// 账户被冻结
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.USER_ACCOUNT_LOCKED);
|
||||
}
|
||||
else if(authException.getCause() instanceof AccountDeletedException){
|
||||
// 账户已注销
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.USER_ACCOUNT_DELETED);
|
||||
}
|
||||
else {
|
||||
// 未认证或者token过期
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ichangzuo.core.security.filter;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ichangzuo.common.constant.SecurityConstants;
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
import com.ichangzuo.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.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, "POST");
|
||||
private static final AntPathRequestMatcher SEND_REG_CODE_MATCHER = new AntPathRequestMatcher(SecurityConstants.SEND_REG_CODE, "POST");
|
||||
private static final AntPathRequestMatcher SEND_BIND_CODE_MATCHER = new AntPathRequestMatcher(SecurityConstants.SEND_BIND_CODE, "POST");
|
||||
|
||||
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) || SEND_REG_CODE_MATCHER.matches(request)
|
||||
|| SEND_BIND_CODE_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(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
|
||||
if (cacheVerifyCode == null) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
|
||||
} else {
|
||||
// 验证码比对
|
||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非登录接口放行
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ichangzuo.core.security.filter;
|
||||
|
||||
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.ichangzuo.common.constant.SecurityConstants;
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
import com.ichangzuo.common.util.ResponseUtils;
|
||||
import com.ichangzuo.core.security.util.JwtUtils;
|
||||
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.HttpHeaders;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JWT token 校验过滤器
|
||||
*
|
||||
* @author Ray Hao
|
||||
* @since 2023/9/13
|
||||
*/
|
||||
public class JwtValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final byte[] secretKey;
|
||||
|
||||
public JwtValidationFilter(RedisTemplate<String, Object> redisTemplate, String secretKey) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.secretKey = secretKey.getBytes();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从请求中获取 JWT Token,校验 JWT Token 是否合法
|
||||
* <p>
|
||||
* 如果合法则将 Authentication 设置到 Spring Security Context 上下文中
|
||||
* 如果不合法则清空 Spring Security Context 上下文,并直接返回响应
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
try {
|
||||
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
|
||||
// 去除 Bearer 前缀
|
||||
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
|
||||
// 解析 Token
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
// 检查 Token 是否有效(验签 + 是否过期)
|
||||
boolean isValidate = jwt.setKey(secretKey).validate(0);
|
||||
if (!isValidate) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
// 检查 Token 是否已被加入黑名单(注销)
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||
boolean isTokenBlacklisted = Boolean.TRUE.equals(redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti));
|
||||
if (isTokenBlacklisted) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
// Token 有效将其解析为 Authentication 对象,并设置到 Spring Security 上下文中
|
||||
Authentication authentication = JwtUtils.getAuthentication(payloads);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
SecurityContextHolder.clearContext();
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
// Token有效或无Token时继续执行过滤链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.ichangzuo.core.security.model;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.ichangzuo.system.model.dto.UserAuthInfo;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
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.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Spring Security 用户对象
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class SysUserDetails implements UserDetails {
|
||||
|
||||
@Getter
|
||||
private Long userId;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
// private Boolean enabled;
|
||||
// private Boolean locked;
|
||||
|
||||
private Collection<SimpleGrantedAuthority> authorities;
|
||||
|
||||
private Set<String> perms;
|
||||
|
||||
// private Long deptId;
|
||||
|
||||
private Integer dataScope;
|
||||
|
||||
public SysUserDetails(UserAuthInfo user) {
|
||||
this.userId = user.getUserId();
|
||||
Set<String> roles = user.getRoles();
|
||||
Set<SimpleGrantedAuthority> authorities;
|
||||
if (CollectionUtil.isNotEmpty(roles)) {
|
||||
authorities = roles.stream()
|
||||
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // 标识角色
|
||||
.collect(Collectors.toSet());
|
||||
} else {
|
||||
authorities = Collections.EMPTY_SET;
|
||||
}
|
||||
this.authorities = authorities;
|
||||
this.username = user.getUsername();
|
||||
this.password = user.getPassword();
|
||||
// this.enabled = user.getStatus()>1;
|
||||
// this.locked = user.getStatus()==1;
|
||||
this.perms = user.getPerms();
|
||||
// this.deptId = user.getDeptId();
|
||||
this.dataScope = user.getDataScope();
|
||||
}
|
||||
|
||||
|
||||
@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 isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.ichangzuo.core.security.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ichangzuo.common.constant.SecurityConstants;
|
||||
import com.ichangzuo.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("用户无操作权限");
|
||||
}
|
||||
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(SecurityConstants.ROLE_PERMS_PREFIX, 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,59 @@
|
||||
package com.ichangzuo.core.security.service;
|
||||
|
||||
import com.ichangzuo.common.exception.AccountDeletedException;
|
||||
import com.ichangzuo.common.exception.AccountLockedException;
|
||||
import com.ichangzuo.common.exception.BusinessException;
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
import com.ichangzuo.core.security.model.SysUserDetails;
|
||||
import com.ichangzuo.system.model.dto.UserAuthInfo;
|
||||
import com.ichangzuo.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
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
|
||||
* @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 {
|
||||
UserAuthInfo userAuthInfo = userService.getUserAuthInfo(username);
|
||||
if (userAuthInfo == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
else if(userAuthInfo.getStatus()==0){
|
||||
throw new AccountDeletedException(username);
|
||||
}
|
||||
else if(userAuthInfo.getStatus()==1){
|
||||
throw new AccountLockedException(username);
|
||||
}
|
||||
return new SysUserDetails(userAuthInfo);
|
||||
} catch (Exception e) {
|
||||
// 记录异常日志
|
||||
log.error("认证异常:{}", e.getMessage());
|
||||
// 抛出异常
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.ichangzuo.core.security.util;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.jwt.JWTPayload;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import com.ichangzuo.common.constant.JwtClaimConstants;
|
||||
import com.ichangzuo.core.security.model.SysUserDetails;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JWT Token 工具类
|
||||
*
|
||||
* @author Ray Hao
|
||||
* @since 2.6.0
|
||||
*/
|
||||
@Component
|
||||
public class JwtUtils {
|
||||
|
||||
/**
|
||||
* JWT 加解密使用的密钥
|
||||
*/
|
||||
private static byte[] key;
|
||||
|
||||
|
||||
/**
|
||||
* JWT Token 的有效时间(单位:秒)
|
||||
*/
|
||||
private static int ttl;
|
||||
|
||||
|
||||
@Value("${security.jwt.key}")
|
||||
public void setKey(String key) {
|
||||
JwtUtils.key = key.getBytes();
|
||||
}
|
||||
|
||||
@Value("${security.jwt.ttl}")
|
||||
public void setTtl(Integer ttl) {
|
||||
JwtUtils.ttl = ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
*
|
||||
* @param authentication 用户认证信息
|
||||
* @return Token 字符串
|
||||
*/
|
||||
public static String createToken(Authentication authentication) {
|
||||
|
||||
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 = userDetails.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet());
|
||||
payload.put(JwtClaimConstants.AUTHORITIES, roles);
|
||||
|
||||
|
||||
Date now = new Date();
|
||||
Date expiration = DateUtil.offsetSecond(now, ttl);
|
||||
payload.put(JWTPayload.ISSUED_AT, now);
|
||||
payload.put(JWTPayload.EXPIRES_AT, expiration);
|
||||
payload.put(JWTPayload.SUBJECT, authentication.getName());
|
||||
payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());
|
||||
|
||||
return JWTUtil.createToken(payload, key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从 JWT Token 中解析 Authentication 用户认证信息
|
||||
*
|
||||
* @param payloads JWT 载体
|
||||
* @return 用户认证信息
|
||||
*/
|
||||
public static UsernamePasswordAuthenticationToken getAuthentication(JSONObject payloads) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ichangzuo.core.security.util;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ichangzuo.common.constant.SystemConstants;
|
||||
import com.ichangzuo.core.security.model.SysUserDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
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() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null) {
|
||||
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
if (CollectionUtil.isNotEmpty(authorities)) {
|
||||
return authorities.stream().filter(item -> item.getAuthority().startsWith("ROLE_"))
|
||||
.map(item -> StrUtil.removePrefix(item.getAuthority(), "ROLE_"))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
return Collections.EMPTY_SET;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否超级管理员
|
||||
* <p>
|
||||
* 超级管理员忽视任何权限判断
|
||||
*/
|
||||
public static boolean isRoot() {
|
||||
Set<String> roles = getRoles();
|
||||
return roles.contains(SystemConstants.ROOT_ROLE_CODE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ichangzuo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* 应用启动类
|
||||
*
|
||||
* @author Ray
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
@EnableScheduling
|
||||
public class iczApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(iczApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.ichangzuo.module.auth.controller;
|
||||
|
||||
import com.ichangzuo.module.auth.service.AuthService;
|
||||
import com.ichangzuo.common.enums.LogModuleEnum;
|
||||
import com.ichangzuo.common.result.Result;
|
||||
import com.ichangzuo.system.model.dto.CaptchaResult;
|
||||
import com.ichangzuo.system.model.dto.LoginResult;
|
||||
import com.ichangzuo.common.annotation.Log;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 认证控制层
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2022/10/16
|
||||
*/
|
||||
@Tag(name = "01.认证中心")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Operation(summary = "登录")
|
||||
@PostMapping("/login")
|
||||
@Log(value = "登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<LoginResult> login(
|
||||
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
|
||||
@Parameter(description = "密码", example = "123456") @RequestParam String password
|
||||
) {
|
||||
LoginResult loginResult = authService.login(username, password);
|
||||
return Result.success(loginResult);
|
||||
}
|
||||
|
||||
@Operation(summary = "注销")
|
||||
@DeleteMapping("/logout")
|
||||
@Log(value = "注销", module = LogModuleEnum.LOGIN)
|
||||
public Result<?> logout() {
|
||||
authService.logout();
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取验证码")
|
||||
@GetMapping("/captcha")
|
||||
public Result<CaptchaResult> getCaptcha() {
|
||||
CaptchaResult captcha = authService.getCaptcha();
|
||||
return Result.success(captcha);
|
||||
}
|
||||
|
||||
@Operation(summary = "注册")
|
||||
@PostMapping("/register")
|
||||
public Result<?> register(
|
||||
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
|
||||
@Parameter(description = "密码", example = "123456") @RequestParam String password,
|
||||
@Parameter(description = "验证码", example = "123456") @RequestParam String verifyCode
|
||||
) {
|
||||
return authService.register(username,password,verifyCode);
|
||||
}
|
||||
|
||||
@Operation(summary = "发送注册验证码")
|
||||
@PostMapping("/sendRegCode")
|
||||
public Result<?> sendRegCode(
|
||||
@Parameter(description = "联系方式(手机号码或邮箱地址)", required = true) @RequestParam String contact
|
||||
) {
|
||||
return authService.sendRegCode(contact);
|
||||
}
|
||||
|
||||
@Operation(summary = "核验注册验证码")
|
||||
@PostMapping("/verifyRegCode")
|
||||
public Result<?> verifyRegCode(
|
||||
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
|
||||
@Parameter(description = "验证码", example = "123456") @RequestParam String verifyCode
|
||||
) {
|
||||
return authService.verifyRegCode(username, verifyCode);
|
||||
}
|
||||
|
||||
@Operation(summary = "自动登录")
|
||||
@PostMapping("/autoLogin")
|
||||
@Log(value = "自动登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<?> autoLogin(
|
||||
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
|
||||
@Parameter(description = "密码", example = "123456") @RequestParam String password
|
||||
){
|
||||
return authService.autoLogin(username, password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ichangzuo.module.auth.service;
|
||||
|
||||
import com.ichangzuo.common.enums.ContactType;
|
||||
import com.ichangzuo.common.result.Result;
|
||||
import com.ichangzuo.system.model.dto.CaptchaResult;
|
||||
import com.ichangzuo.system.model.dto.LoginResult;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public interface AuthService {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 登录结果
|
||||
*/
|
||||
LoginResult login(String username, String password);
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
void logout();
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*
|
||||
* @return 验证码
|
||||
*/
|
||||
CaptchaResult getCaptcha();
|
||||
|
||||
Result<?> register(String username, String password, String verifyCode);
|
||||
|
||||
Result<?> sendRegCode(String contact);
|
||||
|
||||
Result<?> verifyRegCode(String username, String verifyCode);
|
||||
|
||||
Result<?> autoLogin(String username, String password);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package com.ichangzuo.module.auth.service.impl;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.jwt.JWTPayload;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import com.ichangzuo.common.result.Result;
|
||||
import com.ichangzuo.common.result.ResultCode;
|
||||
import com.ichangzuo.module.auth.service.AuthService;
|
||||
import com.ichangzuo.common.constant.SecurityConstants;
|
||||
import com.ichangzuo.common.enums.CaptchaTypeEnum;
|
||||
import com.ichangzuo.system.model.dto.CaptchaResult;
|
||||
import com.ichangzuo.system.model.dto.LoginResult;
|
||||
import com.ichangzuo.config.property.CaptchaProperties;
|
||||
import com.ichangzuo.core.security.util.JwtUtils;
|
||||
import com.ichangzuo.system.service.UserService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证服务实现类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final Font captchaFont;
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 登录结果
|
||||
*/
|
||||
@Override
|
||||
public LoginResult login(String username, String password) {
|
||||
// 创建认证令牌对象
|
||||
UsernamePasswordAuthenticationToken authenticationToken =
|
||||
new UsernamePasswordAuthenticationToken(username.toLowerCase().trim(), password);
|
||||
// 执行用户认证
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
// 认证成功后生成JWT令牌
|
||||
String accessToken = JwtUtils.createToken(authentication);
|
||||
// 将认证信息存入Security上下文,便于在AOP(如日志记录)中获取当前用户信息
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
userService.afterUserLogon();
|
||||
// 返回包含JWT令牌的登录结果
|
||||
return LoginResult.builder()
|
||||
.tokenType("Bearer")
|
||||
.accessToken(accessToken)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销
|
||||
*/
|
||||
@Override
|
||||
public void logout() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
|
||||
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
|
||||
// 解析Token以获取有效载荷(payload)
|
||||
JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
|
||||
// 解析 Token 获取 jti(JWT ID) 和 exp(过期时间)
|
||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||
Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); // 过期时间(秒)
|
||||
// 如果exp存在,则计算Token剩余有效时间
|
||||
if (expiration != null) {
|
||||
long currentTimeSeconds = System.currentTimeMillis() / 1000;
|
||||
if (expiration < currentTimeSeconds) {
|
||||
// Token已过期,不再加入黑名单
|
||||
return;
|
||||
}
|
||||
// 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除
|
||||
long ttl = expiration - currentTimeSeconds;
|
||||
redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS);
|
||||
} else {
|
||||
// 如果exp不存在,说明Token永不过期,则永久加入黑名单
|
||||
redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null);
|
||||
}
|
||||
}
|
||||
// 清空Spring Security上下文
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*
|
||||
* @return 验证码
|
||||
*/
|
||||
@Override
|
||||
public CaptchaResult getCaptcha() {
|
||||
|
||||
String captchaType = captchaProperties.getType();
|
||||
int width = captchaProperties.getWidth();
|
||||
int height = captchaProperties.getHeight();
|
||||
int interfereCount = captchaProperties.getInterfereCount();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
String codeType = captchaProperties.getCode().getType();
|
||||
|
||||
AbstractCaptcha captcha;
|
||||
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
|
||||
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
|
||||
}
|
||||
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.setTextAlpha(captchaProperties.getTextAlpha());
|
||||
captcha.setFont(captchaFont);
|
||||
|
||||
String captchaCode = captcha.getCode();
|
||||
String imageBase64Data = captcha.getImageBase64Data();
|
||||
|
||||
// 验证码文本缓存至Redis,用于登录校验
|
||||
String captchaKey = IdUtil.fastSimpleUUID();
|
||||
redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,
|
||||
captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
|
||||
|
||||
return CaptchaResult.builder()
|
||||
.captchaKey(captchaKey)
|
||||
.captchaBase64(imageBase64Data)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> register(String username, String password, String verifyCode){
|
||||
if(userService.existUsername(username))
|
||||
return Result.failed(ResultCode.USER_NAME_EXISTED);
|
||||
|
||||
int verify = userService.checkVerificationCode(username, verifyCode, "注册新用户");
|
||||
if(verify==-1)
|
||||
return Result.failed(ResultCode.VERIFY_CODE_ERROR);
|
||||
else if(verify==-2)
|
||||
return Result.failed(ResultCode.VERIFY_CODE_TIMEOUT);
|
||||
else if(verify==-3)
|
||||
return Result.failed(ResultCode.PARAM_ERROR);
|
||||
|
||||
if( !userService.register(username, password) )
|
||||
return Result.failed(ResultCode.SYSTEM_EXECUTION_ERROR);
|
||||
|
||||
LoginResult result = login(username, password);
|
||||
if(result==null)
|
||||
return Result.failed(ResultCode.SYSTEM_EXECUTION_ERROR);
|
||||
|
||||
userService.setNewAccount();
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> sendRegCode(String contact){
|
||||
if(userService.existUsername(contact))
|
||||
return Result.failed(ResultCode.USER_NAME_EXISTED);
|
||||
|
||||
String subject = "注册新用户";
|
||||
boolean result = userService.sendVerificationCode(contact, subject);
|
||||
return Result.judge(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> verifyRegCode(String username, String verifyCode) {
|
||||
int verify = userService.checkVerificationCode(username, verifyCode, "注册新用户");
|
||||
if(verify==-1)
|
||||
return Result.failed(ResultCode.VERIFY_CODE_ERROR);
|
||||
else if(verify==-2)
|
||||
return Result.failed(ResultCode.VERIFY_CODE_TIMEOUT);
|
||||
else if(verify==-3)
|
||||
return Result.failed(ResultCode.PARAM_ERROR);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> autoLogin(String username, String password){
|
||||
LoginResult result = login(username, password);
|
||||
if(result==null)
|
||||
return Result.failed(ResultCode.SYSTEM_EXECUTION_ERROR);
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//package com.ichangzuo.module.codegen.controller;
|
||||
//
|
||||
//import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
//import com.ichangzuo.module.codegen.service.CodegenService;
|
||||
//import com.ichangzuo.common.result.PageResult;
|
||||
//import com.ichangzuo.common.result.Result;
|
||||
//import com.ichangzuo.config.property.CodegenProperties;
|
||||
//import com.ichangzuo.common.enums.LogModuleEnum;
|
||||
//import com.ichangzuo.module.codegen.model.form.GenConfigForm;
|
||||
//import com.ichangzuo.module.codegen.model.query.TablePageQuery;
|
||||
//import com.ichangzuo.module.codegen.model.vo.CodegenPreviewVO;
|
||||
//import com.ichangzuo.module.codegen.model.vo.TablePageVO;
|
||||
//import com.ichangzuo.common.annotation.Log;
|
||||
//import com.ichangzuo.module.codegen.service.GenConfigService;
|
||||
//import io.swagger.v3.oas.annotations.Operation;
|
||||
//import io.swagger.v3.oas.annotations.Parameter;
|
||||
//import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
//import jakarta.servlet.ServletOutputStream;
|
||||
//import jakarta.servlet.http.HttpServletResponse;
|
||||
//import lombok.RequiredArgsConstructor;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.springframework.web.bind.annotation.*;
|
||||
//
|
||||
//import java.io.IOException;
|
||||
//import java.net.URLEncoder;
|
||||
//import java.nio.charset.StandardCharsets;
|
||||
//import java.util.List;
|
||||
//
|
||||
///**
|
||||
// * 代码生成器控制层
|
||||
// *
|
||||
// * @author Ray
|
||||
// * @since 2.10.0
|
||||
// */
|
||||
//@Tag(name = "09.代码生成")
|
||||
//@RestController
|
||||
//@RequestMapping("/api/v1/codegen")
|
||||
//@RequiredArgsConstructor
|
||||
//@Slf4j
|
||||
//public class CodegenController {
|
||||
//
|
||||
// private final CodegenService codegenService;
|
||||
// private final GenConfigService genConfigService;
|
||||
// private final CodegenProperties codegenProperties;
|
||||
//
|
||||
// @Operation(summary = "获取数据表分页列表")
|
||||
// @GetMapping("/table/page")
|
||||
// @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER)
|
||||
// public PageResult<TablePageVO> getTablePage(
|
||||
// TablePageQuery queryParams
|
||||
// ) {
|
||||
// Page<TablePageVO> result = codegenService.getTablePage(queryParams);
|
||||
// return PageResult.success(result);
|
||||
// }
|
||||
//
|
||||
// @Operation(summary = "获取代码生成配置")
|
||||
// @GetMapping("/{tableName}/config")
|
||||
// public Result<GenConfigForm> getGenConfigFormData(
|
||||
// @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
||||
// ) {
|
||||
// GenConfigForm formData = genConfigService.getGenConfigFormData(tableName);
|
||||
// return Result.success(formData);
|
||||
// }
|
||||
//
|
||||
// @Operation(summary = "保存代码生成配置")
|
||||
// @PostMapping("/{tableName}/config")
|
||||
// @Log(value = "生成代码", module = LogModuleEnum.OTHER)
|
||||
// public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
|
||||
// genConfigService.saveGenConfig(formData);
|
||||
// return Result.success();
|
||||
// }
|
||||
//
|
||||
// @Operation(summary = "删除代码生成配置")
|
||||
// @DeleteMapping("/{tableName}/config")
|
||||
// public Result<?> deleteGenConfig(
|
||||
// @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
||||
// ) {
|
||||
// genConfigService.deleteGenConfig(tableName);
|
||||
// return Result.success();
|
||||
// }
|
||||
//
|
||||
// @Operation(summary = "获取预览生成代码")
|
||||
// @GetMapping("/{tableName}/preview")
|
||||
// @Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
|
||||
// public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName) {
|
||||
// List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName);
|
||||
// return Result.success(list);
|
||||
// }
|
||||
//
|
||||
// @Operation(summary = "下载代码")
|
||||
// @GetMapping("/{tableName}/download")
|
||||
// @Log(value = "下载代码", module = LogModuleEnum.OTHER)
|
||||
// public void downloadZip(HttpServletResponse response, @PathVariable String tableName) {
|
||||
// String[] tableNames = tableName.split(",");
|
||||
// byte[] data = codegenService.downloadCode(tableNames);
|
||||
//
|
||||
// response.reset();
|
||||
// response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8));
|
||||
// response.setContentType("application/octet-stream; charset=UTF-8");
|
||||
//
|
||||
// try (ServletOutputStream outputStream = response.getOutputStream()) {
|
||||
// outputStream.write(data);
|
||||
// outputStream.flush();
|
||||
// } catch (IOException e) {
|
||||
// log.error("Error while writing the zip file to response", e);
|
||||
// throw new RuntimeException("Failed to write the zip file to response", e);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ichangzuo.module.codegen.converter;
|
||||
|
||||
import com.ichangzuo.module.codegen.model.entity.GenConfig;
|
||||
import com.ichangzuo.module.codegen.model.entity.GenFieldConfig;
|
||||
import com.ichangzuo.module.codegen.model.form.GenConfigForm;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 代码生成配置转换器
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface CodegenConverter {
|
||||
|
||||
@Mapping(source = "genConfig.tableName", target = "tableName")
|
||||
@Mapping(source = "genConfig.businessName", target = "businessName")
|
||||
@Mapping(source = "genConfig.moduleName", target = "moduleName")
|
||||
@Mapping(source = "genConfig.packageName", target = "packageName")
|
||||
@Mapping(source = "genConfig.entityName", target = "entityName")
|
||||
@Mapping(source = "genConfig.author", target = "author")
|
||||
@Mapping(source = "fieldConfigs", target = "fieldConfigs")
|
||||
GenConfigForm toGenConfigForm(GenConfig genConfig, List<GenFieldConfig> fieldConfigs);
|
||||
|
||||
List<GenConfigForm.FieldConfig> toGenFieldConfigForm(List<GenFieldConfig> fieldConfigs);
|
||||
|
||||
GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig);
|
||||
|
||||
GenConfig toGenConfig(GenConfigForm formData);
|
||||
|
||||
List<GenFieldConfig> toGenFieldConfig(List<GenConfigForm.FieldConfig> fieldConfigs);
|
||||
|
||||
GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ichangzuo.module.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ichangzuo.module.codegen.model.query.TablePageQuery;
|
||||
import com.ichangzuo.module.codegen.model.bo.ColumnMetaData;
|
||||
import com.ichangzuo.module.codegen.model.bo.TableMetaData;
|
||||
import com.ichangzuo.module.codegen.model.vo.TablePageVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Mapper
|
||||
public interface DatabaseMapper extends BaseMapper {
|
||||
|
||||
|
||||
Page<TablePageVO> getTablePage(Page<TablePageVO> page, TablePageQuery queryParams);
|
||||
|
||||
List<ColumnMetaData> getTableColumns(String tableName);
|
||||
|
||||
TableMetaData getTableMetadata(String tableName);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ichangzuo.module.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ichangzuo.module.codegen.model.entity.GenConfig;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 代码生成基础配置访问层
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface GenConfigMapper extends BaseMapper<GenConfig> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ichangzuo.module.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ichangzuo.module.codegen.model.entity.GenFieldConfig;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 代码生成字段配置访问层
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface GenFieldConfigMapper extends BaseMapper<GenFieldConfig> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ichangzuo.module.codegen.model.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "数据表字段VO")
|
||||
@Data
|
||||
public class ColumnMetaData {
|
||||
|
||||
/**
|
||||
* 字段名称
|
||||
*/
|
||||
private String columnName;
|
||||
|
||||
/**
|
||||
* 字段类型
|
||||
*/
|
||||
private String dataType;
|
||||
|
||||
/**
|
||||
* 字段描述
|
||||
*/
|
||||
private String columnComment;
|
||||
|
||||
/**
|
||||
* 字段长度
|
||||
*/
|
||||
private Integer characterMaximumLength;
|
||||
|
||||
/**
|
||||
* 是否主键(1-是 0-否)
|
||||
*/
|
||||
private Integer isPrimaryKey;
|
||||
|
||||
/**
|
||||
* 是否可为空(1-是 0-否)
|
||||
*/
|
||||
private String isNullable;
|
||||
|
||||
/**
|
||||
* 字符集
|
||||
*/
|
||||
private String characterSetName;
|
||||
|
||||
/**
|
||||
* 排序规则
|
||||
*/
|
||||
private String collationName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ichangzuo.module.codegen.model.bo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
/**
|
||||
* 数据表元数据
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Data
|
||||
public class TableMetaData {
|
||||
|
||||
/**
|
||||
* 表名称
|
||||
*/
|
||||
private String tableName;
|
||||
|
||||
/**
|
||||
* 表描述
|
||||
*/
|
||||
private String tableComment;
|
||||
|
||||
/**
|
||||
* 排序规则
|
||||
*/
|
||||
private String tableCollation;
|
||||
|
||||
/**
|
||||
* 存储引擎
|
||||
*/
|
||||
private String engine;
|
||||
|
||||
/**
|
||||
* 字符集
|
||||
*/
|
||||
private String charset;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private String createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ichangzuo.module.codegen.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
|
||||
import com.ichangzuo.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 代码生成基础配置
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@TableName(value = "gen_config")
|
||||
@Getter
|
||||
@Setter
|
||||
public class GenConfig extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 表名
|
||||
*/
|
||||
private String tableName;
|
||||
|
||||
/**
|
||||
* 包名
|
||||
*/
|
||||
private String packageName;
|
||||
|
||||
/**
|
||||
* 模块名
|
||||
*/
|
||||
private String moduleName;
|
||||
|
||||
/**
|
||||
* 实体类名
|
||||
*/
|
||||
private String entityName;
|
||||
|
||||
/**
|
||||
* 业务名
|
||||
*/
|
||||
private String businessName;
|
||||
|
||||
/**
|
||||
* 父菜单ID
|
||||
*/
|
||||
private Long parentMenuId;
|
||||
|
||||
/**
|
||||
* 作者
|
||||
*/
|
||||
private String author;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ichangzuo.module.codegen.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.ichangzuo.common.base.BaseEntity;
|
||||
import com.ichangzuo.common.enums.FormTypeEnum;
|
||||
import com.ichangzuo.common.enums.QueryTypeEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 字段生成配置实体
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@TableName(value = "gen_field_config")
|
||||
@Getter
|
||||
@Setter
|
||||
public class GenFieldConfig extends BaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 关联的配置ID
|
||||
*/
|
||||
private Long configId;
|
||||
|
||||
/**
|
||||
* 列名
|
||||
*/
|
||||
private String columnName;
|
||||
|
||||
/**
|
||||
* 列类型
|
||||
*/
|
||||
private String columnType;
|
||||
|
||||
/**
|
||||
* 字段长度
|
||||
*/
|
||||
private Integer maxLength;
|
||||
|
||||
/**
|
||||
* 字段名称
|
||||
*/
|
||||
private String fieldName;
|
||||
|
||||
/**
|
||||
* 字段排序
|
||||
*/
|
||||
private Integer fieldSort;
|
||||
|
||||
/**
|
||||
* 字段类型
|
||||
*/
|
||||
private String fieldType;
|
||||
|
||||
/**
|
||||
* 字段描述
|
||||
*/
|
||||
private String fieldComment;
|
||||
|
||||
/**
|
||||
* 表单类型
|
||||
*/
|
||||
private FormTypeEnum formType;
|
||||
|
||||
/**
|
||||
* 查询方式
|
||||
*/
|
||||
private QueryTypeEnum queryType;
|
||||
|
||||
/**
|
||||
* 是否在列表显示
|
||||
*/
|
||||
private Integer isShowInList;
|
||||
|
||||
/**
|
||||
* 是否在表单显示
|
||||
*/
|
||||
private Integer isShowInForm;
|
||||
|
||||
/**
|
||||
* 是否在查询条件显示
|
||||
*/
|
||||
private Integer isShowInQuery;
|
||||
|
||||
/**
|
||||
* 是否必填
|
||||
*/
|
||||
private Integer isRequired;
|
||||
|
||||
/**
|
||||
* TypeScript类型
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@JsonIgnore
|
||||
private String tsType;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
private String dictType;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.ichangzuo.module.codegen.model.form;
|
||||
|
||||
import com.ichangzuo.common.enums.FormTypeEnum;
|
||||
import com.ichangzuo.common.enums.QueryTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 代码生成配置表单
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Schema(description = "代码生成配置表单")
|
||||
@Data
|
||||
public class GenConfigForm {
|
||||
|
||||
@Schema(description = "主键",example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "表名",example = "sys_user")
|
||||
private String tableName;
|
||||
|
||||
@Schema(description = "业务名",example = "用户")
|
||||
private String businessName;
|
||||
|
||||
@Schema(description = "模块名",example = "system")
|
||||
private String moduleName;
|
||||
|
||||
@Schema(description = "包名",example = "com.youlai")
|
||||
private String packageName;
|
||||
|
||||
@Schema(description = "实体名",example = "User")
|
||||
private String entityName;
|
||||
|
||||
@Schema(description = "作者",example = "youlaitech")
|
||||
private String author;
|
||||
|
||||
@Schema(description = "上级菜单ID",example = "1")
|
||||
private Long parentMenuId;
|
||||
|
||||
@Schema(description = "字段配置列表")
|
||||
private List<FieldConfig> fieldConfigs;
|
||||
|
||||
@Schema(description = "后端应用名")
|
||||
private String backendAppName;
|
||||
|
||||
@Schema(description = "前端应用名")
|
||||
private String frontendAppName;
|
||||
|
||||
@Schema(description = "字段配置")
|
||||
@Data
|
||||
public static class FieldConfig {
|
||||
|
||||
@Schema(description = "主键")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "列名")
|
||||
private String columnName;
|
||||
|
||||
@Schema(description = "列类型")
|
||||
private String columnType;
|
||||
|
||||
@Schema(description = "字段名")
|
||||
private String fieldName;
|
||||
|
||||
@Schema(description = "字段排序")
|
||||
private Integer fieldSort;
|
||||
|
||||
@Schema(description = "字段类型")
|
||||
private String fieldType;
|
||||
|
||||
@Schema(description = "字段描述")
|
||||
private String fieldComment;
|
||||
|
||||
@Schema(description = "是否在列表显示")
|
||||
private Integer isShowInList;
|
||||
|
||||
@Schema(description = "是否在表单显示")
|
||||
private Integer isShowInForm;
|
||||
|
||||
@Schema(description = "是否在查询条件显示")
|
||||
private Integer isShowInQuery;
|
||||
|
||||
@Schema(description = "是否必填")
|
||||
private Integer isRequired;
|
||||
|
||||
@Schema(description = "最大长度")
|
||||
private Integer maxLength;
|
||||
|
||||
@Schema(description = "表单类型")
|
||||
private FormTypeEnum formType;
|
||||
|
||||
@Schema(description = "查询类型")
|
||||
private QueryTypeEnum queryType;
|
||||
|
||||
@Schema(description = "字典类型")
|
||||
private String dictType;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ichangzuo.module.codegen.model.query;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.ichangzuo.common.base.BasePageQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据表分页查询对象
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Schema(description = "数据表分页查询对象")
|
||||
@Getter
|
||||
@Setter
|
||||
public class TablePageQuery extends BasePageQuery {
|
||||
|
||||
@Schema(description="关键字(表名)")
|
||||
private String keywords;
|
||||
|
||||
/**
|
||||
* 排除的表名
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<String> excludeTables;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ichangzuo.module.codegen.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "代码生成代码预览VO")
|
||||
@Data
|
||||
public class CodegenPreviewVO {
|
||||
|
||||
@Schema(description = "生成文件路径")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "生成文件名称",example = "SysUser.java" )
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "生成文件内容")
|
||||
private String content;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ichangzuo.module.codegen.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
@Schema(description = "表视图对象")
|
||||
@Data
|
||||
public class TablePageVO {
|
||||
|
||||
@Schema(description = "表名称", example = "sys_user")
|
||||
private String tableName;
|
||||
|
||||
@Schema(description = "表描述",example = "用户表")
|
||||
private String tableComment;
|
||||
|
||||
@Schema(description = "表排序规则",example = "utf8mb4_general_ci")
|
||||
private String tableCollation;
|
||||
|
||||
@Schema(description = "存储引擎",example = "InnoDB")
|
||||
private String engine;
|
||||
|
||||
@Schema(description = "字符集",example = "utf8mb4")
|
||||
private String charset;
|
||||
|
||||
@Schema(description = "创建时间",example = "2023-08-08 08:08:08")
|
||||
private String createTime;
|
||||
|
||||
@Schema(description="是否已配置")
|
||||
private Integer isConfigured;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ichangzuo.module.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ichangzuo.module.codegen.model.query.TablePageQuery;
|
||||
import com.ichangzuo.module.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.ichangzuo.module.codegen.model.vo.TablePageVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 代码生成配置接口
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public interface CodegenService {
|
||||
|
||||
/**
|
||||
* 获取数据表分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return
|
||||
*/
|
||||
Page<TablePageVO> getTablePage(TablePageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 获取预览生成代码
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName);
|
||||
|
||||
/**
|
||||
* 下载代码
|
||||
* @param tableNames 表名
|
||||
* @return
|
||||
*/
|
||||
byte[] downloadCode(String[] tableNames);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ichangzuo.module.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.ichangzuo.module.codegen.model.entity.GenConfig;
|
||||
import com.ichangzuo.module.codegen.model.form.GenConfigForm;
|
||||
|
||||
/**
|
||||
* 代码生成配置接口
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public interface GenConfigService extends IService<GenConfig> {
|
||||
|
||||
/**
|
||||
* 获取代码生成配置
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
GenConfigForm getGenConfigFormData(String tableName);
|
||||
|
||||
/**
|
||||
* 保存代码生成配置
|
||||
*
|
||||
* @param formData 表单数据
|
||||
* @return
|
||||
*/
|
||||
void saveGenConfig(GenConfigForm formData);
|
||||
|
||||
/**
|
||||
* 删除代码生成配置
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
void deleteGenConfig(String tableName);
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user