first commit

This commit is contained in:
2026-06-12 01:09:25 +08:00
commit 1b577e55fe
349 changed files with 22015 additions and 0 deletions
+15
View File
@@ -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
+111
View File
@@ -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`
- **默认端口**8989dev/test/ 9999prod
- **接口文档**:启动后访问 `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` 为准
+60
View File
@@ -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
View File
@@ -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
+201
View File
@@ -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.
+110
View File
@@ -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)
+323
View File
@@ -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>
* eg2021-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