This commit is contained in:
eibons
2025-08-15 20:58:57 +08:00
parent 694842a4a0
commit 49ac45ca05
730 changed files with 68015 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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-show.oss-cn-shanghai.aliyuncs.com/admin/flow.png)
## 地址
- 官网:[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-plushttps://gitee.com/hlc4417/cool-admin-java-plus
## 演示
[https://show.cool-admin.com](https://show.cool-admin.com)
- 账户admin
- 密码123456
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png)
#### 项目前端
系统是前后端分离的,启动完成后,还需要启动前端项目,前端项目地址:
[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
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/run.png)

View 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
View 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>

View 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();
}
}

View 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";
}
}

View File

@@ -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 "";
}

View File

@@ -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 "";
}

View File

@@ -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;
}

View File

@@ -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 "";
}

View File

@@ -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秒过期时间,单位毫秒
}

View File

@@ -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 {};
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
/**
* 适用于自定义返回值为 mapmap 的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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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) {
}
}

View File

@@ -0,0 +1,13 @@
package com.cool.core.base;
/**
* 修改枚举
*/
public enum ModifyEnum {
// 新增
ADD,
// 修改
UPDATE,
// 删除
DELETE
}

View File

@@ -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;
}

View File

@@ -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;
/**
* 初始化mapperMapkey 为entityClassvalue 为 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;
}
}

View 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));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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", "");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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()));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View 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);
}
}
}
}
}

View 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);
}
}

View File

@@ -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";
}

View 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 };
}

View File

@@ -0,0 +1,10 @@
package com.cool.core.enums;
/**
* 查询模式决定返回值
*/
public enum QueryModeEnum {
ENTITY, // 实体(默认)
ENTITY_WITH_RELATIONS, // 实体关联查询(如实体字段上加 @RelationOneToMany 等注解)
CUSTOM , // 自定义默认为Map
}

View File

@@ -0,0 +1,10 @@
package com.cool.core.enums;
/**
* 用户类型
*/
public enum UserTypeEnum {
ADMIN, // 后台
APP, // app
UNKNOWN, // 未知
}

View 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";
};
}
}

View File

@@ -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信息");
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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 "";
}
}

View File

@@ -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());
}
}

View File

@@ -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("//.*", ""); // 单行注释
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,6 @@
package com.cool.core.leaf;
public interface IDGenService {
long next(String key);
void init();
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,6 @@
package com.cool.core.leaf.common;
public enum Status {
SUCCESS,
EXCEPTION
}

View File

@@ -0,0 +1,5 @@
/**
* 全局唯一id生成
* 来源美团https://github.com/Meituan-Dianping/Leaf
*/
package com.cool.core.leaf;

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View 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;
}
}

View File

@@ -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);
}

View File

@@ -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
);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 序列同步完成。");
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<>();
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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) {
}
};
}
}

View File

@@ -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