Compare commits

..

43 Commits

Author SHA1 Message Date
c002e6c593 release: v1.1.0 2023-12-31 13:30:19 +08:00
0b7eea6dac fix(log/httptrace-pro): 取消记录非 JSON 格式请求体 2023-12-29 22:48:44 +08:00
52dce2acdf style: 使用常量优化部分魔法值,核心模块部分配置前缀调整 2023-12-29 17:46:28 +08:00
7b12454ce6 fix(log/httptrace-pro): 移除拦截 /error 2023-12-28 22:50:22 +08:00
76e282c796 refactor: 新增 PATH_PATTERN 字符串常量 2023-12-28 21:42:47 +08:00
7023be1072 refactor: 优化行为验证码缓存自动配置 2023-12-28 20:13:45 +08:00
2dd697a115 !3 修复行为验证码自定义缓存实现无法加载到缓存工厂问题
Merge pull request !3 from Yoofff/dev
2023-12-28 00:12:26 +00:00
Bull-BCLS
edc47b7b1d fix(captcha): 修复行为验证码自定义缓存实现无法加载到缓存工厂问题 2023-12-28 00:29:31 +08:00
48f894b8b6 chore: 新增 Amazon S3 依赖版本 2023-12-27 23:07:41 +08:00
3788da72c2 refactor(captcha/behavior): 移除部分无用注解 2023-12-27 22:46:54 +08:00
1df1f6de6b !2 修复行为验证码  Redis、自定义缓存无法加载到缓存工厂问题
Merge pull request !2 from Yoofff/dev
2023-12-27 14:25:54 +00:00
Bull-BCLS
d790e6941f fix(captcha): 修复行为验证码 Redis、自定义缓存无法加载到缓存工厂问题 2023-12-27 22:17:47 +08:00
4dae89e0f2 chore: ip2region 3.1.5.1 => 3.1.6 2023-12-26 20:36:46 +08:00
be7972c00b chore: 新增 X File Storage 依赖版本 2023-12-25 23:06:01 +08:00
556bfb924a chore: MyBatis Plus 3.5.4.1 => 3.5.5(修复与 Spring Boot 3.1.7 的 DdlApplicationRunner冲突错误) 2023-12-25 23:05:02 +08:00
70ae383de6 style(extension/crud): 移除部分方法中仅有单个非读操作的事务处理 2023-12-25 22:19:44 +08:00
7c6df66ca5 Merge branch 'dev' of https://gitee.com/Charles7c/continew-starter into dev 2023-12-25 21:09:45 +08:00
dec286d9de !1 feat(captcha): 新增行为验证码自动配置
Merge pull request !1 from Yoofff/dev
2023-12-25 13:08:12 +00:00
Bull-BCLS
bcb13326c6 feat(captcha): 新增行为验证码自动配置 2023-12-25 21:01:38 +08:00
5500e5d840 refactor(log/httptrace-pro): 重构请求头及响应头信息获取 2023-12-25 20:11:33 +08:00
e9f01d05c9 revert(log/httptrace-pro): 请求 URI => 请求 URL,记录协议、域名信息(保持尽可能完整) 2023-12-25 19:50:52 +08:00
0a0d022586 refactor: SaTokenDaoTypeEnum => SaTokenDaoType 2023-12-24 16:01:28 +08:00
72f55697cc chore: 升级后端依赖
1.Spring Boot 3.1.5 => 3.1.7(存在 MyBatis Plus 冲突异常,已临时修复)
2.Just Auth 1.16.5 => 1.16.6
3.Redisson 3.24.3 => 3.25.2
4.Easy Excel 3.3.2 => 3.3.3
5.Knife4j 4.3.0 => 4.4.0
6.Hutool 5.8.23 => 5.8.24
2023-12-24 11:48:43 +08:00
c4007fa290 refactor(log/httptrace-pro): 优化日志记录
1.新增是否打印配置,开启后可打印访问日志(类似于 Nginx access log)
2.TIME_TAKEN 请求耗时,调整为必然记录
3.REQUEST_PARAM 请求参数、RESPONSE_PARAM 响应参数调整为默认记录
4.包含信息配置:BODY 和 PARAM 调整为互斥,包含 BODY 则对应 PARAM 不记录
5.请求 URI 不再记录协议、域名等信息
6.修复部分请求、响应信息记录错误
2023-12-23 22:47:44 +08:00
dac525f177 docs: 完善 README 文档 2023-12-22 20:48:10 +08:00
22fee2f5bd feat(data/mybatis-plus): 调整 IBaseEnum 所属包 enums => base 2023-12-22 20:47:22 +08:00
621a5e3b22 feat(data/mybatis-plus): 新增数据权限默认解决方案
1.新增数据权限默认解决方案
2.调整 BaseMapper 所属模块 crud => mybatis-plus
3.优化 mybatis-plus 模块包结构
2023-12-21 23:20:03 +08:00
2a70a9a252 fix(log/httptrace-pro): 修复 IP 配置解析错误,并缩小日志异常影响范围 2023-12-21 20:57:33 +08:00
e0e5944b45 refactor(captcha/graphic): 优化图形验证码配置前缀 2023-12-18 20:53:26 +08:00
9cf3ae87a1 feat(cache/redisson): RedisUtils 新增限流方法 2023-12-18 20:22:29 +08:00
cd6826a0ab feat(storage): 新增存储模块 - 本地存储 2023-12-17 23:50:21 +08:00
c4459d1b8d refactor(extension/crud): 新增全局异常处理器 2023-12-17 19:50:23 +08:00
65f5fbd6da refactor(core): 优化跨域配置默认值 2023-12-17 19:00:24 +08:00
3e9a59df5a feat(log): 新增日志模块 - HttpTracePro(Spring Boot Actuator HttpTrace 定制增强版) 2023-12-17 14:07:07 +08:00
ad1d001973 Merge branch '1.0.x' into dev
# Conflicts:
#	README.md
#	continew-starter-dependencies/pom.xml
2023-12-14 00:16:58 +08:00
ac70c385f7 release: v1.0.1 2023-12-14 00:00:58 +08:00
1adfddfa3b fix(extension/crud): 修复使用 CrudRequestMapping 后自定义 API 不显示的问题 2023-12-13 23:44:23 +08:00
bc00c9bab0 refactor(data): MyBatis Plus QueryTypeEnum => QueryType,并取消实现 IBaseEnum 接口 2023-12-13 23:24:17 +08:00
7997267060 refactor(api-doc): 新增鉴权配置 2023-12-12 20:35:13 +08:00
083bc7b38a refactor: 调整部分内容所属模块
1.校验等工具类 crud => core
2.@Query crud => mybatis-plus
2023-12-05 20:57:34 +08:00
af351e04b9 chore: 更新版本号 2023-12-05 20:57:06 +08:00
fd14e44648 docs: 调整 README 文档部分内容排序 2023-12-03 13:41:33 +08:00
b9fbd3830d chore: 更新版本号 2023-12-03 13:38:37 +08:00
81 changed files with 3205 additions and 146 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -1,3 +1,56 @@
## [v1.1.0](https://github.com/Charles7c/continew-starter/compare/v1.0.1...v1.1.0) (2023-12-31)
### ✨ 新特性
* 【log/httptrace-pro】新增 continew-starter-log-httptrace-pro 日志模块Spring Boot Actuator HttpTrace 重置增强版)
* 【storage/local】新增 continew-starter-storage-local 本地存储模块 ([7e5ff2e](https://github.com/Charles7c/continew-starter/commit/cd6826a0abe0666f9fe867e92bf70abb47e5ff2e))
* 【cache/redisson】RedisUtils 新增限流方法 ([fc5c20c](https://github.com/Charles7c/continew-starter/commit/9cf3ae87a1a20db9ee8b2b7272e8328b5fc5c20c))
* 【data/mybatis-plus】新增数据权限默认解决方案 ([31e09af](https://github.com/Charles7c/continew-starter/commit/621a5e3b22db9b81d31c65b39ad387a8531e09af))
* 【captcha/behavior】新增 continew-starter-captcha-behavior 行为验证码模块 ([Gitee PR#1](https://gitee.com/Charles7c/continew-starter/pulls/1))
* 【core】新增 PATH_PATTERN 字符串常量 ([40d0f7f](https://github.com/Charles7c/continew-starter/commit/76e282c7965fdfa39854fe77397687bbc40d0f7f))
### 💎 功能优化
- 【core】优化跨域配置默认值 ([23233ce](https://github.com/Charles7c/continew-starter/commit/65f5fbd6daa9ae2c8aedd13c487e8985523233ce))
- 【extension/crud】新增全局异常处理器 ([a285b55](https://github.com/Charles7c/continew-starter/commit/c4459d1b8d701a4405f74ea92cfc87752a285b55))
- 【extension/crud】移除部分方法中仅有单个非读操作的事务处理 ([54f83f5](https://github.com/Charles7c/continew-starter/commit/70ae383de62bc3c6ae0d2e1c3cf5c005d54f83f5))
### 📦 依赖升级
- 【dependencies】Spring Boot 3.1.5 => 3.1.7 ([8636605](https://github.com/Charles7c/continew-starter/commit/72f55697cc8958bf3586daed03a8d1b3c8636605))
- 【dependencies】Just Auth 1.16.5 => 1.16.6 ([8636605](https://github.com/Charles7c/continew-starter/commit/72f55697cc8958bf3586daed03a8d1b3c8636605))
- 【dependencies】Redisson 3.24.3 => 3.25.2 ([8636605](https://github.com/Charles7c/continew-starter/commit/72f55697cc8958bf3586daed03a8d1b3c8636605))
- 【dependencies】Easy Excel 3.3.2 => 3.3.3 ([8636605](https://github.com/Charles7c/continew-starter/commit/72f55697cc8958bf3586daed03a8d1b3c8636605))
- 【dependencies】Knife4j 4.3.0 => 4.4.0 ([8636605](https://github.com/Charles7c/continew-starter/commit/72f55697cc8958bf3586daed03a8d1b3c8636605))
- 【dependencies】Hutool 5.8.23 => 5.8.24 ([8636605](https://github.com/Charles7c/continew-starter/commit/72f55697cc8958bf3586daed03a8d1b3c8636605))
- 【dependencies】MyBatis Plus 3.5.4.1 => 3.5.5(修复与 Spring Boot 3.1.7 的 DdlApplicationRunner冲突错误 ([b36d578](https://github.com/Charles7c/continew-starter/commit/556bfb924a1e5834fe0a101b9ff52cc5bb36d578))
- 【dependencies】新增 X File Storage 依赖版本 2.0.0 ([016de2d](https://github.com/Charles7c/continew-starter/commit/be7972c00be8d62cc25332e053a985532016de2d))
- 【dependencies】ip2region 3.1.5.1 => 3.1.6 ([980c929](https://github.com/Charles7c/continew-starter/commit/4dae89e0f21ac6c532101e983ee4007f3980c929))
- 【dependencies】新增 Amazon S3 依赖版本 1.12.626 ([97e4a2d](https://github.com/Charles7c/continew-starter/commit/48f894b8b62f8b968091dcea51b57336b97e4a2d))
### 💥 破坏性变更
- 【captcha/graphic】优化图形验证码配置前缀 ([7b3e450](https://github.com/Charles7c/continew-starter/commit/e0e5944b45bcbf8a4b7a5066ad347459a7b3e450))
- 【data/mybatis-plus】调整 IBaseEnum 所属包 enums => base ([522833c](https://github.com/Charles7c/continew-starter/commit/22fee2f5bd8211e26c2f6a163a6298f5b522833c))
- 【auth/satoken】SaTokenDaoTypeEnum => SaTokenDaoType ([86e35c4](https://github.com/Charles7c/continew-starter/commit/0a0d022586dc88a773512c5761c68d62786e35c4))
- 【core】使用常量优化部分魔法值核心模块部分配置前缀调整 ([999f899](https://github.com/Charles7c/continew-starter/commit/52dce2acdfa0296c3f6f4875f14a0299f999f899))
## [v1.0.1](https://github.com/Charles7c/continew-starter/compare/v1.0.0...v1.0.1) (2023-12-13)
### 💎 功能优化
- 【data/mybatis-plus】QueryTypeEnum => QueryType并取消实现 IBaseEnum 接口 ([bc00c9b](https://github.com/Charles7c/continew-starter/commit/bc00c9bab0ed4508fd1dc0da8a76ef96739cce1d))
- 【api-doc】新增鉴权配置 ([7997267](https://github.com/Charles7c/continew-starter/commit/7997267060b3e79f80dd73cec722bc295635a93b))
### 🐛 问题修复
- 【extension/crud】修复使用 @CrudRequestMapping 后自定义 API 不显示的问题 ([1adfddf](https://github.com/Charles7c/continew-starter/commit/1adfddfa3b276e764b098512b2e9c75f007d13c1))
### 💥 破坏性变更
- 【extension/crud】调整通用查询注解所属模块 crud => mybatis-plus ([083bc7b](https://github.com/Charles7c/continew-starter/commit/083bc7b38a861339ceb7a06acdd20ea64bc84990))
- 【extension/crud】调整校验工具类所属模块 crud => core ([083bc7b](https://github.com/Charles7c/continew-starter/commit/083bc7b38a861339ceb7a06acdd20ea64bc84990))
## v1.0.0 (2023-12-02)
### ✨ 新特性

110
README.md
View File

@@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/License-LGPL--3.0-blue.svg" alt="License" />
</a>
<a href="https://github.com/Charles7c/continew-starter" target="_blank">
<img src="https://img.shields.io/badge/RELEASE-v1.0.0-%23ff3f59.svg" alt="Release" />
<img src="https://img.shields.io/badge/RELEASE-v1.1.0-%23ff3f59.svg" alt="Release" />
</a>
<a href="https://github.com/Charles7c/continew-starter" target="_blank">
<img src="https://img.shields.io/github/stars/Charles7c/continew-starter?style=social" alt="GitHub stars" />
@@ -48,8 +48,8 @@ ContiNew Starter 就是将脚手架项目中的通用基础配置进行了封装
## 项目源码
| 开源平台 | 源码地址 |
| ------------- | ------------------------------------------- |
| 开源平台 | 源码地址 |
| :------------ | :-------------------------------------------- |
| GitHub | https://github.com/Charles7c/continew-starter |
| Gitee码云 | https://gitee.com/Charles7c/continew-starter |
@@ -63,7 +63,7 @@ ContiNew Starter 就是将脚手架项目中的通用基础配置进行了封装
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter</artifactId>
<version>1.0.0</version>
<version>{latest-version}</version>
</parent>
```
@@ -83,7 +83,7 @@ ContiNew Starter 就是将脚手架项目中的通用基础配置进行了封装
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-dependencies</artifactId>
<version>1.0.0</version>
<version>{latest-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -121,22 +121,50 @@ cors:
exposed-headers: '*'
```
<details>
<summary>抢先体验快照SNAPSHOT版本💡</summary>
> **注意:** 快照版本目前处于开发测试阶段,其中很多特性或改动尚不稳定,可能会因为修复或优化而频繁调整。因此,仅可用于体验,切勿用于生产环境!
1.在项目 pom.xml 中配置 SNAPSHOT快照仓库地址如果你已配有其他仓库地址追加下方快照仓库地址即可
```xml
<repositories>
<repository>
<id>sonatype-nexus-snapshots</id>
<name>Sonatype Nexus Snapshots</name>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<updatePolicy>always</updatePolicy>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
```
2.将 ContiNew Starter 版本改为对应快照版本例如1.1.0-SNAPSHOT
</details>
## 模块结构
| 模块名称 | 模块说明 | 依赖版本 |
| ---------------------------------- | ---------------------------------------- | ------------------------------------------------------------ |
| continew-starter-core | 核心模块:包含跨域、线程池等自动配置 | <a href="https://spring.io/projects/spring-boot" target="_blank">Spring Boot</a>3.1.5<br /><a href="https://undertow.io/" target="_blank">Undertow</a>2.3.10.Final<br /><a href="https://www.hutool.cn/" target="_blank">Hutool</a>5.8.23<br />mica-ip2region3.1.5.1 |
| continew-starter-file-excel | 文件处理模块Excel 相关配置 | <a href="https://easyexcel.opensource.alibaba.com/" target="_blank">Easy Excel</a>3.3.2 |
| continew-starter-json-jackson | JSON 模块Jackson 自动配置 | Jackson2.15.3 |
| continew-starter-api-doc | API 文档模块Knife4j 自动配置 | <a href="https://doc.xiaominfo.com/" target="_blank">Knife4j</a>4.3.0 |
| continew-starter-captcha-graphic | 验证码模块:图形验证码 | Easy Captcha1.6.2 |
| continew-starter-cache-redisson | 缓存模块Redisson 自动配置 | <a href="https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D" target="_blank">Redisson</a>3.24.3 |
| continew-starter-data-mybatis-plus | 数据访问模块MyBatis Plus 自动配置 | <a href="https://baomidou.com/" target="_blank">MyBatis Plus</a>3.5.4.1<br /><a href="https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611" target="_blank">dynamic-datasource-spring-boot-starter</a>4.2.0<br /><a href="https://github.com/p6spy/p6spy" target="_blank">P6Spy</a>3.9.1 |
| continew-starter-auth-satoken | 认证模块SaToken 自动配置 | <a href="https://sa-token.dev33.cn/" target="_blank">Sa-Token</a>1.37.0 |
| continew-starter-auth-justauth | 认证模块:JustAuth 自动配置 | <a href="https://justauth.cn/" target="_blank">Just Auth</a>1.16.5 |
| continew-starter-messaging-mail | 消息模块:邮件 | Jakarta Mail1.1.0 |
| continew-starter-messaging-sms | 消息模块:短信 | <a href="https://sms4j.com/" target="_blank">SMS4J</a>3.0.4 |
| continew-starter-extension | 扩展模块:包含 CRUD 等复杂模组及自动配置 | |
| 模块名称 | 模块说明 | 依赖版本 |
| ---------------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
| continew-starter-core | 核心模块:包含跨域、线程池等自动配置 | <a href="https://spring.io/projects/spring-boot" target="_blank">Spring Boot</a>3.1.7<br /><a href="https://undertow.io/" target="_blank">Undertow</a>2.3.10.Final<br /><a href="https://www.hutool.cn/" target="_blank">Hutool</a>5.8.24<br />mica-ip2region3.1.6 |
| continew-starter-json-jackson | JSON 模块Jackson 自动配置 | Jackson2.15.3 |
| continew-starter-api-doc | API 文档模块Knife4j 自动配置 | <a href="https://doc.xiaominfo.com/" target="_blank">Knife4j</a>4.4.0 |
| continew-starter-log-httptrace-pro | 日志模块Spring Boot Actuator HttpTrace 重置增强版 | |
| continew-starter-storage-local | 存储模块:本地存储 | |
| continew-starter-file-excel | 文件处理模块Excel 相关配置 | <a href="https://easyexcel.opensource.alibaba.com/" target="_blank">Easy Excel</a>3.3.4 |
| continew-starter-captcha-graphic | 验证码模块:图形验证码 | Easy Captcha1.6.2 |
| continew-starter-captcha-behavior | 验证码模块:行为验证码 | AJ-Captcha1.3.0 |
| continew-starter-cache-redisson | 缓存模块:Redisson 自动配置 | <a href="https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D" target="_blank">Redisson</a>3.25.2 |
| continew-starter-data-mybatis-plus | 数据访问模块MyBatis Plus 自动配置 | <a href="https://baomidou.com/" target="_blank">MyBatis Plus</a>3.5.5<br /><a href="https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611" target="_blank">dynamic-datasource-spring-boot-starter</a>4.2.0<br /><a href="https://github.com/p6spy/p6spy" target="_blank">P6Spy</a>3.9.1 |
| continew-starter-auth-satoken | 认证模块:SaToken 自动配置 | <a href="https://sa-token.dev33.cn/" target="_blank">Sa-Token</a>1.37.0 |
| continew-starter-auth-justauth | 认证模块:JustAuth 自动配置 | <a href="https://justauth.cn/" target="_blank">Just Auth</a>1.16.6 |
| continew-starter-messaging-mail | 消息模块:邮件 | Jakarta Mail1.1.0 |
| continew-starter-messaging-sms | 消息模块:短信 | <a href="https://sms4j.com/" target="_blank">SMS4J</a>3.0.4 |
| continew-starter-extension-crud | 扩展模块CRUD 通用内容封装 | |
![模块依赖图](.image/模块依赖图.png)
@@ -171,30 +199,6 @@ ContiNew Starter 的分支目前分为下个大版本的开发分支和上个大
> 2. 在提交代码前,请按照 [Angular 提交规范](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) 编写 commit 的 message建议在 IntelliJ IDEA 中下载并安装 Git Commit Template 插件,以便按照规范进行 commit
> 3. 提交代码之前,请关闭所有代码窗口,执行 mvn compile 命令,编译通过后,不要再打开查看任何代码窗口,直接提交即可
## 谁在使用?
- ✨[ContiNew Admin](https://github.com/Charles7c/continew-admin)ContiNew Admin Continue New Admin中后台管理框架/脚手架持续以最新流行技术栈构建拥抱变化迭代优化。依托开源协作模式提升技术透明度、放大集体智慧、共创优秀实践源源不断地为企业级项目开发提供助力。当前采用的技术栈Spring Boot3Java17、Vue3 & Arco Design、Sa-Token、MyBatis Plus、Redisson、Liquibase、JustAuth、Easy Excel、Hutool、TypeScript、Vite4 等。
## 鸣谢
### 鸣谢
感谢参与贡献的每一位小伙伴🥰
<a href="https://github.com/Charles7c/continew-starter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Charles7c/continew-starter" />
</a>
### 特别鸣谢
- 感谢 <a href="https://www.jetbrains.com/" target="_blank">JetBrains</a> 提供的 <a href="https://www.jetbrains.com/shop/eform/opensource" target="_blank">非商业开源软件开发授权</a>
- 感谢 <a href="https://github.com/baomidou/mybatis-plus" target="_blank">MyBatis Plus</a><a href="https://github.com/dromara/sa-token" target="_blank">Sa-Token</a><a href="https://github.com/xiaoymin/knife4j" target="_blank">Knife4j</a><a href="https://github.com/dromara/hutool" target="_blank">Hutool</a> 等国产开源组件作者为国内开源世界作出的贡献
- 感谢 <a href="https://github.com/elunez/eladmin" target="_blank">ELADMIN</a><a href="https://github.com/dromara/RuoYi-Vue-Plus" target="_blank">RuoYi-Vue-Plus</a><a href="https://gitee.com/herodotus/dante-engine" target="_blank">Dante-Engine</a>,致敬各位作者为开源脚手架领域作出的贡献
- e.g. 脱胎于 ELADMIN 项目开源的 QueryHelper 组件
- e.g. 使用 RuoYi-Vue-Plus 项目封装的 SaToken 相关认证鉴权配置
- e.g. 使用 Dante-Engine 项目封装的 Redisson 相关配置
- 感谢项目使用或未使用到的每一款开源组件,致敬各位开源先驱 :fire:
## 反馈交流
💬 欢迎各位小伙伴儿扫描下方二维码加好友,备注 `cnadmin`,拉你进群,探讨技术、提提需求~
@@ -214,6 +218,26 @@ ContiNew Starter 的分支目前分为下个大版本的开发分支和上个大
💬 如无加群意愿,欢迎在 <a href="https://github.com/Charles7c/continew-starter/issues" target="_blank">Issues</a> 中反馈交流~ 🍻
</details>
## 鸣谢
### 鸣谢
感谢参与贡献的每一位小伙伴🥰
<a href="https://github.com/Charles7c/continew-starter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Charles7c/continew-starter" />
</a>
### 特别鸣谢
- 感谢 <a href="https://www.jetbrains.com/" target="_blank">JetBrains</a> 提供的 <a href="https://www.jetbrains.com/shop/eform/opensource" target="_blank">非商业开源软件开发授权</a>
- 感谢 <a href="https://github.com/baomidou/mybatis-plus" target="_blank">MyBatis Plus</a><a href="https://github.com/dromara/sa-token" target="_blank">Sa-Token</a><a href="https://github.com/xiaoymin/knife4j" target="_blank">Knife4j</a><a href="https://github.com/dromara/hutool" target="_blank">Hutool</a> 等国产开源组件作者为国内开源世界作出的贡献
- 感谢 <a href="https://github.com/elunez/eladmin" target="_blank">ELADMIN</a><a href="https://github.com/dromara/RuoYi-Vue-Plus" target="_blank">RuoYi-Vue-Plus</a><a href="https://gitee.com/herodotus/dante-engine" target="_blank">Dante-Engine</a>,致敬各位作者为开源脚手架领域作出的贡献
- e.g. 脱胎于 ELADMIN 项目开源的 QueryHelper 组件
- e.g. 扩展于 RuoYi-Vue-Plus 项目封装的 SaToken 相关认证鉴权配置
- e.g. 扩展于 Dante-Engine 项目封装的 Redisson 相关配置
- 感谢项目使用或未使用到的每一款开源组件,致敬各位开源先驱 :fire:
## License
- 遵循 <a href="https://github.com/Charles7c/continew-starter/blob/dev/LICENSE" target="_blank">LGPL-3.0</a> 开源许可协议

View File

@@ -16,15 +16,21 @@
package top.charles7c.continew.starter.apidoc.autoconfigure;
import cn.hutool.core.map.MapUtil;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.CacheControl;
@@ -34,6 +40,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.charles7c.continew.starter.core.autoconfigure.project.ProjectProperties;
import top.charles7c.continew.starter.core.handler.GeneralPropertySourceFactory;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -47,6 +55,7 @@ import java.util.concurrent.TimeUnit;
@EnableWebMvc
@AutoConfiguration
@ConditionalOnProperty(name = "springdoc.swagger-ui.enabled", havingValue = "true")
@EnableConfigurationProperties(SpringDocExtensionProperties.class)
@PropertySource(value = "classpath:default-api-doc.yml", factory = GeneralPropertySourceFactory.class)
public class SpringDocAutoConfiguration implements WebMvcConfigurer {
@@ -63,23 +72,62 @@ public class SpringDocAutoConfiguration implements WebMvcConfigurer {
*/
@Bean
@ConditionalOnMissingBean
public OpenAPI openApi(ProjectProperties properties) {
public OpenAPI openApi(ProjectProperties projectProperties, SpringDocExtensionProperties properties) {
Info info = new Info()
.title(String.format("%s %s", properties.getName(), "API 文档"))
.version(properties.getVersion())
.description(properties.getDescription());
ProjectProperties.Contact contact = properties.getContact();
.title(String.format("%s %s", projectProperties.getName(), "API 文档"))
.version(projectProperties.getVersion())
.description(projectProperties.getDescription());
ProjectProperties.Contact contact = projectProperties.getContact();
if (null != contact) {
info.contact(new Contact().name(contact.getName())
.email(contact.getEmail())
.url(contact.getUrl()));
}
ProjectProperties.License license = properties.getLicense();
ProjectProperties.License license = projectProperties.getLicense();
if (null != license) {
info.license(new License().name(license.getName())
.url(license.getUrl()));
}
return new OpenAPI().info(info);
OpenAPI openAPI = new OpenAPI();
openAPI.info(info);
Components components = properties.getComponents();
if (null != components) {
openAPI.components(components);
// 鉴权配置
Map<String, SecurityScheme> securitySchemeMap = components.getSecuritySchemes();
if (MapUtil.isNotEmpty(securitySchemeMap)) {
SecurityRequirement securityRequirement = new SecurityRequirement();
List<String> list = securitySchemeMap.values().stream().map(SecurityScheme::getName).toList();
list.forEach(securityRequirement::addList);
openAPI.addSecurityItem(securityRequirement);
}
}
return openAPI;
}
/**
* 全局自定义配置(全局添加鉴权参数)
*/
@Bean
@ConditionalOnMissingBean
public GlobalOpenApiCustomizer globalOpenApiCustomizer(SpringDocExtensionProperties properties) {
return openApi -> {
if (null != openApi.getPaths()) {
openApi.getPaths().forEach((s, pathItem) -> {
// 为所有接口添加鉴权
Components components = properties.getComponents();
if (null != components && MapUtil.isNotEmpty(components.getSecuritySchemes())) {
Map<String, SecurityScheme> securitySchemeMap = components.getSecuritySchemes();
pathItem.readOperations().forEach(operation -> {
SecurityRequirement securityRequirement = new SecurityRequirement();
List<String> list = securitySchemeMap.values().stream().map(SecurityScheme::getName).toList();
list.forEach(securityRequirement::addList);
operation.addSecurityItem(securityRequirement);
});
}
});
}
};
}
@PostConstruct

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.apidoc.autoconfigure;
import io.swagger.v3.oas.models.Components;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* API 文档扩展配置属性
*
* @author Charles7c
* @since 1.0.1
*/
@Data
@ConfigurationProperties(prefix = "springdoc")
public class SpringDocExtensionProperties {
/**
* 组件配置(包括鉴权配置等)
*/
@NestedConfigurationProperty
private Components components;
}

View File

@@ -35,6 +35,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.core.handler.GeneralPropertySourceFactory;
/**
@@ -56,7 +57,7 @@ public class SaTokenAutoConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns("/**")
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns(StringConstants.PATH_PATTERN)
.excludePathPatterns(properties.getSecurity().getExcludes());
}

View File

@@ -22,7 +22,7 @@ package top.charles7c.continew.starter.auth.satoken.enums;
* @author Charles7c
* @since 1.0.0
*/
public enum SaTokenDaoTypeEnum {
public enum SaTokenDaoType {
/**
* Redis

View File

@@ -18,7 +18,7 @@ package top.charles7c.continew.starter.auth.satoken.properties;
import cn.dev33.satoken.dao.SaTokenDao;
import lombok.Data;
import top.charles7c.continew.starter.auth.satoken.enums.SaTokenDaoTypeEnum;
import top.charles7c.continew.starter.auth.satoken.enums.SaTokenDaoType;
/**
* SaToken 持久层配置属性
@@ -32,7 +32,7 @@ public class SaTokenDaoProperties {
/**
* 持久层类型
*/
private SaTokenDaoTypeEnum type;
private SaTokenDaoType type;
/**
* 自定义持久层实现类(当 type 为 CUSTOM 时必填)

View File

@@ -81,7 +81,7 @@ public class RedisUtils {
* 删除缓存
*
* @param key 键
* @return true 设置成功false 设置失败
* @return true设置成功false设置失败
*/
public static boolean delete(final String key) {
return CLIENT.getBucket(key).delete();
@@ -92,7 +92,7 @@ public class RedisUtils {
*
* @param key 键
* @param timeout 过期时间(单位:秒)
* @return true 设置成功false 设置失败
* @return true设置成功false设置失败
*/
public static boolean expire(final String key, final long timeout) {
return expire(key, Duration.ofSeconds(timeout));
@@ -103,7 +103,7 @@ public class RedisUtils {
*
* @param key 键
* @param duration 过期时间
* @return true 设置成功false 设置失败
* @return true设置成功false设置失败
*/
public static boolean expire(final String key, final Duration duration) {
return CLIENT.getBucket(key).expire(duration);
@@ -123,7 +123,7 @@ public class RedisUtils {
* 是否存在指定缓存
*
* @param key 键
* @return true 存在false 不存在
* @return true存在false不存在
*/
public static boolean hasKey(String key) {
RKeys keys = CLIENT.getKeys();
@@ -141,6 +141,21 @@ public class RedisUtils {
return stream.map(key -> getNameMapper().unmap(key)).collect(Collectors.toList());
}
/**
* 限流
*
* @param key 键
* @param rateType 限流类型OVERALL全局限流PER_CLIENT单机限流
* @param rate 速率(指定时间间隔产生的令牌数)
* @param rateInterval 速率间隔(时间间隔,单位:秒)
* @return true成功false失败
*/
public static boolean rateLimit(String key, RateType rateType, int rate, int rateInterval) {
RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS);
return rateLimiter.tryAcquire(1);
}
/**
* 格式化键,将各子键用 : 拼接起来
*

View File

@@ -0,0 +1,31 @@
<?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>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-captcha</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-captcha-behavior</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 验证码模块 - 行为验证码</description>
<dependencies>
<!-- AJ-Captcha行为验证码包含滑动拼图、文字点选两种方式UI支持弹出和嵌入两种方式 -->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>captcha</artifactId>
</dependency>
<!-- 缓存模块 - Redisson -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-cache-redisson</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.captcha.behavior.autoconfigure;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.anji.captcha.model.common.Const;
import com.anji.captcha.service.CaptchaService;
import com.anji.captcha.service.impl.CaptchaServiceFactory;
import com.anji.captcha.util.ImageUtils;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 行为验证码自动配置
*
* @author Bull-BCLS
* @since 1.1.0
*/
@Slf4j
@AutoConfiguration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "continew-starter.captcha.behavior", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(BehaviorCaptchaProperties.class)
public class BehaviorCaptchaAutoConfiguration {
private final BehaviorCaptchaProperties properties;
/**
* 自定义缓存实现配置
*/
@Configuration
@Import({BehaviorCaptchaCacheConfiguration.Redis.class, BehaviorCaptchaCacheConfiguration.Custom.class})
protected static class BehaviorCaptchaCacheAutoConfiguration {
}
/**
* 行为验证码服务接口
*/
@Bean
@ConditionalOnMissingBean
public CaptchaService captchaService() {
Properties config = new Properties();
config.put(Const.CAPTCHA_CACHETYPE, properties.getCacheType().name().toLowerCase());
config.put(Const.CAPTCHA_WATER_MARK, properties.getWaterMark());
config.put(Const.CAPTCHA_FONT_TYPE, properties.getFontType());
config.put(Const.CAPTCHA_TYPE, properties.getType().getCodeValue());
config.put(Const.CAPTCHA_INTERFERENCE_OPTIONS, properties.getInterferenceOptions());
config.put(Const.ORIGINAL_PATH_JIGSAW, StrUtil.emptyIfNull(properties.getJigsawBaseMapPath()));
config.put(Const.ORIGINAL_PATH_PIC_CLICK, StrUtil.emptyIfNull(properties.getPicClickBaseMapPath()));
config.put(Const.CAPTCHA_SLIP_OFFSET, properties.getSlipOffset());
config.put(Const.CAPTCHA_AES_STATUS, String.valueOf(properties.getEnableAes()));
config.put(Const.CAPTCHA_WATER_FONT, properties.getWaterFont());
config.put(Const.CAPTCHA_CACAHE_MAX_NUMBER, properties.getCacheNumber());
config.put(Const.CAPTCHA_TIMING_CLEAR_SECOND, properties.getTimingClear());
config.put(Const.HISTORY_DATA_CLEAR_ENABLE, properties.getHistoryDataClearEnable());
config.put(Const.REQ_FREQUENCY_LIMIT_ENABLE, properties.getReqFrequencyLimitEnable());
config.put(Const.REQ_GET_LOCK_LIMIT, properties.getReqGetLockLimit());
config.put(Const.REQ_GET_LOCK_SECONDS, properties.getReqGetLockSeconds());
config.put(Const.REQ_GET_MINUTE_LIMIT, properties.getReqGetMinuteLimit());
config.put(Const.REQ_CHECK_MINUTE_LIMIT, properties.getReqCheckMinuteLimit());
config.put(Const.REQ_VALIDATE_MINUTE_LIMIT, properties.getReqVerifyMinuteLimit());
config.put(Const.CAPTCHA_FONT_SIZE, properties.getFontSize());
config.put(Const.CAPTCHA_FONT_STYLE, properties.getFontStyle());
config.put(Const.CAPTCHA_WORD_COUNT, 4);
if (StrUtil.startWith(properties.getJigsawBaseMapPath(), "classpath:")
|| StrUtil.startWith(properties.getPicClickBaseMapPath(), "classpath:")) {
// 自定义 resources 目录下初始化底图
config.put(Const.CAPTCHA_INIT_ORIGINAL, true);
initializeBaseMap(properties.getJigsawBaseMapPath(), properties.getPicClickBaseMapPath());
}
return CaptchaServiceFactory.getInstance(config);
}
/**
* 初始化 行为/点选 验证码底图
*
* @param jigsaw 行为验证码底图路径
* @param picClick 点选验证码底图路径
*/
private static void initializeBaseMap(String jigsaw, String picClick) {
ImageUtils.cacheBootImage(getResourcesImagesFile(jigsaw + "/original/*.png"),
getResourcesImagesFile(jigsaw + "/slidingBlock/*.png"),
getResourcesImagesFile(picClick + "/*.png"));
}
/**
* 获取图片
*
* @param path 图片路径
* @return key图片文件名称value图片
*/
private static Map<String, String> getResourcesImagesFile(String path) {
Map<String, String> imgMap = new HashMap<>();
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
Resource[] resources = resolver.getResources(path);
for (Resource resource : resources) {
String imageName = resource.getFilename();
byte[] imageValue = FileUtil.readBytes(resource.getFile());
imgMap.put(imageName, Base64.encode(imageValue));
}
} catch (Exception e) {
log.error("读取路径为 [{}] 下的图片文件失败", path, e);
}
return imgMap;
}
@PostConstruct
public void postConstruct() {
log.info("[ContiNew Starter] - Auto Configuration 'Behavior Captcha' completed initialization.");
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.captcha.behavior.autoconfigure;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.anji.captcha.service.CaptchaCacheService;
import com.anji.captcha.service.impl.CaptchaServiceFactory;
import jakarta.annotation.PostConstruct;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.client.RedisClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import top.charles7c.continew.starter.cache.redisson.autoconfigure.RedissonAutoConfiguration;
import top.charles7c.continew.starter.captcha.behavior.enums.StorageType;
import top.charles7c.continew.starter.captcha.behavior.impl.BehaviorCaptchaCacheServiceImpl;
/**
* 行为验证码缓存配置
*
* @author Bull-BCLS
* @since 1.1.0
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
abstract class BehaviorCaptchaCacheConfiguration {
/**
* 自定义缓存实现类-Redis
*/
@ConditionalOnClass(RedisClient.class)
@AutoConfigureBefore(RedissonAutoConfiguration.class)
@ConditionalOnProperty(name = "continew-starter.captcha.behavior.cache-type", havingValue = "redis")
static class Redis {
static {
CaptchaServiceFactory.cacheService.put(StorageType.REDIS.name().toLowerCase(), new BehaviorCaptchaCacheServiceImpl());
log.debug("[ContiNew Starter] - Auto Configuration 'Behavior-CaptchaCache-Redis' completed initialization.");
}
}
/**
* 自定义缓存实现类-自定义
*/
@ConditionalOnProperty(name = "continew-starter.captcha.behavior.cache-type", havingValue = "custom")
static class Custom {
@Bean
@ConditionalOnMissingBean
public CaptchaCacheService captchaCacheService(BehaviorCaptchaProperties properties) {
return ReflectUtil.newInstance(properties.getCacheImpl());
}
@PostConstruct
public void postConstruct() {
CaptchaServiceFactory.cacheService.put(StorageType.CUSTOM.name().toLowerCase(), SpringUtil.getBean(CaptchaCacheService.class));
log.debug("[ContiNew Starter] - Auto Configuration 'Behavior-CaptchaCache-Custom' completed initialization.");
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.captcha.behavior.autoconfigure;
import com.anji.captcha.model.common.CaptchaTypeEnum;
import com.anji.captcha.service.CaptchaCacheService;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.charles7c.continew.starter.captcha.behavior.enums.StorageType;
import java.awt.*;
/**
* 行为验证码配置属性
*
* @author Bull-BCLS
* @since 1.1.0
*/
@Data
@ConfigurationProperties(prefix = "continew-starter.captcha.behavior")
public class BehaviorCaptchaProperties {
/**
* 是否启用行为验证码
*/
private boolean enabled = false;
/**
* 是否开启 AES 坐标加密默认true
*/
private Boolean enableAes = true;
/**
* 验证码类型(默认:滑动验证码)
*/
private CaptchaTypeEnum type = CaptchaTypeEnum.BLOCKPUZZLE;
/**
* 缓存类型默认LOCAL 内存)
*/
private StorageType cacheType = StorageType.LOCAL;
/**
* 自定义缓存类型(当 cacheType 为 CUSTOM 时必填)
*/
private Class<? extends CaptchaCacheService> cacheImpl;
/**
* 滑动拼图底图路径(为空则使用默认底图)(路径下需要有两个文件夹,分别为 original存放底图slidingBlock存放滑块
*/
private String jigsawBaseMapPath;
/**
* 校验滑动拼图允许误差偏移量默认5像素
*/
private String slipOffset = "5";
/**
* 点选文字底图路径(为空则使用默认底图)
*/
private String picClickBaseMapPath;
/**
* 点选文字验证码的文字字体(默认:文泉驿正黑)
*/
private String fontType = "WenQuanZhengHei.ttf";
/**
* 历史数据清除开关0关闭1开启
*/
private Integer historyDataClearEnable = 0;
/**
* 一分钟内接口请求次数限制开关0关闭1开启
*/
private Integer reqFrequencyLimitEnable = 0;
/**
* 一分钟内验证码最多失败次数限制默认5次
*/
private int reqGetLockLimit = 5;
/**
* 一分钟内验证码最多失败次数限制达标后锁定时间默认300秒
*/
private int reqGetLockSeconds = 300;
/**
* 获取验证码接口一分钟内请求次数限制默认100次
*/
private int reqGetMinuteLimit = 100;
/**
* 校验检验码接口一分内请求次数限制默认100次
*/
private int reqCheckMinuteLimit = 100;
/**
* 二次校验检验码接口一分钟内请求次数限制默认100次
*/
private int reqVerifyMinuteLimit = 100;
/**
* local缓存的阈值默认1000个
*/
private String cacheNumber = "1000";
/**
* 定时清理过期local缓存默认180秒
*/
private String timingClear = "180";
/**
* 右下角水印文字
*/
private String waterMark = "我的水印";
/**
* 右下角水印字体(默认:文泉驿正黑)
*/
private String waterFont = "WenQuanZhengHei.ttf";
/**
* 滑块干扰项默认0
*/
private String interferenceOptions = "0";
/**
* 点选字体样式默认BOLD
*/
private int fontStyle = Font.BOLD;
/**
* 点选字体大小默认25
*/
private int fontSize = 25;
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.captcha.behavior.enums;
import lombok.Getter;
/**
* 缓存类型枚举
*
* @author Bull-BCLS
* @since 1.1.0
*/
@Getter
public enum StorageType {
/**
* 内存
*/
LOCAL,
/**
* Redis
*/
REDIS,
/**
* 自定义
*/
CUSTOM,
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.captcha.behavior.impl;
import com.anji.captcha.service.CaptchaCacheService;
import top.charles7c.continew.starter.cache.redisson.util.RedisUtils;
import top.charles7c.continew.starter.captcha.behavior.enums.StorageType;
import java.time.Duration;
/**
* 行为验证码 Redis 缓存实现
*
* @author Bull-BCLS
* @since 1.1.0
*/
public class BehaviorCaptchaCacheServiceImpl implements CaptchaCacheService {
@Override
public void set(String key, String value, long expiresInSeconds) {
RedisUtils.set(key, value, Duration.ofSeconds(expiresInSeconds));
}
@Override
public boolean exists(String key) {
return RedisUtils.hasKey(key);
}
@Override
public void delete(String key) {
RedisUtils.delete(key);
}
@Override
public String get(String key) {
return RedisUtils.get(key);
}
@Override
public String type() {
return StorageType.REDIS.name().toLowerCase();
}
}

View File

@@ -0,0 +1 @@
top.charles7c.continew.starter.captcha.behavior.autoconfigure.BehaviorCaptchaAutoConfiguration

View File

@@ -31,7 +31,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(GraphicCaptchaProperties.class)
@ConditionalOnProperty(prefix = "captcha.graphic", name = "enabled", havingValue = "true")
@ConditionalOnProperty(prefix = "continew-starter.captcha.graphic", name = "enabled", havingValue = "true")
public class GraphicCaptchaAutoConfiguration {
@PostConstruct

View File

@@ -21,7 +21,7 @@ import cn.hutool.core.util.StrUtil;
import com.wf.captcha.base.Captcha;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.charles7c.continew.starter.captcha.graphic.enums.GraphicCaptchaTypeEnum;
import top.charles7c.continew.starter.captcha.graphic.enums.GraphicCaptchaType;
import java.awt.*;
@@ -32,7 +32,7 @@ import java.awt.*;
* @since 1.0.0
*/
@Data
@ConfigurationProperties(prefix = "captcha.graphic")
@ConfigurationProperties(prefix = "continew-starter.captcha.graphic")
public class GraphicCaptchaProperties {
/**
@@ -43,7 +43,7 @@ public class GraphicCaptchaProperties {
/**
* 类型
*/
private GraphicCaptchaTypeEnum type;
private GraphicCaptchaType type;
/**
* 内容长度

View File

@@ -17,6 +17,7 @@
<modules>
<module>continew-starter-captcha-graphic</module>
<module>continew-starter-captcha-behavior</module>
</modules>
<dependencies>

View File

@@ -39,7 +39,7 @@ import top.charles7c.continew.starter.core.constant.StringConstants;
@Lazy
@AutoConfiguration
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "cors", name = "enabled", havingValue = "true")
@ConditionalOnProperty(prefix = "continew-starter.cors", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(CorsProperties.class)
public class CorsAutoConfiguration {
@@ -68,7 +68,7 @@ public class CorsAutoConfiguration {
properties.getExposedHeaders().forEach(config::addExposedHeader);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
source.registerCorsConfiguration(StringConstants.PATH_PATTERN, config);
CorsFilter corsFilter = new CorsFilter(source);
log.info("[ContiNew Starter] - Auto Configuration 'CorsFilter' completed initialization.");
return corsFilter;

View File

@@ -18,8 +18,10 @@ package top.charles7c.continew.starter.core.autoconfigure.cors;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.charles7c.continew.starter.core.constant.StringConstants;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@@ -29,7 +31,7 @@ import java.util.List;
* @since 1.0.0
*/
@Data
@ConfigurationProperties(prefix = "cors")
@ConfigurationProperties(prefix = "continew-starter.cors")
public class CorsProperties {
/**
@@ -40,20 +42,22 @@ public class CorsProperties {
/**
* 允许跨域的域名
*/
private List<String> allowedOrigins = new ArrayList<>();
private List<String> allowedOrigins = new ArrayList<>(ALL);
/**
* 允许跨域的请求方式
*/
private List<String> allowedMethods = new ArrayList<>();
private List<String> allowedMethods = new ArrayList<>(ALL);
/**
* 允许跨域的请求头
*/
private List<String> allowedHeaders = new ArrayList<>();
private List<String> allowedHeaders = new ArrayList<>(ALL);
/**
* 允许跨域的响应头
*/
private List<String> exposedHeaders = new ArrayList<>();
private static final List<String> ALL = Collections.singletonList(StringConstants.ASTERISK);
}

View File

@@ -16,7 +16,6 @@
package top.charles7c.continew.starter.core.autoconfigure.project;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -82,9 +81,8 @@ public class ProjectProperties {
public static final boolean IP_ADDR_LOCAL_PARSE_ENABLED;
static {
String underlineCaseProperty = SpringUtil.getProperty("ip-addr-local-parse-enabled");
String camelCaseProperty = SpringUtil.getProperty("ipAddrLocalParseEnabled");
IP_ADDR_LOCAL_PARSE_ENABLED = Convert.toBool(underlineCaseProperty, false) || Convert.toBool(camelCaseProperty, false);
IP_ADDR_LOCAL_PARSE_ENABLED = SpringUtil.getProperty("project.ip-addr-local-parse-enabled", boolean.class, false)
|| SpringUtil.getProperty("project.ipAddrLocalParseEnabled", boolean.class, false);
}
/**

View File

@@ -42,7 +42,7 @@ import java.util.concurrent.ScheduledExecutorService;
@Lazy
@AutoConfiguration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
@ConditionalOnProperty(prefix = "continew-starter.thread-pool", name = "enabled", havingValue = "true")
@EnableAsync(proxyTargetClass = true)
public class AsyncAutoConfiguration implements AsyncConfigurer {

View File

@@ -42,7 +42,7 @@ import java.util.concurrent.ThreadPoolExecutor;
@Slf4j
@Lazy
@AutoConfiguration
@ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
@ConditionalOnProperty(prefix = "continew-starter.thread-pool", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(ThreadPoolProperties.class)
public class ThreadPoolAutoConfiguration {

View File

@@ -27,7 +27,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @since 1.0.0
*/
@Data
@ConfigurationProperties(prefix = "thread-pool")
@ConfigurationProperties(prefix = "continew-starter.thread-pool")
public class ThreadPoolProperties {
/**

View File

@@ -58,4 +58,9 @@ public class StringConstants implements StrPool {
* 中文逗号
*/
public static final String CHINESE_COMMA = "";
/**
* 路径模式
*/
public static final String PATH_PATTERN = "/**";
}

View File

@@ -14,10 +14,9 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.exception;
package top.charles7c.continew.starter.core.exception;
import lombok.NoArgsConstructor;
import top.charles7c.continew.starter.core.exception.BaseException;
/**
* 自定义验证异常-错误请求

View File

@@ -14,10 +14,9 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.exception;
package top.charles7c.continew.starter.core.exception;
import lombok.NoArgsConstructor;
import top.charles7c.continew.starter.core.exception.BaseException;
/**
* 业务异常

View File

@@ -16,7 +16,9 @@
package top.charles7c.continew.starter.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.http.HttpUtil;
@@ -28,6 +30,9 @@ import lombok.extern.slf4j.Slf4j;
import net.dreamlu.mica.ip2region.core.Ip2regionSearcher;
import net.dreamlu.mica.ip2region.core.IpInfo;
import top.charles7c.continew.starter.core.autoconfigure.project.ProjectProperties;
import top.charles7c.continew.starter.core.constant.StringConstants;
import java.util.Set;
/**
* IP 工具类
@@ -45,26 +50,26 @@ public class IpUtils {
private static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true";
/**
* 根据 IP 获取归属地信息
* 查询 IP 归属地
*
* @param ip IP 地址
* @return 归属地信息
* @return IP 归属地
*/
public static String getCityInfo(String ip) {
public static String getAddress(String ip) {
if (ProjectProperties.IP_ADDR_LOCAL_PARSE_ENABLED) {
return getLocalCityInfo(ip);
return getAddressByLocal(ip);
} else {
return getHttpCityInfo(ip);
return getAddressByHttp(ip);
}
}
/**
* 根据 IP 获取归属地信息(网络解析)
* 查询 IP 归属地(网络解析)
*
* @param ip IP 地址
* @return 归属地信息
* @return IP 归属地
*/
public static String getHttpCityInfo(String ip) {
public static String getAddressByHttp(String ip) {
if (isInnerIp(ip)) {
return "内网IP";
}
@@ -74,19 +79,21 @@ public class IpUtils {
}
/**
* 根据 IP 获取归属地信息(本地解析)
* 查询 IP 归属地(本地解析)
*
* @param ip IP 地址
* @return 归属地信息
* @return IP 归属地
*/
public static String getLocalCityInfo(String ip) {
public static String getAddressByLocal(String ip) {
if (isInnerIp(ip)) {
return "内网IP";
}
Ip2regionSearcher ip2regionSearcher = SpringUtil.getBean(Ip2regionSearcher.class);
IpInfo ipInfo = ip2regionSearcher.memorySearch(ip);
if (null != ipInfo) {
return ipInfo.getAddress();
Set<String> regionSet = CollUtil.newLinkedHashSet(ipInfo.getAddress(), ipInfo.getIsp());
regionSet.removeIf(StrUtil::isBlank);
return String.join(StringConstants.SPACE, regionSet);
}
return null;
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.util;
package top.charles7c.continew.starter.core.util;
import cn.hutool.core.util.ReflectUtil;
import lombok.AccessLevel;

View File

@@ -16,6 +16,7 @@
package top.charles7c.continew.starter.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import jakarta.servlet.http.HttpServletRequest;
@@ -24,8 +25,9 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.charles7c.continew.starter.core.constant.StringConstants;
import java.util.Objects;
import java.util.*;
/**
* Servlet 工具类
@@ -64,8 +66,57 @@ public class ServletUtils {
if (null == request) {
return null;
}
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
return userAgent.getBrowser().getName() + " " + userAgent.getVersion();
return getBrowser(request.getHeader("User-Agent"));
}
/**
* 获取浏览器及其版本信息
*
* @param userAgentString User-Agent 字符串
* @return 浏览器及其版本信息
*/
public static String getBrowser(String userAgentString) {
UserAgent userAgent = UserAgentUtil.parse(userAgentString);
return userAgent.getBrowser().getName() + StringConstants.SPACE + userAgent.getVersion();
}
/**
* 获取操作系统
*
* @param request 请求对象
* @return 操作系统
*/
public static String getOs(HttpServletRequest request) {
if (null == request) {
return null;
}
return getOs(request.getHeader("User-Agent"));
}
/**
* 获取操作系统
*
* @param userAgentString User-Agent 字符串
* @return 操作系统
*/
public static String getOs(String userAgentString) {
UserAgent userAgent = UserAgentUtil.parse(userAgentString);
return userAgent.getOs().getName();
}
/**
* 获取响应所有的头header信息
*
* @param response 响应对象{@link HttpServletResponse}
* @return header值
*/
public static Map<String, String> getHeaderMap(HttpServletResponse response) {
final Collection<String> headerNames = response.getHeaderNames();
final Map<String, String> headerMap = MapUtil.newHashMap(headerNames.size(), true);
for (String name : headerNames) {
headerMap.put(name, response.getHeader(name));
}
return headerMap;
}
private static ServletRequestAttributes getServletRequestAttributes() {

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.util.validate;
package top.charles7c.continew.starter.core.util.validate;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.extension.crud.exception.BusinessException;
import top.charles7c.continew.starter.core.exception.BusinessException;
import java.util.function.BooleanSupplier;

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.util.validate;
package top.charles7c.continew.starter.core.util.validate;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import top.charles7c.continew.starter.extension.crud.exception.BadRequestException;
import top.charles7c.continew.starter.core.exception.BadRequestException;
import java.util.function.BooleanSupplier;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.util.validate;
package top.charles7c.continew.starter.core.util.validate;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.data.mybatis.plus.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.lang.annotation.*;
/**
* 是否启用数据权限注解
*
* @author Charles7c
* @since 1.1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ConditionalOnProperty(prefix = "mybatis-plus.extension.data-permission", name = "enabled", havingValue = "true")
public @interface ConditionalOnEnabledDataPermission {}

View File

@@ -17,7 +17,6 @@
package top.charles7c.continew.starter.data.mybatis.plus.autoconfigure;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -46,15 +45,28 @@ public class MyBatisPlusExtensionProperties {
private String mapperPackage;
/**
* 数据权限处理器实现类
* 数据权限插件配置
*/
private Class<? extends DataPermissionHandler> dataPermissionHandlerImpl;
private DataPermissionProperties dataPermission;
/**
* 分页插件配置
*/
private PaginationProperties pagination;
/**
* 数据权限插件配置属性
*/
@Data
public static class DataPermissionProperties {
/**
* 是否启用数据权限插件
*/
private boolean enabled = false;
}
/**
* 分页插件配置属性
*/

View File

@@ -17,7 +17,7 @@
package top.charles7c.continew.starter.data.mybatis.plus.autoconfigure;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
@@ -36,6 +36,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import top.charles7c.continew.starter.core.handler.GeneralPropertySourceFactory;
import top.charles7c.continew.starter.data.mybatis.plus.datapermission.DataPermissionFilter;
import top.charles7c.continew.starter.data.mybatis.plus.datapermission.DataPermissionHandlerImpl;
/**
* MyBatis Plus 自动配置
@@ -60,13 +63,13 @@ public class MybatisPlusAutoConfiguration {
public MybatisPlusInterceptor mybatisPlusInterceptor(MyBatisPlusExtensionProperties properties) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 数据权限插件
Class<? extends DataPermissionHandler> dataPermissionHandlerImpl = properties.getDataPermissionHandlerImpl();
if (null != dataPermissionHandlerImpl) {
interceptor.addInnerInterceptor(new DataPermissionInterceptor(ReflectUtil.newInstance(dataPermissionHandlerImpl)));
MyBatisPlusExtensionProperties.DataPermissionProperties dataPermissionProperties = properties.getDataPermission();
if (null != dataPermissionProperties && dataPermissionProperties.isEnabled()) {
interceptor.addInnerInterceptor(new DataPermissionInterceptor(SpringUtil.getBean(DataPermissionHandler.class)));
}
// 分页插件
MyBatisPlusExtensionProperties.PaginationProperties paginationProperties = properties.getPagination();
if (properties.isEnabled() && paginationProperties.isEnabled()) {
if (null != paginationProperties && paginationProperties.isEnabled()) {
interceptor.addInnerInterceptor(this.paginationInnerInterceptor(paginationProperties));
}
// 防全表更新与删除插件
@@ -74,6 +77,16 @@ public class MybatisPlusAutoConfiguration {
return interceptor;
}
/**
* 数据权限处理器
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledDataPermission
public DataPermissionHandler dataPermissionHandler(DataPermissionFilter dataPermissionFilter) {
return new DataPermissionHandlerImpl(dataPermissionFilter);
}
/**
* ID 生成器配置仅在主键类型idType配置为 ASSIGN_ID 或 ASSIGN_UUID 时有效)
* <p>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.base;
package top.charles7c.continew.starter.data.mybatis.plus.base;
import cn.hutool.core.util.ClassUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.base;
package top.charles7c.continew.starter.data.mybatis.plus.base;
import com.baomidou.mybatisplus.annotation.IEnum;

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.data.mybatis.plus.datapermission;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 数据权限注解
*
* @author Charles7c
* @since 1.1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
/**
* Alias for the {@link #tableAlias()} attribute.
*/
@AliasFor("tableAlias")
String value() default "";
/**
* 表别名
*/
@AliasFor("value")
String tableAlias() default "";
/**
* ID
*/
String id() default "id";
/**
* 部门 ID
*/
String deptId() default "dept_id";
/**
* 用户 ID
*/
String userId() default "create_user";
/**
* 角色 ID角色和部门关联表
*/
String roleId() default "role_id";
/**
* 部门表别名
*/
String deptTableAlias() default "sys_dept";
/**
* 角色和部门关联表别名
*/
String roleDeptTableAlias() default "sys_role_dept";
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.data.mybatis.plus.datapermission;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Set;
/**
* 当前用户信息
*
* @author Charles7c
* @since 1.1.0
*/
@Data
public class DataPermissionCurrentUser {
/**
* 用户 ID
*/
private String userId;
/**
* 角色列表
*/
private Set<CurrentUserRole> roles;
/**
* 部门 ID
*/
private String deptId;
/**
* 当前用户角色信息
*/
@Data
@AllArgsConstructor
public static class CurrentUserRole {
/**
* 角色 ID
*/
private String roleId;
/**
* 数据权限
*/
private DataScope dataScope;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.data.mybatis.plus.datapermission;
/**
* 数据权限过滤器接口
*
* @author Charles7c
* @since 1.1.0
*/
public interface DataPermissionFilter {
/**
* 是否过滤
*
* @return true过滤false不过滤
*/
boolean isFilter();
/**
* 获取当前用户信息
*
* @return 当前用户信息
*/
DataPermissionCurrentUser getCurrentUser();
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.data.mybatis.plus.datapermission;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import top.charles7c.continew.starter.core.constant.StringConstants;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
import net.sf.jsqlparser.statement.select.SubSelect;
/**
* 数据权限处理器实现类
*
* @author <a href="https://gitee.com/baomidou/mybatis-plus/issues/I37I90">DataPermissionInterceptor 如何使用?</a>
* @author Charles7c
* @since 1.1.0
*/
@Slf4j
@RequiredArgsConstructor
public class DataPermissionHandlerImpl implements DataPermissionHandler {
private final DataPermissionFilter dataPermissionFilter;
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
try {
Class<?> clazz =
Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringConstants.DOT)));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringConstants.DOT) + 1);
Method[] methodArr = clazz.getMethods();
for (Method method : methodArr) {
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
if (null != dataPermission
&& (method.getName().equals(methodName) || (method.getName() + "_COUNT").equals(methodName))) {
if (dataPermissionFilter.isFilter()) {
return buildDataScopeFilter(dataPermission, where);
}
}
}
} catch (ClassNotFoundException e) {
log.error("Data permission handler build data scope filter occurred an error: {}.", e.getMessage(), e);
}
return where;
}
/**
* 构建数据范围过滤条件
*
* @param dataPermission
* 数据权限
* @param where
* 当前查询条件
* @return 构建后查询条件
*/
private Expression buildDataScopeFilter(DataPermission dataPermission, Expression where) {
Expression expression = null;
String tableAlias = dataPermission.tableAlias();
String id = dataPermission.id();
String deptId = dataPermission.deptId();
DataPermissionCurrentUser currentUser = dataPermissionFilter.getCurrentUser();
Set<DataPermissionCurrentUser.CurrentUserRole> roles = currentUser.getRoles();
for (DataPermissionCurrentUser.CurrentUserRole role : roles) {
DataScope dataScope = role.getDataScope();
if (DataScope.ALL.equals(dataScope)) {
return where;
}
if (DataScope.DEPT_AND_CHILD.equals(dataScope)) {
// select t1.* from table as t1 where t1.`dept_id` in (select `id` from `sys_dept` where `id` = xxx or
// find_in_set(xxx, `ancestors`));
// 构建子查询
SubSelect subSelect = new SubSelect();
PlainSelect select = new PlainSelect();
select.setSelectItems(Collections.singletonList(new SelectExpressionItem(new Column(id))));
select.setFromItem(new Table(dataPermission.deptTableAlias()));
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(id));
equalsTo.setRightExpression(new LongValue(currentUser.getDeptId()));
Function function = new Function();
function.setName("find_in_set");
function.setParameters(new ExpressionList(new LongValue(currentUser.getDeptId()), new Column("ancestors")));
select.setWhere(new OrExpression(equalsTo, function));
subSelect.setSelectBody(select);
// 构建父查询
InExpression inExpression = new InExpression();
inExpression.setLeftExpression(this.buildColumn(tableAlias, deptId));
inExpression.setRightExpression(subSelect);
expression = null != expression ? new OrExpression(expression, inExpression) : inExpression;
} else if (DataScope.DEPT.equals(dataScope)) {
// select t1.* from table as t1 where t1.`dept_id` = xxx;
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(this.buildColumn(tableAlias, deptId));
equalsTo.setRightExpression(new LongValue(currentUser.getDeptId()));
expression = null != expression ? new OrExpression(expression, equalsTo) : equalsTo;
} else if (DataScope.SELF.equals(dataScope)) {
// select t1.* from table as t1 where t1.`create_user` = xxx;
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(this.buildColumn(tableAlias, dataPermission.userId()));
equalsTo.setRightExpression(new LongValue(currentUser.getUserId()));
expression = null != expression ? new OrExpression(expression, equalsTo) : equalsTo;
} else if (DataScope.CUSTOM.equals(dataScope)) {
// select t1.* from table as t1 where t1.`dept_id` in (select `dept_id` from `sys_role_dept` where
// `role_id` = xxx);
// 构建子查询
SubSelect subSelect = new SubSelect();
PlainSelect select = new PlainSelect();
select.setSelectItems(Collections.singletonList(new SelectExpressionItem(new Column(deptId))));
select.setFromItem(new Table(dataPermission.roleDeptTableAlias()));
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(dataPermission.roleId()));
equalsTo.setRightExpression(new LongValue(role.getRoleId()));
select.setWhere(equalsTo);
subSelect.setSelectBody(select);
// 构建父查询
InExpression inExpression = new InExpression();
inExpression.setLeftExpression(this.buildColumn(tableAlias, deptId));
inExpression.setRightExpression(subSelect);
expression = null != expression ? new OrExpression(expression, inExpression) : inExpression;
}
}
return null != where ? new AndExpression(where, new Parenthesis(expression)) : expression;
}
/**
* 构建 Column
*
* @param tableAlias
* 表别名
* @param columnName
* 字段名称
* @return 带表别名字段
*/
private Column buildColumn(String tableAlias, String columnName) {
if (StringUtils.isNotEmpty(tableAlias)) {
columnName = String.format("%s.%s", tableAlias, columnName);
}
return new Column(columnName);
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.data.mybatis.plus.datapermission;
/**
* 数据权限枚举
*
* @author Charles7c
* @since 1.1.0
*/
public enum DataScope {
/**
* 全部数据权限
*/
ALL,
/**
* 本部门及以下数据权限
*/
DEPT_AND_CHILD,
/**
* 本部门数据权限
*/
DEPT,
/**
* 仅本人数据权限
*/
SELF,
/**
* 自定义数据权限
*/
CUSTOM,
}

View File

@@ -14,9 +14,7 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.annotation;
import top.charles7c.continew.starter.extension.crud.enums.QueryTypeEnum;
package top.charles7c.continew.starter.data.mybatis.plus.query;
import java.lang.annotation.*;
@@ -40,7 +38,7 @@ public @interface Query {
/**
* 查询类型等值查询模糊查询范围查询等
*/
QueryTypeEnum type() default QueryTypeEnum.EQUAL;
QueryType type() default QueryType.EQUAL;
/**
* 多属性模糊查询仅支持 String 类型属性

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.util;
package top.charles7c.continew.starter.data.mybatis.plus.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -23,10 +23,9 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import top.charles7c.continew.starter.extension.crud.annotation.Query;
import top.charles7c.continew.starter.extension.crud.enums.QueryTypeEnum;
import top.charles7c.continew.starter.extension.crud.exception.BadRequestException;
import top.charles7c.continew.starter.extension.crud.util.validate.ValidationUtils;
import top.charles7c.continew.starter.core.exception.BadRequestException;
import top.charles7c.continew.starter.core.util.ReflectUtils;
import top.charles7c.continew.starter.core.util.validate.ValidationUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -129,7 +128,7 @@ public class QueryHelper {
// 注意数据库规范中列采用下划线连接法命名程序规范中变量采用驼峰法命名
String property = queryAnnotation.property();
String columnName = StrUtil.toUnderlineCase(StrUtil.blankToDefault(property, fieldName));
QueryTypeEnum queryType = queryAnnotation.type();
QueryType queryType = queryAnnotation.type();
switch (queryType) {
case EQUAL -> queryWrapper.eq(columnName, fieldValue);
case NOT_EQUAL -> queryWrapper.ne(columnName, fieldValue);

View File

@@ -14,11 +14,10 @@
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.enums;
package top.charles7c.continew.starter.data.mybatis.plus.query;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.charles7c.continew.starter.extension.crud.base.IBaseEnum;
/**
* 查询类型枚举
@@ -28,7 +27,7 @@ import top.charles7c.continew.starter.extension.crud.base.IBaseEnum;
*/
@Getter
@RequiredArgsConstructor
public enum QueryTypeEnum implements IBaseEnum<Integer> {
public enum QueryType {
/**
* 等值查询例如WHERE `age` = 18

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.1.5</version>
<version>3.1.7</version>
<relativePath/>
</parent>
@@ -54,19 +54,23 @@
</scm>
<properties>
<revision>1.0.0</revision>
<just-auth.version>1.16.5</just-auth.version>
<revision>1.1.0</revision>
<just-auth.version>1.16.6</just-auth.version>
<sa-token.version>1.37.0</sa-token.version>
<mybatis-plus.version>3.5.4.1</mybatis-plus.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<dynamic-datasource.version>4.2.0</dynamic-datasource.version>
<p6spy.version>3.9.1</p6spy.version>
<redisson.version>3.24.3</redisson.version>
<redisson.version>3.25.2</redisson.version>
<sms4j.version>3.0.4</sms4j.version>
<aj-captcha.version>1.3.0</aj-captcha.version>
<easy-captcha.version>1.6.2</easy-captcha.version>
<easy-excel.version>3.3.2</easy-excel.version>
<knife4j.version>4.3.0</knife4j.version>
<ip2region.version>3.1.5.1</ip2region.version>
<hutool.version>5.8.23</hutool.version>
<easy-excel.version>3.3.3</easy-excel.version>
<x-file-storage.version>2.0.0</x-file-storage.version>
<aws-s3.version>1.12.626</aws-s3.version>
<knife4j.version>4.4.0</knife4j.version>
<ttl.version>2.14.4</ttl.version>
<ip2region.version>3.1.6</ip2region.version>
<hutool.version>5.8.24</hutool.version>
</properties>
<dependencyManagement>
@@ -148,6 +152,13 @@
<version>${sms4j.version}</version>
</dependency>
<!-- AJ-Captcha行为验证码包含滑动拼图、文字点选两种方式UI支持弹出和嵌入两种方式 -->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>captcha</artifactId>
<version>${aj-captcha.version}</version>
</dependency>
<!-- Easy CaptchaJava 图形验证码,支持 gif、中文、算术等类型可用于 Java Web、JavaSE 等项目) -->
<dependency>
<groupId>com.github.whvcse</groupId>
@@ -162,6 +173,20 @@
<version>${easy-excel.version}</version>
</dependency>
<!-- X File Storage一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台) -->
<dependency>
<groupId>org.dromara.x-file-storage</groupId>
<artifactId>x-file-storage-spring</artifactId>
<version>${x-file-storage.version}</version>
</dependency>
<!-- Amazon S3Amazon Simple Storage Service亚马逊简单存储服务通用存储协议 S3兼容主流云厂商对象存储 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-s3.version}</version>
</dependency>
<!-- Knife4j前身是 swagger-bootstrap-ui集 Swagger2 和 OpenAPI3 为一体的增强解决方案) -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
@@ -171,6 +196,13 @@
<scope>import</scope>
</dependency>
<!-- TTL线程间传递 ThreadLocal异步执行时上下文传递的解决方案 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${ttl.version}</version>
</dependency>
<!-- 第三方封装 Ip2region离线 IP 数据管理框架和定位库支持亿级别的数据段10 微秒级别的查询性能,提供了许多主流编程语言的 xdb 数据管理引擎的实现) -->
<dependency>
<groupId>net.dreamlu</groupId>
@@ -235,6 +267,13 @@
<version>${revision}</version>
</dependency>
<!-- 验证码模块 - 行为验证码 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-captcha-behavior</artifactId>
<version>${revision}</version>
</dependency>
<!-- 验证码模块 - 图形验证码 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
@@ -249,6 +288,27 @@
<version>${revision}</version>
</dependency>
<!-- 存储模块 - 本地存储 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-storage-local</artifactId>
<version>${revision}</version>
</dependency>
<!-- 日志模块 - HttpTraceProSpring Boot Actuator HttpTrace 定制增强版) -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log-httptrace-pro</artifactId>
<version>${revision}</version>
</dependency>
<!-- 日志模块 - 公共模块 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- API 文档模块 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
@@ -274,6 +334,7 @@
<build>
<plugins>
<!-- 扁平化 Maven 插件(统一版本号) -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>

View File

@@ -23,17 +23,18 @@ import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorContro
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import top.charles7c.continew.starter.extension.crud.handler.GlobalErrorHandler;
import top.charles7c.continew.starter.extension.crud.handler.GlobalExceptionHandler;
/**
* 全局错误处理器自动配置
* 全局异常处理器自动配置
*
* @author Charles7c
* @since 1.0.0
*/
@Slf4j
@AutoConfiguration
@Import(GlobalErrorHandler.class)
@Import({GlobalExceptionHandler.class, GlobalErrorHandler.class})
@ConditionalOnMissingBean(BasicErrorController.class)
@ComponentScan("top.charles7c.continew.starter.extension.crud.handler")
public class GlobalErrorHandlerAutoConfiguration {
public class GlobalExceptionHandlerAutoConfiguration {
}

View File

@@ -34,14 +34,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.core.util.ReflectUtils;
import top.charles7c.continew.starter.core.util.validate.CheckUtils;
import top.charles7c.continew.starter.data.mybatis.plus.base.BaseMapper;
import top.charles7c.continew.starter.data.mybatis.plus.query.QueryHelper;
import top.charles7c.continew.starter.extension.crud.annotation.TreeField;
import top.charles7c.continew.starter.extension.crud.model.query.PageQuery;
import top.charles7c.continew.starter.extension.crud.model.query.SortQuery;
import top.charles7c.continew.starter.extension.crud.model.resp.PageDataResp;
import top.charles7c.continew.starter.extension.crud.util.QueryHelper;
import top.charles7c.continew.starter.extension.crud.util.ReflectUtils;
import top.charles7c.continew.starter.extension.crud.util.TreeUtils;
import top.charles7c.continew.starter.extension.crud.util.validate.CheckUtils;
import top.charles7c.continew.starter.file.excel.util.ExcelUtils;
import java.lang.reflect.Field;
@@ -162,7 +163,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseDO,
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long add(C req) {
if (null == req) {
return 0L;
@@ -173,7 +173,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseDO,
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(C req, Long id) {
T entity = this.getById(id);
BeanUtil.copyProperties(req, entity, CopyOptions.create().ignoreNullValue());

View File

@@ -49,11 +49,11 @@ public class CrudRequestMappingHandlerMapping extends RequestMappingHandlerMappi
if (!handlerType.isAnnotationPresent(CrudRequestMapping.class)) {
return requestMappingInfo;
}
// 过滤 API如果 API 列表中不包含,则忽略
CrudRequestMapping crudRequestMapping = handlerType.getDeclaredAnnotation(CrudRequestMapping.class);
// 过滤 API如果非本类中定义且 API 列表中不包含,则忽略
Api[] apiArr = crudRequestMapping.api();
Api api = ExceptionUtils.exToNull(() -> Api.valueOf(method.getName().toUpperCase()));
if (!ArrayUtil.containsAny(apiArr, Api.ALL, api)) {
if (method.getDeclaringClass() != handlerType && !ArrayUtil.containsAny(apiArr, Api.ALL, api)) {
return null;
}
// 拼接路径(合并了 @RequestMapping 的部分能力)

View File

@@ -0,0 +1,187 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.extension.crud.handler;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.core.exception.BadRequestException;
import top.charles7c.continew.starter.core.exception.BusinessException;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import java.util.Objects;
/**
* 全局异常处理器
*
* @author Charles7c
* @since 1.1.0
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 拦截自定义验证异常-错误请求
*/
@ExceptionHandler(BadRequestException.class)
public R handleBadRequestException(BadRequestException e, HttpServletRequest request) {
log.warn("请求地址 [{}],自定义验证失败。", request.getRequestURI(), e);
return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage());
}
/**
* 拦截校验异常-违反约束异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public R constraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
String errorMsg =
CollUtil.join(e.getConstraintViolations(), StringConstants.CHINESE_COMMA, ConstraintViolation::getMessage);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
/**
* 拦截校验异常-绑定异常
*/
@ExceptionHandler(BindException.class)
public R handleBindException(BindException e, HttpServletRequest request) {
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
String errorMsg = CollUtil.join(e.getAllErrors(), StringConstants.CHINESE_COMMA,
DefaultMessageSourceResolvable::getDefaultMessage);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
/**
* 拦截校验异常-方法参数无效异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
String errorMsg = ExceptionUtils
.exToNull(() -> Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
/**
* 拦截校验异常-方法参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e,
HttpServletRequest request) {
String errorMsg = StrUtil.format("参数名:[{}],期望参数类型:[{}]", e.getName(), e.getParameter().getParameterType());
log.warn("请求地址 [{}],参数转换失败,{}。", request.getRequestURI(), errorMsg, e);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
/**
* 拦截文件上传异常-超过上传大小限制
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public R handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) {
log.warn("请求地址 [{}],上传文件失败,文件大小超过限制。", request.getRequestURI(), e);
String sizeLimit = StrUtil.subBetween(e.getMessage(), "The maximum size ", " for");
String errorMsg = String.format("请上传小于 %sMB 的文件", NumberUtil.parseLong(sizeLimit) / 1024 / 1024);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
/**
* 认证异常-登录认证
*/
@ExceptionHandler(NotLoginException.class)
public R handleNotLoginException(NotLoginException e, HttpServletRequest request) {
log.error("请求地址 [{}],认证失败,无法访问系统资源。", request.getRequestURI(), e);
String errorMsg = switch (e.getType()) {
case NotLoginException.KICK_OUT -> "您已被踢下线。";
case NotLoginException.BE_REPLACED_MESSAGE -> "您已被顶下线。";
default -> "您的登录状态已过期,请重新登录。";
};
return R.fail(HttpStatus.UNAUTHORIZED.value(), errorMsg);
}
/**
* 认证异常-权限认证
*/
@ExceptionHandler(NotPermissionException.class)
public R handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
log.error("请求地址 [{}],权限码校验失败。", request.getRequestURI(), e);
return R.fail(HttpStatus.FORBIDDEN.value(), "没有访问权限,请联系管理员授权");
}
/**
* 认证异常-角色认证
*/
@ExceptionHandler(NotRoleException.class)
public R handleNotRoleException(NotRoleException e, HttpServletRequest request) {
log.error("请求地址 [{}],角色权限校验失败。", request.getRequestURI(), e);
return R.fail(HttpStatus.FORBIDDEN.value(), "没有访问权限,请联系管理员授权");
}
/**
* 拦截校验异常-请求方式不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
log.error("请求地址 [{}],不支持 [{}] 请求。", request.getRequestURI(), e.getMethod());
return R.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), e.getMessage());
}
/**
* 拦截业务异常
*/
@ExceptionHandler(BusinessException.class)
public R handleServiceException(BusinessException e, HttpServletRequest request) {
log.error("请求地址 [{}],发生业务异常。", request.getRequestURI(), e);
return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public R handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error("请求地址 [{}],发生系统异常。", request.getRequestURI(), e);
return R.fail(e.getMessage());
}
/**
* 拦截未知的系统异常
*/
@ExceptionHandler(Throwable.class)
public R handleException(Throwable e, HttpServletRequest request) {
log.error("请求地址 [{}],发生未知异常。", request.getRequestURI(), e);
return R.fail(e.getMessage());
}
}

View File

@@ -24,8 +24,8 @@ import cn.hutool.core.lang.tree.parser.NodeParser;
import cn.hutool.core.util.ReflectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import top.charles7c.continew.starter.core.util.validate.CheckUtils;
import top.charles7c.continew.starter.extension.crud.annotation.TreeField;
import top.charles7c.continew.starter.extension.crud.util.validate.CheckUtils;
import java.util.ArrayList;
import java.util.List;

View File

@@ -1,2 +1,2 @@
top.charles7c.continew.starter.extension.crud.autoconfigure.CrudAutoConfiguration
top.charles7c.continew.starter.extension.crud.autoconfigure.GlobalErrorHandlerAutoConfiguration
top.charles7c.continew.starter.extension.crud.autoconfigure.GlobalExceptionHandlerAutoConfiguration

View File

@@ -0,0 +1,17 @@
<?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>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-log-common</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 日志模块 - 公共模块</description>
</project>

View File

@@ -0,0 +1,53 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.annotation;
import java.lang.annotation.*;
/**
* 日志注解
* <p>用于接口方法或类上,辅助 Spring Doc 使用效果最佳</p>
*
* @author Charles7c
* @since 1.1.0
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
/**
* 日志描述(仅用于接口方法上)
* <p>
* 优先级:@Log("描述") > @Operation(summary="描述")
* </p>
*/
String value() default "";
/**
* 所属模块(用于接口方法或类上)
* <p>
* 优先级: 接口方法上的 @Log(module = "模块") > 接口类上的 @Log(module = "模块") > @Tag(name = "模块") 内容
* </p>
*/
String module() default "";
/**
* 是否忽略日志记录(用于接口方法或类上)
*/
boolean ignore() default false;
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.dao;
import top.charles7c.continew.starter.log.common.model.LogRecord;
import java.util.Collections;
import java.util.List;
/**
* 日志持久层接口
*
* @author Charles7c
* @since 1.1.0
*/
public interface LogDao {
/**
* 查询日志列表
*
* @return 日志列表
*/
default List<LogRecord> list() {
return Collections.emptyList();
}
/**
* 记录日志
*
* @param logRecord 日志信息
*/
void add(LogRecord logRecord);
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.dao.impl;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.common.model.LogRecord;
import java.util.LinkedList;
import java.util.List;
/**
* 日志持久层接口默认实现类(基于内存)
*
* @author Dave SyerSpring Boot Actuator
* @author Olivier BourgainSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
public class LogDaoDefaultImpl implements LogDao {
/**
* 容量
*/
private int capacity = 100;
/**
* 是否降序
*/
private boolean reverse = true;
/**
* 日志列表
*/
private final List<LogRecord> logRecords = new LinkedList<>();
@Override
public List<LogRecord> list() {
synchronized (this.logRecords) {
return List.copyOf(this.logRecords);
}
}
@Override
public void add(LogRecord logRecord) {
synchronized (this.logRecords) {
while (this.logRecords.size() >= this.capacity) {
this.logRecords.remove(this.reverse ? this.capacity - 1 : 0);
}
if (this.reverse) {
this.logRecords.add(0, logRecord);
} else {
this.logRecords.add(logRecord);
}
}
}
/**
* 设置内存中存储的最大日志容量
*
* @param capacity 容量
*/
public void setCapacity(int capacity) {
synchronized (this.logRecords) {
this.capacity = capacity;
}
}
/**
* 设置是否降序
*
* @param reverse 是否降序默认true
*/
public void setReverse(boolean reverse) {
synchronized (this.logRecords) {
this.reverse = reverse;
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.enums;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 日志包含信息
*
* @author Wallace WadgeSpring Boot Actuator
* @author Emily TsanovaSpring Boot Actuator
* @author Joseph BeetonSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
public enum Include {
/**
* 描述
*/
DESCRIPTION,
/**
* 模块
*/
MODULE,
/**
* 请求头(默认)
*/
REQUEST_HEADERS,
/**
* 请求体(如包含请求体,则请求参数无效)
*/
REQUEST_BODY,
/**
* 请求参数(默认)
*/
REQUEST_PARAM,
/**
* IP 归属地
*/
IP_ADDRESS,
/**
* 浏览器
*/
BROWSER,
/**
* 操作系统
*/
OS,
/**
* 响应头(默认)
*/
RESPONSE_HEADERS,
/**
* 响应体(如包含响应体,则响应参数无效)
*/
RESPONSE_BODY,
/**
* 响应参数(默认)
*/
RESPONSE_PARAM,
;
private static final Set<Include> DEFAULT_INCLUDES;
static {
Set<Include> defaultIncludes = new LinkedHashSet<>();
defaultIncludes.add(Include.REQUEST_HEADERS);
defaultIncludes.add(Include.RESPONSE_HEADERS);
defaultIncludes.add(Include.REQUEST_PARAM);
defaultIncludes.add(Include.RESPONSE_PARAM);
DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes);
}
/**
* 获取默认包含信息
*
* @return 默认包含信息
*/
public static Set<Include> defaultIncludes() {
return DEFAULT_INCLUDES;
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.model;
import lombok.Data;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
/**
* 日志信息
*
* @author Dave SyerSpring Boot Actuator
* @author Andy WilkinsonSpring Boot Actuator
* @author Phillip WebbSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
@Data
public class LogRecord {
/**
* 描述
*/
private String description;
/**
* 模块
*/
private String module;
/**
* 请求信息
*/
private LogRequest request;
/**
* 响应信息
*/
private LogResponse response;
/**
* 耗时
*/
private Duration timeTaken;
/**
* 时间戳
*/
private final Instant timestamp;
public LogRecord(Instant timestamp, LogRequest request, LogResponse response, Duration timeTaken) {
this.timestamp = timestamp;
this.request = request;
this.response = response;
this.timeTaken = timeTaken;
}
/**
* 开始记录日志
*
* @param request 请求信息
* @return 日志记录器
*/
public static Started start(RecordableHttpRequest request) {
return start(Clock.systemUTC(), request);
}
/**
* 开始记录日志
*
* @param timestamp 开始时间
* @param request 请求信息
* @return 日志记录器
*/
public static Started start(Clock timestamp, RecordableHttpRequest request) {
return new Started(timestamp, request);
}
/**
* 日志记录器
*/
public static final class Started {
private final Instant timestamp;
private final RecordableHttpRequest request;
private Started(Clock clock, RecordableHttpRequest request) {
this.timestamp = Instant.now(clock);
this.request = request;
}
/**
* 结束日志记录
*
* @param response 响应信息
* @param includes 包含信息
* @return 日志记录
*/
public LogRecord finish(RecordableHttpResponse response, Set<Include> includes) {
return finish(Clock.systemUTC(), response, includes);
}
/**
* 结束日志记录
*
* @param clock 时间
* @param response 响应信息
* @param includes 包含信息
* @return 日志记录
*/
public LogRecord finish(Clock clock, RecordableHttpResponse response, Set<Include> includes) {
LogRequest logRequest = new LogRequest(this.request, includes);
LogResponse logResponse = new LogResponse(response, includes);
Duration duration = Duration.between(this.timestamp, Instant.now(clock));
return new LogRecord(this.timestamp, logRequest, logResponse, duration);
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.model;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import org.springframework.http.HttpHeaders;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.core.util.IpUtils;
import top.charles7c.continew.starter.core.util.ServletUtils;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.net.URI;
import java.util.Map;
import java.util.Set;
/**
* 请求信息
*
* @author Charles7c
* @since 1.1.0
*/
@Data
public class LogRequest {
/**
* 请求方式
*/
private String method;
/**
* 请求 URL
*/
private URI url;
/**
* IP
*/
private String ip;
/**
* 请求头
*/
private Map<String, String> headers;
/**
* 请求体JSON 字符串)
*/
private String body;
/**
* 请求参数
*/
private Map<String, Object> param;
/**
* IP 归属地
*/
private String address;
/**
* 浏览器
*/
private String browser;
/**
* 操作系统
*/
private String os;
public LogRequest(RecordableHttpRequest request, Set<Include> includes) {
this.method = request.getMethod();
this.url = request.getUrl();
this.ip = request.getIp();
this.headers = (includes.contains(Include.REQUEST_HEADERS)) ? request.getHeaders() : null;
if (includes.contains(Include.REQUEST_BODY)) {
this.body = request.getBody();
} else if (includes.contains(Include.REQUEST_PARAM)) {
this.param = request.getParam();
}
this.address = (includes.contains(Include.IP_ADDRESS)) ? IpUtils.getAddress(this.ip) : null;
String userAgentString = ExceptionUtils.exToNull(() -> this.headers.get(HttpHeaders.USER_AGENT));
if (StrUtil.isNotBlank(userAgentString)) {
this.browser = (includes.contains(Include.BROWSER)) ? ServletUtils.getBrowser(userAgentString) : null;
this.os = (includes.contains(Include.OS)) ? ServletUtils.getOs(userAgentString) : null;
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.model;
import lombok.Data;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.util.*;
/**
* 响应信息
*
* @author Charles7c
* @since 1.1.0
*/
@Data
public class LogResponse {
/**
* 状态码
*/
private Integer status;
/**
* 响应头
*/
private Map<String, String> headers;
/**
* 响应体JSON 字符串)
*/
private String body;
/**
* 响应参数
*/
private Map<String, Object> param;
public LogResponse(RecordableHttpResponse response, Set<Include> includes) {
this.status = response.getStatus();
this.headers = (includes.contains(Include.RESPONSE_HEADERS)) ? response.getHeaders() : null;
if (includes.contains(Include.RESPONSE_BODY)) {
this.body = response.getBody();
} else if (includes.contains(Include.RESPONSE_PARAM)) {
this.param = response.getParam();
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.model;
import java.net.URI;
import java.util.Map;
/**
* 可记录的 HTTP 请求信息
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Phillip WebbSpring Boot Actuator
* @author Charles7c
* @see RecordableHttpResponse
* @since 1.1.0
*/
public interface RecordableHttpRequest {
/**
* 获取请求方式
*
* @return 请求方式
*/
String getMethod();
/**
* 获取 URL
*
* @return URL
*/
URI getUrl();
/**
* 获取 IP
*
* @return IP
*/
String getIp();
/**
* 获取请求头
*
* @return 请求头
*/
Map<String, String> getHeaders();
/**
* 获取请求体
*
* @return 请求体
*/
String getBody();
/**
* 获取请求参数
*
* @return 请求参数
*/
Map<String, Object> getParam();
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.common.model;
import java.util.Map;
/**
* 可记录的 HTTP 响应信息
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
* @see RecordableHttpRequest
* @since 1.1.0
*/
public interface RecordableHttpResponse {
/**
* 获取状态码
*
* @return 状态码
*/
int getStatus();
/**
* 获取响应头
*
* @return 响应头
*/
Map<String, String> getHeaders();
/**
* 获取响应体
*
* @return 响应体
*/
String getBody();
/**
* 获取响应参数
*
* @return 响应参数
*/
Map<String, Object> getParam();
}

View File

@@ -0,0 +1,37 @@
<?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>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-log-httptrace-pro</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 日志模块 - HttpTraceProSpring Boot Actuator HttpTrace 重置增强版)</description>
<dependencies>
<!-- Swagger 注解 -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
<!-- TTL线程间传递 ThreadLocal异步执行时上下文传递的解决方案 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<!-- 日志模块 - 公共模块 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.lang.annotation.*;
/**
* 是否启用日志记录注解
*
* @author Charles7c
* @since 1.1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ConditionalOnProperty(prefix = "continew-starter.log", name = "enabled", havingValue = "true")
public @interface ConditionalOnEnabledLog {}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.autoconfigure;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.common.dao.impl.LogDaoDefaultImpl;
import top.charles7c.continew.starter.log.httptracepro.handler.LogFilter;
import top.charles7c.continew.starter.log.httptracepro.handler.LogInterceptor;
/**
* 日志自动配置
*
* @author Charles7c
* @since 1.1.0
*/
@Slf4j
@Configuration
@ConditionalOnEnabledLog
@RequiredArgsConstructor
@EnableConfigurationProperties(LogProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class LogAutoConfiguration implements WebMvcConfigurer {
private final LogProperties properties;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor(logDao(), properties));
}
/**
* 日志过滤器
*/
@Bean
@ConditionalOnMissingBean
public LogFilter logFilter() {
return new LogFilter();
}
/**
* 日志持久层接口
*/
@Bean
@ConditionalOnMissingBean
public LogDao logDao() {
return new LogDaoDefaultImpl();
}
@PostConstruct
public void postConstruct() {
log.info("[ContiNew Starter] - Auto Configuration 'Log-HttpTracePro' completed initialization.");
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.autoconfigure;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.util.HashSet;
import java.util.Set;
/**
* 日志配置属性
*
* @author Charles7c
* @since 1.1.0
*/
@Data
@ConfigurationProperties(prefix = "continew-starter.log")
public class LogProperties {
/**
* 是否启用日志
*/
private boolean enabled = false;
/**
* 是否打印日志,开启后可打印访问日志(类似于 Nginx access log
*/
private Boolean isPrint = false;
/**
* 包含信息
*/
private Set<Include> include = new HashSet<>(Include.defaultIncludes());
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
/**
* 日志过滤器
*
* @author Dave SyerSpring Boot Actuator
* @author Wallace WadgeSpring Boot Actuator
* @author Andy WilkinsonSpring Boot Actuator
* @author Venil NoronhaSpring Boot Actuator
* @author Madhura BhaveSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
@RequiredArgsConstructor
public class LogFilter extends OncePerRequestFilter implements Ordered {
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
if (!isRequestValid(request)) {
filterChain.doFilter(request, response);
return;
}
// 包装输入、输出流,可重复读取
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
if (!(response instanceof ContentCachingResponseWrapper)) {
response = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(request, response);
// 更新响应(不操作这一步,会导致接口响应空白)
updateResponse(response);
}
private boolean isRequestValid(HttpServletRequest request) {
try {
new URI(request.getRequestURL().toString());
return true;
} catch (URISyntaxException e) {
return false;
}
}
private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
Objects.requireNonNull(responseWrapper).copyBodyToResponse();
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import top.charles7c.continew.starter.log.common.annotation.Log;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.common.enums.Include;
import top.charles7c.continew.starter.log.common.model.LogRecord;
import top.charles7c.continew.starter.log.common.model.LogResponse;
import top.charles7c.continew.starter.log.httptracepro.autoconfigure.LogProperties;
import java.time.Clock;
import java.util.Set;
/**
* 日志拦截器
*
* @author Charles7c
* @since 1.1.0
*/
@Slf4j
@RequiredArgsConstructor
public class LogInterceptor implements HandlerInterceptor {
private final LogDao dao;
private final LogProperties properties;
private final TransmittableThreadLocal<LogRecord.Started> timestampTtl = new TransmittableThreadLocal<>();
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler) {
Clock timestamp = Clock.systemUTC();
if (this.isRequestRecord(handler, request)) {
if (Boolean.TRUE.equals(properties.getIsPrint())) {
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
}
LogRecord.Started startedLogRecord = LogRecord.start(timestamp, new RecordableServletHttpRequest(request));
timestampTtl.set(startedLogRecord);
}
return true;
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, Exception e) {
LogRecord.Started startedLogRecord = timestampTtl.get();
if (null == startedLogRecord) {
return;
}
timestampTtl.remove();
Set<Include> includeSet = properties.getInclude();
try {
LogRecord finishedLogRecord = startedLogRecord.finish(new RecordableServletHttpResponse(response, response.getStatus()), includeSet);
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 记录日志描述
if (includeSet.contains(Include.DESCRIPTION)) {
this.logDescription(finishedLogRecord, handlerMethod);
}
// 记录所属模块
if (includeSet.contains(Include.MODULE)) {
this.logModule(finishedLogRecord, handlerMethod);
}
if (Boolean.TRUE.equals(properties.getIsPrint())) {
LogResponse logResponse = finishedLogRecord.getResponse();
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), logResponse.getStatus(), finishedLogRecord.getTimeTaken().toMillis());
}
dao.add(finishedLogRecord);
} catch (Exception ex) {
log.error("Logging http log occurred an error: {}.", ex.getMessage(), ex);
}
}
/**
* 记录描述
*
* @param logRecord 日志信息
* @param handlerMethod 处理器方法
*/
private void logDescription(LogRecord logRecord, HandlerMethod handlerMethod) {
// 例如:@Operation(summary="新增部门") -> 新增部门
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
if (null != methodOperation) {
logRecord.setDescription(StrUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
}
// 例如:@Log("新增部门") -> 新增部门
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
if (null != methodLog && StrUtil.isNotBlank(methodLog.value())) {
logRecord.setDescription(methodLog.value());
}
}
/**
* 记录模块
*
* @param logRecord 日志信息
* @param handlerMethod 处理器方法
*/
private void logModule(LogRecord logRecord, HandlerMethod handlerMethod) {
// 例如:@Tag(name = "部门管理") -> 部门管理
Tag classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Tag.class);
if (null != classTag) {
String name = classTag.name();
logRecord.setModule(StrUtil.blankToDefault(name, "请在该接口类上指定所属模块"));
}
// 例如:@Log(module = "部门管理") -> 部门管理
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
if (null != classLog && StrUtil.isNotBlank(classLog.module())) {
logRecord.setModule(classLog.module());
}
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
if (null != methodLog && StrUtil.isNotBlank(methodLog.module())) {
logRecord.setModule(methodLog.module());
}
}
/**
* 是否要记录日志
*
* @param handler 处理器
* @param request 请求对象
* @return true需要记录false不需要记录
*/
private boolean isRequestRecord(Object handler, HttpServletRequest request) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return false;
}
// 不拦截 /error
ServerProperties serverProperties = SpringUtil.getBean(ServerProperties.class);
if (request.getRequestURI().equals(serverProperties.getError().getPath())) {
return false;
}
// 如果接口被隐藏,不记录日志
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
if (null != methodOperation && methodOperation.hidden()) {
return false;
}
// 如果接口方法或类上有 @Log 注解,且要求忽略该接口,则不记录日志
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
if (null != methodLog && methodLog.ignore()) {
return false;
}
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
return null == classLog || !classLog.ignore();
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.log.common.model.RecordableHttpRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 可记录的 HTTP 请求信息适配器
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
*/
public final class RecordableServletHttpRequest implements RecordableHttpRequest {
private final HttpServletRequest request;
public RecordableServletHttpRequest(HttpServletRequest request) {
this.request = request;
}
@Override
public String getMethod() {
return request.getMethod();
}
@Override
public URI getUrl() {
String queryString = request.getQueryString();
if (StrUtil.isBlank(queryString)) {
return URI.create(request.getRequestURL().toString());
}
try {
StringBuffer urlBuffer = this.appendQueryString(queryString);
return new URI(urlBuffer.toString());
} catch (URISyntaxException e) {
String encoded = UriUtils.encodeQuery(queryString, StandardCharsets.UTF_8);
StringBuffer urlBuffer = this.appendQueryString(encoded);
return URI.create(urlBuffer.toString());
}
}
@Override
public String getIp() {
return JakartaServletUtil.getClientIP(request);
}
@Override
public Map<String, String> getHeaders() {
return JakartaServletUtil.getHeaderMap(request);
}
@Override
public String getBody() {
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (null != wrapper) {
String body = StrUtil.utf8Str(wrapper.getContentAsByteArray());
return JSONUtil.isTypeJSON(body) ? body : null;
}
return null;
}
@Override
public Map<String, Object> getParam() {
String body = this.getBody();
return StrUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
? JSONUtil.toBean(body, Map.class)
: Collections.unmodifiableMap(request.getParameterMap());
}
private StringBuffer appendQueryString(String queryString) {
return request.getRequestURL().append(StringConstants.QUESTION_MARK).append(queryString);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.core.util.ServletUtils;
import top.charles7c.continew.starter.log.common.model.RecordableHttpResponse;
import java.util.*;
/**
* 可记录的 HTTP 响应信息适配器
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
*/
public final class RecordableServletHttpResponse implements RecordableHttpResponse {
private final HttpServletResponse response;
private final int status;
public RecordableServletHttpResponse(HttpServletResponse response, int status) {
this.response = response;
this.status = status;
}
@Override
public int getStatus() {
return this.status;
}
@Override
public Map<String, String> getHeaders() {
return ServletUtils.getHeaderMap(response);
}
@Override
public String getBody() {
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (null != wrapper) {
return StrUtil.utf8Str(wrapper.getContentAsByteArray());
}
return StringConstants.EMPTY;
}
@Override
public Map<String, Object> getParam() {
String body = this.getBody();
return StrUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
? JSONUtil.toBean(body, Map.class)
: null;
}
}

View File

@@ -0,0 +1 @@
top.charles7c.continew.starter.log.httptracepro.autoconfigure.LogAutoConfiguration

View File

@@ -0,0 +1,30 @@
<?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>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-log</artifactId>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 日志模块</description>
<modules>
<module>continew-starter-log-common</module>
<module>continew-starter-log-httptrace-pro</module>
</modules>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,17 @@
<?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>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-storage</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-storage-local</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 存储模块 - 本地存储</description>
</project>

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.storage.local.autoconfigure;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.charles7c.continew.starter.core.constant.StringConstants;
import java.util.Map;
/**
* 本地文件自动配置
*
* @author Charles7c
* @since 1.1.0
*/
@Slf4j
@EnableWebMvc
@AutoConfiguration
@RequiredArgsConstructor
@EnableConfigurationProperties(LocalStorageProperties.class)
@ConditionalOnProperty(name = "continew-starter.storage.local.enabled", havingValue = "true")
public class LocalStorageAutoConfiguration implements WebMvcConfigurer {
private final LocalStorageProperties properties;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
Map<String, LocalStorageProperties.LocalStorageMapping> mappingMap = properties.getMapping();
for (Map.Entry<String, LocalStorageProperties.LocalStorageMapping> mappingEntry : mappingMap.entrySet()) {
LocalStorageProperties.LocalStorageMapping mapping = mappingEntry.getValue();
String pathPattern = mapping.getPathPattern();
String location = mapping.getLocation();
if (StrUtil.isBlank(location)) {
throw new IllegalArgumentException(String.format("Path pattern [%s] location is null.", pathPattern));
}
registry.addResourceHandler(StrUtil.appendIfMissing(pathPattern, StringConstants.PATH_PATTERN))
.addResourceLocations(!location.startsWith("file:") ? String.format("file:%s", this.format(location)) : this.format(location))
.setCachePeriod(0);
}
}
private String format(String location) {
return location.replace(StringConstants.BACKSLASH, StringConstants.SLASH);
}
@PostConstruct
public void postConstruct() {
log.info("[ContiNew Starter] - Auto Configuration 'Storage-Local' completed initialization.");
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.storage.local.autoconfigure;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.unit.DataSize;
import java.util.HashMap;
import java.util.Map;
/**
* 本地存储配置属性
*
* @author Charles7c
* @since 1.1.0
*/
@Data
@ConfigurationProperties(prefix = "continew-starter.storage.local")
public class LocalStorageProperties {
/**
* 是否启用本地存储
*/
private boolean enabled = false;
/**
* 存储映射
*/
private Map<String, LocalStorageMapping> mapping = new HashMap<>();
/**
* 本地存储映射
*/
@Data
public static class LocalStorageMapping {
/**
* 路径模式
*/
private String pathPattern;
/**
* 资源路径
*/
private String location;
/**
* 单文件上传大小限制
*/
private DataSize maxFileSize = DataSize.ofMegabytes(1);
}
}

View File

@@ -0,0 +1 @@
top.charles7c.continew.starter.storage.local.autoconfigure.LocalStorageAutoConfiguration

View File

@@ -0,0 +1,29 @@
<?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>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-storage</artifactId>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 存储模块</description>
<modules>
<module>continew-starter-storage-local</module>
</modules>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -72,6 +72,8 @@
<module>continew-starter-core</module>
<module>continew-starter-json</module>
<module>continew-starter-api-doc</module>
<module>continew-starter-log</module>
<module>continew-starter-storage</module>
<module>continew-starter-file</module>
<module>continew-starter-captcha</module>
<module>continew-starter-cache</module>
@@ -92,7 +94,7 @@
<build>
<plugins>
<!-- 代码格式化插件 -->
<!-- 代码格式化插件 -->
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
@@ -114,6 +116,7 @@
</java>
</configuration>
</plugin>
<!-- 扁平化 Maven 插件(统一版本号) -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>