init
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
|
||||
<file url="file://$PROJECT_DIR$/cool-admin-java/src/main/java" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
57
.idea/misc.xml
generated
Normal file
57
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/cool-admin-java/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectInspectionProfilesVisibleTreeState">
|
||||
<entry key="Project Default">
|
||||
<profile-state>
|
||||
<expanded-state>
|
||||
<State>
|
||||
<id>EditorConfig</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>Java</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>JavaScript 和 TypeScript</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>Markdown</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>XPath</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>代码样式问题JavaScript 和 TypeScript</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>可移植性Java</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>国际化</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>国际化Java</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>属性文件Java</id>
|
||||
</State>
|
||||
</expanded-state>
|
||||
<selected-state>
|
||||
<State>
|
||||
<id>用户定义</id>
|
||||
</State>
|
||||
</selected-state>
|
||||
</profile-state>
|
||||
</entry>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
37
cool-admin-java/.gitignore
vendored
Normal file
37
cool-admin-java/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
assets/
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
.DS_Store
|
||||
lib
|
||||
plugin
|
14
cool-admin-java/Dockerfile
Normal file
14
cool-admin-java/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
# 使用 GraalVM 17 作为基础镜像
|
||||
FROM ghcr.io/graalvm/graalvm-ce:latest
|
||||
|
||||
# 设置容器内的工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 将可执行的jar文件复制到容器内
|
||||
COPY target/cool-admin-8.0.0.jar /app/cool-admin-8.0.0.jar
|
||||
|
||||
# 暴露Spring Boot应用程序运行的端口
|
||||
EXPOSE 8001
|
||||
|
||||
# 运行Spring Boot应用程序的命令
|
||||
ENTRYPOINT ["java", "-jar", "/app/cool-admin-8.0.0.jar", "--spring.profiles.active=prod"]
|
21
cool-admin-java/LICENSE
Normal file
21
cool-admin-java/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 cool-team-official
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
103
cool-admin-java/README.md
Normal file
103
cool-admin-java/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://midwayjs.org/" target="blank"><img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/logo.png" width="200" alt="Midway Logo" /></a>
|
||||
</p>
|
||||
<p align="center">cool-admin(java版)后台权限管理系统,开源免费,Ai编码、流程编排、模块化、插件化,用于快速构建后台应用程序,详情可到<a href="https://cool-admin.com" target="_blank">官网</a> 进一步了解。
|
||||
<p align="center">
|
||||
<a href="https://github.com/cool-team-official/cool-admin-midway/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
|
||||
<a href=""><img src="https://img.shields.io/github/package-json/v/cool-team-official/cool-admin-midway?style=flat-square" alt="GitHub tag"></a>
|
||||
<img src="https://img.shields.io/github/last-commit/cool-team-official/cool-admin-midway?style=flat-square" alt="GitHub tag"></a>
|
||||
</p>
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 后端:**`Springboot3` `Mybatis-Flex`**
|
||||
- 前端:**`Vue3` `Vite` `Element-Ui` `Typescript`**
|
||||
- 数据库:**`Mysql` `Postgresql` `Sqlite(适配中)` `...`**
|
||||
|
||||
## 特性
|
||||
|
||||
Ai时代,很多老旧的框架已经无法满足现代化的开发需求,Cool-Admin开发了一系列的功能,让开发变得更简单、更快速、更高效。
|
||||
|
||||
- **Ai编码**:通过微调大模型学习框架特有写法,实现简单功能从Api接口到前端页面的一键生成[详情](https://java.cool-admin.com/src/guide/ai.html)
|
||||
- **流程编排**:通过拖拽编排方式,即可实现类似像智能客服这样的功能[详情](https://cool-js.com/plugin/118)
|
||||
- **多租户**:支持多租户,采用全局动态注入查询条件[详情](https://java.cool-admin.com/src/guide/tenant.html)
|
||||
- **多语言**:基于大模型自动翻译,无需更改原有代码[详情](https://java.cool-admin.com/src/guide/i18n.html)
|
||||
- **模块化**:代码是模块化的,清晰明了,方便维护
|
||||
- **插件化**:插件化的设计,可以通过安装插件的方式扩展如:支付、短信、邮件等功能
|
||||
- **自动初始化**:数据自动化,无需再手动维护,启动时自动生成数据库表和表结构数据
|
||||
- **cool-admin-java-plus**: [详情](https://gitee.com/hlc4417/cool-admin-java-plus)
|
||||
- ......
|
||||

|
||||
|
||||
## 地址
|
||||
|
||||
- 官网:[https://cool-admin.com](https://cool-admin.com)
|
||||
- 文档:[https://java.cool-admin.com](https://java.cool-admin.com)
|
||||
- 商城项目:[https://cool-js.com/plugin/140](https://cool-js.com/plugin/140)
|
||||
- Ai流程编排+知识库项目:[https://cool-js.com/plugin/118](https://cool-js.com/plugin/118)
|
||||
- cool-admin-java-plus:https://gitee.com/hlc4417/cool-admin-java-plus
|
||||
|
||||
## 演示
|
||||
|
||||
[https://show.cool-admin.com](https://show.cool-admin.com)
|
||||
|
||||
- 账户:admin
|
||||
- 密码:123456
|
||||
|
||||

|
||||
|
||||
#### 项目前端
|
||||
|
||||
系统是前后端分离的,启动完成后,还需要启动前端项目,前端项目地址:
|
||||
|
||||
[https://github.com/cool-team-official/cool-admin-vue](https://github.com/cool-team-official/cool-admin-vue)
|
||||
|
||||
或
|
||||
|
||||
[https://gitee.com/cool-team-official/cool-admin-vue](https://gitee.com/cool-team-official/cool-admin-vue)
|
||||
|
||||
或
|
||||
|
||||
[https://gitcode.com/cool_team/cool-admin-vue](https://gitcode.com/cool_team/cool-admin-vue)
|
||||
|
||||
## 微信群
|
||||
|
||||
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg?v=1" alt="Admin Wechat"></a>
|
||||
|
||||
## 运行
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Java Graalvm 17+
|
||||
- Maven 3.6+
|
||||
|
||||
### 配置
|
||||
|
||||
修改数据库配置,配置文件位于`src/resources/application-local.yml`
|
||||
|
||||
以 Mysql 为例,其他数据库适配中...
|
||||
|
||||
Mysql(`>=5.7版本`),建议 8.0,首次启动会自动初始化并导入数据
|
||||
|
||||
```yaml
|
||||
# mysql,驱动已经内置,无需安装
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://127.0.0.1:3306/cool?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
```
|
||||
|
||||
### 启动
|
||||
|
||||
注:项目使用到了[Mybatis-Flex 的Apt功能](https://mybatis-flex.com/zh/others/apt.html),如果启动报错,请先执行`mvn compile`编译
|
||||
|
||||
1、启动文件:`src/main/java/com/cool/CoolApplication.java`
|
||||
|
||||
2、启动完成后,访问:[http://localhost:8001](http://localhost:8001)
|
||||
|
||||
3、如果看到以下界面,说明启动成功。这时候再启动前端项目即可,数据库会自动初始化,默认账号:admin,密码:123456
|
||||
|
||||

|
43
cool-admin-java/docker-compose.yml
Normal file
43
cool-admin-java/docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
# 本地数据库环境
|
||||
# 数据存放在当前目录下的 data里
|
||||
# 推荐使用安装了docker扩展的vscode打开目录 在本文件上右键可以快速启动,停止
|
||||
# 如不需要相关容器开机自启动,可注释掉 restart: always
|
||||
# 如遇端口冲突 可调整ports下 :前面的端口号
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql # 使用官方 MySQL 镜像,你可以根据需要选择版本
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "123456" # 设置 root 用户密码
|
||||
MYSQL_DATABASE: "cool" # 创建一个初始数据库
|
||||
networks:
|
||||
- backend
|
||||
ports:
|
||||
- "3306:3306" # 将主机的 3306 端口映射到容器的 3306 端口
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql # 挂载数据卷以持久化数据
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
# command: --requirepass "12345678" # Uncomment if you need a password
|
||||
restart: always
|
||||
environment:
|
||||
TZ: Asia/Shanghai # 指定时区
|
||||
volumes:
|
||||
- ./data/redis/:/data/
|
||||
networks:
|
||||
- backend
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
|
||||
networks:
|
||||
backend:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
213
cool-admin-java/pom.xml
Normal file
213
cool-admin-java/pom.xml
Normal file
@@ -0,0 +1,213 @@
|
||||
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
<!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.cool</groupId>
|
||||
<artifactId>cool-admin</artifactId>
|
||||
<version>8.0.0</version>
|
||||
<name>cool-admin</name>
|
||||
<description>cool admin for java</description>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<lombok.version>1.18.34</lombok.version>
|
||||
<mybatis-flex.version>1.10.9</mybatis-flex.version>
|
||||
<mybatis-flex.ext.version>1.10.9.125</mybatis-flex.ext.version>
|
||||
<hutool.version>5.8.26</hutool.version>
|
||||
<ognl.version>3.3.2</ognl.version>
|
||||
<fastjson2.version>2.0.51</fastjson2.version>
|
||||
<springdoc-openapi.version>2.5.0</springdoc-openapi.version>
|
||||
<perf4j.version>0.9.16</perf4j.version>
|
||||
<weixin-java.version>4.7.0</weixin-java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- springframework start -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter Data Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Configuration Processor -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<!-- springframework end -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ognl</groupId>
|
||||
<artifactId>ognl</artifactId>
|
||||
<version>${ognl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>${fastjson2.version}</version>
|
||||
</dependency>
|
||||
<!-- mybatis-flex-ext -->
|
||||
<dependency>
|
||||
<groupId>com.tangzc</groupId>
|
||||
<artifactId>mybatis-flex-ext-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-flex.ext.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc-openapi.version}</version>
|
||||
</dependency>
|
||||
<!-- 微信相关 start-->
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-miniapp</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-mp</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
<!-- 微信相关 end-->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.perf4j</groupId>
|
||||
<artifactId>perf4j</artifactId>
|
||||
<version>${perf4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-processor</artifactId>
|
||||
<version>${mybatis-flex.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>local</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<spring.active>local</spring.active>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>prod</id>
|
||||
<properties>
|
||||
<spring.active>prod</spring.active>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
74
cool-admin-java/src/main/java/com/cool/CoolApplication.java
Normal file
74
cool-admin-java/src/main/java/com/cool/CoolApplication.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.cool;
|
||||
|
||||
import com.cool.core.util.PathUtils;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.autotable.springboot.EnableAutoTable;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
/**
|
||||
* CoolApplication - 应用程序的主类
|
||||
* 该类配置并运行应用程序。
|
||||
*/
|
||||
@Slf4j
|
||||
@EnableAutoTable // 开启自动建表
|
||||
@EnableAsync // 开启异步处理
|
||||
@EnableCaching // 开启缓存
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.cool.**.mapper") // 扫描指定包中的MyBatis映射器
|
||||
public class CoolApplication {
|
||||
|
||||
private static volatile ConfigurableApplicationContext context;
|
||||
private static ClassLoader mainThreadClassLoader;
|
||||
|
||||
public static void main(String[] args) {
|
||||
mainThreadClassLoader = Thread.currentThread().getContextClassLoader();
|
||||
context = SpringApplication.run(CoolApplication.class, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过关闭当前上下文并启动新上下文来重启应用程序。
|
||||
*/
|
||||
public static void restart(List<String> javaPathList) {
|
||||
// 从当前上下文获取应用程序参数
|
||||
ApplicationArguments args = context.getBean(ApplicationArguments.class);
|
||||
|
||||
// 创建新线程来重启应用程序
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
// 关闭当前应用程序上下文
|
||||
context.close();
|
||||
// 等待上下文完全关闭
|
||||
while (context.isActive()) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
// 加载动态生成的代码
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
javaPathList.forEach(javaPath -> {
|
||||
try {
|
||||
classLoader.loadClass(PathUtils.getClassName(javaPath));
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.error("loadClassErr {}", javaPath, e);
|
||||
}
|
||||
});
|
||||
// 使用相同的参数运行Spring Boot应用程序并设置上下文
|
||||
context = SpringApplication.run(CoolApplication.class, args.getSourceArgs());
|
||||
});
|
||||
// 设置线程的上下文类加载器
|
||||
thread.setContextClassLoader(mainThreadClassLoader);
|
||||
// 确保线程不是守护线程
|
||||
thread.setDaemon(false);
|
||||
// 启动线程
|
||||
thread.start();
|
||||
}
|
||||
|
||||
}
|
17
cool-admin-java/src/main/java/com/cool/Welcome.java
Normal file
17
cool-admin-java/src/main/java/com/cool/Welcome.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.cool;
|
||||
|
||||
import com.cool.core.annotation.TokenIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class Welcome {
|
||||
|
||||
@RequestMapping("/")
|
||||
@TokenIgnore
|
||||
public String welcome() {
|
||||
return "welcome";
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.cool.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface CoolPlugin {
|
||||
String value() default "";
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package com.cool.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 自定义路由注解
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@RestController
|
||||
@RequestMapping
|
||||
public @interface CoolRestController {
|
||||
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String name() default "";
|
||||
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String[] value() default {};
|
||||
|
||||
String[] api() default {};
|
||||
|
||||
/**
|
||||
* 如前缀: /admin/goods/searchKeyword
|
||||
* 没指定该字段 cname="searchKeyword",
|
||||
* 按规则是解析为: /admin/goods/search/keyword
|
||||
* 前端和node版本已经定义为 searchKeyword,没按规则解析,使用该字段自定义规则 进行兼容
|
||||
* com.cool.core.request.prefix.AutoPrefixUrlMapping#getCName(java.lang.Class, java.lang.String)
|
||||
*/
|
||||
String cname() default "";
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.cool.core.annotation;
|
||||
|
||||
import com.cool.core.enums.AdminComponentsEnum;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface EpsField {
|
||||
String component() default AdminComponentsEnum.INPUT;
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.cool.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface IgnoreRecycleData {
|
||||
String value() default "";
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.cool.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface NoRepeatSubmit {
|
||||
long expireTime() default 2000; // 默认2秒过期时间,单位毫秒
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.cool.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 忽略Token验证
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TokenIgnore {
|
||||
String[] value() default {};
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.cool.core.aop;
|
||||
|
||||
import com.cool.core.annotation.NoRepeatSubmit;
|
||||
import com.cool.core.exception.CoolPreconditions;
|
||||
import com.cool.core.lock.CoolLock;
|
||||
import com.cool.core.util.CoolSecurityUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class NoRepeatSubmitAspect {
|
||||
|
||||
private final CoolLock coolLock;
|
||||
|
||||
@Around("@annotation(noRepeatSubmit)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(
|
||||
RequestContextHolder.getRequestAttributes())).getRequest();
|
||||
String key = request.getRequestURI() + ":" + CoolSecurityUtil.getCurrentUserId();
|
||||
// 加锁
|
||||
CoolPreconditions.check(!coolLock.tryLock(key, Duration.ofMillis(noRepeatSubmit.expireTime())), "请勿重复操作");
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
// 移除锁
|
||||
coolLock.unlock(key);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,277 @@
|
||||
package com.cool.core.base;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.lang.Editor;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.TypeUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.cool.core.enums.QueryModeEnum;
|
||||
import com.cool.core.exception.CoolPreconditions;
|
||||
import com.cool.core.request.CrudOption;
|
||||
import com.cool.core.request.PageResult;
|
||||
import com.cool.core.request.R;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Getter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* 控制层基类
|
||||
*
|
||||
* @param <S>
|
||||
* @param <T>
|
||||
*/
|
||||
public abstract class BaseController<S extends BaseService<T>, T extends BaseEntity<T>> {
|
||||
|
||||
@Getter
|
||||
@Autowired
|
||||
protected S service;
|
||||
protected Class<T> entityClass;
|
||||
|
||||
protected final String COOL_PAGE_OP = "COOL_PAGE_OP";
|
||||
protected final String COOL_LIST_OP = "COOL_LIST_OP";
|
||||
protected final String COOL_INFO_OP = "COOL_INFO_OP";
|
||||
|
||||
private final ThreadLocal<CrudOption<T>> pageOption = new ThreadLocal<>();
|
||||
private final ThreadLocal<CrudOption<T>> listOption = new ThreadLocal<>();
|
||||
private final ThreadLocal<CrudOption<T>> infoOption = new ThreadLocal<>();
|
||||
private final ThreadLocal<JSONObject> requestParams = new ThreadLocal<>();
|
||||
|
||||
@ModelAttribute
|
||||
protected void preHandle(HttpServletRequest request,
|
||||
@RequestAttribute JSONObject requestParams) {
|
||||
String requestPath = ((ServletRequestAttributes) Objects.requireNonNull(
|
||||
RequestContextHolder.getRequestAttributes())).getRequest().getRequestURI();
|
||||
if (!requestPath.endsWith("/page") && !requestPath.endsWith("/list")
|
||||
&& !requestPath.endsWith("/info")) {
|
||||
// 非page或list不执行
|
||||
return;
|
||||
}
|
||||
this.pageOption.set(new CrudOption<>(requestParams));
|
||||
this.listOption.set(new CrudOption<>(requestParams));
|
||||
this.infoOption.set(new CrudOption<>(requestParams));
|
||||
this.requestParams.set(requestParams);
|
||||
init(request, requestParams);
|
||||
request.setAttribute(COOL_PAGE_OP, this.pageOption.get());
|
||||
request.setAttribute(COOL_LIST_OP, this.listOption.get());
|
||||
request.setAttribute(COOL_INFO_OP, this.infoOption.get());
|
||||
|
||||
removeThreadLocal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动移除变量
|
||||
*/
|
||||
private void removeThreadLocal() {
|
||||
this.listOption.remove();
|
||||
this.pageOption.remove();
|
||||
this.requestParams.remove();
|
||||
}
|
||||
|
||||
public CrudOption<T> createOp() {
|
||||
return new CrudOption<>(this.requestParams.get());
|
||||
}
|
||||
|
||||
public void setInfoOption(CrudOption<T> infoOption) {
|
||||
this.infoOption.set(infoOption);
|
||||
}
|
||||
|
||||
public void setListOption(CrudOption<T> listOption) {
|
||||
this.listOption.set(listOption);
|
||||
}
|
||||
|
||||
public void setPageOption(CrudOption<T> pageOption) {
|
||||
this.pageOption.set(pageOption);
|
||||
}
|
||||
|
||||
protected abstract void init(HttpServletRequest request, JSONObject requestParams);
|
||||
|
||||
/**
|
||||
* 新增
|
||||
* <p>
|
||||
* // * @param t 实体对象
|
||||
*/
|
||||
@Operation(summary = "新增", description = "新增信息,对应后端的实体类")
|
||||
@PostMapping("/add")
|
||||
protected R add(@RequestAttribute() JSONObject requestParams) {
|
||||
String body = requestParams.getStr("body");
|
||||
if (JSONUtil.isTypeJSONArray(body)) {
|
||||
JSONArray array = JSONUtil.parseArray(body);
|
||||
return R.ok(Dict.create()
|
||||
.set("id", service.addBatch(requestParams, array.toList(currentEntityClass()))));
|
||||
} else {
|
||||
return R.ok(Dict.create().set("id",
|
||||
service.add(requestParams, requestParams.toBean(currentEntityClass()))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param params 请求参数 ids 数组 或者按","隔开
|
||||
*/
|
||||
@Operation(summary = "删除", description = "支持批量删除 请求参数 ids 数组 或者按\",\"隔开")
|
||||
@PostMapping("/delete")
|
||||
protected R delete(HttpServletRequest request, @RequestBody Map<String, Object> params,
|
||||
@RequestAttribute() JSONObject requestParams) {
|
||||
service.delete(requestParams, Convert.toLongArray(getIds(params)));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改
|
||||
*
|
||||
* @param t 修改对象
|
||||
*/
|
||||
@Operation(summary = "修改", description = "根据ID修改")
|
||||
@PostMapping("/update")
|
||||
protected R update(@RequestBody T t, @RequestAttribute() JSONObject requestParams) {
|
||||
Long id = t.getId();
|
||||
JSONObject info = JSONUtil.parseObj(JSONUtil.toJsonStr(service.getById(id)));
|
||||
requestParams.forEach(info::set);
|
||||
info.set("updateTime", new Date());
|
||||
service.update(requestParams, JSONUtil.toBean(info, currentEntityClass()));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息
|
||||
*
|
||||
* @param id ID
|
||||
*/
|
||||
@Operation(summary = "信息", description = "根据ID查询单个信息")
|
||||
@GetMapping("/info")
|
||||
protected R<T> info(@RequestAttribute() JSONObject requestParams,
|
||||
@RequestParam() Long id,
|
||||
@RequestAttribute(COOL_INFO_OP) CrudOption<T> option) {
|
||||
T info = (T) service.info(requestParams, id);
|
||||
invokerTransform(option, info);
|
||||
return R.ok(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
*/
|
||||
@Operation(summary = "查询", description = "查询多个信息")
|
||||
@PostMapping("/list")
|
||||
protected R<List<T>> list(@RequestAttribute() JSONObject requestParams,
|
||||
@RequestAttribute(COOL_LIST_OP) CrudOption<T> option) {
|
||||
QueryModeEnum queryModeEnum = option.getQueryModeEnum();
|
||||
List list = (List) switch (queryModeEnum) {
|
||||
case ENTITY_WITH_RELATIONS -> service.listWithRelations(requestParams, option.getQueryWrapper(currentEntityClass()));
|
||||
case CUSTOM -> transformList(service.list(requestParams, option.getQueryWrapper(currentEntityClass()), option.getAsType()), option.getAsType());
|
||||
default -> service.list(requestParams, option.getQueryWrapper(currentEntityClass()));
|
||||
};
|
||||
invokerTransform(option, list);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
*/
|
||||
@Operation(summary = "分页", description = "分页查询多个信息")
|
||||
@PostMapping("/page")
|
||||
protected R<PageResult<T>> page(@RequestAttribute() JSONObject requestParams,
|
||||
@RequestAttribute(COOL_PAGE_OP) CrudOption<T> option) {
|
||||
Integer page = requestParams.getInt("page", 1);
|
||||
Integer size = requestParams.getInt("size", 20);
|
||||
QueryModeEnum queryModeEnum = option.getQueryModeEnum();
|
||||
Object obj = switch (queryModeEnum) {
|
||||
case ENTITY_WITH_RELATIONS -> service.pageWithRelations(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()));
|
||||
case CUSTOM -> transformPage(service.page(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()), option.getAsType()), option.getAsType());
|
||||
default -> service.page(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()));
|
||||
};
|
||||
Page pageResult = (Page) obj;
|
||||
invokerTransform(option, pageResult.getRecords());
|
||||
return R.ok(pageResult(pageResult));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换参数,组装数据
|
||||
*/
|
||||
private void invokerTransform(CrudOption<T> option, Object obj) {
|
||||
if (ObjUtil.isNotEmpty(option.getTransform())) {
|
||||
if (obj instanceof List) {
|
||||
((List)obj).forEach(o -> {
|
||||
option.getTransform().apply(o);
|
||||
});
|
||||
} else {
|
||||
option.getTransform().apply(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页结果
|
||||
*
|
||||
* @param page 分页返回数据
|
||||
*/
|
||||
protected PageResult<T> pageResult(Page<T> page) {
|
||||
return PageResult.of(page);
|
||||
}
|
||||
|
||||
public Class<T> currentEntityClass() {
|
||||
if (entityClass != null) {
|
||||
return this.entityClass;
|
||||
}
|
||||
// 使用 获取泛型参数类型
|
||||
Type type = TypeUtil.getTypeArgument(this.getClass(), 1); // 获取第二个泛型参数
|
||||
if (type instanceof Class<?>) {
|
||||
entityClass = (Class<T>) type;
|
||||
return entityClass;
|
||||
}
|
||||
throw new IllegalStateException("Unable to determine entity class type");
|
||||
}
|
||||
|
||||
protected List<Long> getIds(Map<String, Object> params) {
|
||||
Object ids = params.get("ids");
|
||||
CoolPreconditions.checkEmpty(ids, "ids 参数错误");
|
||||
if (!(ids instanceof ArrayList)) {
|
||||
ids = ids.toString().split(",");
|
||||
}
|
||||
return Convert.toList(Long.class, ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 适用于自定义返回值为 map,map 的key为数据库字段,转驼峰命名
|
||||
*/
|
||||
protected List transformList(List records, Class<?> asType) {
|
||||
if (ObjUtil.isEmpty(asType) || !Map.class.isAssignableFrom(asType)) {
|
||||
return records;
|
||||
}
|
||||
List<Map> list = new ArrayList<>();
|
||||
Editor<String> keyEditor = property -> StrUtil.toCamelCase(property);
|
||||
records.forEach(o ->
|
||||
list.add(BeanUtil.beanToMap(o, new HashMap(), false, keyEditor)));
|
||||
return list;
|
||||
}
|
||||
protected Page transformPage(Page page, Class<?> asType) {
|
||||
page.setRecords(transformList(page.getRecords(), asType));
|
||||
return page;
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package com.cool.core.base;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.core.activerecord.Model;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.dromara.autotable.annotation.Ignore;
|
||||
|
||||
/**
|
||||
* 基础实体类
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public abstract class BaseEntity<T extends Model<T>> extends Model<T> implements Serializable {
|
||||
|
||||
@Id(keyType = KeyType.Auto, comment = "ID")
|
||||
protected Long id;
|
||||
|
||||
@Column(onInsertValue = "now()")
|
||||
@ColumnDefine(comment = "创建时间")
|
||||
protected Date createTime;
|
||||
|
||||
@Column(onInsertValue = "now()", onUpdateValue = "now()")
|
||||
@ColumnDefine(comment = "更新时间")
|
||||
protected Date updateTime;
|
||||
|
||||
@Ignore
|
||||
@Column(ignore = true)
|
||||
@JsonIgnore
|
||||
private QueryWrapper queryWrapper;
|
||||
}
|
@@ -0,0 +1,175 @@
|
||||
package com.cool.core.base;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基础service类
|
||||
*
|
||||
* @param <T> 实体
|
||||
*/
|
||||
public interface BaseService<T> extends IService<T> {
|
||||
/**
|
||||
* 新增
|
||||
*
|
||||
* @param entity 对应的实体
|
||||
*/
|
||||
Long add(T entity);
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param entity 对应的实体
|
||||
* @return ID
|
||||
*/
|
||||
Object add(JSONObject requestParams, T entity);
|
||||
|
||||
/**
|
||||
* 批量添加
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param entitys 请求参数
|
||||
* @return ID 集合
|
||||
*/
|
||||
Object addBatch(JSONObject requestParams, List<T> entitys);
|
||||
|
||||
/**
|
||||
* 删除, 支持单个或者批量删除
|
||||
*
|
||||
* @param ids ID数组
|
||||
*/
|
||||
boolean delete(Long... ids);
|
||||
|
||||
/**
|
||||
* 多个删除,带请求参数
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param ids ID数组
|
||||
*/
|
||||
boolean delete(JSONObject requestParams, Long... ids);
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*
|
||||
* @param entity 实体
|
||||
*/
|
||||
boolean update(T entity);
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param entity 实体
|
||||
*/
|
||||
boolean update(JSONObject requestParams, T entity);
|
||||
|
||||
/**
|
||||
* 查询所有
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 列表信息
|
||||
*/
|
||||
Object list(JSONObject requestParams, QueryWrapper queryWrapper);
|
||||
|
||||
/**
|
||||
* 查询所有
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 列表信息
|
||||
*/
|
||||
<R> List<R> list(JSONObject requestParams, QueryWrapper queryWrapper, Class<R> asType);
|
||||
|
||||
/**
|
||||
* 查询所有
|
||||
* 带关联查询
|
||||
* @param requestParams 请求参数
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 列表信息
|
||||
*/
|
||||
Object listWithRelations(JSONObject requestParams, QueryWrapper queryWrapper);
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param page 分页信息
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 分页信息
|
||||
*/
|
||||
Object page(JSONObject requestParams, Page<T> page, QueryWrapper queryWrapper);
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param page 分页信息
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 分页信息
|
||||
*/
|
||||
<R> Page<R> page(JSONObject requestParams, Page page, QueryWrapper queryWrapper, Class<R> asType);
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* 带关联查询
|
||||
* @param requestParams 请求参数
|
||||
* @param page 分页信息
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 分页信息
|
||||
*/
|
||||
Object pageWithRelations(JSONObject requestParams, Page<T> page, QueryWrapper queryWrapper);
|
||||
|
||||
/**
|
||||
* 查询信息
|
||||
*
|
||||
* @param id ID
|
||||
*/
|
||||
Object info(Long id);
|
||||
|
||||
/**
|
||||
* 查询信息
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param id ID
|
||||
*/
|
||||
Object info(JSONObject requestParams, Long id);
|
||||
|
||||
/**
|
||||
* 修改之后
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param t 对应实体
|
||||
*/
|
||||
void modifyAfter(JSONObject requestParams, T t);
|
||||
|
||||
/**
|
||||
* 修改之后
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param t 对应实体
|
||||
* @param type 修改类型
|
||||
*/
|
||||
void modifyAfter(JSONObject requestParams, T t, ModifyEnum type);
|
||||
|
||||
/**
|
||||
* 修改之前
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param t 对应实体
|
||||
*/
|
||||
void modifyBefore(JSONObject requestParams, T t);
|
||||
|
||||
/**
|
||||
* 修改之前
|
||||
*
|
||||
* @param requestParams 请求参数
|
||||
* @param t 对应实体
|
||||
* @param type 修改类型
|
||||
*/
|
||||
void modifyBefore(JSONObject requestParams, T t, ModifyEnum type);
|
||||
}
|
@@ -0,0 +1,137 @@
|
||||
package com.cool.core.base;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基础service实现类
|
||||
*
|
||||
* @param <M> Mapper 类
|
||||
* @param <T> 实体
|
||||
*/
|
||||
public class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseEntity<T>> extends
|
||||
ServiceImpl<M, T>
|
||||
implements BaseService<T> {
|
||||
|
||||
@Override
|
||||
public Long add(T entity) {
|
||||
mapper.insertSelective(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object add(JSONObject requestParams, T entity) {
|
||||
this.modifyBefore(requestParams, entity, ModifyEnum.ADD);
|
||||
this.add(entity);
|
||||
this.modifyAfter(requestParams, entity, ModifyEnum.ADD);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object addBatch(JSONObject requestParams, List<T> entitys) {
|
||||
this.modifyBefore(requestParams, null, ModifyEnum.ADD);
|
||||
List<Long> ids = new ArrayList<>();
|
||||
entitys.forEach(e -> ids.add(this.add(e)));
|
||||
requestParams.set("ids", ids);
|
||||
this.modifyAfter(requestParams, null, ModifyEnum.ADD);
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(Long... ids) {
|
||||
return mapper.deleteBatchByIds(Arrays.asList(ids)) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(JSONObject requestParams, Long... ids) {
|
||||
this.modifyBefore(requestParams, null, ModifyEnum.DELETE);
|
||||
boolean flag = this.delete(ids);
|
||||
if (flag) {
|
||||
this.modifyAfter(requestParams, null, ModifyEnum.DELETE);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(T entity) {
|
||||
return mapper.update(entity) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(JSONObject requestParams, T entity) {
|
||||
this.modifyBefore(requestParams, entity, ModifyEnum.UPDATE);
|
||||
boolean flag = this.update(entity);
|
||||
if (flag) {
|
||||
this.modifyAfter(requestParams, entity, ModifyEnum.UPDATE);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object list(JSONObject requestParams, QueryWrapper queryWrapper) {
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> List<R> list(JSONObject requestParams, QueryWrapper queryWrapper, Class<R> asType) {
|
||||
return mapper.selectListByQueryAs(queryWrapper, asType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object listWithRelations(JSONObject requestParams, QueryWrapper queryWrapper) {
|
||||
return mapper.selectListWithRelationsByQuery(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object page(JSONObject requestParams, Page<T> page, QueryWrapper queryWrapper) {
|
||||
return this.page(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> Page<R> page(JSONObject requestParams, Page page, QueryWrapper queryWrapper,
|
||||
Class<R> asType) {
|
||||
return mapper.paginateAs(page, queryWrapper, asType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object pageWithRelations(JSONObject requestParams, Page<T> page,
|
||||
QueryWrapper queryWrapper) {
|
||||
return mapper.paginateWithRelations(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object info(JSONObject requestParams, Long id) {
|
||||
return info(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object info(Long id) {
|
||||
return mapper.selectOneById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modifyAfter(JSONObject requestParams, T t) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modifyAfter(JSONObject requestParams, T t, ModifyEnum type) {
|
||||
modifyAfter(requestParams, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modifyBefore(JSONObject requestParams, T t) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modifyBefore(JSONObject requestParams, T t, ModifyEnum type) {
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.cool.core.base;
|
||||
|
||||
/**
|
||||
* 修改枚举
|
||||
*/
|
||||
public enum ModifyEnum {
|
||||
// 新增
|
||||
ADD,
|
||||
// 修改
|
||||
UPDATE,
|
||||
// 删除
|
||||
DELETE
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.cool.core.base;
|
||||
|
||||
import com.mybatisflex.core.activerecord.Model;
|
||||
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.dromara.autotable.annotation.Index;
|
||||
|
||||
/** 租户ID实体类 */
|
||||
@Getter
|
||||
@Setter
|
||||
public class TenantEntity<T extends Model<T>> extends BaseEntity<T> {
|
||||
@Index
|
||||
@ColumnDefine(comment = "租户id")
|
||||
protected Long tenantId;
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package com.cool.core.base.service;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.TypeUtil;
|
||||
import com.cool.core.util.SpringContextUtils;
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class MapperProviderService {
|
||||
|
||||
private Map<Class<?>, BaseMapper<?>> mapperMap;
|
||||
|
||||
/**
|
||||
* 初始化mapperMap,key 为entityClass,value 为 mapper
|
||||
*/
|
||||
private void init() {
|
||||
// 获取所有BaseMapper类型的Bean
|
||||
Map<String, BaseMapper> beansOfType = SpringContextUtils.getBeansOfType(BaseMapper.class);
|
||||
mapperMap = new HashMap<>();
|
||||
for (BaseMapper mapper : beansOfType.values()) {
|
||||
// 通过反射获取泛型参数,即实体类
|
||||
Class<?> entityClass = getGenericType(mapper);
|
||||
if (entityClass != null) {
|
||||
mapperMap.put(entityClass, mapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过entity类获取 对应的mapper接口
|
||||
*/
|
||||
public <T> BaseMapper<T> getMapperByEntityClass(Class<T> entityClass) {
|
||||
if (ObjUtil.isEmpty(mapperMap)) {
|
||||
init();
|
||||
}
|
||||
return (BaseMapper<T>) mapperMap.get(entityClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取mapper对应的entity对象
|
||||
*/
|
||||
private Class<?> getGenericType(BaseMapper<?> mapper) {
|
||||
// 使用 获取泛型参数类型
|
||||
Type[] types = mapper.getClass().getGenericInterfaces();
|
||||
Type typeArgument = TypeUtil.getTypeArgument(types[0], 0);
|
||||
return ObjUtil.isEmpty(typeArgument) ? null : (Class<?>) typeArgument;
|
||||
}
|
||||
}
|
180
cool-admin-java/src/main/java/com/cool/core/cache/CoolCache.java
vendored
Normal file
180
cool-admin-java/src/main/java/com/cool/core/cache/CoolCache.java
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
package com.cool.core.cache;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.cool.core.util.ConvertUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheType;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 缓存工具类
|
||||
*/
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CoolCache {
|
||||
|
||||
// 缓存类型
|
||||
@Value("${spring.cache.type}")
|
||||
private String type;
|
||||
|
||||
// redis
|
||||
public RedisCacheWriter redisCache;
|
||||
|
||||
private Cache cache;
|
||||
|
||||
@Value("${cool.cacheName}")
|
||||
private String cacheName;
|
||||
|
||||
private final static String NULL_VALUE = "@_NULL_VALUE$@";
|
||||
|
||||
final private CacheManager cacheManager;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
cache = cacheManager.getCache(cacheName);
|
||||
this.type = type.toLowerCase();
|
||||
assert cache != null : "Cache not found: " + cacheName; // Ensure cache is not null
|
||||
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
|
||||
redisCache = (RedisCacheWriter) cache.getNativeCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据来源
|
||||
*/
|
||||
public interface ToCacheData {
|
||||
Object apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*
|
||||
* @param keys 一个或多个key
|
||||
*/
|
||||
public void del(String... keys) {
|
||||
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
|
||||
Arrays.stream(keys).forEach(o -> cache.evict(o));
|
||||
}
|
||||
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
|
||||
Arrays.stream(keys).forEach(key -> redisCache.remove(cacheName, key.getBytes()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存获取
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public Object get(String key) {
|
||||
Object ifNullValue = getIfNullValue(key);
|
||||
if (ObjUtil.equals(ifNullValue, NULL_VALUE)) {
|
||||
return null;
|
||||
}
|
||||
return ifNullValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存获取
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public Object get(String key, Duration duration, ToCacheData toCacheData) {
|
||||
Object ifNullValue = getIfNullValue(key);
|
||||
if (ObjUtil.equals(ifNullValue, NULL_VALUE)) {
|
||||
return null;
|
||||
}
|
||||
if (ObjUtil.isEmpty(ifNullValue)) {
|
||||
Object obj = toCacheData.apply();
|
||||
set(key, obj, duration.toSeconds());
|
||||
return obj;
|
||||
}
|
||||
return ifNullValue;
|
||||
}
|
||||
|
||||
private Object getIfNullValue(String key) {
|
||||
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
|
||||
Cache.ValueWrapper valueWrapper = cache.get(key);
|
||||
if (valueWrapper != null) {
|
||||
return valueWrapper.get(); // 获取实际的缓存值
|
||||
}
|
||||
}
|
||||
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
|
||||
byte[] bytes = redisCache.get(cacheName, key.getBytes());
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
return ConvertUtil.toObject(bytes);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得对象
|
||||
*
|
||||
* @param key 键
|
||||
* @param valueType 值类型
|
||||
*/
|
||||
public <T> T get(String key, Class<T> valueType) {
|
||||
Object result = get(key);
|
||||
if (result != null && JSONUtil.isTypeJSONObject(result.toString())) {
|
||||
return JSONUtil.parseObj(result).toBean(valueType);
|
||||
}
|
||||
return result != null ? (T) result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得缓存类型
|
||||
*/
|
||||
public String getMode() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得原生缓存实例
|
||||
*/
|
||||
public Object getMetaCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存放入
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public void set(String key, Object value) {
|
||||
set(key, value, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存放入并设置时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 时间(秒) time要大于0 如果time小于等于0 将设置无限期
|
||||
*/
|
||||
public void set(String key, Object value, long ttl) {
|
||||
if (ObjUtil.isNull(value)) {
|
||||
value = NULL_VALUE;
|
||||
}
|
||||
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
|
||||
// 放入缓存
|
||||
cache.put(key, value);
|
||||
} else if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
|
||||
redisCache.put(cacheName, key.getBytes(), ObjectUtil.serialize(value),
|
||||
java.time.Duration.ofSeconds(ttl));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,108 @@
|
||||
package com.cool.core.code;
|
||||
|
||||
import cn.hutool.core.io.file.FileWriter;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.template.Template;
|
||||
import cn.hutool.extra.template.TemplateConfig;
|
||||
import cn.hutool.extra.template.TemplateEngine;
|
||||
import cn.hutool.extra.template.TemplateUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 代码生成器
|
||||
*/
|
||||
@Component
|
||||
public class CodeGenerator {
|
||||
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
private String baseSrcPath;
|
||||
|
||||
private String baseResPath;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
templateEngine = coolTemplateEngine();
|
||||
baseSrcPath = System.getProperty("user.dir") + "/src/main/java/com/cool/modules/";
|
||||
baseResPath = System.getProperty("user.dir") + "/src/main/resources/";
|
||||
}
|
||||
|
||||
public TemplateEngine coolTemplateEngine() {
|
||||
return TemplateUtil.createEngine(
|
||||
new TemplateConfig("cool/code", TemplateConfig.ResourceMode.CLASSPATH));
|
||||
}
|
||||
|
||||
private String filePath(CodeModel codeModel, String type) {
|
||||
if (type.equals("controller")) {
|
||||
return StrUtil.isEmpty(codeModel.getSubModule())
|
||||
? baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getType()
|
||||
.value()
|
||||
: baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getType()
|
||||
.value() + "/"
|
||||
+ codeModel.getSubModule();
|
||||
}
|
||||
if (type.equals("xmlMapper")) {
|
||||
return StrUtil.isEmpty(codeModel.getSubModule()) ? baseResPath + "mapper/"
|
||||
+ codeModel.getModule()
|
||||
: baseResPath + "mapper/" + codeModel.getModule() + "/" + codeModel.getSubModule();
|
||||
}
|
||||
return StrUtil.isEmpty(codeModel.getSubModule()) ? baseSrcPath + codeModel.getModule() + "/"
|
||||
+ type
|
||||
: baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getSubModule();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Mapper
|
||||
*
|
||||
* @param codeModel 代码模型
|
||||
*/
|
||||
public void mapper(CodeModel codeModel) {
|
||||
Template template = templateEngine.getTemplate("/mapper/interface.th");
|
||||
String result = template.render(Dict.parse(codeModel));
|
||||
FileWriter writer = new FileWriter(
|
||||
filePath(codeModel, "mapper") + "/" + codeModel.getEntity() + "Mapper.java");
|
||||
writer.write(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Service
|
||||
*
|
||||
* @param codeModel 代码模型
|
||||
*/
|
||||
public void service(CodeModel codeModel) {
|
||||
Template interfaceTemplate = templateEngine.getTemplate("/service/interface.th");
|
||||
String interfaceResult = interfaceTemplate.render(Dict.parse(codeModel));
|
||||
FileWriter interfaceWriter = new FileWriter(
|
||||
filePath(codeModel, "service") + "/" + codeModel.getEntity() + "Service.java");
|
||||
interfaceWriter.write(interfaceResult);
|
||||
|
||||
Template template = templateEngine.getTemplate("/service/impl.th");
|
||||
String result = template.render(Dict.parse(codeModel));
|
||||
FileWriter writer = new FileWriter(
|
||||
filePath(codeModel, "service") + "/impl/" + codeModel.getEntity() + "ServiceImpl.java");
|
||||
writer.write(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Controller
|
||||
*
|
||||
* @param codeModel 代码模型
|
||||
*/
|
||||
public void controller(CodeModel codeModel) {
|
||||
Template template = templateEngine.getTemplate("controller.th");
|
||||
System.out.println(codeModel.getType().value());
|
||||
Dict data = Dict.create().set("upperType", StrUtil.upperFirst(codeModel.getType().value()))
|
||||
.set("url",
|
||||
"/" + codeModel.getType() + "/" + StrUtil.toUnderlineCase(codeModel.getEntity())
|
||||
.replace("_", "/"));
|
||||
data.putAll(Dict.parse(codeModel));
|
||||
data.set("type", codeModel.getType().value());
|
||||
String result = template.render(data);
|
||||
FileWriter writer = new FileWriter(filePath(codeModel, "controller") + "/"
|
||||
+ StrUtil.upperFirst(codeModel.getType().value()) + codeModel.getEntity()
|
||||
+ "Controller.java");
|
||||
writer.write(result);
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package com.cool.core.code;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 代码模型
|
||||
*/
|
||||
@Data
|
||||
public class CodeModel {
|
||||
/**
|
||||
* 类型 后台还是对外的接口 admin app
|
||||
*/
|
||||
private CodeTypeEnum type;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 模块
|
||||
*/
|
||||
private String module;
|
||||
|
||||
/**
|
||||
* 子模块
|
||||
*/
|
||||
private String subModule;
|
||||
|
||||
/**
|
||||
* 实体类
|
||||
*/
|
||||
private String entity;
|
||||
|
||||
public void setEntity(Class entity) {
|
||||
this.entity = entity.getSimpleName().replace("Entity", "");
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package com.cool.core.code;
|
||||
|
||||
/**
|
||||
* 代码类型
|
||||
*/
|
||||
public enum CodeTypeEnum {
|
||||
ADMIN("admin", "后端接口"), APP("app", "对外接口");
|
||||
|
||||
private String value;
|
||||
|
||||
private String des;
|
||||
|
||||
CodeTypeEnum(String value, String des) {
|
||||
this.value = value;
|
||||
this.des = des;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* cool的配置
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "cool")
|
||||
public class CoolProperties {
|
||||
// 是否自动导入数据
|
||||
private Boolean initData = false;
|
||||
// token配置
|
||||
@NestedConfigurationProperty
|
||||
private TokenProperties token;
|
||||
// 文件配置
|
||||
@NestedConfigurationProperty
|
||||
private FileProperties file;
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Locale;
|
||||
import org.springdoc.core.customizers.SpringDocCustomizers;
|
||||
import org.springdoc.core.properties.SpringDocConfigProperties;
|
||||
import org.springdoc.core.providers.SpringDocProviders;
|
||||
import org.springdoc.core.service.AbstractRequestService;
|
||||
import org.springdoc.core.service.GenericResponseService;
|
||||
import org.springdoc.core.service.OpenAPIService;
|
||||
import org.springdoc.core.service.OperationService;
|
||||
import org.springdoc.webmvc.api.OpenApiResource;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 自定义 OpenApiResource
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(
|
||||
name = "springdoc.api-docs.enabled",
|
||||
havingValue = "true"
|
||||
)
|
||||
public class CustomOpenApiResource extends OpenApiResource {
|
||||
|
||||
public CustomOpenApiResource(ObjectFactory<OpenAPIService> openAPIBuilderObjectFactory, AbstractRequestService requestBuilder, GenericResponseService responseBuilder, OperationService operationParser, SpringDocConfigProperties springDocConfigProperties, SpringDocProviders springDocProviders, SpringDocCustomizers springDocCustomizers) {
|
||||
super("springdocDefault", openAPIBuilderObjectFactory, requestBuilder, responseBuilder, operationParser, springDocConfigProperties, springDocProviders, springDocCustomizers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getServerUrl(HttpServletRequest request, String apiDocsUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
public byte[] getOpenApiJson() throws JsonProcessingException {
|
||||
return writeJsonValue(getOpenApi(Locale.getDefault()));
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
/**
|
||||
* 文件模式
|
||||
*/
|
||||
public enum FileModeEnum {
|
||||
LOCAL("local", "local", "本地"), CLOUD("cloud", "oss", "云存储"), OTHER("other", "other", "其他");
|
||||
|
||||
private String value;
|
||||
|
||||
private String type;
|
||||
|
||||
private String des;
|
||||
|
||||
FileModeEnum(String value, String type, String des) {
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
this.des = des;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public String type() {
|
||||
return this.type;
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 文件
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "cool.file")
|
||||
public class FileProperties {
|
||||
// 上传模式
|
||||
private FileModeEnum mode;
|
||||
// 上传类型
|
||||
private String type;
|
||||
// 本地文件上传
|
||||
@NestedConfigurationProperty
|
||||
private LocalFileProperties local;
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.text.SimpleDateFormat;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
|
||||
final Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
|
||||
final ObjectMapper objectMapper = builder.build();
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
// Long,BigInteger 转为 String 防止 js 丢失精度
|
||||
simpleModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
|
||||
simpleModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
|
||||
simpleModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
|
||||
objectMapper.registerModule(simpleModule);
|
||||
// 配置日期格式为 yyyy-MM-dd HH:mm:ss
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
objectMapper.setDateFormat(dateFormat);
|
||||
return new MappingJackson2HttpMessageConverter(objectMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 超出 JS 最大最小值 处理
|
||||
*/
|
||||
@JacksonStdImpl
|
||||
public static class BigNumberSerializer extends NumberSerializer {
|
||||
|
||||
/**
|
||||
* 根据 JS Number.MAX_SAFE_INTEGER 与 Number.MIN_SAFE_INTEGER 得来
|
||||
*/
|
||||
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
|
||||
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
|
||||
|
||||
/**
|
||||
* 提供实例
|
||||
*/
|
||||
public static final BigNumberSerializer INSTANCE = new BigNumberSerializer(Number.class);
|
||||
|
||||
public BigNumberSerializer(Class<? extends Number> rawType) {
|
||||
super(rawType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
// 超出范围 序列化位字符串
|
||||
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
|
||||
super.serialize(value, gen, provider);
|
||||
} else {
|
||||
gen.writeString(value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import com.cool.core.util.PathUtils;
|
||||
import java.io.File;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 文件
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "cool.file.local")
|
||||
public class LocalFileProperties {
|
||||
|
||||
// 跟域名
|
||||
private String baseUrl;
|
||||
|
||||
private String uploadPath = "assets/public/upload";
|
||||
|
||||
public String getAbsoluteUploadFolder() {
|
||||
if (!PathUtils.isAbsolutePath(uploadPath)) {
|
||||
// 相对路径
|
||||
return System.getProperty("user.dir") + File.separator + uploadPath;
|
||||
}
|
||||
// 绝对路径
|
||||
return uploadPath;
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class LogDiscardPolicy implements RejectedExecutionHandler {
|
||||
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||
log.warn("logTaskExecutor 当前已超过线程池最大队列容量,拒绝策略为丢弃该线程 {}", r.toString());
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "cool.log")
|
||||
public class LogProperties {
|
||||
|
||||
/**
|
||||
* 请求参数最大字节,超过请求参数不记录
|
||||
*/
|
||||
private int maxByteLength;
|
||||
/**
|
||||
* 核心线程数的倍数
|
||||
*/
|
||||
private int corePoolSizeMultiplier;
|
||||
/**
|
||||
* 最大线程数的倍数
|
||||
*/
|
||||
private int maxPoolSizeMultiplier;
|
||||
/**
|
||||
* 队列容量的倍数
|
||||
*/
|
||||
private int queueCapacityMultiplier;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import com.cool.core.tenant.CoolTenantFactory;
|
||||
import com.mybatisflex.core.FlexGlobalConfig;
|
||||
import com.mybatisflex.core.tenant.TenantFactory;
|
||||
import com.mybatisflex.spring.boot.MyBatisFlexCustomizer;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MyBatisFlexConfiguration implements MyBatisFlexCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(FlexGlobalConfig globalConfig) {
|
||||
// 我们可以在这里进行一些列的初始化配置
|
||||
|
||||
// 指定多租户列的列名
|
||||
FlexGlobalConfig.getDefaultConfig().setTenantColumn("tenant_id");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "cool.multi-tenant.enable", havingValue = "true")
|
||||
public TenantFactory tenantFactory(){
|
||||
return new CoolTenantFactory();
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 文件
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "cool.file.oss")
|
||||
public class OssFileProperties {
|
||||
// accessKeyId
|
||||
private String accessKeyId;
|
||||
// accessKeySecret
|
||||
private String accessKeySecret;
|
||||
// 文件空间
|
||||
private String bucket;
|
||||
// 地址
|
||||
private String endpoint;
|
||||
// 超时时间
|
||||
private Long timeout;
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class PluginJson {
|
||||
/**
|
||||
* 插件名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 插件标识
|
||||
*/
|
||||
private String key;
|
||||
/**
|
||||
* 插件钩子,比如替换系统的上传组件,upload
|
||||
*/
|
||||
private String hook;
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
private String version;
|
||||
/**
|
||||
* 插件描述
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 作者
|
||||
*/
|
||||
private String author;
|
||||
/**
|
||||
* 插件 logo,建议尺寸 256x256
|
||||
*/
|
||||
private String logo;
|
||||
/**
|
||||
* 插件介绍,会展示在插件的详情中
|
||||
*/
|
||||
private String readme;
|
||||
/**
|
||||
* 插件配置, 每个插件的配置各不相同
|
||||
*/
|
||||
private Map<String, Object> config;
|
||||
|
||||
/**
|
||||
* jar包存放路径
|
||||
*/
|
||||
private String jarPath;
|
||||
|
||||
/**
|
||||
* 同名hook id
|
||||
*/
|
||||
@JsonIgnore
|
||||
private Long sameHookId;
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.ExternalDocumentation;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.info.Contact;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
|
||||
@OpenAPIDefinition(info = @Info(title = "COOL-ADMIN", version = "4.0", description = "一个很酷的后台权限管理系统开发框架", contact = @Contact(name = "闪酷科技")), security = @SecurityRequirement(name = "Authorization"), externalDocs = @ExternalDocumentation(description = "参考文档", url = "https://cool-js.com"))
|
||||
@SecurityScheme(type = SecuritySchemeType.APIKEY, name = "Authorization", in = SecuritySchemeIn.HEADER)
|
||||
public class SwaggerConfig {
|
||||
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
private final LogProperties logProperties;
|
||||
|
||||
@Bean(name = "logTaskExecutor")
|
||||
public Executor loggingTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
|
||||
int corePoolSize = Runtime.getRuntime().availableProcessors() * logProperties.getCorePoolSizeMultiplier();
|
||||
int maxPoolSize = corePoolSize * logProperties.getMaxPoolSizeMultiplier();
|
||||
int queueCapacity = maxPoolSize * logProperties.getQueueCapacityMultiplier();
|
||||
|
||||
executor.setCorePoolSize(corePoolSize);
|
||||
executor.setMaxPoolSize(maxPoolSize);
|
||||
executor.setQueueCapacity(queueCapacity);
|
||||
executor.setThreadNamePrefix("logTask-");
|
||||
|
||||
// 自定义拒绝策略
|
||||
executor.setRejectedExecutionHandler(new LogDiscardPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
@Bean(name = "cachedThreadPool")
|
||||
public ExecutorService cachedThreadPool() {
|
||||
// 创建一个虚拟线程池,每个任务使用一个虚拟线程执行
|
||||
return Executors.newCachedThreadPool();
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package com.cool.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* token配置
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "cool.token")
|
||||
public class TokenProperties {
|
||||
// token 过期时间
|
||||
private Long expire;
|
||||
// refreshToken 过期时间
|
||||
private Long refreshExpire;
|
||||
}
|
122
cool-admin-java/src/main/java/com/cool/core/config/cache/CaffeineConfig.java
vendored
Normal file
122
cool-admin-java/src/main/java/com/cool/core/config/cache/CaffeineConfig.java
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
package com.cool.core.config.cache;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "CAFFEINE")
|
||||
public class CaffeineConfig {
|
||||
|
||||
@Value("${spring.cache.file}")
|
||||
private String cacheFile;
|
||||
|
||||
@Value("${cool.cacheName}")
|
||||
private String cacheName;
|
||||
|
||||
@Bean
|
||||
public Caffeine<Object, Object> caffeine() {
|
||||
return Caffeine.newBuilder().maximumSize(10000);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CaffeineCacheManager cacheManager(Caffeine<Object, Object> caffeine) {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||
cacheManager.setCaffeine(caffeine);
|
||||
loadCache(cacheManager);
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
File cacheDir = new File(cacheFile).getParentFile();
|
||||
if (!cacheDir.exists()) {
|
||||
if (cacheDir.mkdirs()) {
|
||||
log.info("Created directory: " + cacheDir.getAbsolutePath());
|
||||
} else {
|
||||
log.error("Failed to create directory: " + cacheDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCache(CaffeineCacheManager cacheManager) {
|
||||
if (cacheManager == null) {
|
||||
log.error("CacheManager is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cacheFile == null || cacheFile.isEmpty()) {
|
||||
log.error("Cache file path is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(cacheFile);
|
||||
if (!file.exists()) {
|
||||
log.warn("Cache file does not exist: " + cacheFile);
|
||||
return;
|
||||
}
|
||||
try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) {
|
||||
Map<Object, Object> cacheMap = (Map<Object, Object>) inputStream.readObject();
|
||||
com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache = Caffeine.newBuilder()
|
||||
.build();
|
||||
caffeineCache.putAll(cacheMap);
|
||||
cacheManager.registerCustomCache(cacheName, caffeineCache);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log.error("loadCacheErr", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheLoader cacheLoader(CaffeineCacheManager cacheManager) {
|
||||
return new CacheLoader(cacheManager, cacheFile);
|
||||
}
|
||||
|
||||
class CacheLoader {
|
||||
|
||||
private final CaffeineCacheManager cacheManager;
|
||||
private final String cacheFile;
|
||||
|
||||
public CacheLoader(CaffeineCacheManager cacheManager, String cacheFile) {
|
||||
this.cacheManager = cacheManager;
|
||||
this.cacheFile = cacheFile;
|
||||
}
|
||||
|
||||
@EventListener(ContextClosedEvent.class)
|
||||
@Scheduled(fixedRate = 10000)
|
||||
public void persistCache() {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache != null
|
||||
&& cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
|
||||
Map<Object, Object> cacheMap = ((com.github.benmanes.caffeine.cache.Cache<Object, Object>) cache
|
||||
.getNativeCache()).asMap();
|
||||
try (ObjectOutputStream outputStream = new ObjectOutputStream(
|
||||
new FileOutputStream(cacheFile))) {
|
||||
outputStream.writeObject(new HashMap<>(cacheMap));
|
||||
} catch (IOException e) {
|
||||
log.error("persistCacheErr", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
cool-admin-java/src/main/java/com/cool/core/config/cache/RedisConfig.java
vendored
Normal file
29
cool-admin-java/src/main/java/com/cool/core/config/cache/RedisConfig.java
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.cool.core.config.cache;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(
|
||||
RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(redisConnectionFactory);
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
|
||||
return RedisCacheManager.create(redisConnectionFactory);
|
||||
}
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package com.cool.core.enums;
|
||||
|
||||
public class AdminComponentsEnum {
|
||||
|
||||
|
||||
/**
|
||||
* 省市区选择器 - 用户选择省市区
|
||||
*/
|
||||
public static final String PCA = "pca";
|
||||
|
||||
/**
|
||||
* 文本输入 - 文本编辑框
|
||||
*/
|
||||
public static final String INPUT = "input";
|
||||
|
||||
/**
|
||||
* 文本域 - 多行文本编辑框
|
||||
*/
|
||||
public static final String TEXTAREA = "textarea";
|
||||
|
||||
/**
|
||||
* 富文本编辑器 - 用于文章,商品详情的编辑
|
||||
*/
|
||||
public static final String EDITOR_RICH = "editor-rich";
|
||||
|
||||
/**
|
||||
* 代码编辑器 - 用于开发人员编写代码,支持多种语言,支持代码高亮,支持代码格式化
|
||||
*/
|
||||
public static final String CODING = "coding";
|
||||
|
||||
/**
|
||||
* 数字输入 - 数字输入编辑框
|
||||
*/
|
||||
public static final String INPUT_NUMBER = "input-number";
|
||||
|
||||
/**
|
||||
* 日期选择器 - 用户选择 年-月-日
|
||||
*/
|
||||
public static final String DATE = "date";
|
||||
|
||||
/**
|
||||
* 日期范围选择器 - 用户选择起始 年-月-日
|
||||
*/
|
||||
public static final String DATERANGE = "daterange";
|
||||
|
||||
/**
|
||||
* 时间选择器 - 用户选择 时:分:秒
|
||||
*/
|
||||
public static final String DATETIME = "datetime";
|
||||
|
||||
/**
|
||||
* 时间范围选择器 - 用户选择起始 年-月-日 时:分:秒
|
||||
*/
|
||||
public static final String DATETIMERANGE = "datetimerange";
|
||||
|
||||
/**
|
||||
* 单图上传 - 用户上传单张图片,如:头像、logo、封面
|
||||
*/
|
||||
public static final String UPLOAD_IMG = "upload-img";
|
||||
|
||||
/**
|
||||
* 多图上传 - 用户上传多张图片, 如:照片、图片
|
||||
*/
|
||||
public static final String UPLOAD_IMG_MULTIPLE = "upload-img-multiple";
|
||||
|
||||
/**
|
||||
* 单个文件上传 - 用户上传单个文件
|
||||
*/
|
||||
public static final String UPLOAD_FILE = "upload-file";
|
||||
|
||||
/**
|
||||
* 多个文件上传 - 用户上传多个文件
|
||||
*/
|
||||
public static final String UPLOAD_FILE_MULTIPLE = "upload-file-multiple";
|
||||
|
||||
/**
|
||||
* 状态选择器 - 用户开启或者关闭操作,如:是否启用、是否推荐、是否默认、置顶、启用禁用等
|
||||
*/
|
||||
public static final String SWITCH = "switch";
|
||||
|
||||
/**
|
||||
* 评分选择器 - 用户评分
|
||||
*/
|
||||
public static final String RATE = "rate";
|
||||
|
||||
/**
|
||||
* 滑块选择器 - 在一个固定区间内进行选择, 如:进度
|
||||
*/
|
||||
public static final String PROGRESS = "progress";
|
||||
|
||||
/**
|
||||
* 单选框 - 在一组备选项中进行单选,如:审批状态
|
||||
*/
|
||||
public static final String RADIO = "radio";
|
||||
|
||||
/**
|
||||
* 多选框 - 适用于选项比较少的情况,在一组备选项中进行多选, 如:学历、爱好等
|
||||
*/
|
||||
public static final String CHECKBOX = "checkbox";
|
||||
|
||||
/**
|
||||
* 下拉框 - 适用于当选项过多时,使用下拉菜单展示并选择内容,如:分类、标签等
|
||||
*/
|
||||
public static final String SELECT = "select";
|
||||
}
|
13
cool-admin-java/src/main/java/com/cool/core/enums/Apis.java
Normal file
13
cool-admin-java/src/main/java/com/cool/core/enums/Apis.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.cool.core.enums;
|
||||
|
||||
public class Apis {
|
||||
public static final String ADD = "add";
|
||||
public static final String DELETE = "delete";
|
||||
public static final String UPDATE = "update";
|
||||
public static final String PAGE = "page";
|
||||
public static final String LIST = "list";
|
||||
public static final String INFO = "info";
|
||||
|
||||
|
||||
public static final String[] ALL_API = new String[]{ ADD, DELETE, UPDATE, PAGE, LIST, INFO };
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.cool.core.enums;
|
||||
|
||||
/**
|
||||
* 查询模式决定返回值
|
||||
*/
|
||||
public enum QueryModeEnum {
|
||||
ENTITY, // 实体(默认)
|
||||
ENTITY_WITH_RELATIONS, // 实体关联查询(如实体字段上加 @RelationOneToMany 等注解)
|
||||
CUSTOM , // 自定义,默认为Map
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.cool.core.enums;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
public enum UserTypeEnum {
|
||||
ADMIN, // 后台
|
||||
APP, // app
|
||||
UNKNOWN, // 未知
|
||||
}
|
410
cool-admin-java/src/main/java/com/cool/core/eps/CoolEps.java
Normal file
410
cool-admin-java/src/main/java/com/cool/core/eps/CoolEps.java
Normal file
@@ -0,0 +1,410 @@
|
||||
package com.cool.core.eps;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.cool.core.annotation.EpsField;
|
||||
import com.cool.core.annotation.TokenIgnore;
|
||||
import com.cool.core.config.CustomOpenApiResource;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
/**
|
||||
* 实体信息与路径
|
||||
*/
|
||||
@Getter
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class CoolEps {
|
||||
|
||||
@Value("${server.port}")
|
||||
private int serverPort;
|
||||
|
||||
private Dict entityInfo;
|
||||
|
||||
private Dict menuInfo;
|
||||
|
||||
private JSONObject swaggerInfo;
|
||||
|
||||
@Value("${springdoc.api-docs.enabled:false}")
|
||||
private boolean apiDocsEnabled;
|
||||
|
||||
public Dict admin;
|
||||
|
||||
public Dict app;
|
||||
|
||||
final private RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
|
||||
@Async
|
||||
public void init() {
|
||||
if (!apiDocsEnabled) {
|
||||
log.info("服务启动成功,端口:{}", serverPort);
|
||||
return;
|
||||
}
|
||||
entityInfo = Dict.create();
|
||||
menuInfo = Dict.create();
|
||||
swaggerInfo = swaggerInfo();
|
||||
Runnable task = () -> {
|
||||
entity();
|
||||
urls();
|
||||
log.info("初始化eps完成,服务启动成功,端口:{}", serverPort);
|
||||
};
|
||||
// ThreadUtil.safeSleep(3000);
|
||||
ThreadUtil.execute(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有的数据
|
||||
*/
|
||||
public void clear() {
|
||||
admin = Dict.create();
|
||||
app = Dict.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建所有的url
|
||||
*/
|
||||
private void urls() {
|
||||
Dict admin = Dict.create();
|
||||
Dict app = Dict.create();
|
||||
ArrayList<Object> emptyList = new ArrayList<>();
|
||||
Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> methodEntry : map.entrySet()) {
|
||||
RequestMappingInfo info = methodEntry.getKey();
|
||||
HandlerMethod method = methodEntry.getValue();
|
||||
TokenIgnore tokenIgnore = method.getMethodAnnotation(TokenIgnore.class);
|
||||
String module = getModule(method);
|
||||
if (StrUtil.isNotEmpty(module)) {
|
||||
String entityName = getEntity(method.getBeanType());
|
||||
String methodPath = getMethodUrl(method);
|
||||
String escapedMethodPath = methodPath.replace("{", "\\{").replace("}", "\\}");
|
||||
String prefix = Objects.requireNonNull(getUrl(info))
|
||||
.replaceFirst("(?s)(.*)" + escapedMethodPath, "$1");
|
||||
Dict result;
|
||||
int type = 0;
|
||||
if (prefix.startsWith("/admin")) {
|
||||
result = admin;
|
||||
} else if (prefix.startsWith("/app")) {
|
||||
result = app;
|
||||
type = 1;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (result.get(module) == null) {
|
||||
result.set(module, new ArrayList<Dict>());
|
||||
}
|
||||
|
||||
List<Dict> urls = result.getBean(module);
|
||||
Dict item = CollUtil.findOne(urls, dict -> {
|
||||
if (dict != null) {
|
||||
return dict.getStr("module").equals(module)
|
||||
&& dict.getStr("controller")
|
||||
.equals(method.getBeanType().getSimpleName());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (item != null) {
|
||||
item.set("api", apis(prefix, methodPath, item.getBean("api"), tokenIgnore));
|
||||
} else {
|
||||
item = Dict.create();
|
||||
item.set("controller", method.getBeanType().getSimpleName());
|
||||
item.set("module", module);
|
||||
item.set("info", Dict.create().set("type",
|
||||
Dict.create()
|
||||
.set("name", getLastPathSegment(prefix))
|
||||
.set("description", "")
|
||||
));
|
||||
item.set("api", apis(prefix, methodPath, item.getBean("api"), tokenIgnore));
|
||||
item.set("name", entityName);
|
||||
item.set("columns", entityInfo.get(entityName));
|
||||
item.set("pageQueryOp", Dict.create().set("keyWordLikeFields", emptyList)
|
||||
.set("fieldEq", emptyList)
|
||||
.set("fieldLike", emptyList));
|
||||
item.set("prefix", prefix);
|
||||
item.set("menu", menuInfo.get(entityName));
|
||||
urls.add(item);
|
||||
}
|
||||
if (type == 0) {
|
||||
admin.set(module, urls);
|
||||
}
|
||||
if (type == 1) {
|
||||
app.set(module, urls);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
this.admin = admin;
|
||||
this.app = app;
|
||||
|
||||
}
|
||||
/**
|
||||
* 提取URL路径中的最后一个路径段
|
||||
* 示例:输入 "/api/getData" 返回 "getData"
|
||||
*/
|
||||
private String getLastPathSegment(String url) {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
int queryIndex = url.indexOf('?');
|
||||
if (queryIndex != -1) {
|
||||
url = url.substring(0, queryIndex);
|
||||
}
|
||||
|
||||
int slashIndex = url.lastIndexOf('/');
|
||||
if (slashIndex != -1 && slashIndex < url.length() - 1) {
|
||||
return url.substring(slashIndex + 1);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置所有的api
|
||||
*
|
||||
* @param prefix 路由前缀
|
||||
* @param methodPath 方法路由
|
||||
* @param list api列表
|
||||
* @return api列表
|
||||
*/
|
||||
private List<Dict> apis(String prefix, String methodPath, List<Dict> list, TokenIgnore tokenIgnore) {
|
||||
if (ObjUtil.isNull(list)) {
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
Dict item = Dict.create();
|
||||
item.set("path", methodPath);
|
||||
item.set("tag", "");
|
||||
item.set("dts", Dict.create());
|
||||
item.set("ignoreToken", false);
|
||||
if (tokenIgnore != null) {
|
||||
item.set("ignoreToken", true);
|
||||
}
|
||||
setSwaggerInfo(item, prefix + methodPath);
|
||||
list.add(item);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置swagger相关信息
|
||||
*
|
||||
* @param item 信息载体
|
||||
* @param url url地址
|
||||
*/
|
||||
private void setSwaggerInfo(Dict item, String url) {
|
||||
JSONObject paths = swaggerInfo.getJSONObject("paths");
|
||||
JSONObject urlInfo = paths.getJSONObject(url);
|
||||
String method = urlInfo.keySet().iterator().next();
|
||||
JSONObject methodInfo = urlInfo.getJSONObject(method);
|
||||
item.set("method", method);
|
||||
item.set("summary", methodInfo.getStr("summary"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得方法的url地址
|
||||
*
|
||||
* @param handlerMethod 方法
|
||||
* @return 方法url地址
|
||||
*/
|
||||
private String getMethodUrl(HandlerMethod handlerMethod) {
|
||||
String url = "";
|
||||
Method method = handlerMethod.getMethod();
|
||||
Annotation[] annotations = method.getDeclaredAnnotations();
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
Class<? extends Annotation> annotationType = annotation.annotationType();
|
||||
if (annotationType.getName().contains("org.springframework.web.bind.annotation")) {
|
||||
Map<String, Object> attributes = Arrays.stream(annotationType.getDeclaredMethods())
|
||||
.collect(Collectors.toMap(Method::getName, m -> {
|
||||
try {
|
||||
return m.invoke(annotation);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to access annotation attribute",
|
||||
e);
|
||||
}
|
||||
}));
|
||||
|
||||
if (attributes.containsKey("value") && ObjUtil.isNotEmpty(attributes.get("value"))) {
|
||||
url = ((String[]) attributes.get("value"))[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得url地址
|
||||
*
|
||||
* @param info 路由信息
|
||||
* @return url地址
|
||||
*/
|
||||
private String getUrl(RequestMappingInfo info) {
|
||||
if (info.getPathPatternsCondition() == null) {
|
||||
return null;
|
||||
}
|
||||
Set<String> paths = info.getPathPatternsCondition().getPatternValues();
|
||||
return paths.iterator().next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得模块
|
||||
*
|
||||
* @param method 方法
|
||||
* @return 模块
|
||||
*/
|
||||
private String getModule(HandlerMethod method) {
|
||||
String beanName = method.getBeanType().getName();
|
||||
String[] beanNames = beanName.split("[.]");
|
||||
int index = ArrayUtil.indexOf(beanNames, "modules");
|
||||
if (index > 0) {
|
||||
return beanNames[index + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得swagger的json信息
|
||||
*/
|
||||
private JSONObject swaggerInfo() {
|
||||
try {
|
||||
byte[] bytes = SpringUtil.getBean(CustomOpenApiResource.class).getOpenApiJson();
|
||||
return JSONUtil.parseObj(new String(bytes));
|
||||
} catch (Exception e) {
|
||||
return new JSONObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得Controller上的实体类型
|
||||
*
|
||||
* @param controller Controller类
|
||||
* @return 实体名称
|
||||
*/
|
||||
private String getEntity(Class<?> controller) {
|
||||
try {
|
||||
ParameterizedType parameterizedType = (ParameterizedType) controller.getGenericSuperclass();
|
||||
Class<?> entityClass = (Class<?>) parameterizedType.getActualTypeArguments()[1];
|
||||
return entityClass.getSimpleName();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void entity() {
|
||||
// 扫描所有的实体类
|
||||
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation("", Table.class);
|
||||
classes.forEach(e -> {
|
||||
// 获得属性
|
||||
Field[] fields = getAllDeclaredFields(e);
|
||||
List<Dict> columns = columns(fields);
|
||||
entityInfo.set(e.getSimpleName(), columns);
|
||||
|
||||
|
||||
Table mergedAnnotation = AnnotatedElementUtils.findMergedAnnotation(e, Table.class);
|
||||
|
||||
menuInfo.set(e.getSimpleName(), mergedAnnotation.comment());
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类及其所有父类中声明的字段
|
||||
*
|
||||
* @param clazz 要检查的类
|
||||
* @return 包含类及其所有父类中声明的所有字段的数组
|
||||
*/
|
||||
public static Field[] getAllDeclaredFields(Class<?> clazz) {
|
||||
// 参数校验
|
||||
if (clazz == null) {
|
||||
throw new IllegalArgumentException("Class cannot be null");
|
||||
}
|
||||
|
||||
List<Field> allFields = new ArrayList<>();
|
||||
Class<?> currentClass = clazz;
|
||||
|
||||
// 循环遍历类及其父类
|
||||
while (currentClass != null) {
|
||||
Field[] declaredFields = currentClass.getDeclaredFields();
|
||||
allFields.addAll(Arrays.asList(declaredFields));
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
|
||||
// 将列表转换为数组返回
|
||||
return allFields.toArray(new Field[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得所有的列
|
||||
*
|
||||
* @param fields 字段名
|
||||
* @return 所有的列
|
||||
*/
|
||||
private List<Dict> columns(Field[] fields) {
|
||||
List<Dict> dictList = new ArrayList<>();
|
||||
for (Field field : fields) {
|
||||
Dict dict = Dict.create();
|
||||
|
||||
EpsField epsField = AnnotatedElementUtils.findMergedAnnotation(field, EpsField.class);
|
||||
if (epsField != null) {
|
||||
dict.set("component", epsField.component());
|
||||
}
|
||||
|
||||
ColumnDefine columnInfo = AnnotatedElementUtils.findMergedAnnotation(field, ColumnDefine.class);
|
||||
if (columnInfo == null) {
|
||||
continue;
|
||||
}
|
||||
dict.set("comment", columnInfo.comment());
|
||||
dict.set("length", columnInfo.length());
|
||||
dict.set("propertyName", field.getName());
|
||||
dict.set("type", matchType(field.getType().getName()));
|
||||
dict.set("nullable", !columnInfo.notNull());
|
||||
dict.set("source", "a." + field.getName());
|
||||
dictList.add(dict);
|
||||
}
|
||||
return dictList;
|
||||
}
|
||||
|
||||
/**
|
||||
* java类型转换成JavaScript对应的类型
|
||||
*
|
||||
* @param type 类型
|
||||
* @return JavaScript类型
|
||||
*/
|
||||
private String matchType(String type) {
|
||||
return switch (type) {
|
||||
case "java.lang.Boolean" -> "boolean";
|
||||
case "java.lang.Long", "java.lang.Integer", "java.lang.Short", "java.lang.Float",
|
||||
"java.lang.Double" -> "number";
|
||||
case "java.util.Date" -> "date";
|
||||
default -> "string";
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.cool.core.eps;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 事件监听
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile({"local"})
|
||||
@RequiredArgsConstructor
|
||||
public class EpsEvent {
|
||||
|
||||
final private CoolEps coolEps;
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ApplicationReadyEvent event) {
|
||||
coolEps.init();
|
||||
log.info("构建eps信息");
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package com.cool.core.exception;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 自定义异常处理
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class CoolException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String msg;
|
||||
private int code = 500;
|
||||
private Object data;
|
||||
|
||||
public CoolException(String msg) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public CoolException(String msg, Throwable e) {
|
||||
super(msg, e);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public CoolException(String msg, int code) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public CoolException(String msg, int code, Throwable e) {
|
||||
super(msg, e);
|
||||
this.msg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public CoolException(Object data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.cool.core.exception;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.cool.core.request.R;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 异常处理器
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class CoolExceptionHandler {
|
||||
|
||||
@ExceptionHandler(CoolException.class)
|
||||
public R handleRRException(CoolException e) {
|
||||
R r = new R();
|
||||
if (ObjUtil.isNotEmpty(e.getData())) {
|
||||
r.setData( e.getData() );
|
||||
} else {
|
||||
r.setCode( e.getCode() );
|
||||
r.setMessage( e.getMessage() );
|
||||
}
|
||||
if (ObjUtil.isNotEmpty(e.getCause())) {
|
||||
log.error(e.getCause().getMessage(), e.getCause());
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@ExceptionHandler(DuplicateKeyException.class)
|
||||
public R handleDuplicateKeyException(DuplicateKeyException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return R.error("已存在该记录或值不能重复");
|
||||
}
|
||||
|
||||
@ExceptionHandler(BadCredentialsException.class)
|
||||
public R handleBadCredentialsException(BadCredentialsException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return R.error("账户密码不正确");
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public R handleHttpRequestMethodNotSupportedException(
|
||||
HttpRequestMethodNotSupportedException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return R.error("不支持该请求方式,请区分POST、GET等请求方式是否正确");
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public R handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return R.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R handleException(Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return R.error();
|
||||
}
|
||||
|
||||
@ExceptionHandler(WxErrorException.class)
|
||||
public R handleException(WxErrorException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return R.error(e.getMessage());
|
||||
}
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
package com.cool.core.exception;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.cool.core.util.I18nUtil;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 校验处理
|
||||
*/
|
||||
public class CoolPreconditions {
|
||||
|
||||
/**
|
||||
* 条件如果为真 就抛异常 如 CoolPreconditions.check(StrUtil.isEmptyIfStr(name), 500,
|
||||
* "名称不能为空"); name 字段如果为 null或空字符串,就抛异常
|
||||
*/
|
||||
public static void check(boolean flag, int code, String message, Object... arguments) {
|
||||
if (flag) {
|
||||
throw getCoolException(message, code, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
public static void check(boolean flag, String message, Object... arguments) {
|
||||
if (flag) {
|
||||
throw getCoolException(message, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
public static void alwaysThrow(String message, Object... arguments) {
|
||||
throw getCoolException(message, arguments);
|
||||
}
|
||||
|
||||
private static CoolException getCoolException(String message, Object... arguments) {
|
||||
Optional<Object> first = Arrays.stream(arguments).filter(o -> o instanceof Throwable)
|
||||
.findFirst();
|
||||
return new CoolException(formatMessage(message, arguments), (Throwable) first.orElse(null));
|
||||
}
|
||||
|
||||
private static CoolException getCoolException(String message, int code, Object... arguments) {
|
||||
Optional<Object> first = Arrays.stream(arguments).filter(o -> o instanceof Throwable)
|
||||
.findFirst();
|
||||
return new CoolException(formatMessage(message, arguments), code, (Throwable) first.orElse(null));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回data
|
||||
*/
|
||||
public static void returnData(boolean flag, Object data) {
|
||||
if (flag) {
|
||||
throw new CoolException(data);
|
||||
}
|
||||
}
|
||||
|
||||
public static void returnData(Object data) {
|
||||
returnData(true, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象如果为空 就抛异常
|
||||
*/
|
||||
public static void checkEmpty(Object object, String message, Object... arguments) {
|
||||
check(ObjectUtil.isEmpty(object), formatMessage(message, arguments));
|
||||
}
|
||||
|
||||
public static void checkEmpty(Object object) {
|
||||
check(ObjectUtil.isEmpty(object), "参数不能为空");
|
||||
}
|
||||
|
||||
private static String formatMessage(String messagePattern, Object... arguments) {
|
||||
messagePattern = I18nUtil.getI18nMsg(messagePattern);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int argumentIndex = 0;
|
||||
int placeholderIndex = messagePattern.indexOf("{}");
|
||||
while (placeholderIndex != -1) {
|
||||
sb.append(messagePattern, 0, placeholderIndex);
|
||||
if (argumentIndex < arguments.length) {
|
||||
sb.append(arguments[argumentIndex++]);
|
||||
} else {
|
||||
sb.append("{}"); // 如果参数不足,保留原样
|
||||
}
|
||||
messagePattern = messagePattern.substring(placeholderIndex + 2);
|
||||
placeholderIndex = messagePattern.indexOf("{}");
|
||||
}
|
||||
sb.append(messagePattern); // 添加剩余部分
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public static class ReturnData {
|
||||
private Integer type;
|
||||
private String message;
|
||||
|
||||
public ReturnData(Integer type, String message, Object... arguments) {
|
||||
this.type = type;
|
||||
this.message = formatMessage(message, arguments);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package com.cool.core.file;
|
||||
|
||||
import static com.cool.core.plugin.consts.PluginConsts.uploadHook;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.cool.core.exception.CoolPreconditions;
|
||||
import com.cool.core.file.strategy.FileUploadStrategy;
|
||||
import com.cool.core.plugin.service.CoolPluginService;
|
||||
import com.cool.modules.plugin.entity.PluginInfoEntity;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FileUploadStrategyFactory {
|
||||
|
||||
final private ApplicationContext applicationContext;
|
||||
|
||||
final private CoolPluginService coolPluginService;
|
||||
|
||||
private FileUploadStrategy getStrategy(PluginInfoEntity pluginInfoEntity) {
|
||||
if (ObjUtil.isEmpty(pluginInfoEntity)) {
|
||||
return applicationContext.getBean("localFileUploadStrategy", FileUploadStrategy.class);
|
||||
}
|
||||
return applicationContext.getBean("cloudFileUploadStrategy", FileUploadStrategy.class);
|
||||
}
|
||||
|
||||
public Object upload(MultipartFile[] files, HttpServletRequest request) {
|
||||
PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook(uploadHook);
|
||||
try {
|
||||
return getStrategy(pluginInfoEntity).upload(files, request, pluginInfoEntity);
|
||||
} catch (IOException e) {
|
||||
log.error("上传文件失败", e);
|
||||
CoolPreconditions.alwaysThrow("上传文件失败 {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object getMode() {
|
||||
PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook(uploadHook);
|
||||
String key = null;
|
||||
if (ObjUtil.isNotEmpty(pluginInfoEntity)) {
|
||||
key = pluginInfoEntity.getKey();
|
||||
}
|
||||
return getStrategy(pluginInfoEntity).getMode(key);
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.cool.core.file;
|
||||
|
||||
import com.cool.core.config.FileModeEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 上传模式类型
|
||||
*/
|
||||
@Data
|
||||
public class UpLoadModeType {
|
||||
|
||||
/**
|
||||
* 模式
|
||||
*/
|
||||
private FileModeEnum mode;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private String type;
|
||||
|
||||
public UpLoadModeType(FileModeEnum mode) {
|
||||
this.mode = mode;
|
||||
this.type = mode.type();
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.cool.core.file.strategy;
|
||||
|
||||
import com.cool.core.config.FileModeEnum;
|
||||
import com.cool.core.util.CoolPluginInvokers;
|
||||
import com.cool.modules.plugin.entity.PluginInfoEntity;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Component("cloudFileUploadStrategy")
|
||||
public class CloudFileUploadStrategy implements FileUploadStrategy {
|
||||
|
||||
@Override
|
||||
public Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity)
|
||||
throws IOException {
|
||||
return CoolPluginInvokers.invokePlugin(pluginInfoEntity.getKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getMode(String key) {
|
||||
try{
|
||||
Object mode = CoolPluginInvokers.invoke(key, "getMode");
|
||||
if (Objects.nonNull(mode)) {
|
||||
return (Map) mode;
|
||||
}
|
||||
} catch (Exception ignore){}
|
||||
return Map.of("mode", FileModeEnum.CLOUD.value(),
|
||||
"type", FileModeEnum.CLOUD.type());
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package com.cool.core.file.strategy;
|
||||
|
||||
import com.cool.modules.plugin.entity.PluginInfoEntity;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface FileUploadStrategy {
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* 文件上传模式
|
||||
*
|
||||
* @return 上传模式
|
||||
*/
|
||||
Map<String, String> getMode(String key);
|
||||
|
||||
default boolean isAbsolutePath(String pathStr) {
|
||||
Path path = Paths.get(pathStr);
|
||||
return path.isAbsolute();
|
||||
}
|
||||
|
||||
default String getExtensionName(String fileName) {
|
||||
if (fileName.contains(".")) {
|
||||
String[] names = fileName.split("[.]");
|
||||
return "." + names[names.length - 1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
package com.cool.core.file.strategy;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.cool.core.config.FileModeEnum;
|
||||
import com.cool.core.config.LocalFileProperties;
|
||||
import com.cool.core.exception.CoolException;
|
||||
import com.cool.core.exception.CoolPreconditions;
|
||||
import com.cool.modules.plugin.entity.PluginInfoEntity;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Component("localFileUploadStrategy")
|
||||
@RequiredArgsConstructor
|
||||
public class LocalFileUploadStrategy implements FileUploadStrategy {
|
||||
|
||||
final private LocalFileProperties localFileProperties;
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param files 上传的文件
|
||||
* @return 文件路径
|
||||
*/
|
||||
@Override
|
||||
public Object upload(MultipartFile[] files, HttpServletRequest request,
|
||||
PluginInfoEntity pluginInfoEntity) {
|
||||
CoolPreconditions.check(StrUtil.isEmpty(localFileProperties.getBaseUrl()),
|
||||
"filePath 或 baseUrl 未配置");
|
||||
try {
|
||||
List<String> fileUrls = new ArrayList<>();
|
||||
String baseUrl = localFileProperties.getBaseUrl();
|
||||
String date = DateUtil.format(new Date(),
|
||||
DatePattern.PURE_DATE_PATTERN);
|
||||
String absoluteUploadFolder = localFileProperties.getAbsoluteUploadFolder();
|
||||
String fullPath = absoluteUploadFolder + "/" + date;
|
||||
FileUtil.mkdir(fullPath);
|
||||
for (MultipartFile file : files) {
|
||||
// 保存文件
|
||||
String fileName = StrUtil.uuid().replaceAll("-", "") + getExtensionName(
|
||||
Objects.requireNonNull(file.getOriginalFilename()));
|
||||
file.transferTo(new File(fullPath
|
||||
+ "/" + fileName));
|
||||
fileUrls.add(baseUrl + "/" + date + "/" + fileName);
|
||||
}
|
||||
if (fileUrls.size() == 1) {
|
||||
return fileUrls.get(0);
|
||||
}
|
||||
return fileUrls;
|
||||
} catch (Exception e) {
|
||||
throw new CoolException("文件上传失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传模式
|
||||
*
|
||||
* @return 上传模式
|
||||
*/
|
||||
public Map<String, String> getMode(String key) {
|
||||
return Map.of("mode", FileModeEnum.LOCAL.value(),
|
||||
"type", FileModeEnum.LOCAL.type());
|
||||
}
|
||||
}
|
@@ -0,0 +1,219 @@
|
||||
package com.cool.core.i18n;
|
||||
|
||||
import static com.cool.core.util.I18nUtil.*;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.cool.core.lock.CoolLock;
|
||||
import com.cool.core.util.CoolPluginInvokers;
|
||||
import com.cool.core.util.I18nUtil;
|
||||
import com.cool.core.util.PathUtils;
|
||||
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
|
||||
import com.cool.modules.base.service.sys.BaseSysMenuService;
|
||||
import com.cool.modules.dict.entity.DictInfoEntity;
|
||||
import com.cool.modules.dict.entity.DictTypeEntity;
|
||||
import com.cool.modules.dict.service.DictInfoService;
|
||||
import com.cool.modules.dict.service.DictTypeService;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class I18nGenerator {
|
||||
private final BaseSysMenuService baseSysMenuService;
|
||||
private final DictTypeService dictTypeService;
|
||||
private final DictInfoService dictInfoService;
|
||||
private final CoolLock coolLock;
|
||||
private final I18nUtil i18nUtil;
|
||||
|
||||
private List<String> languages;
|
||||
private static final Duration DURATION = Duration.ofSeconds(30);
|
||||
public void run(Map<String, Object> map) {
|
||||
log.info("国际化 翻译...");
|
||||
languages = (List<String>) map.getOrDefault("languages", List.of("zh-cn", "zh-tw", "en"));
|
||||
path = (String) map.getOrDefault("path", "assets/i18n");
|
||||
init();
|
||||
log.info("✅国际化 翻译 成功!!!");
|
||||
enable = true;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
// 四个任务并发执行
|
||||
CompletableFuture<Void> futureMsg = CompletableFuture.runAsync(this::genBaseMsg);
|
||||
CompletableFuture<Void> futureMenu = CompletableFuture.runAsync(this::genBaseMenu);
|
||||
CompletableFuture<Void> futureDictInfo = CompletableFuture.runAsync(this::genBaseDictInfo);
|
||||
CompletableFuture<Void> futureDictType = CompletableFuture.runAsync(this::genBaseDictType);
|
||||
|
||||
// 等待全部执行完成
|
||||
CompletableFuture.allOf(futureMsg, futureMenu, futureDictInfo, futureDictType).join();
|
||||
}
|
||||
|
||||
private void genBaseMsg() {
|
||||
try {
|
||||
Map<String, String> msgMap = new HashMap<>();
|
||||
// 从idea本地启动时,从项目目录中读取
|
||||
Files.walk(Paths.get(System.getProperty("user.dir")))
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> path.toString().endsWith(".java"))
|
||||
.filter(path -> !path.toString().contains("/target/") && !path.toString().contains("/.git/"))
|
||||
.forEach(path -> msgMap.putAll(processFile(path)));
|
||||
if (ObjUtil.isNotEmpty(msgMap)) {
|
||||
// 系统异常信息,输出到resources/i18n 文件夹下,只有本地运行会生成
|
||||
File msgfile = FileUtil.file(PathUtils.getUserDir(),
|
||||
"src", "main", "resources", "cool", "i18n", "msg", "template.json");
|
||||
// 确保父目录存在
|
||||
FileUtil.mkParentDirs(msgfile);
|
||||
// 写入内容
|
||||
FileUtil.writeUtf8String(JSONUtil.toJsonStr(msgMap), msgfile);
|
||||
} else {
|
||||
try {
|
||||
// jar启动时,从jar包中读取
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
Resource resource = resolver.getResource("classpath:cool/i18n/msg/template.json");
|
||||
String content = FileUtil.readUtf8String(resource.getFile());
|
||||
msgMap.putAll(JSONUtil.toBean(content, Map.class));
|
||||
} catch (Exception e) {
|
||||
log.error("获取系统异常信息失败", e);
|
||||
}
|
||||
}
|
||||
extracted(MSG_PREFIX, msgMap);
|
||||
} catch (Exception e) {
|
||||
log.error("国际化系统异常信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成菜单信息国际化
|
||||
*/
|
||||
@Async
|
||||
public void asyncGenBaseMenu() {
|
||||
if (coolLock.tryLock(MENU_PREFIX, DURATION)) {
|
||||
genBaseMenu();
|
||||
coolLock.unlock(MENU_PREFIX);
|
||||
}
|
||||
}
|
||||
private void genBaseMenu() {
|
||||
try {
|
||||
Map<String, String> menuMap = baseSysMenuService.list(QueryWrapper.create().select(BaseSysMenuEntity::getName))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
BaseSysMenuEntity::getName,
|
||||
BaseSysMenuEntity::getName,
|
||||
(oldValue, newValue) -> oldValue
|
||||
));
|
||||
extracted(MENU_PREFIX, menuMap);
|
||||
} catch (Exception e) {
|
||||
log.error("国际化菜单信息失败", e);
|
||||
}
|
||||
}
|
||||
@Async
|
||||
public void asyncGenBaseDictType() {
|
||||
if (coolLock.tryLock(DICT_TYPE_PREFIX, DURATION)) {
|
||||
genBaseDictType();
|
||||
coolLock.unlock(DICT_TYPE_PREFIX);
|
||||
}
|
||||
}
|
||||
private void genBaseDictType() {
|
||||
try {
|
||||
Map<String, String> dataMap = dictTypeService.list(QueryWrapper.create().select(DictTypeEntity::getName))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
DictTypeEntity::getName,
|
||||
DictTypeEntity::getName,
|
||||
(oldValue, newValue) -> oldValue
|
||||
));
|
||||
extracted(DICT_TYPE_PREFIX, dataMap);
|
||||
} catch (Exception e) {
|
||||
log.error("国际化字段类型信息失败", e);
|
||||
}
|
||||
}
|
||||
@Async
|
||||
public void asyncGenBaseDictInfo() {
|
||||
if (coolLock.tryLock(DICT_INFO_PREFIX, DURATION)) {
|
||||
genBaseDictInfo();
|
||||
coolLock.unlock(DICT_INFO_PREFIX);
|
||||
}
|
||||
}
|
||||
private void genBaseDictInfo() {
|
||||
try {
|
||||
Map<String, String> dataMap = dictInfoService.list(QueryWrapper.create().select(DictInfoEntity::getName))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
DictInfoEntity::getName,
|
||||
DictInfoEntity::getName,
|
||||
(oldValue, newValue) -> oldValue
|
||||
));
|
||||
extracted(DICT_INFO_PREFIX, dataMap);
|
||||
} catch (Exception e) {
|
||||
log.error("国际化字段类型信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void extracted(String prefix, Map<String, String> dataMap) {
|
||||
languages.forEach(language -> {
|
||||
String key = prefix + language;
|
||||
if (!i18nUtil.exist(key)) {
|
||||
JSONObject jsonObject = invokeTranslate(dataMap, language);
|
||||
if (ObjUtil.isNotNull(jsonObject)) {
|
||||
i18nUtil.update(key, jsonObject);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private JSONObject invokeTranslate(Map<String, String> map, String language) {
|
||||
return (JSONObject) CoolPluginInvokers.invoke("i18n", "invokeTranslate", map, language);
|
||||
}
|
||||
|
||||
// 匹配 CoolPreconditions 抛异常语句中的中文字符串
|
||||
private static final Pattern EXCEPTION_PATTERN = Pattern.compile(
|
||||
"CoolPreconditions\\.(\\w+)\\s*\\([^;]*?\"([^\"]*[\u4e00-\u9fa5]+[^\"]*)\"", Pattern.MULTILINE
|
||||
);
|
||||
|
||||
private static Map<String, String> processFile(Path path) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
try {
|
||||
String content = Files.readString(path, StandardCharsets.UTF_8);
|
||||
// 去掉注释
|
||||
content = removeComments(content);
|
||||
|
||||
// 仅查找方法体内的 CoolPreconditions 调用
|
||||
Matcher matcher = EXCEPTION_PATTERN.matcher(content);
|
||||
while (matcher.find()) {
|
||||
String chineseText = matcher.group(2).trim();
|
||||
map.put(chineseText, chineseText);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// 移除注释(单行与多行)
|
||||
private static String removeComments(String code) {
|
||||
String noMultiLine = code.replaceAll("/\\*.*?\\*/", ""); // 多行注释
|
||||
return noMultiLine.replaceAll("//.*", ""); // 单行注释
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package com.cool.core.init;
|
||||
|
||||
import com.cool.core.plugin.service.CoolPluginService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 历史安装过的插件执行初始化
|
||||
**/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CoolPluginInit {
|
||||
|
||||
final private CoolPluginService coolPluginService;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void run() {
|
||||
coolPluginService.init();
|
||||
}
|
||||
}
|
@@ -0,0 +1,252 @@
|
||||
package com.cool.core.init;
|
||||
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.cool.core.base.service.MapperProviderService;
|
||||
import com.cool.core.mybatis.pg.PostgresSequenceSyncService;
|
||||
import com.cool.core.util.DatabaseDialectUtils;
|
||||
import com.cool.core.util.EntityUtils;
|
||||
import com.cool.modules.base.entity.sys.BaseSysConfEntity;
|
||||
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
|
||||
import com.cool.modules.base.service.sys.BaseSysConfService;
|
||||
import com.cool.modules.base.service.sys.BaseSysMenuService;
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 数据库初始数据初始化 在 classpath:cool/data/db 目录下创建.json文件 并定义表数据, 由该类统一执行初始化
|
||||
**/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DBFromJsonInit {
|
||||
|
||||
final private BaseSysConfService baseSysConfService;
|
||||
|
||||
final private BaseSysMenuService baseSysMenuService;
|
||||
|
||||
final private MapperProviderService mapperProviderService;
|
||||
|
||||
final private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
final private PostgresSequenceSyncService postgresSequenceSyncService;
|
||||
|
||||
@Value("${cool.initData}")
|
||||
private boolean initData;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void run() {
|
||||
if (!initData) {
|
||||
return;
|
||||
}
|
||||
// 初始化自定义的数据
|
||||
boolean initFlag = extractedDb();
|
||||
// 初始化菜单数据
|
||||
initFlag = extractedMenu() || initFlag;
|
||||
// 发送数据库初始化完成事件
|
||||
eventPublisher.publishEvent(new DbInitCompleteEvent(this));
|
||||
if (initFlag) {
|
||||
// 如果是postgresql,同步序列
|
||||
syncIdentitySequences();
|
||||
}
|
||||
log.info("数据初始化完成!");
|
||||
}
|
||||
|
||||
private void syncIdentitySequences() {
|
||||
if (DatabaseDialectUtils.isPostgresql()) {
|
||||
postgresSequenceSyncService.syncIdentitySequences();
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class DbInitCompleteEvent {
|
||||
private final Object source;
|
||||
|
||||
public DbInitCompleteEvent(Object source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析插入业务数据
|
||||
*/
|
||||
private boolean extractedDb() {
|
||||
try {
|
||||
// 加载 JSON 文件
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
Resource[] resources = resolver.getResources("classpath:cool/data/db/*.json");
|
||||
// 遍历所有.json文件
|
||||
return analysisResources(resources);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initialize data", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean analysisResources(Resource[] resources)
|
||||
throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
String prefix = "db_";
|
||||
boolean isInit = false;
|
||||
for (Resource resource : resources) {
|
||||
File resourceFile = new File(resource.getURL().getFile());
|
||||
String fileName = prefix + resourceFile.getName();
|
||||
String value = baseSysConfService.getValue(fileName);
|
||||
if (StrUtil.isNotEmpty(value)) {
|
||||
log.info("{} 业务数据已初始化过...", fileName);
|
||||
continue;
|
||||
}
|
||||
String jsonStr = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
|
||||
JSONObject jsonObject = JSONUtil.parseObj(jsonStr);
|
||||
// 遍历 JSON 文件中的数据
|
||||
analysisJson(jsonObject);
|
||||
|
||||
BaseSysConfEntity baseSysUserEntity = new BaseSysConfEntity();
|
||||
baseSysUserEntity.setCKey(fileName);
|
||||
baseSysUserEntity.setCValue("success");
|
||||
// 当前文件已加载
|
||||
baseSysConfService.add(baseSysUserEntity);
|
||||
isInit = true;
|
||||
log.info("{} 业务数据初始化成功...", fileName);
|
||||
}
|
||||
return isInit;
|
||||
}
|
||||
|
||||
private void analysisJson(JSONObject jsonObject)
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
Map<String, Class<?>> tableMap = EntityUtils.findTableMap();
|
||||
for (String tableName : jsonObject.keySet()) {
|
||||
JSONArray records = jsonObject.getJSONArray(tableName);
|
||||
// 根据表名生成实体类名和 Mapper 接口名
|
||||
Class<?> entityClass = tableMap.get(tableName);
|
||||
BaseMapper<?> baseMapper = mapperProviderService.getMapperByEntityClass(entityClass);
|
||||
// 插入
|
||||
insertList(baseMapper, entityClass, records);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入列表数据
|
||||
*/
|
||||
private void insertList(BaseMapper baseMapper, Class<?> entityClass,
|
||||
JSONArray records)
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
// 插入数据
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
JSONObject record = records.getJSONObject(i);
|
||||
Object entity = JSONUtil.toBean(record, entityClass);
|
||||
Method getIdMethod = entityClass.getMethod("getId");
|
||||
Object id = getIdMethod.invoke(entity);
|
||||
if (ObjUtil.isNotEmpty(id) && ObjUtil.isNotEmpty(
|
||||
baseMapper.selectOneById((Long) id))) {
|
||||
// 数据库已经有值了
|
||||
continue;
|
||||
}
|
||||
if (ObjUtil.isNotEmpty(id)) {
|
||||
// 带id插入
|
||||
baseMapper.insertSelectiveWithPk(entity);
|
||||
} else {
|
||||
baseMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析插入菜单数据
|
||||
*/
|
||||
public boolean extractedMenu() {
|
||||
boolean initFlag = false;
|
||||
try {
|
||||
String prefix = "menu_";
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
Resource[] resources = resolver.getResources("classpath:cool/data/menu/*.json");
|
||||
// 遍历所有.json文件
|
||||
for (Resource resource : resources) {
|
||||
File resourceFile = new File(resource.getURL().getFile());
|
||||
String fileName = prefix + resourceFile.getName();
|
||||
String value = baseSysConfService.getValue(fileName);
|
||||
if (StrUtil.isNotEmpty(value)) {
|
||||
log.info("{} 菜单数据已初始化过...", fileName);
|
||||
continue;
|
||||
}
|
||||
analysisResources(resource, fileName);
|
||||
initFlag = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initialize data", e);
|
||||
}
|
||||
return initFlag;
|
||||
}
|
||||
|
||||
private void analysisResources(Resource resource, String fileName) throws IOException {
|
||||
String jsonStr = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
|
||||
|
||||
// 使用 解析 JSON 字符串
|
||||
JSONArray jsonArray = JSONUtil.parseArray(jsonStr);
|
||||
|
||||
// 遍历 JSON 数组
|
||||
for (Object obj : jsonArray) {
|
||||
JSONObject jsonObj = (JSONObject) obj;
|
||||
// 将 JSON 对象转换为 Menu 对象
|
||||
parseMenu(jsonObj, null);
|
||||
}
|
||||
BaseSysConfEntity baseSysUserEntity = new BaseSysConfEntity();
|
||||
baseSysUserEntity.setCKey(fileName);
|
||||
baseSysUserEntity.setCValue("success");
|
||||
// 当前文件已加载
|
||||
baseSysConfService.add(baseSysUserEntity);
|
||||
log.info("{} 菜单数据初始化成功...", fileName);
|
||||
}
|
||||
|
||||
// 递归解析 JSON 对象为 Menu 对象
|
||||
private void parseMenu(JSONObject jsonObj, BaseSysMenuEntity parentMenuEntity) {
|
||||
BaseSysMenuEntity menuEntity = BeanUtil.copyProperties(jsonObj, BaseSysMenuEntity.class);
|
||||
if (ObjUtil.isNotEmpty(parentMenuEntity)) {
|
||||
menuEntity.setParentName(parentMenuEntity.getName());
|
||||
menuEntity.setParentId(parentMenuEntity.getId());
|
||||
}
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(BaseSysMenuEntity::getName, menuEntity.getName());
|
||||
if (ObjUtil.isNull(menuEntity.getParentId())) {
|
||||
queryWrapper.isNull(BaseSysMenuEntity::getParentId);
|
||||
} else {
|
||||
queryWrapper.eq(BaseSysMenuEntity::getParentId, menuEntity.getParentId());
|
||||
}
|
||||
BaseSysMenuEntity dbBaseSysMenuEntity = baseSysMenuService.getOne(queryWrapper);
|
||||
if (ObjUtil.isNull(dbBaseSysMenuEntity)) {
|
||||
baseSysMenuService.add(menuEntity);
|
||||
} else {
|
||||
menuEntity = dbBaseSysMenuEntity;
|
||||
}
|
||||
// 递归处理子菜单
|
||||
JSONArray childMenus = jsonObj.getJSONArray("childMenus");
|
||||
if (childMenus != null) {
|
||||
for (Object obj : childMenus) {
|
||||
JSONObject childObj = (JSONObject) obj;
|
||||
parseMenu(childObj, menuEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package com.cool.core.init;
|
||||
|
||||
import com.cool.core.leaf.IDGenService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 唯一ID 组件初始化
|
||||
**/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IDGenInit {
|
||||
|
||||
final private IDGenService idGenService;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void run() {
|
||||
idGenService.init();
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.cool.core.leaf;
|
||||
|
||||
public interface IDGenService {
|
||||
long next(String key);
|
||||
void init();
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.cool.core.leaf.common;
|
||||
|
||||
public class CheckVO {
|
||||
private long timestamp;
|
||||
private int workID;
|
||||
|
||||
public CheckVO(long timestamp, int workID) {
|
||||
this.timestamp = timestamp;
|
||||
this.workID = workID;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public int getWorkID() {
|
||||
return workID;
|
||||
}
|
||||
|
||||
public void setWorkID(int workID) {
|
||||
this.workID = workID;
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.cool.core.leaf.common;
|
||||
|
||||
public class Result {
|
||||
private long id;
|
||||
private Status status;
|
||||
|
||||
public Result() {
|
||||
|
||||
}
|
||||
public Result(long id, Status status) {
|
||||
this.id = id;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("Result{");
|
||||
sb.append("id=").append(id);
|
||||
sb.append(", status=").append(status);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.cool.core.leaf.common;
|
||||
|
||||
public enum Status {
|
||||
SUCCESS,
|
||||
EXCEPTION
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 全局唯一id生成
|
||||
* 来源美团:https://github.com/Meituan-Dianping/Leaf
|
||||
*/
|
||||
package com.cool.core.leaf;
|
@@ -0,0 +1,310 @@
|
||||
package com.cool.core.leaf.segment;
|
||||
|
||||
import static com.cool.core.leaf.segment.entity.table.LeafAllocEntityTableDef.LEAF_ALLOC_ENTITY;
|
||||
|
||||
import com.cool.core.exception.CoolPreconditions;
|
||||
import com.cool.core.leaf.IDGenService;
|
||||
import com.cool.core.leaf.common.Result;
|
||||
import com.cool.core.leaf.common.Status;
|
||||
import com.cool.core.leaf.segment.entity.LeafAllocEntity;
|
||||
import com.cool.core.leaf.segment.mapper.LeafAllocMapper;
|
||||
import com.cool.core.leaf.segment.model.Segment;
|
||||
import com.cool.core.leaf.segment.model.SegmentBuffer;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.update.UpdateChain;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.perf4j.StopWatch;
|
||||
import org.perf4j.slf4j.Slf4JStopWatch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SegmentIDGenImpl implements IDGenService, DisposableBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SegmentIDGenImpl.class);
|
||||
|
||||
@Value("${leaf.segment.enable:false}")
|
||||
private boolean enable;
|
||||
|
||||
/**
|
||||
* IDCache未初始化成功时的异常码
|
||||
*/
|
||||
private static final long EXCEPTION_ID_IDCACHE_INIT_FALSE = -1;
|
||||
/**
|
||||
* key不存在时的异常码
|
||||
*/
|
||||
private static final long EXCEPTION_ID_KEY_NOT_EXISTS = -2;
|
||||
/**
|
||||
* SegmentBuffer中的两个Segment均未从DB中装载时的异常码
|
||||
*/
|
||||
private static final long EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL = -3;
|
||||
/**
|
||||
* 最大步长不超过100,0000
|
||||
*/
|
||||
private static final int MAX_STEP = 1000000;
|
||||
/**
|
||||
* 一个Segment维持时间为15分钟
|
||||
*/
|
||||
private static final long SEGMENT_DURATION = 15 * 60 * 1000L;
|
||||
private final ExecutorService executorService = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new UpdateThreadFactory());
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("check-idCache-thread");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
private volatile boolean initOK = false;
|
||||
private final Map<String, SegmentBuffer> cache = new ConcurrentHashMap<>();
|
||||
private final LeafAllocMapper leafAllocMapper;
|
||||
|
||||
public static class UpdateThreadFactory implements ThreadFactory {
|
||||
|
||||
private static int threadInitNumber = 0;
|
||||
|
||||
private static synchronized int nextThreadNum() {
|
||||
return threadInitNumber++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
return new Thread(r, "Thread-Segment-Update-" + nextThreadNum());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long next(String key) {
|
||||
Result result = get(key);
|
||||
CoolPreconditions.check(result.getId() < 0, "获取失败,code值: {}", result.getId());
|
||||
return result.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
if (enable) {
|
||||
// 确保加载到kv后才初始化成功
|
||||
updateCacheFromDb();
|
||||
initOK = true;
|
||||
updateCacheFromDbAtEveryMinute();
|
||||
logger.info("唯一ID组件初始化成功 ...");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCacheFromDbAtEveryMinute() {
|
||||
scheduledExecutorService.scheduleWithFixedDelay(this::updateCacheFromDb, 60, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void updateCacheFromDb() {
|
||||
logger.info("update cache from db");
|
||||
StopWatch sw = new Slf4JStopWatch();
|
||||
try {
|
||||
List<String> dbTags = leafAllocMapper.selectListByQuery(QueryWrapper.create().select(
|
||||
LeafAllocEntity::getKey)).stream().map(LeafAllocEntity::getKey).toList();
|
||||
if (dbTags.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> cacheTags = new ArrayList<String>(cache.keySet());
|
||||
Set<String> insertTagsSet = new HashSet<>(dbTags);
|
||||
Set<String> removeTagsSet = new HashSet<>(cacheTags);
|
||||
//db中新加的tags灌进cache
|
||||
for (String tmp : cacheTags) {
|
||||
insertTagsSet.remove(tmp);
|
||||
}
|
||||
for (String tag : insertTagsSet) {
|
||||
SegmentBuffer buffer = new SegmentBuffer();
|
||||
buffer.setKey(tag);
|
||||
Segment segment = buffer.getCurrent();
|
||||
segment.setValue(new AtomicLong(0));
|
||||
segment.setMax(0);
|
||||
segment.setStep(0);
|
||||
cache.put(tag, buffer);
|
||||
logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);
|
||||
}
|
||||
//cache中已失效的tags从cache删除
|
||||
for (String tmp : dbTags) {
|
||||
removeTagsSet.remove(tmp);
|
||||
}
|
||||
for (String tag : removeTagsSet) {
|
||||
cache.remove(tag);
|
||||
logger.info("Remove tag {} from IdCache", tag);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("update cache from db exception", e);
|
||||
} finally {
|
||||
sw.stop("updateCacheFromDb");
|
||||
}
|
||||
}
|
||||
|
||||
private Result get(final String key) {
|
||||
if (!initOK) {
|
||||
return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION);
|
||||
}
|
||||
CoolPreconditions.check(!initOK, "IDCache未初始化成功");
|
||||
if (cache.containsKey(key)) {
|
||||
SegmentBuffer buffer = cache.get(key);
|
||||
if (!buffer.isInitOk()) {
|
||||
synchronized (buffer) {
|
||||
if (!buffer.isInitOk()) {
|
||||
try {
|
||||
updateSegmentFromDb(key, buffer.getCurrent());
|
||||
logger.info("Init buffer. Update leafkey {} {} from db", key, buffer.getCurrent());
|
||||
buffer.setInitOk(true);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Init buffer {} exception", buffer.getCurrent(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return getIdFromSegmentBuffer(cache.get(key));
|
||||
}
|
||||
return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION);
|
||||
}
|
||||
|
||||
public void updateSegmentFromDb(String key, Segment segment) {
|
||||
StopWatch sw = new Slf4JStopWatch();
|
||||
SegmentBuffer buffer = segment.getBuffer();
|
||||
LeafAllocEntity leafAllocEntity;
|
||||
if (!buffer.isInitOk()) {
|
||||
leafAllocEntity = updateMaxIdAndGetLeafAlloc(key);
|
||||
buffer.setStep(leafAllocEntity.getStep());
|
||||
buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc中的step为DB中的step
|
||||
} else if (buffer.getUpdateTimestamp() == 0) {
|
||||
leafAllocEntity = updateMaxIdAndGetLeafAlloc(key);
|
||||
buffer.setUpdateTimestamp(System.currentTimeMillis());
|
||||
buffer.setStep(leafAllocEntity.getStep());
|
||||
buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc中的step为DB中的step
|
||||
} else {
|
||||
long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp();
|
||||
int nextStep = buffer.getStep();
|
||||
if (duration < SEGMENT_DURATION) {
|
||||
if (nextStep * 2 > MAX_STEP) {
|
||||
//do nothing
|
||||
} else {
|
||||
nextStep = nextStep * 2;
|
||||
}
|
||||
} else if (duration < SEGMENT_DURATION * 2) {
|
||||
//do nothing with nextStep
|
||||
} else {
|
||||
nextStep = nextStep / 2 >= buffer.getMinStep() ? nextStep / 2 : nextStep;
|
||||
}
|
||||
logger.info("leafKey[{}], step[{}], duration[{}mins], nextStep[{}]", key, buffer.getStep(), String.format("%.2f",((double)duration / (1000 * 60))), nextStep);
|
||||
LeafAllocEntity temp = new LeafAllocEntity();
|
||||
temp.setKey(key);
|
||||
temp.setStep(nextStep);
|
||||
leafAllocEntity = updateMaxIdByCustomStepAndGetLeafAlloc(temp);
|
||||
buffer.setUpdateTimestamp(System.currentTimeMillis());
|
||||
buffer.setStep(nextStep);
|
||||
buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc的step为DB中的step
|
||||
}
|
||||
// must set value before set max
|
||||
long value = leafAllocEntity.getMaxId() - buffer.getStep();
|
||||
segment.getValue().set(value);
|
||||
segment.setMax(leafAllocEntity.getMaxId());
|
||||
segment.setStep(buffer.getStep());
|
||||
sw.stop("updateSegmentFromDb", key + " " + segment);
|
||||
}
|
||||
|
||||
private LeafAllocEntity updateMaxIdByCustomStepAndGetLeafAlloc(LeafAllocEntity temp) {
|
||||
UpdateChain.of(LeafAllocEntity.class)
|
||||
.setRaw(LeafAllocEntity::getMaxId, LEAF_ALLOC_ENTITY.MAX_ID.getName() + " + " + temp.getStep())
|
||||
.where(LeafAllocEntity::getKey).eq(temp.getKey())
|
||||
.update();
|
||||
return leafAllocMapper.selectOneByQuery(QueryWrapper.create().select(
|
||||
LEAF_ALLOC_ENTITY.KEY, LEAF_ALLOC_ENTITY.MAX_ID, LEAF_ALLOC_ENTITY.STEP).eq(LeafAllocEntity::getKey, temp.getKey()));
|
||||
}
|
||||
|
||||
private LeafAllocEntity updateMaxIdAndGetLeafAlloc(String key) {
|
||||
UpdateChain.of(LeafAllocEntity.class)
|
||||
.setRaw(LeafAllocEntity::getMaxId, LEAF_ALLOC_ENTITY.MAX_ID.getName() + " + " + LEAF_ALLOC_ENTITY.STEP.getName())
|
||||
.where(LeafAllocEntity::getKey).eq(key)
|
||||
.update();
|
||||
return leafAllocMapper.selectOneByQuery(QueryWrapper.create().select(
|
||||
LEAF_ALLOC_ENTITY.KEY, LEAF_ALLOC_ENTITY.MAX_ID, LEAF_ALLOC_ENTITY.STEP).eq(LeafAllocEntity::getKey, key));
|
||||
}
|
||||
|
||||
public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {
|
||||
while (true) {
|
||||
buffer.rLock().lock();
|
||||
try {
|
||||
final Segment segment = buffer.getCurrent();
|
||||
if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {
|
||||
executorService.execute(() -> {
|
||||
Segment next = buffer.getSegments()[buffer.nextPos()];
|
||||
boolean updateOk = false;
|
||||
try {
|
||||
updateSegmentFromDb(buffer.getKey(), next);
|
||||
updateOk = true;
|
||||
logger.info("update segment {} from db {}", buffer.getKey(), next);
|
||||
} catch (Exception e) {
|
||||
logger.warn(buffer.getKey() + " updateSegmentFromDb exception", e);
|
||||
} finally {
|
||||
if (updateOk) {
|
||||
buffer.wLock().lock();
|
||||
buffer.setNextReady(true);
|
||||
buffer.getThreadRunning().set(false);
|
||||
buffer.wLock().unlock();
|
||||
} else {
|
||||
buffer.getThreadRunning().set(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
long value = segment.getValue().getAndIncrement();
|
||||
if (value < segment.getMax()) {
|
||||
return new Result(value, Status.SUCCESS);
|
||||
}
|
||||
} finally {
|
||||
buffer.rLock().unlock();
|
||||
}
|
||||
waitAndSleep(buffer);
|
||||
buffer.wLock().lock();
|
||||
try {
|
||||
final Segment segment = buffer.getCurrent();
|
||||
long value = segment.getValue().getAndIncrement();
|
||||
if (value < segment.getMax()) {
|
||||
return new Result(value, Status.SUCCESS);
|
||||
}
|
||||
if (buffer.isNextReady()) {
|
||||
buffer.switchPos();
|
||||
buffer.setNextReady(false);
|
||||
} else {
|
||||
logger.error("Both two segments in {} are not ready!", buffer);
|
||||
return new Result(EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL, Status.EXCEPTION);
|
||||
}
|
||||
} finally {
|
||||
buffer.wLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void waitAndSleep(SegmentBuffer buffer) {
|
||||
int roll = 0;
|
||||
while (buffer.getThreadRunning().get()) {
|
||||
roll += 1;
|
||||
if(roll > 10000) {
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(10);
|
||||
break;
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Thread {} Interrupted",Thread.currentThread().getName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
executorService.shutdown();
|
||||
executorService.awaitTermination(10, TimeUnit.SECONDS);
|
||||
|
||||
scheduledExecutorService.shutdown();
|
||||
scheduledExecutorService.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.cool.core.leaf.segment.entity;
|
||||
|
||||
import com.cool.core.base.BaseEntity;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
|
||||
import com.tangzc.mybatisflex.autotable.annotation.UniIndex;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(value = "leaf_alloc", comment = "唯一id分配")
|
||||
public class LeafAllocEntity extends BaseEntity<LeafAllocEntity> {
|
||||
|
||||
@UniIndex(name = "uk_key")
|
||||
@ColumnDefine(comment = "业务key ,比如orderId", length = 20, notNull = true)
|
||||
private String key;
|
||||
|
||||
@ColumnDefine(comment = "当前最大id", defaultValue = "1", notNull = true)
|
||||
private Long maxId;
|
||||
|
||||
@ColumnDefine(comment = "步长", defaultValue = "500", notNull = true)
|
||||
private Integer step;
|
||||
|
||||
@ColumnDefine(comment = "描述")
|
||||
private String description;
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.cool.core.leaf.segment.mapper;
|
||||
|
||||
import com.cool.core.leaf.segment.entity.LeafAllocEntity;
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
|
||||
public interface LeafAllocMapper extends BaseMapper<LeafAllocEntity> {
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.cool.core.leaf.segment.model;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Segment {
|
||||
private AtomicLong value = new AtomicLong(0);
|
||||
private volatile long max;
|
||||
private volatile int step;
|
||||
private SegmentBuffer buffer;
|
||||
|
||||
public Segment(SegmentBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
public AtomicLong getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(AtomicLong value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public long getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
public void setMax(long max) {
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public int getStep() {
|
||||
return step;
|
||||
}
|
||||
|
||||
public void setStep(int step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
public SegmentBuffer getBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public long getIdle() {
|
||||
return this.getMax() - getValue().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("Segment(");
|
||||
sb.append("value:");
|
||||
sb.append(value);
|
||||
sb.append(",max:");
|
||||
sb.append(max);
|
||||
sb.append(",step:");
|
||||
sb.append(step);
|
||||
sb.append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
package com.cool.core.leaf.segment.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/**
|
||||
* 双buffer
|
||||
*/
|
||||
public class SegmentBuffer {
|
||||
private String key;
|
||||
private Segment[] segments; //双buffer
|
||||
private volatile int currentPos; //当前的使用的segment的index
|
||||
private volatile boolean nextReady; //下一个segment是否处于可切换状态
|
||||
private volatile boolean initOk; //是否初始化完成
|
||||
private final AtomicBoolean threadRunning; //线程是否在运行中
|
||||
private final ReadWriteLock lock;
|
||||
|
||||
private volatile int step;
|
||||
private volatile int minStep;
|
||||
private volatile long updateTimestamp;
|
||||
|
||||
public SegmentBuffer() {
|
||||
segments = new Segment[]{new Segment(this), new Segment(this)};
|
||||
currentPos = 0;
|
||||
nextReady = false;
|
||||
initOk = false;
|
||||
threadRunning = new AtomicBoolean(false);
|
||||
lock = new ReentrantReadWriteLock();
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Segment[] getSegments() {
|
||||
return segments;
|
||||
}
|
||||
|
||||
public Segment getCurrent() {
|
||||
return segments[currentPos];
|
||||
}
|
||||
|
||||
public int getCurrentPos() {
|
||||
return currentPos;
|
||||
}
|
||||
|
||||
public int nextPos() {
|
||||
return (currentPos + 1) % 2;
|
||||
}
|
||||
|
||||
public void switchPos() {
|
||||
currentPos = nextPos();
|
||||
}
|
||||
|
||||
public boolean isInitOk() {
|
||||
return initOk;
|
||||
}
|
||||
|
||||
public void setInitOk(boolean initOk) {
|
||||
this.initOk = initOk;
|
||||
}
|
||||
|
||||
public boolean isNextReady() {
|
||||
return nextReady;
|
||||
}
|
||||
|
||||
public void setNextReady(boolean nextReady) {
|
||||
this.nextReady = nextReady;
|
||||
}
|
||||
|
||||
public AtomicBoolean getThreadRunning() {
|
||||
return threadRunning;
|
||||
}
|
||||
|
||||
public Lock rLock() {
|
||||
return lock.readLock();
|
||||
}
|
||||
|
||||
public Lock wLock() {
|
||||
return lock.writeLock();
|
||||
}
|
||||
|
||||
public int getStep() {
|
||||
return step;
|
||||
}
|
||||
|
||||
public void setStep(int step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
public int getMinStep() {
|
||||
return minStep;
|
||||
}
|
||||
|
||||
public void setMinStep(int minStep) {
|
||||
this.minStep = minStep;
|
||||
}
|
||||
|
||||
public long getUpdateTimestamp() {
|
||||
return updateTimestamp;
|
||||
}
|
||||
|
||||
public void setUpdateTimestamp(long updateTimestamp) {
|
||||
this.updateTimestamp = updateTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("SegmentBuffer{");
|
||||
sb.append("key='").append(key).append('\'');
|
||||
sb.append(", segments=").append(Arrays.toString(segments));
|
||||
sb.append(", currentPos=").append(currentPos);
|
||||
sb.append(", nextReady=").append(nextReady);
|
||||
sb.append(", initOk=").append(initOk);
|
||||
sb.append(", threadRunning=").append(threadRunning);
|
||||
sb.append(", step=").append(step);
|
||||
sb.append(", minStep=").append(minStep);
|
||||
sb.append(", updateTimestamp=").append(updateTimestamp);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
108
cool-admin-java/src/main/java/com/cool/core/lock/CoolLock.java
Normal file
108
cool-admin-java/src/main/java/com/cool/core/lock/CoolLock.java
Normal file
@@ -0,0 +1,108 @@
|
||||
package com.cool.core.lock;
|
||||
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheType;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CoolLock {
|
||||
// 缓存类型
|
||||
@Value("${spring.cache.type}")
|
||||
private String type;
|
||||
|
||||
@Value("${cool.cacheName}")
|
||||
private String cacheName;
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
private RedisCacheWriter redisCache ;
|
||||
|
||||
// 非redis方式时使用
|
||||
private static final Map<String, Lock> lockMap = new ConcurrentHashMap<>();
|
||||
|
||||
private static final String LOCK_PREFIX = "lock:";
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
this.type = type.toLowerCase();
|
||||
if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
|
||||
redisCache = (RedisCacheWriter) Objects.requireNonNull(cacheManager.getCache(cacheName))
|
||||
.getNativeCache();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 尝试获取锁
|
||||
*
|
||||
* @param key 锁的 key
|
||||
* @param expireTime 锁的过期时间
|
||||
* @return 如果成功获取锁则返回 true,否则返回 false
|
||||
*/
|
||||
public boolean tryLock(String key, Duration expireTime) {
|
||||
String lockKey = getLockKey(key);
|
||||
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
|
||||
Lock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
|
||||
return lock.tryLock();
|
||||
}
|
||||
byte[] lockKeyBytes = lockKey.getBytes();
|
||||
// 使用 putIfAbsent 来尝试设置锁,如果成功返回 true,否则返回 false
|
||||
return redisCache.putIfAbsent(cacheName, lockKeyBytes, new byte[0], expireTime) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*/
|
||||
public void unlock(String key) {
|
||||
String lockKey = getLockKey(key);
|
||||
if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) {
|
||||
Lock lock = lockMap.get(lockKey);
|
||||
if (lock != null && lock.tryLock()) {
|
||||
lock.unlock();
|
||||
lockMap.remove(lockKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
redisCache.remove(cacheName, lockKey.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接锁前缀
|
||||
*/
|
||||
private String getLockKey(String key) {
|
||||
return LOCK_PREFIX + key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待锁
|
||||
*
|
||||
* @param key 锁的 key
|
||||
* @param expireTime 锁的过期时间
|
||||
* @return 如果成功获取锁则返回 true,否则返回 false
|
||||
*/
|
||||
public boolean waitForLock(String key, Duration expireTime, Duration waitTime) {
|
||||
long endTime = System.currentTimeMillis() + waitTime.toMillis();
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
if (tryLock(key, expireTime)) {
|
||||
return true;
|
||||
}
|
||||
// 等待锁释放
|
||||
try {
|
||||
Thread.sleep(100); // 可以根据需要调整等待时间
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package com.cool.core.mybatis.handler;
|
||||
|
||||
import com.cool.core.util.DatabaseDialectUtils;
|
||||
import com.mybatisflex.core.util.StringUtil;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.postgresql.util.PGobject;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
public abstract class BaseJsonTypeHandler<T> extends BaseTypeHandler<T> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
|
||||
if (DatabaseDialectUtils.isPostgresql()) {
|
||||
PGobject jsonObject = new PGobject();
|
||||
jsonObject.setType("json");
|
||||
jsonObject.setValue(toJson(parameter));
|
||||
ps.setObject(i, jsonObject);
|
||||
} else {
|
||||
ps.setString(i, toJson(parameter));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
final String json = rs.getString(columnName);
|
||||
return StringUtil.noText(json) ? null : parseJson(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
final String json = rs.getString(columnIndex);
|
||||
return StringUtil.noText(json) ? null : parseJson(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
final String json = cs.getString(columnIndex);
|
||||
return StringUtil.noText(json) ? null : parseJson(json);
|
||||
}
|
||||
|
||||
protected abstract T parseJson(String json);
|
||||
|
||||
protected abstract String toJson(T object);
|
||||
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
package com.cool.core.mybatis.handler;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONReader;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import com.alibaba.fastjson2.TypeReference;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collection;
|
||||
|
||||
public class Fastjson2TypeHandler extends BaseJsonTypeHandler<Object> {
|
||||
|
||||
private final Class<?> propertyType;
|
||||
private Class<?> genericType;
|
||||
private Type type;
|
||||
|
||||
private boolean supportAutoType = false;
|
||||
|
||||
public Fastjson2TypeHandler(Class<?> propertyType) {
|
||||
this.propertyType = propertyType;
|
||||
this.supportAutoType = propertyType.isInterface() || Modifier.isAbstract(propertyType.getModifiers());
|
||||
}
|
||||
|
||||
|
||||
public Fastjson2TypeHandler(Class<?> propertyType, Class<?> genericType) {
|
||||
this.propertyType = propertyType;
|
||||
this.genericType = genericType;
|
||||
this.type = TypeReference.collectionType((Class<? extends Collection>) propertyType, genericType);
|
||||
|
||||
Type actualTypeArgument = ((ParameterizedType) type).getActualTypeArguments()[0];
|
||||
if (actualTypeArgument instanceof Class) {
|
||||
this.supportAutoType = ((Class<?>) actualTypeArgument).isInterface()
|
||||
|| Modifier.isAbstract(((Class<?>) actualTypeArgument).getModifiers());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object parseJson(String json) {
|
||||
if (genericType != null && Collection.class.isAssignableFrom(propertyType)) {
|
||||
if (supportAutoType) {
|
||||
return JSON.parseArray(json, Object.class, JSONReader.Feature.SupportAutoType);
|
||||
} else {
|
||||
return JSON.parseObject(json, type);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (supportAutoType) {
|
||||
return JSON.parseObject(json, Object.class, JSONReader.Feature.SupportAutoType);
|
||||
} else {
|
||||
return JSON.parseObject(json, propertyType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toJson(Object object) {
|
||||
if (supportAutoType) {
|
||||
return JSON.toJSONString(object
|
||||
, JSONWriter.Feature.WriteMapNullValue
|
||||
, JSONWriter.Feature.WriteNullListAsEmpty
|
||||
, JSONWriter.Feature.WriteNullStringAsEmpty, JSONWriter.Feature.WriteClassName
|
||||
);
|
||||
} else {
|
||||
return JSON.toJSONString(object
|
||||
, JSONWriter.Feature.WriteMapNullValue
|
||||
, JSONWriter.Feature.WriteNullListAsEmpty
|
||||
, JSONWriter.Feature.WriteNullStringAsEmpty
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
package com.cool.core.mybatis.handler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.exception.FlexExceptions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
public class JacksonTypeHandler extends BaseJsonTypeHandler<Object> {
|
||||
|
||||
private static ObjectMapper objectMapper;
|
||||
private final Class<?> propertyType;
|
||||
private Class<?> genericType;
|
||||
private JavaType javaType;
|
||||
|
||||
public JacksonTypeHandler(Class<?> propertyType) {
|
||||
this.propertyType = propertyType;
|
||||
}
|
||||
|
||||
public JacksonTypeHandler(Class<?> propertyType, Class<?> genericType) {
|
||||
this.propertyType = propertyType;
|
||||
this.genericType = genericType;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object parseJson(String json) {
|
||||
try {
|
||||
if (genericType != null && Collection.class.isAssignableFrom(propertyType)) {
|
||||
return getObjectMapper().readValue(json, getJavaType());
|
||||
} else {
|
||||
return getObjectMapper().readValue(json, propertyType);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw FlexExceptions.wrap(e, "Can not parseJson by JacksonTypeHandler: " + json);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toJson(Object object) {
|
||||
try {
|
||||
return getObjectMapper().writeValueAsString(object);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw FlexExceptions.wrap(e, "Can not convert object to Json by JacksonTypeHandler: " + object);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public JavaType getJavaType() {
|
||||
if (javaType == null){
|
||||
javaType = getObjectMapper().getTypeFactory().constructCollectionType((Class<? extends Collection>) propertyType, genericType);
|
||||
}
|
||||
return javaType;
|
||||
}
|
||||
|
||||
public static ObjectMapper getObjectMapper() {
|
||||
if (null == objectMapper) {
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
public static void setObjectMapper(ObjectMapper objectMapper) {
|
||||
JacksonTypeHandler.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
package com.cool.core.mybatis.pg;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
/**
|
||||
* PostgreSQL Identity 序列同步服务
|
||||
* 解决PostgreSQL 默认的序列机制,序列会自动递增,当手动插入指定id时需调用同步接口,否则id会重复。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PostgresSequenceSyncService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public PostgresSequenceSyncService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public void syncIdentitySequences() {
|
||||
log.info("⏳ 开始同步 PostgreSQL Identity 序列...");
|
||||
|
||||
// 查询所有 identity 字段
|
||||
String identityColumnQuery = """
|
||||
SELECT table_schema, table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE is_identity = 'YES'
|
||||
AND table_schema = 'public'
|
||||
""";
|
||||
|
||||
List<Map<String, Object>> identityColumns = jdbcTemplate.queryForList(identityColumnQuery);
|
||||
|
||||
for (Map<String, Object> col : identityColumns) {
|
||||
String schema = (String) col.get("table_schema");
|
||||
String table = (String) col.get("table_name");
|
||||
String column = (String) col.get("column_name");
|
||||
|
||||
String fullTable = schema + "." + table;
|
||||
|
||||
// 获取对应的序列名
|
||||
String seqNameSql = "SELECT pg_get_serial_sequence(?, ?)";
|
||||
String seqName = jdbcTemplate.queryForObject(seqNameSql, String.class, fullTable, column);
|
||||
|
||||
if (seqName == null) {
|
||||
log.warn("⚠️ 无法获取序列:{}.{}", table, column);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取当前最大 ID
|
||||
Long maxId = jdbcTemplate.queryForObject(
|
||||
String.format("SELECT COALESCE(MAX(%s), 0) FROM %s", column, fullTable),
|
||||
Long.class
|
||||
);
|
||||
|
||||
if (maxId != null && maxId > 0) { // 正确的:setval 有返回值,必须用 queryForObject
|
||||
String setvalSql = "SELECT setval(?, ?)";
|
||||
Long newVal = jdbcTemplate.queryForObject(setvalSql, Long.class, seqName, maxId);
|
||||
log.info("✅ 同步序列 [{}] -> 当前最大 ID: {}", seqName, newVal);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("✅ PostgreSQL Identity 序列同步完成。");
|
||||
}
|
||||
}
|
@@ -0,0 +1,202 @@
|
||||
package com.cool.core.request;
|
||||
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.cool.core.enums.QueryModeEnum;
|
||||
import com.cool.core.util.ConvertUtil;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryTable;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* 查询构建器
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
@Data
|
||||
public class CrudOption<T> {
|
||||
|
||||
private QueryWrapper queryWrapper;
|
||||
private QueryColumn[] fieldEq;
|
||||
private QueryColumn[] keyWordLikeFields;
|
||||
private QueryColumn[] select;
|
||||
private JSONObject requestParams;
|
||||
|
||||
private QueryModeEnum queryModeEnum;
|
||||
|
||||
private Transform<Object> transform;
|
||||
|
||||
public interface Transform<B> {
|
||||
void apply(B obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* queryModeEnum 为 CUSTOM,可设置 默认为Map
|
||||
*/
|
||||
private Class<?> asType;
|
||||
|
||||
private Environment evn;
|
||||
|
||||
public CrudOption(JSONObject requestParams) {
|
||||
this.requestParams = requestParams;
|
||||
this.queryWrapper = QueryWrapper.create();
|
||||
this.evn = SpringUtil.getBean(Environment.class);
|
||||
queryModeEnum = QueryModeEnum.ENTITY;
|
||||
}
|
||||
|
||||
public QueryWrapper getQueryWrapper(Class<T> entityClass) {
|
||||
return build(this.queryWrapper, entityClass);
|
||||
}
|
||||
|
||||
public CrudOption<T> queryWrapper(QueryWrapper queryWrapper) {
|
||||
this.queryWrapper = queryWrapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按前端传上来的字段值做eq
|
||||
*/
|
||||
public CrudOption<T> fieldEq(QueryColumn... fields) {
|
||||
this.fieldEq = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按前端传上来的字段值做like
|
||||
*/
|
||||
public CrudOption<T> keyWordLikeFields(QueryColumn... fields) {
|
||||
this.keyWordLikeFields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要返回给前端的字段
|
||||
*/
|
||||
public CrudOption<T> select(QueryColumn... selects) {
|
||||
this.select = selects;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模式决定返回值
|
||||
* 目前有三种模式,按实体查询返回、关联查询返回(实体字段上加 @RelationOneToMany 等注解)、自定义返回结果
|
||||
*/
|
||||
public CrudOption<T> queryModeEnum(QueryModeEnum queryModeEnum) {
|
||||
this.queryModeEnum = queryModeEnum;
|
||||
if (ObjUtil.equal(queryModeEnum, QueryModeEnum.CUSTOM)
|
||||
&& ObjUtil.isEmpty(asType)) {
|
||||
asType = Map.class;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义返回结果对象类型
|
||||
*/
|
||||
public CrudOption<T> asType(Class<?> asType) {
|
||||
this.asType = asType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换参数,组装数据
|
||||
*/
|
||||
public CrudOption<T> transform(Transform<Object> transform) {
|
||||
this.transform = transform;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*
|
||||
* @return QueryWrapper
|
||||
*/
|
||||
private QueryWrapper build(QueryWrapper queryWrapper, Class<T> entityClass) {
|
||||
if (ObjectUtil.isNotEmpty(fieldEq)) {
|
||||
Arrays.stream(fieldEq).toList().forEach(filed -> {
|
||||
String filedName = StrUtil.toCamelCase(filed.getName());
|
||||
Object obj = requestParams.get(filedName);
|
||||
if (ObjUtil.isEmpty(obj)) {
|
||||
return;
|
||||
}
|
||||
if (obj instanceof JSONArray) {
|
||||
// 集合
|
||||
queryWrapper.and(filed.in(ConvertUtil.covertListByClass(filedName, (JSONArray)obj, entityClass).toArray()));
|
||||
} else {
|
||||
// 对象
|
||||
queryWrapper.and(filed.eq(ConvertUtil.convertByClass(filedName, obj, entityClass)));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(this.keyWordLikeFields)) {
|
||||
Object keyWord = requestParams.get("keyWord");
|
||||
if (ObjectUtil.isEmpty(keyWord)) {
|
||||
// // keyWord值为空,遍历keyWordLikeFields字段,根据queryColumn字段名构建查询条件
|
||||
for (QueryColumn queryColumn : keyWordLikeFields) {
|
||||
String fieldName = queryColumn.getName();
|
||||
String paramName = StrUtil.toCamelCase(fieldName);
|
||||
String paramValue = requestParams.getStr(paramName);
|
||||
if (ObjectUtil.isNotEmpty(paramValue)) {
|
||||
queryWrapper.and(queryColumn.like(paramValue));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// keyWord值非空,使用keyWord构建
|
||||
// 初始化一个空的 QueryCondition
|
||||
QueryCondition orCondition = null;
|
||||
for (QueryColumn queryColumn : keyWordLikeFields) {
|
||||
QueryCondition condition = queryColumn.like(keyWord);
|
||||
if (orCondition == null) {
|
||||
orCondition = condition;
|
||||
} else {
|
||||
orCondition = orCondition.or(condition);
|
||||
}
|
||||
}
|
||||
queryWrapper.and(orCondition);
|
||||
}
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(select)) {
|
||||
queryWrapper.select(select);
|
||||
}
|
||||
// 排序
|
||||
order(queryWrapper, entityClass);
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
private void order(QueryWrapper queryWrapper, Class<T> entityClass) {
|
||||
Table tableAnnotation = AnnotationUtil.getAnnotation(entityClass, Table.class);
|
||||
if (ObjectUtil.isEmpty(tableAnnotation)) {
|
||||
// 该对象没有@Table注解,非Entity对象
|
||||
return;
|
||||
}
|
||||
String tableAlias = "";
|
||||
List<QueryTable> queryTables = (List<QueryTable>) ReflectUtil.getFieldValue(queryWrapper, "queryTables");
|
||||
if (ObjectUtil.isNotEmpty(queryTables)) {
|
||||
// 取主表作为排序字段别名
|
||||
QueryTable queryTable = queryTables.get(0);
|
||||
tableAlias = queryTable.getName() + ".";
|
||||
}
|
||||
String order = requestParams.getStr("order",
|
||||
tableAnnotation.camelToUnderline() ? "create_time" : "createTime");
|
||||
String sort = requestParams.getStr("sort", "desc");
|
||||
if (StrUtil.isNotEmpty(order) && StrUtil.isNotEmpty(sort)) {
|
||||
queryWrapper.orderBy(
|
||||
tableAlias + (tableAnnotation.camelToUnderline() ? StrUtil.toUnderlineCase(order) : order),
|
||||
sort.equals("asc"));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.cool.core.request;
|
||||
|
||||
import java.util.List;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema( title = "分页数据模型" )
|
||||
public class PageResult<T> {
|
||||
@Schema( title = "分页数据" )
|
||||
private List<T> list;
|
||||
private Pagination pagination = new Pagination();
|
||||
|
||||
@Data
|
||||
public static class Pagination {
|
||||
@Schema( title = "页码" )
|
||||
private Long page;
|
||||
@Schema( title = "本页数量" )
|
||||
private Long size;
|
||||
@Schema( title = "总页数" )
|
||||
private Long total;
|
||||
}
|
||||
|
||||
static public <B> PageResult<B> of(Page<B> page ){
|
||||
PageResult<B> result = new PageResult<B>();
|
||||
result.setList(page.getRecords());
|
||||
result.pagination.setPage( page.getPageNumber() );
|
||||
result.pagination.setSize( page.getPageSize() );
|
||||
result.pagination.setTotal( page.getTotalRow() );
|
||||
return result;
|
||||
}
|
||||
}
|
74
cool-admin-java/src/main/java/com/cool/core/request/R.java
Normal file
74
cool-admin-java/src/main/java/com/cool/core/request/R.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.cool.core.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 返回信息
|
||||
*/
|
||||
@Schema(title = "响应数据结构")
|
||||
@Data
|
||||
public class R<T> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
@Schema(title = "编码:1000表示成功,其他值表示失败")
|
||||
private int code = 1000;
|
||||
|
||||
@Schema(title = "消息内容")
|
||||
private String message = "success";
|
||||
|
||||
@Schema(title = "响应数据")
|
||||
private T data;
|
||||
|
||||
public R() {
|
||||
|
||||
}
|
||||
|
||||
public R( int code, String message, T data ) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static R error() {
|
||||
return error(1001, "请求方式不正确或服务出现异常");
|
||||
}
|
||||
|
||||
public static R error(String msg) {
|
||||
return error(1001, msg);
|
||||
}
|
||||
|
||||
public static R error(int code, String msg) {
|
||||
R r = new R();
|
||||
r.code = code;
|
||||
r.message = msg;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R okMsg(String msg) {
|
||||
R r = new R();
|
||||
r.message = msg;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R ok() {
|
||||
return new R();
|
||||
}
|
||||
|
||||
public static <B> R<B> ok(B data) {
|
||||
return new R<B>(1000 , "success", data);
|
||||
}
|
||||
|
||||
|
||||
public R<T> put(String key, Object value) {
|
||||
switch (key) {
|
||||
case "code" -> this.code = (int) value;
|
||||
case "message" -> this.message = (String) value;
|
||||
case "data" -> this.data = (T) value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
package com.cool.core.request;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import com.cool.core.enums.UserTypeEnum;
|
||||
import com.cool.core.util.BodyReaderHttpServletRequestWrapper;
|
||||
import com.cool.core.util.CoolSecurityUtil;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 封装请求参数 URL参数 和 body JSON 到同一个 JSONObject 方便读取
|
||||
*/
|
||||
@Component
|
||||
@Order(2)
|
||||
public class RequestParamsFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
Filter.super.init(filterConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
// 防止流读取一次后就没有了, 所以需要将流继续写出去
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
JSONObject requestParams = new JSONObject();
|
||||
String language = request.getHeader("language");
|
||||
String coolEid = request.getHeader("cool-admin-eid");
|
||||
Long tenantId = StrUtil.isEmpty(coolEid) ? null : Long.parseLong(coolEid);
|
||||
if (StrUtil.isNotEmpty(request.getContentType()) && request.getContentType().contains("multipart/form-data")) {
|
||||
servletRequest.setAttribute("requestParams", requestParams);
|
||||
servletRequest.setAttribute("cool-language", language);
|
||||
servletRequest.setAttribute("tenantId", tenantId);
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
} else {
|
||||
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||
String body = requestWrapper.getBodyString(requestWrapper);
|
||||
if (StrUtil.isNotEmpty(body) && JSONUtil.isTypeJSON(body) && !JSONUtil.isTypeJSONArray(
|
||||
body)) {
|
||||
requestParams = JSONUtil.parseObj(body);
|
||||
}
|
||||
Object jwtObj = request.getAttribute("tokenInfo");
|
||||
if (jwtObj != null) {
|
||||
requestParams.set("tokenInfo", ((JWT) jwtObj).getPayload().getClaimsJson());
|
||||
}
|
||||
// 登录状态,设置用户id
|
||||
Long currTenantId = setUserId(requestParams);
|
||||
if (ObjUtil.isNotNull(currTenantId)) {
|
||||
tenantId = currTenantId;
|
||||
}
|
||||
requestWrapper.setAttribute("cool-language", language);
|
||||
request.setAttribute("tenantId", tenantId);
|
||||
requestParams.set("body", body);
|
||||
requestParams.putAll(getAllRequestParam(request));
|
||||
|
||||
requestWrapper.setAttribute("requestParams", requestParams);
|
||||
filterChain.doFilter(requestWrapper, servletResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private Long setUserId(JSONObject requestParams) {
|
||||
UserTypeEnum userTypeEnum = CoolSecurityUtil.getCurrentUserType();
|
||||
switch (userTypeEnum) {
|
||||
// 只有登录了,才有用户类型, 不然为 UNKNOWN 状态
|
||||
case ADMIN -> {
|
||||
// 管理后台由于之前已经有逻辑再了,怕会影响到,如果自己有传了值不覆盖
|
||||
Object o = requestParams.get("userId");
|
||||
if (ObjUtil.isNull(o)) {
|
||||
requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
|
||||
}
|
||||
}
|
||||
// app端,userId 为当前登录的用户id
|
||||
case APP -> requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
|
||||
}
|
||||
return CoolSecurityUtil.getTenantId(requestParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端请求参数中所有的信息
|
||||
*
|
||||
*/
|
||||
private Map<String, Object> getAllRequestParam(final HttpServletRequest request) {
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
Enumeration<?> temp = request.getParameterNames();
|
||||
if (null != temp) {
|
||||
while (temp.hasMoreElements()) {
|
||||
String en = (String) temp.nextElement();
|
||||
String value = request.getParameter(en);
|
||||
res.put(en, value);
|
||||
// 如果字段的值为空,判断若值为空,则删除这个字段>
|
||||
if (null == res.get(en) || "".equals(res.get(en))) {
|
||||
res.remove(en);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
Filter.super.destroy();
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.cool.core.request;
|
||||
|
||||
import com.cool.core.annotation.CoolRestController;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 通用方法rest接口
|
||||
*/
|
||||
@Component
|
||||
public class RestInterceptor implements HandlerInterceptor {
|
||||
private final static String[] rests = { "add", "delete", "update", "info", "list", "page" };
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 判断有无通用方法
|
||||
if (handler instanceof HandlerMethod) {
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
CoolRestController coolRestController = handlerMethod.getBeanType().getAnnotation(CoolRestController.class);
|
||||
if (null != coolRestController) {
|
||||
String[] urls = request.getRequestURI().split("/");
|
||||
String rest = urls[urls.length - 1];
|
||||
if (Arrays.asList(rests).contains(rest)) {
|
||||
if (!Arrays.asList(coolRestController.api()).contains(rest)) {
|
||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.cool.core.request.prefix;
|
||||
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
/**
|
||||
* 自定义路由规则
|
||||
*/
|
||||
@Component
|
||||
public class AutoPrefixConfiguration implements WebMvcRegistrations {
|
||||
|
||||
@Override
|
||||
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
|
||||
return new AutoPrefixUrlMapping();
|
||||
}
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
package com.cool.core.request.prefix;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.cool.core.annotation.CoolRestController;
|
||||
import com.cool.core.enums.Apis;
|
||||
import com.cool.core.util.ConvertUtil;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
/**
|
||||
* 自动配置模块的路由
|
||||
*/
|
||||
@Slf4j
|
||||
public class AutoPrefixUrlMapping extends RequestMappingHandlerMapping {
|
||||
|
||||
@Override
|
||||
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
|
||||
CoolRestController[] annotations = handlerType.getAnnotationsByType(CoolRestController.class);
|
||||
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
|
||||
String packageName = handlerType.getPackage().getName();
|
||||
if (info != null && annotations.length > 0 && annotations[0].value().length == 0
|
||||
&& packageName.contains("modules")) {
|
||||
if (!checkApis(annotations, info)) {
|
||||
return null;
|
||||
}
|
||||
String prefix = getPrefix(packageName);
|
||||
String cName = getCName(annotations[0].cname(), handlerType, prefix);
|
||||
info = info.mutate().paths(prefix + "/" + cName).build().combine(info);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置检查是否构建路由
|
||||
*
|
||||
* @param annotations 注解
|
||||
* @param info 路由信息
|
||||
* @return 是否需要构建路由
|
||||
*/
|
||||
private boolean checkApis(CoolRestController[] annotations, RequestMappingInfo info) {
|
||||
String[] apis = Apis.ALL_API;
|
||||
if (info.getPathPatternsCondition() == null) {
|
||||
return true;
|
||||
}
|
||||
List<String> setApis;
|
||||
if (ArrayUtil.isNotEmpty(annotations)) {
|
||||
CoolRestController coolRestController = annotations[0];
|
||||
setApis = CollUtil.toList(coolRestController.api());
|
||||
|
||||
Set<String> methodPaths = info.getPathPatternsCondition().getPatternValues();
|
||||
String methodPath = methodPaths.iterator().next().replace("/", "");
|
||||
if (!CollUtil.toList(apis).contains(methodPath)) {
|
||||
return true;
|
||||
} else {
|
||||
return setApis.contains(methodPath);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Controller名称构建路由地址
|
||||
*
|
||||
* @param handlerType 类
|
||||
* @param prefix 路由前缀
|
||||
* @return url地址
|
||||
*/
|
||||
private String getCName(String cname, Class<?> handlerType, String prefix) {
|
||||
if (ObjUtil.isNotEmpty(cname)) {
|
||||
return cname;
|
||||
}
|
||||
String name = handlerType.getName();
|
||||
String[] names = name.split("[.]");
|
||||
name = names[names.length - 1];
|
||||
return ConvertUtil.extractController2Path(ConvertUtil.pathToClassName(prefix), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建路由前缀
|
||||
*
|
||||
* @param packageName 包名
|
||||
* @return 返回路由前缀
|
||||
*/
|
||||
private String getPrefix(String packageName) {
|
||||
String dotPath = packageName.split("modules")[1]; // 将包路径中多于的部分截取掉
|
||||
String[] dotPaths = dotPath.replace(".controller", "").split("[.]");
|
||||
List<String> paths = CollUtil.toList(dotPaths);
|
||||
paths.removeIf(String::isEmpty);
|
||||
// 第一和第二位互换位置
|
||||
String p0 = paths.get(0);
|
||||
String p1 = paths.get(1);
|
||||
paths.set(0, p1);
|
||||
paths.set(1, p0);
|
||||
dotPath = "/" + CollUtil.join(paths, "/");
|
||||
return dotPath;
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package com.cool.core.security;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 自定401返回值
|
||||
*/
|
||||
@Component
|
||||
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
|
||||
throws IOException {
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json; charset=utf-8");
|
||||
response.getWriter().write(JSONUtil.toJsonStr(new HashMap<String, Object>() {
|
||||
{
|
||||
put("code", "401");
|
||||
put("message", "未登录");
|
||||
}
|
||||
}));
|
||||
response.setStatus(401);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.cool.core.security;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 忽略地址配置
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "ignored")
|
||||
public class IgnoredUrlsProperties {
|
||||
|
||||
// 忽略后台校验权限列表
|
||||
private List<String> adminAuthUrls = new ArrayList<>();
|
||||
|
||||
// 忽略记录请求日志列表
|
||||
private List<String> logUrls = new ArrayList<>();
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
package com.cool.core.security;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import com.cool.core.cache.CoolCache;
|
||||
import com.cool.core.enums.UserTypeEnum;
|
||||
import com.cool.core.security.jwt.JwtTokenUtil;
|
||||
import com.cool.core.security.jwt.JwtUser;
|
||||
import com.cool.core.util.PathUtils;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Token过滤器
|
||||
*/
|
||||
@Order(1)
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
|
||||
|
||||
final private JwtTokenUtil jwtTokenUtil;
|
||||
final private CoolCache coolCache;
|
||||
final private IgnoredUrlsProperties ignoredUrlsProperties;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
String requestURI = request.getRequestURI();
|
||||
if (PathUtils.isMatch(ignoredUrlsProperties.getAdminAuthUrls(), requestURI)) {
|
||||
// 请求路径在忽略后台鉴权url里支持通配符,放行
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
String authToken = request.getHeader("Authorization");
|
||||
if (!StrUtil.isEmpty(authToken)) {
|
||||
JWT jwt = jwtTokenUtil.getTokenInfo(authToken);
|
||||
|
||||
Object userType = jwt.getPayload("userType");
|
||||
if (Objects.equals(userType, UserTypeEnum.APP.name())) {
|
||||
// app
|
||||
handlerAppRequest(request, jwt, authToken);
|
||||
} else {
|
||||
// admin
|
||||
handlerAdminRequest(request, jwt, authToken);
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
/**
|
||||
* 处理app请求
|
||||
*/
|
||||
private void handlerAppRequest(HttpServletRequest request, JWT jwt, String authToken) {
|
||||
String userId = jwt.getPayload("userId").toString();
|
||||
if (ObjectUtil.isNotEmpty(userId)
|
||||
&& SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = coolCache.get("app:userDetails:" + userId,
|
||||
JwtUser.class);
|
||||
if (jwtTokenUtil.validateToken(authToken) && userDetails != null) {
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(
|
||||
new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
request.setAttribute("userId", jwt.getPayload("userId"));
|
||||
request.setAttribute("tokenInfo", jwt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后台请求
|
||||
*/
|
||||
private void handlerAdminRequest(HttpServletRequest request, JWT jwt, String authToken) {
|
||||
String username = jwt.getPayload("username").toString();
|
||||
if (username != null
|
||||
&& SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = coolCache.get("admin:userDetails:" + username,
|
||||
JwtUser.class);
|
||||
Integer passwordV = Convert.toInt(jwt.getPayload("passwordVersion"));
|
||||
Integer rv = coolCache.get("admin:passwordVersion:" + jwt.getPayload("userId"),
|
||||
Integer.class);
|
||||
if (jwtTokenUtil.validateToken(authToken, username) && Objects.equals(passwordV, rv)
|
||||
&& userDetails != null) {
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(
|
||||
new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
request.setAttribute("adminUsername", jwt.getPayload("username"));
|
||||
request.setAttribute("adminUserId", jwt.getPayload("userId"));
|
||||
request.setAttribute("tokenInfo", jwt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,152 @@
|
||||
package com.cool.core.security;
|
||||
|
||||
import com.cool.core.annotation.TokenIgnore;
|
||||
import com.cool.core.enums.UserTypeEnum;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JwtSecurityConfig {
|
||||
|
||||
// 用户详情
|
||||
final private UserDetailsService userDetailsService;
|
||||
final private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
|
||||
// 401
|
||||
final private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
|
||||
// 403
|
||||
final private RestAccessDeniedHandler restAccessDeniedHandler;
|
||||
// 忽略权限控制的地址
|
||||
final private IgnoredUrlsProperties ignoredUrlsProperties;
|
||||
|
||||
final private RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
|
||||
// 动态获取忽略的URL
|
||||
configureIgnoredUrls();
|
||||
|
||||
return httpSecurity
|
||||
.authorizeHttpRequests(
|
||||
conf -> {
|
||||
conf.requestMatchers(
|
||||
ignoredUrlsProperties.getAdminAuthUrls().toArray(String[]::new))
|
||||
.permitAll();
|
||||
conf.requestMatchers("/admin/**").authenticated();
|
||||
conf.requestMatchers("/app/**").hasRole(UserTypeEnum.APP.name());
|
||||
})
|
||||
.headers(config -> config.frameOptions(FrameOptionsConfig::disable))
|
||||
// 允许网页iframe
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.addFilterBefore(jwtAuthenticationTokenFilter,
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.exceptionHandling(config -> {
|
||||
config.authenticationEntryPoint(entryPointUnauthorizedHandler);
|
||||
config.accessDeniedHandler(restAccessDeniedHandler);
|
||||
}).build();
|
||||
}
|
||||
|
||||
private void configureIgnoredUrls() {
|
||||
Map<RequestMappingInfo, HandlerMethod> mappings = requestMappingHandlerMapping.getHandlerMethods();
|
||||
List<String> handlerCtr = new ArrayList<>();
|
||||
mappings.forEach((requestMappingInfo, handlerMethod) -> {
|
||||
Method method = handlerMethod.getMethod();
|
||||
TokenIgnore tokenIgnore = AnnotatedElementUtils.findMergedAnnotation(method, TokenIgnore.class);
|
||||
TokenIgnore tokenIgnoreCtr = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), TokenIgnore.class);
|
||||
if (!handlerCtr.contains(handlerMethod.getBeanType().getName()) && tokenIgnoreCtr != null) {
|
||||
requestMappingInfo.getPathPatternsCondition().getPatterns().forEach(pathPattern -> {
|
||||
String[] prefixs = pathPattern.getPatternString().split("/");
|
||||
// 去除最后一个路径
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (int i = 0; i < prefixs.length - 1; i++) {
|
||||
urls.add(prefixs[i]);
|
||||
}
|
||||
// 遍历 tokenIgnoreCtr.value()
|
||||
for (String path : tokenIgnoreCtr.value()) {
|
||||
ignoredUrlsProperties.getAdminAuthUrls().add(String.join("/", urls) + "/" + path);
|
||||
}
|
||||
if (tokenIgnoreCtr.value().length == 0) {
|
||||
// 通配
|
||||
ignoredUrlsProperties.getAdminAuthUrls().add(String.join("/", urls)+ "/**");
|
||||
}
|
||||
handlerCtr.add(handlerMethod.getBeanType().getName());
|
||||
});
|
||||
}
|
||||
if (tokenIgnore != null) {
|
||||
StringBuilder url = new StringBuilder();
|
||||
RequestMapping classRequestMapping = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequestMapping.class);
|
||||
if (classRequestMapping != null) {
|
||||
for (String path : classRequestMapping.value()) {
|
||||
url.append(path);
|
||||
}
|
||||
}
|
||||
if (requestMappingInfo.getPathPatternsCondition() == null) {
|
||||
return;
|
||||
}
|
||||
for (PathPattern path : requestMappingInfo.getPathPatternsCondition().getPatterns()) {
|
||||
url.append(path);
|
||||
}
|
||||
ignoredUrlsProperties.getAdminAuthUrls().add(url.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new PasswordEncoder() {
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
return DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
return encodedPassword.equals(
|
||||
DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
|
||||
throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package com.cool.core.security;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.AccessDecisionManager;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.FilterInvocation;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 权限管理决断器 判断用户拥有的权限或角色是否有资源访问权限
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MyAccessDecisionManager implements AccessDecisionManager {
|
||||
// 忽略权限控制的地址
|
||||
final private IgnoredUrlsProperties ignoredUrlsProperties;
|
||||
|
||||
@Override
|
||||
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes)
|
||||
throws AccessDeniedException, InsufficientAuthenticationException {
|
||||
if (configAttributes == null) {
|
||||
return;
|
||||
}
|
||||
List<String> urls = ignoredUrlsProperties.getAdminAuthUrls();
|
||||
String url = ((FilterInvocation) o).getRequestUrl().split("[?]")[0];
|
||||
if (urls.contains(url)) {
|
||||
return;
|
||||
}
|
||||
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ConfigAttribute c = iterator.next();
|
||||
String needPerm = c.getAttribute();
|
||||
for (GrantedAuthority ga : authentication.getAuthorities()) {
|
||||
// 匹配用户拥有的ga 和 系统中的needPerm
|
||||
if (needPerm.trim().equals(ga.getAuthority())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new AccessDeniedException("抱歉,您没有访问权限");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(ConfigAttribute configAttribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
package com.cool.core.security;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.SecurityMetadataSource;
|
||||
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
|
||||
import org.springframework.security.access.intercept.InterceptorStatusToken;
|
||||
import org.springframework.security.web.FilterInvocation;
|
||||
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 权限管理拦截器 监控用户行为
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
|
||||
|
||||
final private FilterInvocationSecurityMetadataSource securityMetadataSource;
|
||||
|
||||
@Resource
|
||||
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
|
||||
super.setAccessDecisionManager(myAccessDecisionManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
FilterInvocation fi = new FilterInvocation(request, response, chain);
|
||||
invoke(fi);
|
||||
}
|
||||
|
||||
public void invoke(FilterInvocation fi) throws IOException, ServletException {
|
||||
InterceptorStatusToken token = super.beforeInvocation(fi);
|
||||
try {
|
||||
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
|
||||
} finally {
|
||||
super.afterInvocation(token, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getSecureObjectClass() {
|
||||
return FilterInvocation.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecurityMetadataSource obtainSecurityMetadataSource() {
|
||||
return this.securityMetadataSource;
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package com.cool.core.security;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 自定403返回值
|
||||
*/
|
||||
@Component
|
||||
public class RestAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
|
||||
throws IOException {
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json; charset=utf-8");
|
||||
response.getWriter().write(JSONUtil.toJsonStr(new HashMap<String, Object>() {
|
||||
{
|
||||
put("code", "403");
|
||||
put("message", "无权限");
|
||||
}
|
||||
}));
|
||||
response.setStatus(403);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,165 @@
|
||||
package com.cool.core.security.jwt;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import cn.hutool.jwt.JWTValidator;
|
||||
import com.cool.core.config.CoolProperties;
|
||||
import com.cool.modules.base.service.sys.BaseSysConfService;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtTokenUtil implements Serializable {
|
||||
|
||||
final private CoolProperties coolProperties;
|
||||
final private BaseSysConfService baseSysConfService;
|
||||
final String tokenKey = "JWT_SECRET_TOKEN";
|
||||
final String refreshTokenKey = "JWT_SECRET_REFRESH_TOKEN";
|
||||
|
||||
public long getExpire() {
|
||||
return this.coolProperties.getToken().getExpire();
|
||||
}
|
||||
|
||||
public long getRefreshExpire() {
|
||||
return this.coolProperties.getToken().getRefreshExpire();
|
||||
}
|
||||
|
||||
public String getTokenSecret() {
|
||||
String secret = baseSysConfService.getValueWithCache(tokenKey);
|
||||
if (StrUtil.isBlank(secret)) {
|
||||
secret = StrUtil.uuid().replaceAll("-", "");
|
||||
baseSysConfService.setValue(tokenKey, secret);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
public String getRefreshTokenSecret() {
|
||||
String secret = baseSysConfService.getValueWithCache(refreshTokenKey);
|
||||
if (StrUtil.isBlank(secret)) {
|
||||
secret = StrUtil.uuid().replaceAll("-", "");
|
||||
baseSysConfService.setValue(refreshTokenKey, secret);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成令牌
|
||||
*
|
||||
* @param tokenInfo 保存的用户信息
|
||||
* @return 令牌
|
||||
*/
|
||||
public String generateToken(Map<String, Object> tokenInfo) {
|
||||
tokenInfo.put("isRefresh", false);
|
||||
Date expirationDate = new Date(System.currentTimeMillis() + getExpire() * 1000);
|
||||
JWT jwt = JWT.create().setExpiresAt(expirationDate).setKey(getTokenSecret().getBytes())
|
||||
.setPayload("created", new Date());
|
||||
tokenInfo.forEach(jwt::setPayload);
|
||||
return jwt.sign();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成令牌
|
||||
*
|
||||
* @param tokenInfo 保存的用户信息
|
||||
* @return 令牌
|
||||
*/
|
||||
public String generateRefreshToken(Map<String, Object> tokenInfo) {
|
||||
tokenInfo.put("isRefresh", true);
|
||||
Date expirationDate = new Date(System.currentTimeMillis() + getRefreshExpire() * 1000);
|
||||
JWT jwt = JWT.create().setExpiresAt(expirationDate).setKey(getRefreshTokenSecret().getBytes())
|
||||
.setPayload("created", new Date());
|
||||
tokenInfo.forEach(jwt::setPayload);
|
||||
return jwt.sign();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌中获取用户名
|
||||
*
|
||||
* @param token 令牌
|
||||
* @return 用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
JWT jwt = JWT.of(token);
|
||||
return jwt.getPayload("username").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得token信息
|
||||
*
|
||||
* @param token 令牌
|
||||
* @return token信息
|
||||
*/
|
||||
public JWT getTokenInfo(String token) {
|
||||
return JWT.of(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断令牌是否过期
|
||||
*
|
||||
* @param token 令牌
|
||||
* @return 是否过期
|
||||
*/
|
||||
public Boolean isTokenExpired(String token) {
|
||||
try {
|
||||
JWTValidator.of(token).validateDate(DateUtil.date());
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌
|
||||
*
|
||||
* @param token 令牌
|
||||
* @param username 用户
|
||||
* @return 是否有效
|
||||
*/
|
||||
public Boolean validateToken(String token, String username) {
|
||||
if (ObjectUtil.isEmpty(token)) {
|
||||
return false;
|
||||
}
|
||||
String tokenUsername = getUsernameFromToken(token);
|
||||
String secret = getTokenSecret();
|
||||
boolean isValidSignature = JWTUtil.verify(token, secret.getBytes());
|
||||
return (tokenUsername.equals(username) && !isTokenExpired(token) && isValidSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验token是否有效
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
public Boolean validateToken(String token) {
|
||||
if (ObjectUtil.isEmpty(token)) {
|
||||
return false;
|
||||
}
|
||||
String secret = getTokenSecret();
|
||||
boolean isValidSignature = JWTUtil.verify(token, secret.getBytes());
|
||||
return (!isTokenExpired(token) && isValidSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验refresh token是否有效
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
public Boolean validateRefreshToken(String token) {
|
||||
if (ObjectUtil.isEmpty(token)) {
|
||||
return false;
|
||||
}
|
||||
String secret = getRefreshTokenSecret();
|
||||
boolean isValidSignature = JWTUtil.verify(token, secret.getBytes());
|
||||
return (!isTokenExpired(token) && isValidSignature);
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package com.cool.core.security.jwt;
|
||||
|
||||
import com.cool.core.enums.UserTypeEnum;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* 后台用户信息
|
||||
*/
|
||||
@Data
|
||||
public class JwtUser implements UserDetails {
|
||||
|
||||
/******
|
||||
* 后台用户
|
||||
* ********/
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String password;
|
||||
private Boolean status;
|
||||
private UserTypeEnum userTypeEnum;
|
||||
private List<GrantedAuthority> perms;
|
||||
public JwtUser(Long userId, String username, String password, List<GrantedAuthority> perms, Boolean status) {
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.perms = perms;
|
||||
this.status = status;
|
||||
this.userTypeEnum = UserTypeEnum.ADMIN;
|
||||
}
|
||||
|
||||
/******
|
||||
* app用户
|
||||
* ********/
|
||||
public JwtUser(Long userId, List<GrantedAuthority> perms, Boolean status) {
|
||||
this.userId = userId;
|
||||
this.perms = perms;
|
||||
this.status = status;
|
||||
this.userTypeEnum = UserTypeEnum.APP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return perms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return status;
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.cool.core.tenant;
|
||||
|
||||
import com.cool.core.util.TenantUtil;
|
||||
import com.mybatisflex.core.tenant.TenantFactory;
|
||||
|
||||
public class CoolTenantFactory implements TenantFactory {
|
||||
public Object[] getTenantIds(){
|
||||
Long tenantId = TenantUtil.getTenantId();
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
return new Object[]{tenantId};
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
package com.cool.core.util;
|
||||
|
||||
import com.cool.core.annotation.CoolPlugin;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Modifier;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
public class AnnotationUtils {
|
||||
|
||||
/**
|
||||
* 判断一个类是否有 Spring 核心注解
|
||||
*
|
||||
* @param clazz 要检查的类
|
||||
* @return true 如果该类上添加了相应的 Spring 注解;否则返回 false
|
||||
*/
|
||||
public static boolean hasSpringAnnotation(Class<?> clazz) {
|
||||
if (clazz == null) {
|
||||
return false;
|
||||
}
|
||||
// 是否是接口
|
||||
if (clazz.isInterface()) {
|
||||
return false;
|
||||
}
|
||||
// 是否是抽象类
|
||||
if (Modifier.isAbstract(clazz.getModifiers())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (clazz.getAnnotation(Component.class) != null || clazz.getAnnotation(Repository.class) != null
|
||||
|| clazz.getAnnotation(Service.class) != null || clazz.getAnnotation(Controller.class) != null
|
||||
|| clazz.getAnnotation(Configuration.class) != null) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("出现异常:{}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件
|
||||
*/
|
||||
public static boolean hasCoolPluginAnnotation(Class<?> clazz) {
|
||||
if (clazz == null) {
|
||||
return false;
|
||||
}
|
||||
// 是否是接口
|
||||
if (clazz.isInterface()) {
|
||||
return false;
|
||||
}
|
||||
// 是否是抽象类
|
||||
if (Modifier.isAbstract(clazz.getModifiers())) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
|
||||
if (clazz.getAnnotation(
|
||||
(Class<? extends Annotation>) contextClassLoader.loadClass(CoolPlugin.class.getName())) != null) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("出现异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.cool.core.util;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class AutoTypeConverter {
|
||||
/**
|
||||
* 将字符串自动转换为数字或保留为字符串
|
||||
*
|
||||
* @param input 输入字符串
|
||||
* @return Integer / Long / String
|
||||
*/
|
||||
public static Serializable autoConvert(Object input) {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
if (NumberUtil.isInteger(input.toString())) {
|
||||
return Convert.convert(Integer.class, input);
|
||||
} else if (NumberUtil.isLong(input.toString())) {
|
||||
return Convert.convert(Long.class, input);
|
||||
} else {
|
||||
return (Serializable) input;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
package com.cool.core.util;
|
||||
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* 保存流
|
||||
*/
|
||||
@Slf4j
|
||||
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
String sessionStream = getBodyString(request);
|
||||
body = sessionStream.getBytes(Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求Body
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
public String getBodyString(final ServletRequest request) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
InputStream inputStream = null;
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
inputStream = cloneInputStream(request.getInputStream());
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
|
||||
String line = "";
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("err", e);
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
log.error("err", e);
|
||||
}
|
||||
}
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
log.error("err", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Description: 复制输入流</br>
|
||||
*
|
||||
* @param inputStream
|
||||
* @return</br>
|
||||
*/
|
||||
public InputStream cloneInputStream(ServletInputStream inputStream) {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
try {
|
||||
while ((len = inputStream.read(buffer)) > -1) {
|
||||
byteArrayOutputStream.write(buffer, 0, len);
|
||||
}
|
||||
byteArrayOutputStream.flush();
|
||||
} catch (IOException e) {
|
||||
log.error("err", e);
|
||||
}
|
||||
InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
|
||||
return byteArrayInputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
|
||||
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return bais.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,180 @@
|
||||
package com.cool.core.util;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import com.mybatisflex.processor.MybatisFlexProcessor;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import javax.annotation.processing.Processor;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
import javax.tools.StandardJavaFileManager;
|
||||
import javax.tools.StandardLocation;
|
||||
import javax.tools.ToolProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class CompilerUtils {
|
||||
|
||||
public final static String META_INF_VERSIONS = "META-INF/versions/";
|
||||
|
||||
// jdk版本
|
||||
private static String JVM_VERSION = null;
|
||||
|
||||
/**
|
||||
* 获取jdk版本
|
||||
*/
|
||||
public static String getJdkVersion() {
|
||||
if (JVM_VERSION == null) {
|
||||
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
|
||||
JVM_VERSION = runtimeMXBean.getSpecVersion();
|
||||
}
|
||||
return JVM_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件, 先删除在创建
|
||||
*/
|
||||
public static void createFile(String content, String filePathStr) {
|
||||
FileUtil.del(filePathStr);
|
||||
File file = FileUtil.touch(filePathStr);
|
||||
FileUtil.appendString(content, file, StandardCharsets.UTF_8.name());
|
||||
compileAndSave(filePathStr);
|
||||
}
|
||||
|
||||
public static String createMapper(String actModulePath, String fileName, String mapper) {
|
||||
String pathStr = actModulePath + File.separator + "mapper" + File.separator;
|
||||
String filePathStr = pathStr + fileName + "Mapper.java";
|
||||
createFile(mapper, filePathStr);
|
||||
return filePathStr;
|
||||
}
|
||||
|
||||
public static String createServiceImpl(String actModulePath, String fileName,
|
||||
String serviceImpl) {
|
||||
String pathStr = actModulePath + File.separator + "service" + File.separator + "impl" + File.separator;
|
||||
String filePathStr = pathStr + fileName + "ServiceImpl.java";
|
||||
createFile(serviceImpl, filePathStr);
|
||||
return filePathStr;
|
||||
}
|
||||
|
||||
public static String createService(String actModulePath, String fileName, String service) {
|
||||
String pathStr = actModulePath + File.separator + "service" + File.separator;
|
||||
String filePathStr = pathStr + fileName + "Service.java";
|
||||
createFile(service, filePathStr);
|
||||
return filePathStr;
|
||||
}
|
||||
|
||||
public static String createEntity(String actModulePath, String fileName, String entity) {
|
||||
String pathStr = actModulePath + File.separator + "entity" + File.separator;
|
||||
String filePathStr = pathStr + fileName + "Entity.java";
|
||||
createFile(entity, filePathStr);
|
||||
return filePathStr;
|
||||
}
|
||||
|
||||
public static String createController(String actModulePath, String fileName, String controller) {
|
||||
String pathStr = actModulePath + File.separator + "controller" + File.separator + "admin" + File.separator;
|
||||
String filePathStr = pathStr + "Admin" + fileName + "Controller.java";
|
||||
createFile(controller, filePathStr);
|
||||
return filePathStr;
|
||||
}
|
||||
|
||||
public static String createModule(String modulesPath, String module) {
|
||||
String pathStr = modulesPath + File.separator + module;
|
||||
PathUtils.noExistsMk(pathStr);
|
||||
return pathStr;
|
||||
}
|
||||
|
||||
public static boolean compileAndSave(String sourceFile) {
|
||||
// 获取系统 Java 编译器
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
|
||||
// 获取标准文件管理器
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
|
||||
|
||||
// 设置编译输出目录
|
||||
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(new File("target" + File.separator + "classes")));
|
||||
|
||||
// 获取源文件
|
||||
List<File> javaFiles = List.of(new File(sourceFile));
|
||||
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(javaFiles);
|
||||
|
||||
// 创建编译任务
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
|
||||
// 执行编译任务
|
||||
return task.call();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void compilerEntityTableDef(String actModulePath, String fileName, String entityPath, List<String> javaPathList) {
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
|
||||
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(
|
||||
entityPath);
|
||||
// 设置注解处理器
|
||||
Iterable<? extends Processor> processors = List.of(new MybatisFlexProcessor());
|
||||
// 添加 -proc:only 选项
|
||||
List<String> options = List.of("-proc:only");
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options,
|
||||
null, compilationUnits);
|
||||
task.setProcessors(processors);
|
||||
task.call();
|
||||
compilationUnits = fileManager.getJavaFileObjects(
|
||||
javaPathList.toArray(new String[0]));
|
||||
// 设置编译输出目录
|
||||
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(new File("target/classes")));
|
||||
|
||||
task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
|
||||
String pathStr = actModulePath + File.separator + "entity" + File.separator + "table" + File.separator;
|
||||
String filePathStr = pathStr + fileName + "EntityTableDef.java";
|
||||
// 需在entity之后加载
|
||||
javaPathList.add(1, filePathStr);
|
||||
boolean success = task.call();
|
||||
if (success) {
|
||||
System.out.println("Compilation and annotation processing completed successfully.");
|
||||
// 指定源文件夹和目标文件夹
|
||||
File sourceDir = new File("com");
|
||||
File destinationDir = new File(PathUtils.getTargetGeneratedAnnotations());
|
||||
// 确保目标文件夹存在
|
||||
destinationDir.mkdirs();
|
||||
// 移动源文件夹内容到目标文件夹
|
||||
if (sourceDir.exists()) {
|
||||
FileUtil.move(sourceDir, destinationDir, true);
|
||||
}
|
||||
if (countFiles(sourceDir) <= 1) {
|
||||
FileUtil.clean(sourceDir);
|
||||
FileUtil.del(sourceDir);
|
||||
}
|
||||
} else {
|
||||
System.out.println("Compilation and annotation processing failed.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("compilerEntityTableDefError", e);
|
||||
}
|
||||
}
|
||||
private static int countFiles(File directory) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (File file : files) {
|
||||
if (file.isFile()) {
|
||||
count++;
|
||||
} else if (file.isDirectory()) {
|
||||
count += countFiles(file);
|
||||
}
|
||||
// If more than one file is found, no need to continue counting
|
||||
if (count > 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user