Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b1c1045b | ||
|
|
458266297c | ||
|
|
4139d5bd7b | ||
|
|
8b09976db3 | ||
|
|
8e61b43c3f | ||
|
|
b2f2772a17 | ||
|
|
4314d3410f | ||
|
|
a2a07a2549 | ||
|
|
25c6b49775 | ||
|
|
ee9e51947e | ||
|
|
49e05081f9 | ||
|
|
9420a17bcc | ||
|
|
3b6a95c731 | ||
|
|
c59192c144 | ||
|
|
299bec29e0 | ||
|
|
86bb9c2f2a | ||
|
|
121b6653ab | ||
|
|
9fc3990411 | ||
|
|
7a53390946 | ||
|
|
2e8a930b16 | ||
|
|
444c288079 | ||
|
|
669c8261f5 | ||
|
|
2b752f56e1 | ||
|
|
9b8858999c | ||
|
|
548919485c | ||
|
|
464ee5c738 | ||
|
|
623599998f | ||
|
|
37ba418f87 | ||
|
|
affcfb9234 | ||
|
|
add6362fb8 | ||
|
|
ea85ff64e1 | ||
|
|
f5ff5e4f54 | ||
|
|
2183f2d2f6 | ||
|
|
4e477d86c3 | ||
|
|
284eb0394d | ||
|
|
8e56270cf5 | ||
|
|
6b0c4bea49 | ||
|
|
9edd1c3e00 | ||
|
|
3f0cf9af78 | ||
|
|
ad8636ced0 | ||
|
|
03486448f1 | ||
|
|
5925263443 | ||
|
|
dc7cc0bd79 | ||
|
|
a805e90911 | ||
|
|
9225973cf8 | ||
|
|
3243a4b4e3 | ||
|
|
fd45c67416 | ||
|
|
75025818f5 | ||
|
|
0e954b2d1d | ||
|
|
07c840a55b | ||
|
|
55b7ffcb70 | ||
|
|
4594373799 | ||
|
|
df38332a7f | ||
|
|
ab4aa2f493 | ||
|
|
863ca9120c | ||
|
|
ebeca48061 | ||
|
|
7c21c9ceb4 | ||
|
|
49393ec3aa | ||
|
|
2cf2f7be5c | ||
|
|
02b79a1060 | ||
|
|
4bfe209ec0 | ||
|
|
a222ecd50d | ||
|
|
bdc48ac634 | ||
|
|
3947e6eca3 | ||
|
|
b4dccc642c | ||
|
|
7747c1b4bc | ||
|
|
46a0398a98 | ||
|
|
c129fa6191 | ||
|
|
aebe1b8cbb | ||
|
|
91e8dcb740 | ||
|
|
c26a528e8c | ||
|
|
96fcf8d447 | ||
|
|
0773c9275c | ||
|
|
907765831a | ||
|
|
d99350bdfb | ||
|
|
79bfc632a8 |
No files matched your search
+1
-1
@@ -45,12 +45,12 @@ buildNumber.properties
|
||||
####################### svn ##############################
|
||||
.svn
|
||||
|
||||
|
||||
####################### eclipse ##############################
|
||||
.project
|
||||
.settings
|
||||
bin/
|
||||
logs/
|
||||
db/
|
||||
.classpath
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# AGENTS.md - Coding Guidelines for crabc-api
|
||||
|
||||
## Project Overview
|
||||
|
||||
A Java Spring Boot API development platform with multi-module Maven architecture for dynamic SQL-based API generation.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Compile all modules
|
||||
mvn clean compile
|
||||
|
||||
# Run tests (no tests currently exist)
|
||||
mvn test
|
||||
|
||||
# Run a single test class (when tests are added)
|
||||
mvn test -Dtest=ClassName
|
||||
|
||||
# Run a single test method
|
||||
mvn test -Dtest=ClassName#methodName
|
||||
|
||||
# Package application
|
||||
mvn clean package
|
||||
|
||||
# Package without tests
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# Run the application
|
||||
cd crabc-api/crabc-admin && mvn spring-boot:run
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cn.crabc
|
||||
├── crabc-api/ # API modules parent
|
||||
│ ├── crabc-admin/ # Boot module (port 9377)
|
||||
│ ├── crabc-core/ # Core business logic
|
||||
│ ├── crabc-datasource/ # Dynamic data sources
|
||||
│ └── crabc-spi/ # Plugin interfaces
|
||||
└── crabc-spring-boot-starter/ # Maven starter (optional)
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Java**: 17
|
||||
- **Spring Boot**: 3.5.9
|
||||
- **MyBatis**: 3.0.5 (with MyBatis-Plus 3.5.15)
|
||||
- **Database**: MySQL 8.0+, Oracle, PostgreSQL, SQL Server, TiDB
|
||||
- **Connection Pool**: Druid 1.2.27
|
||||
- **Cache**: Caffeine
|
||||
- **JSON**: Fastjson 2.0.49
|
||||
- **Auth**: JWT 0.12.5
|
||||
- **Utilities**: Lombok, Apache Commons
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Packages**: `cn.crabc.core.{module}.{layer}` (e.g., `cn.crabc.core.app.controller`)
|
||||
- **Classes**: PascalCase (e.g., `DataSourceController`, `BaseApiService`)
|
||||
- **Interfaces**: PascalCase with `I` prefix (e.g., `IBaseDataService`, `IApiMapper`)
|
||||
- **Methods**: camelCase (e.g., `getDataSourcePage`, `testConnection`)
|
||||
- **Variables**: camelCase (e.g., `dataSourceId`, `pageNum`)
|
||||
- **Constants**: UPPER_SNAKE_CASE (e.g., `CODE_TAG`, `MSG_TAG`)
|
||||
|
||||
### Code Formatting
|
||||
|
||||
- **Indentation**: 4 spaces (no tabs)
|
||||
- **Braces**: Allman style (opening brace on new line for class/method)
|
||||
- **Line endings**: LF
|
||||
- **Encoding**: UTF-8
|
||||
- **Max line length**: 120 characters
|
||||
|
||||
### Import Organization
|
||||
|
||||
1. `java.*` and `javax.*`
|
||||
2. Third-party libraries (Spring, MyBatis, etc.)
|
||||
3. Project internal imports
|
||||
4. Static imports (if any)
|
||||
|
||||
### Documentation
|
||||
|
||||
- All classes must have class-level Javadoc with:
|
||||
- Description in Chinese
|
||||
- `@author yuqf` tag
|
||||
- All public methods should have Javadoc with Chinese descriptions
|
||||
- Use block comments for complex logic sections
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
- Use `@Autowired` for field injection (existing pattern)
|
||||
- Prefer constructor injection for new code when possible
|
||||
|
||||
### REST API Controllers
|
||||
|
||||
- Use `@RestController` annotation
|
||||
- Base path pattern: `/api/box/{module}/{resource}`
|
||||
- Return `Result` wrapper for consistent API responses
|
||||
- HTTP methods:
|
||||
- `GET` for queries with `@GetMapping`
|
||||
- `POST` for create with `@PostMapping`
|
||||
- `PUT` for update with `@PutMapping`
|
||||
- `DELETE` for delete with `@DeleteMapping`
|
||||
|
||||
### Entities and DTOs
|
||||
|
||||
- Use Lombok `@Getter` and `@Setter` annotations
|
||||
- Include `serialVersionUID` for serializable classes
|
||||
- Use `Long` for primary keys
|
||||
- Database column names: use underscores (e.g., `api_id`)
|
||||
- Java field names: use camelCase (e.g., `apiId`)
|
||||
|
||||
### MyBatis Mappers
|
||||
|
||||
- Place XML files in `src/main/resources/mapper/`
|
||||
- Namespace must match mapper interface fully qualified name
|
||||
- Use `<if>` tags for dynamic SQL conditions
|
||||
- Parameter references: `#{fieldName}`
|
||||
- Use `resultType` for selects, `parameterType` for inserts/updates
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use `Result.error()` for business errors
|
||||
- Return HTTP 200 with error code in response body
|
||||
- Success code: `0`, Error code: `500` or custom
|
||||
|
||||
### Database Guidelines
|
||||
|
||||
- Support multiple database types (MySQL, Oracle, PostgreSQL, etc.)
|
||||
- Use parameterized queries (no string concatenation)
|
||||
- Use PageHelper for pagination
|
||||
|
||||
## Testing
|
||||
|
||||
- Framework: JUnit 4 (from dependencies)
|
||||
- Spring Boot Test for integration tests
|
||||
- Test classes should follow `*Test.java` or `Test*.java` naming
|
||||
- Place tests in `src/test/java` matching package structure
|
||||
|
||||
## Configuration
|
||||
|
||||
- Application config: `application.yml`
|
||||
- MyBatis config: `mybatis-config.xml`
|
||||
- Port: 9377 (development)
|
||||
- Banner mode: off (`banner-mode: off`)
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- This is a Chinese project with Chinese comments and documentation
|
||||
- Licensed under Apache License 2.0
|
||||
- No automated linting tools (checkstyle/spotbugs) currently configured
|
||||
- Focus on dynamic SQL execution and API generation capabilities
|
||||
+6
-7
@@ -1,11 +1,10 @@
|
||||
FROM openjdk:8-jdk-alpine
|
||||
MAINTAINER crabc <creabc@qq.com>
|
||||
FROM eclipse-temurin:21-jdk-alpine
|
||||
LABEL version="4.0.0"
|
||||
|
||||
ADD crabc-admin.jar /app.jar
|
||||
COPY crabc-admin.jar /app.jar
|
||||
ENV LANG="en_US.UTF-8"
|
||||
EXPOSE 9377
|
||||
ENV db_url ${db_url}
|
||||
ENV db_user ${db_user}
|
||||
ENV db_pwd ${db_pwd}
|
||||
|
||||
CMD java -jar /app.jar --spring.datasource.url=${db_url} --spring.datasource.username=${db_user} --spring.datasource.password=${db_pwd}
|
||||
ENV DB_URL=${db_url} DB_USER=${db_user} DB_PWD=${db_pwd}
|
||||
|
||||
CMD ["java", "-jar", "/app.jar", "--spring.datasource.url=${DB_URL}", "--spring.datasource.username=${DB_USER}", "--spring.datasource.password=${DB_PWD}"]
|
||||
@@ -1,18 +1,25 @@
|
||||
|
||||
## 介绍
|
||||
**Crabc** 是低代码接口开发平台,企业数据服务平台,深度整合SpringBoot和Mybatis实现动态数据源和动态SQL。
|
||||
支持接入(mysql、oracle、postgresql、sqlserver、达梦、TiDB、es和mongodb)等SQL或/NoSQL数据源,
|
||||
在线可视化编写SQL后即可快速生成接口对外提供服务,减少通用接口的SQL编写,让开发人员专注更复杂的业务逻辑实现。
|
||||
支持Mybatis中if等标签语法、数据脱敏、数据转换等功能,集成微服务网关支持接口代理转发、权限认证、限流、缓存、告警监控等一站式API数据服务。
|
||||
# 由来
|
||||
crabc由create + abc 组合而来,意在为项目系统构建的通用的接口底座。
|
||||
|
||||
# 介绍
|
||||
**crabc-api** 是接口快速开发平台,企业数据服务平台,sql2Api,深度整合SpringBoot和Mybatis实现动态数据源和动态SQL、函数和存储过程。
|
||||
支持接入(mysql、oracle、postgresql、sqlserver、达梦、TiDB、es和hive)等SQL或/NoSQL数据源,
|
||||
在线可视化编写SQL后即可快速生成接口对外提供服务,接口一键上下线。减少通用接口的SQL编写,让开发人员专注更复杂的业务逻辑实现。
|
||||
支持Mybatis中if等标签语法、数据脱敏、数据转换、国密加密、协同开发、接口编排等功能,可集成微服务网关支持接入第三方接口、权限认证、限流、缓存、告警监控和统一日志等一站式API数据服务,
|
||||
助力于企业数据资产价值升级,成为企业数字化转型的重要推动力。
|
||||
|
||||

|
||||
## 功能
|
||||
## 社区版功能
|
||||
- **工作台**:可视化编写SQL快速的对外发布成API接口,支持多SQL同时执行、Mybatis标签语法,实现动态SQL和动态标签
|
||||
- **数据源管理**:动态加载数据库,支持多种SQL/NoSQL数据库。
|
||||
- **应用列表**:Code认证、签名认证的应用创建管理以及对API调用在线授权。
|
||||
- **应用列表**:AppKey认证、签名认证的应用创建管理以及对API调用在线授权。
|
||||
- **接口列表**:查看开发中和已发布的API接口,可进行上下线管理,编辑升级等
|
||||
- **接口文档**:生成在线接口文档,支持在线测试。
|
||||
- **接口日志**:查看发布的接口被调用日志列表和请求详情。
|
||||
|
||||
## 接口开发流程
|
||||

|
||||
## 模块
|
||||
~~~
|
||||
cn.crabc
|
||||
@@ -27,9 +34,17 @@ cn.crabc
|
||||
├──pom.xml // 依赖
|
||||
~~~
|
||||
|
||||
## 数据库脚本
|
||||
数据库脚本文件可加QQ群文件中获取
|
||||
|
||||
## 版本说明
|
||||
dev分支框架SpringBoot已升级至3.x,jdk需17及以上版本 \
|
||||
2.x分支框架SpringBoot还是2.x
|
||||
dev分支框架SpringBoot已升级至3.5,jdk需17及以上版本 \
|
||||
jdk8分支框架SpringBoot是2.x + jdk8
|
||||
|
||||
## 企业版
|
||||
https://pingapi.cn
|
||||
|
||||
## 企业版架构图
|
||||
|
||||
## 运行启动
|
||||
```
|
||||
@@ -58,7 +73,7 @@ gitee: https://gitee.com/linebyte/crabc
|
||||
<dependency>
|
||||
<groupId>cn.crabc</groupId>
|
||||
<artifactId>crabc-spring-boot-starter</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<version>4.1.0</version>
|
||||
</dependency>
|
||||
```
|
||||
在程序启动类中添加下面注解
|
||||
@@ -69,68 +84,91 @@ gitee: https://gitee.com/linebyte/crabc
|
||||
|
||||
## Docker启动
|
||||
先执行db/dml.sql脚本,创建库表和初始化数据,建议mysql 8.0+版本
|
||||
|
||||
X86/AMD架构
|
||||
```
|
||||
docker pull crabc/crabc-admin:latest
|
||||
docker run -p 9377:9377 --env db_url=jdbc连接(如:jdbc:mysql://localhost:3306/crabc) --env db_user=数据库用户 --env db_pwd=数据库密码 -d --name crabc-admin crabc/crabc-admin:latest
|
||||
docker run -p 9377:9377 -e DB_URL=jdbc:mysql://localhost:3306/crabc -e DB_USER=root -e DB_PWD=root -d --name crabc-api crabc/crabc-api:latest
|
||||
```
|
||||
ARM架构
|
||||
```
|
||||
docker run -p 9377:9377 -e DB_URL=jdbc:mysql://localhost:3306/crabc -e DB_USER=root -e DB_PWD=root -d --name crabc-admin crabc/crabc-admin:latest
|
||||
```
|
||||
访问地址:http://127.0.0.1:9377
|
||||
账号密码:admin/admin123
|
||||
|
||||
## 企业版和开源版对比
|
||||
| 功能 | 社区版 | 企业版 |
|
||||
|--------|--------------------------------| --------------------------|
|
||||
| 架构设计 | 单体 | 微服务 |
|
||||
| 接口创建模式 | SQL脚本 | SQL脚本、图表、代理转发 |
|
||||
| 动态标签 | 支持 | 支持 |
|
||||
| 动态路由 | 不支持 | 支持 |
|
||||
| 数据库 | SQL:mysql/oracle/postgresql/sqlserver <br>TiDB/OpenGauss/Doris |SQL:mysql/oracle/postgresql/sqlserver/tidb<br>/opengauss/doris/oceanbase/dm/gbase <br> NoSQL:es/mongodb <br> TSDB: TDengine |
|
||||
| 认证方式 | 无/Code认证/签名认证 | 无/Code认证/签名认证 |
|
||||
| SQL类型 | 查询SQL | 查询/插入/更新/删除等SQL |
|
||||
| 结果类型 | json | json |
|
||||
| 接口日志 | 支持 | 支持 |
|
||||
| 数据转换 | 不支持 | 支持
|
||||
| 接口市场 | 不支持 | 支持 |
|
||||
| 申请审批 | 不支持 | 支持 |
|
||||
| 接口授权 | 不支持 | 支持 |
|
||||
| 系统权限 | 不支持 | 支持 |
|
||||
| 用户管理 | 不支持 | 支持 |
|
||||
| 数据脱敏 | 不支持 | 支持 |
|
||||
| 版本管理 | 不支持 | 支持
|
||||
| 接口缓存 | 不支持 | 支持 |
|
||||
| 限流熔断 | 仅限流 | 限流和熔断 |
|
||||
| IP黑白名单 | 不支持 | 支持 |
|
||||
| 监控报表 | 支持 | 支持 |
|
||||
| 集群模式 | 不支持 | 支持 |
|
||||
_## Windows桌面运行
|
||||
先执行db/dml.sql脚本,创建库表和初始化数据,建议mysql 8.0+版本
|
||||
```
|
||||
java -jar crabc-api.jar --spring.datasource.url=jdbc:mysql://127.0.0.1:3306/crabc --spring.datasource.username=root --spring.datasource.password=root
|
||||
```
|
||||
|
||||
## 效果截图
|
||||
### 接口开发
|
||||

|
||||
#### 接口属性
|
||||

|
||||
#### 请求参数
|
||||
#### 返回参数
|
||||

|
||||
#### 在线预览
|
||||

|
||||
#### 接口列表
|
||||

|
||||
#### 接口列表
|
||||

|
||||
#### 日志管理
|
||||

|
||||
#### 数据库类型
|
||||

|
||||
### 限流熔断
|
||||

|
||||
|
||||
## 交流群
|
||||
**进群前请先点【Star】谢谢**
|
||||
|
||||
QQ群(请先Star):748993036
|
||||
|
||||
商用咨询加微信 (请备注crabc)
|
||||
|
||||

|
||||
|
||||
## 产品使用登记
|
||||
以下为部分接入crabc-api的用户 ,<a href="https://gitee.com/linebyte/crabc/issues/IA98CA" target="_blank"> 点击进行登记</a>
|
||||
|
||||
| 公司机构 |
|
||||
|:-------------------|
|
||||
| 联通集团西部分公司 |
|
||||
| 青岛巴士科技有限公司 |
|
||||
| 阳光智维科技股份有限公司 |
|
||||
| 一步数据科技有限公司 |
|
||||
| 成都像素云图科技有限公司 |
|
||||
| 公司机构 |
|
||||
|:-------------|
|
||||
| 西部通信研究院 |
|
||||
| 青岛巴士科技有限公司 |
|
||||
| 阳光智维科技股份有限公司 |
|
||||
| 一步数据科技有限公司 |
|
||||
| 像素云图科技有限公司 |
|
||||
| 前景科技有限公司 |
|
||||
| 方寸图信息有限公司 |
|
||||
| 鹭栖技术信息公司 |
|
||||
| 循脉医疗科技有限公司 |
|
||||
| 云上贵州大数据有限公司 |
|
||||
| 巨元瀚洋有限公司 |
|
||||
|
||||
|
||||
## 交流群
|
||||
**进群前请先点 Star**
|
||||
|
||||
加微信进群 (备注crabc)
|
||||

|
||||
## 企业版和开源版对比
|
||||
| 功能 | 社区版 | 企业版 |
|
||||
|-------|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 架构设计 | 单体 | 微服务 |
|
||||
| 开发模式 | SQL脚本 | SQL脚本、图表、接口转发、原生脚本 |
|
||||
| 接口编排 | 不支持 | 支持 |
|
||||
| 动态标签 | 支持 | 支持 |
|
||||
| 动态路由 | 不支持 | 支持 |
|
||||
| 数据库 | SQL:mysql/oracle/postgresql/sqlserver <br>TiDB/OpenGauss/Doris | SQL:mysql/oracle/postgresql/sqlserver/tidb<br>/opengauss/doris/oceanbase/dm/gbase <br> NoSQL:es/mongodb/hbase <br> TSDB: TDengine |
|
||||
| 认证方式 | 无/Code认证/签名认证 | 无/Code认证/签名认证 |
|
||||
| SQL类型 | 查询SQL | 查询/插入/更新/删除等SQL |
|
||||
| 结果类型 | json | json/Excel |
|
||||
| 接口日志 | 支持 | 支持 |
|
||||
| 数据模型 | 不支持 | 支持 |
|
||||
| 协同开发 | 不支持 | 支持 |
|
||||
| 结果转换 | 不支持 | 支持 |
|
||||
| 接口市场 | 不支持 | 支持 |
|
||||
| 申请审批 | 不支持 | 支持 |
|
||||
| 接口授权 | 不支持 | 支持 |
|
||||
| 系统权限 | 不支持 | 支持 |
|
||||
| 用户管理 | 不支持 | 支持 |
|
||||
| 数据脱敏 | 不支持 | 支持 |
|
||||
| 版本管理 | 不支持 | 支持
|
||||
| 接口缓存 | 不支持 | 支持 |
|
||||
| 限流熔断 | 不支持 | 支持 |
|
||||
| IP控制 | 不支持 | 支持 |
|
||||
| 监控报表 | 支持 | 支持 |
|
||||
| 集群模式 | 不支持 | 支持 |
|
||||
@@ -5,14 +5,14 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.crabc</groupId>
|
||||
<artifactId>crabc-boot</artifactId>
|
||||
<artifactId>crabc-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>crabc-admin</artifactId>
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
@@ -37,6 +37,16 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
File renamed without changes.
+3
-13
@@ -3,7 +3,7 @@ server:
|
||||
port: 9377
|
||||
spring:
|
||||
application:
|
||||
name: crabc-admin
|
||||
name: crabc-api
|
||||
main:
|
||||
banner-mode: off
|
||||
jackson:
|
||||
@@ -13,7 +13,7 @@ spring:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
url: jdbc:mysql://127.0.0.1:3306/crabc?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
password:
|
||||
# mybatis配置
|
||||
mybatis:
|
||||
# 搜索指定包别名
|
||||
@@ -25,14 +25,4 @@ mybatis:
|
||||
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
pagehelper:
|
||||
autoRuntimeDialect: true # 开启多数据源分页识别
|
||||
|
||||
# es健康检查
|
||||
management:
|
||||
health:
|
||||
elasticsearch:
|
||||
enabled: false
|
||||
# 签名认证过期时间
|
||||
crabc:
|
||||
auth:
|
||||
expiresTime: 10
|
||||
autoRuntimeDialect: true # 开启多数据源分页识别
|
||||
File renamed without changes.
File renamed without changes.
@@ -3,7 +3,7 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>crabc-boot</artifactId>
|
||||
<artifactId>crabc-api</artifactId>
|
||||
<groupId>cn.crabc</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
@@ -12,8 +12,8 @@
|
||||
<artifactId>crabc-core</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -63,8 +63,13 @@
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.34</version>
|
||||
<version>${lombok.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>${jdk18on.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package cn.crabc.core.app.api;
|
||||
|
||||
import cn.crabc.core.app.entity.BaseApiParam;
|
||||
import cn.crabc.core.app.entity.dto.ApiInfoDTO;
|
||||
import cn.crabc.core.app.enums.ResultTypeEnum;
|
||||
import cn.crabc.core.app.service.core.IBaseDataService;
|
||||
import cn.crabc.core.app.util.ApiThreadLocal;
|
||||
import cn.crabc.core.app.util.Result;
|
||||
import cn.crabc.core.datasource.constant.BaseConstant;
|
||||
import cn.crabc.core.datasource.enums.ErrorStatusEnum;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* API 接收处理层
|
||||
*
|
||||
* @author yuqf
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/web")
|
||||
public class ApiServiceController {
|
||||
|
||||
@Autowired
|
||||
private IBaseDataService baseDataService;
|
||||
|
||||
/**
|
||||
* API GET请求方法
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/**", method = {RequestMethod.GET, RequestMethod.DELETE})
|
||||
public Result getService(@RequestParam(required = false) Map<String, Object> paramMap, HttpServletResponse response) {
|
||||
ApiInfoDTO api = ApiThreadLocal.get();
|
||||
if (api == null) {
|
||||
response.setStatus(400);
|
||||
return Result.error(ErrorStatusEnum.API_INVALID.getCode(), ErrorStatusEnum.API_INVALID.getMassage());
|
||||
}
|
||||
|
||||
// 参数校验
|
||||
if (!validateParams(api, paramMap)) {
|
||||
response.setStatus(400);
|
||||
return Result.error(ErrorStatusEnum.PARAM_NOT_FOUNT.getCode(), ErrorStatusEnum.PARAM_NOT_FOUNT.getMassage());
|
||||
}
|
||||
|
||||
return executeApi(api, paramMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* API post请求
|
||||
*
|
||||
* @param paramMap
|
||||
* @param body
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/**", method = {RequestMethod.POST, RequestMethod.PUT})
|
||||
public Result postService(@RequestParam(required = false) Map<String, Object> paramMap, @RequestBody(required = false) Object body, HttpServletResponse response) {
|
||||
ApiInfoDTO api = ApiThreadLocal.get();
|
||||
if (api == null) {
|
||||
response.setStatus(400);
|
||||
return Result.error(ErrorStatusEnum.API_INVALID.getCode(), ErrorStatusEnum.API_INVALID.getMassage());
|
||||
}
|
||||
|
||||
// 合并请求参数
|
||||
Map<String, Object> mergedParams = mergeParams(paramMap, body);
|
||||
|
||||
// 参数校验
|
||||
if (!checkParams(api.getRequestParams(), mergedParams)) {
|
||||
response.setStatus(400);
|
||||
return Result.error(ErrorStatusEnum.PARAM_NOT_FOUNT.getCode(), ErrorStatusEnum.PARAM_NOT_FOUNT.getMassage());
|
||||
}
|
||||
|
||||
mergedParams.put(BaseConstant.PAGE_SETUP, api.getPageSetup());
|
||||
return executeApi(api, mergedParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验参数
|
||||
*
|
||||
* @param apiParams
|
||||
* @param paramMap
|
||||
* @return
|
||||
*/
|
||||
public boolean checkParams(List<BaseApiParam> apiParams, Map<String, Object> paramMap) {
|
||||
if (apiParams == null || paramMap == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (BaseApiParam param : apiParams) {
|
||||
String paramName = param.getParamName();
|
||||
String paramType = param.getParamType();
|
||||
Object value = paramMap.get(paramName);
|
||||
|
||||
// 必填参数校验
|
||||
if ("Y".equals(param.getRequired()) && !paramMap.containsKey(paramName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Array类型参数处理
|
||||
if ("Array".equalsIgnoreCase(paramType)) {
|
||||
if (value == null || "".equals(value)) {
|
||||
return false;
|
||||
}
|
||||
String[] values = value.toString().split(",");
|
||||
paramMap.put(paramName, Arrays.asList(values));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求参数
|
||||
*/
|
||||
private boolean validateParams(ApiInfoDTO api, Map<String, Object> paramMap) {
|
||||
if (paramMap == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
paramMap.put(BaseConstant.PAGE_SETUP, api.getPageSetup() == null ? 0 : api.getPageSetup());
|
||||
|
||||
return checkParams(api.getRequestParams(), paramMap) &&
|
||||
!(api.getPageSetup() == 1 && !paramMap.containsKey(BaseConstant.PAGE_NUM));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并请求参数
|
||||
*/
|
||||
private Map<String, Object> mergeParams(Map<String, Object> paramMap, Object body) {
|
||||
Map<String, Object> mergedParams = new HashMap<>();
|
||||
if (paramMap != null) {
|
||||
mergedParams.putAll(paramMap);
|
||||
}
|
||||
if (body instanceof Map) {
|
||||
mergedParams.putAll((Map<String, Object>) body);
|
||||
}
|
||||
return mergedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行API调用
|
||||
*/
|
||||
private Result executeApi(ApiInfoDTO api, Map<String, Object> params) {
|
||||
Object data = baseDataService.execute(api.getDatasourceId().toString(), api.getDatasourceType(),
|
||||
api.getSchemaName(), api.getSqlScript(), params);
|
||||
|
||||
if (ResultTypeEnum.ONE.getName().equals(api.getResultType()) && data instanceof List) {
|
||||
List<Object> list = (List<Object>) data;
|
||||
return Result.success(list.isEmpty() ? null : list.get(0));
|
||||
}
|
||||
return Result.success(data);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
package cn.crabc.core.app.config;
|
||||
|
||||
import cn.crabc.core.app.entity.dto.ApiInfoDTO;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.springframework.cache.CacheManager;
|
||||
@@ -28,9 +29,9 @@ public class CacheConfig {
|
||||
* @return
|
||||
*/
|
||||
@Bean("apiCache")
|
||||
public Cache<String, Object> apiCaffeine() {
|
||||
public Cache<String, ApiInfoDTO> apiCaffeine() {
|
||||
return Caffeine.newBuilder()
|
||||
.expireAfterAccess(10, TimeUnit.HOURS)
|
||||
.expireAfterAccess(1, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
+4
-1
@@ -56,7 +56,10 @@ public class DataSourceConfig {
|
||||
dataSource.setTestWhileIdle(true);
|
||||
dataSource.setTestOnBorrow(false);
|
||||
dataSource.setTestOnReturn(false);
|
||||
dataSource.setTestOnReturn(false);
|
||||
// 连接泄漏检测配置
|
||||
dataSource.setRemoveAbandoned(true);
|
||||
dataSource.setRemoveAbandonedTimeout(300);
|
||||
dataSource.setLogAbandoned(true);
|
||||
JdbcDataSourceRouter dynamic = new JdbcDataSourceRouter();
|
||||
dynamic.setTargetDataSources(new HashMap<>());
|
||||
// 设置默认数据源
|
||||
File renamed without changes.
File renamed without changes.
+39
-46
@@ -15,8 +15,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
@@ -41,7 +39,7 @@ public class ApiInfoController {
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
public Result page(String keyword, String devType, Integer pageNum, Integer pageSize) {
|
||||
public Result page(@RequestParam(required = false) String keyword, String devType, Integer pageNum, Integer pageSize) {
|
||||
PageInfo<BaseApiInfo> page = apiInfoService.getApiPage(keyword, devType, pageNum, pageSize);
|
||||
return Result.success(page);
|
||||
}
|
||||
@@ -122,6 +120,16 @@ public class ApiInfoController {
|
||||
return Result.success(apiInfoService.deleteApi(apiId, UserThreadLocal.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁发布接口
|
||||
* @param apiId
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping("/destroy/{apiId}")
|
||||
public Result destroy(@PathVariable Long apiId) {
|
||||
apiInfoService.destroyApiInfo(apiId);
|
||||
return Result.success();
|
||||
}
|
||||
/**
|
||||
* SQL解析
|
||||
*
|
||||
@@ -132,63 +140,48 @@ public class ApiInfoController {
|
||||
public Result sqlParse(@RequestBody SqlParseVO sqlParse) {
|
||||
SqlParseVO sqlParseVO = new SqlParseVO();
|
||||
String sql = sqlParse.getSqlScript();
|
||||
|
||||
// 去除末尾分号
|
||||
if (sql.endsWith(";")) {
|
||||
sql = sql.substring(0, sql.lastIndexOf(";"));
|
||||
}
|
||||
// 对</foreach>标签取值特殊处理
|
||||
Set<String> fields = new HashSet<>();
|
||||
|
||||
// 收集所有请求参数
|
||||
Set<String> paramNames = new HashSet<>();
|
||||
|
||||
// 解析foreach标签中的collection参数
|
||||
if (sql.contains("</foreach>")) {
|
||||
String regex = "collection='(.*?)'";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
String field = matcher.group(1);
|
||||
fields.add(field);
|
||||
}
|
||||
paramNames.addAll(SQLUtil.extractForeachParams(sql));
|
||||
}
|
||||
// 提取if标签里面的请求参数
|
||||
|
||||
// 解析if/when标签中的test条件参数
|
||||
if (sql.contains("<if ") || sql.contains("<when ")) {
|
||||
String regex = " test=['\"](\\w+)\\s*(?:!=|>=|<=|==|<|>|<|<=|>|>=)";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
String field = matcher.group(1);
|
||||
fields.add(field);
|
||||
}
|
||||
}
|
||||
String forRegex = "<foreach[\\s\\S]*?</foreach>";
|
||||
sql = sql.replaceAll(forRegex,"()");
|
||||
// 条件字段
|
||||
Set<String> paramNames = SQLUtil.parseParams(sql);
|
||||
if (!fields.isEmpty()) {
|
||||
paramNames.addAll(fields);
|
||||
paramNames.addAll(SQLUtil.extractIfParams(sql));
|
||||
}
|
||||
|
||||
// 移除foreach标签内容
|
||||
sql = sql.replaceAll("<foreach[\\s\\S]*?</foreach>", "()");
|
||||
|
||||
// 解析SQL中的参数
|
||||
paramNames.addAll(SQLUtil.parseParams(sql));
|
||||
sqlParseVO.setReqColumns(paramNames);
|
||||
// 返回字段
|
||||
|
||||
// 过滤SQL并补全条件
|
||||
sql = SQLUtil.sqlFilter(sql);
|
||||
// 判断去除标签后的 sql是否正确,自动补全条件
|
||||
if (sql.trim().toLowerCase().endsWith("from")) {
|
||||
sql = sql +" test ";
|
||||
}else if (sql.trim().toLowerCase().endsWith("where")) {
|
||||
sql = sql +" 1=1 ";
|
||||
}
|
||||
sql = SQLUtil.completeSql(sql);
|
||||
|
||||
// 解析返回字段
|
||||
Set<ColumnParseVo> resColumns = new HashSet<>();
|
||||
Set<String> resNames = SQLUtil.analyzeSQL(sql.trim(), sqlParse.getDatasourceType());
|
||||
if (resNames.isEmpty()){
|
||||
sql = SQLUtil.checkTable(sql, " test ");
|
||||
resNames = SQLUtil.analyzeSQL(sql.trim(), sqlParse.getDatasourceType());
|
||||
}
|
||||
Set<String> resNames = SQLUtil.parseResultColumns(sql, sqlParse.getDatasourceType());
|
||||
|
||||
// 构建返回字段信息
|
||||
for (String name : resNames) {
|
||||
if (name.startsWith("*")){
|
||||
continue;
|
||||
if (!name.startsWith("*")) {
|
||||
resColumns.add(SQLUtil.buildColumnInfo(name));
|
||||
}
|
||||
ColumnParseVo resColumn = new ColumnParseVo();
|
||||
resColumn.setColName(name);
|
||||
resColumn.setColType("String");
|
||||
resColumn.setItemIndex(0);
|
||||
resColumns.add(resColumn);
|
||||
}
|
||||
sqlParseVO.setResColumns(resColumns);
|
||||
|
||||
return Result.success(sqlParseVO);
|
||||
}
|
||||
|
||||
File renamed without changes.
+157
@@ -0,0 +1,157 @@
|
||||
package cn.crabc.core.app.controller;
|
||||
|
||||
import cn.crabc.core.app.entity.param.ApiTestParam;
|
||||
import cn.crabc.core.app.entity.vo.PreviewVO;
|
||||
import cn.crabc.core.app.enums.ResultTypeEnum;
|
||||
import cn.crabc.core.app.service.core.IBaseDataService;
|
||||
import cn.crabc.core.app.util.Result;
|
||||
import cn.crabc.core.datasource.enums.ErrorStatusEnum;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* API运行和测试
|
||||
*
|
||||
* @author yuqf
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/box/sys/test")
|
||||
public class ApiTestController {
|
||||
|
||||
@Autowired
|
||||
private IBaseDataService baseDataService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 运行预览
|
||||
*
|
||||
* @param api
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/running")
|
||||
public Result runApiSql(@RequestBody ApiTestParam api) {
|
||||
if (api.getDatasourceId() == null) {
|
||||
return Result.error(ErrorStatusEnum.PARAM_NOT_FOUNT.getCode(), ErrorStatusEnum.PARAM_NOT_FOUNT.getMassage());
|
||||
}
|
||||
PreviewVO previewVO = baseDataService.sqlPreview(api.getDatasourceId(),api.getDatasourceType(), api.getSchemaName(), api.getSqlScript());
|
||||
return Result.success(previewVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线测试API
|
||||
*
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/verify/{apiId}")
|
||||
public Result testApiSql(@PathVariable Long apiId, @RequestBody ApiTestParam params) throws Exception {
|
||||
// 参数校验
|
||||
if (params.getDatasourceId() == null) {
|
||||
return Result.error(ErrorStatusEnum.PARAM_NOT_FOUNT.getCode(), ErrorStatusEnum.PARAM_NOT_FOUNT.getMassage());
|
||||
}
|
||||
|
||||
// 设置默认数据源类型
|
||||
params.setDatasourceType(params.getDatasourceType() == null ? "mysql" : params.getDatasourceType());
|
||||
|
||||
try {
|
||||
// 构建参数Map
|
||||
Map<String, Object> paramsMap = buildParamsMap(params);
|
||||
|
||||
// 执行SQL
|
||||
Object data = baseDataService.execute(
|
||||
params.getDatasourceId(),
|
||||
params.getDatasourceType(),
|
||||
params.getSchemaName(),
|
||||
params.getSqlScript(),
|
||||
paramsMap
|
||||
);
|
||||
|
||||
// 处理返回结果
|
||||
return Result.success(formatResultData(data, params.getResultType()));
|
||||
|
||||
} catch (Exception e) {
|
||||
return Result.error("测试异常,请检查参数或者SQL是否正常!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建参数Map
|
||||
*/
|
||||
private Map<String, Object> buildParamsMap(ApiTestParam params) throws Exception {
|
||||
Map<String, Object> paramsMap = new HashMap<>();
|
||||
|
||||
// 处理body参数
|
||||
if (params.getBodyData() != null && params.getBodyData().startsWith("{")) {
|
||||
paramsMap = objectMapper.readValue(params.getBodyData(), HashMap.class);
|
||||
}else if (params.getBodyData() != null && params.getBodyData().startsWith("[")) {
|
||||
List list = objectMapper.readValue(params.getBodyData(), List.class);
|
||||
paramsMap.put("list", list);
|
||||
}
|
||||
|
||||
// 处理query参数
|
||||
Object requestParams = params.getRequestParams();
|
||||
if (requestParams instanceof Map) {
|
||||
Map<String, Object> queryParam = (Map<String, Object>) requestParams;
|
||||
if (validateParams(queryParam)) {
|
||||
paramsMap.putAll(queryParam);
|
||||
} else {
|
||||
throw new IllegalArgumentException("参数校验失败");
|
||||
}
|
||||
paramsMap.put("pageSetup", params.getPageSetup());
|
||||
} else if (requestParams instanceof List) {
|
||||
List<Map<String,Object>> paramsList = (List<Map<String, Object>>) requestParams;
|
||||
for (Map<String, Object> entry : paramsList) {
|
||||
if (validateParams(entry)) {
|
||||
if (entry.containsKey("name")) {
|
||||
paramsMap.put(entry.get("name").toString(), entry.get("value"));
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("参数校验失败");
|
||||
}
|
||||
}
|
||||
if (paramsList.isEmpty()) {
|
||||
paramsMap.put("pageSetup", params.getPageSetup());
|
||||
}
|
||||
}
|
||||
return paramsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化返回数据
|
||||
*/
|
||||
private String formatResultData(Object data, String resultType) throws Exception {
|
||||
if (ResultTypeEnum.ONE.getName().equals(resultType) && data instanceof List) {
|
||||
List<Object> list = (List<Object>) data;
|
||||
return objectMapper.writeValueAsString(Result.success(list.isEmpty() ? null : list.get(0)));
|
||||
}
|
||||
return objectMapper.writeValueAsString(Result.success(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并处理参数
|
||||
*/
|
||||
private boolean validateParams(Map<String, Object> params) {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String paramType = String.valueOf(params.get("paramType"));
|
||||
Object paramValue = params.get("value");
|
||||
|
||||
if ("Array".equalsIgnoreCase(paramType)) {
|
||||
if (paramValue == null || "".equals(paramValue)) {
|
||||
return false;
|
||||
}
|
||||
String[] values = paramValue.toString().split(",");
|
||||
params.put("value", Arrays.asList(values));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
-3
@@ -35,9 +35,6 @@ public class CommonController {
|
||||
public Result detail(Long apiId) {
|
||||
Integer applyCount = 1;
|
||||
BaseApiInfoVO apiDetail = apiInfoService.getApiDetail(apiId);
|
||||
if ("0".equals(apiDetail.getShowSqlScript())){
|
||||
apiDetail.setSqlScript("***");
|
||||
}
|
||||
apiDetail.setApplyed(applyCount);
|
||||
return Result.success(apiDetail);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+20
-1
@@ -4,6 +4,8 @@ import cn.crabc.core.app.entity.BaseUser;
|
||||
import cn.crabc.core.app.entity.param.UserParam;
|
||||
import cn.crabc.core.app.service.system.IBaseUserService;
|
||||
import cn.crabc.core.app.util.*;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import org.apache.tomcat.util.codec.binary.Base64;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -26,7 +29,8 @@ public class SysUserController {
|
||||
@Autowired
|
||||
@Qualifier("dataCache")
|
||||
Cache<String, Object> caffeineCache;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
@@ -75,6 +79,21 @@ public class SysUserController {
|
||||
BaseUser userInfo = iBaseUserService.getUserById(Long.parseLong(UserThreadLocal.getUserId()));
|
||||
return Result.success(userInfo);
|
||||
}
|
||||
/**
|
||||
* 用户菜单
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/menus")
|
||||
public Result userMenus(){
|
||||
// 固定菜单
|
||||
String menus ="[{\"name\":\"API Hub\",\"checked\":null,\"id\":2,\"parentId\":1,\"open\":null,\"path\":null,\"url\":\"/hub\",\"sort\":2,\"menuType\":1,\"code\":\"Home\",\"hidden\":false,\"permission\":\"/api/box/sys/user/info;/api/box/sys/user/menus;/api/box/sys/message/**;/api/box/sys/user/update/pwd\",\"subMenus\":[{\"name\":\"接口详情\",\"checked\":null,\"id\":7,\"parentId\":2,\"open\":null,\"path\":null,\"url\":\"/doc\",\"sort\":6,\"menuType\":1,\"code\":\"DataCatelogApiDetails\",\"hidden\":true,\"permission\":\"doc\",\"subMenus\":[]}]},{\"name\":\"工作台\",\"checked\":null,\"id\":3,\"parentId\":1,\"open\":null,\"path\":null,\"url\":\"/working\",\"sort\":3,\"menuType\":1,\"code\":\"DataService\",\"hidden\":false,\"permission\":\"/api/box/sys/common/**;/api/box/sys/api/info/**;/api/box/sys/metadata/**;/api/box/sys/test/**;/api/box/sys/group/**;/api/box/sys/user/check/name\",\"subMenus\":[]},{\"name\":\"服务台\",\"checked\":null,\"id\":4,\"parentId\":1,\"open\":null,\"path\":null,\"url\":\"/manage\",\"sort\":4,\"menuType\":1,\"code\":\"Manage\",\"hidden\":false,\"permission\":\"manage\",\"subMenus\":[{\"name\":\"应用管理\",\"checked\":null,\"id\":8,\"parentId\":4,\"open\":null,\"path\":null,\"url\":\"/manage/keys\",\"sort\":1,\"menuType\":1,\"code\":\"Application\",\"hidden\":true,\"permission\":\"/api/box/sys/app/**\",\"subMenus\":[]},{\"name\":\"接口列表\",\"checked\":null,\"id\":9,\"parentId\":4,\"open\":null,\"path\":null,\"url\":\"/manage/apis\",\"sort\":1,\"menuType\":1,\"code\":\"MyApi\",\"hidden\":true,\"permission\":\"/api/box/sys/api/info/**\",\"subMenus\":[]},{\"name\":\"日志管理\",\"checked\":null,\"id\":12,\"parentId\":4,\"open\":null,\"path\":null,\"url\":\"/manage/logs\",\"sort\":1,\"menuType\":1,\"code\":\"UseLog\",\"hidden\":true,\"permission\":\"/api/box/sys/api/log/**\",\"subMenus\":[{\"name\":\"日志列表\",\"checked\":null,\"id\":13,\"parentId\":12,\"open\":null,\"path\":null,\"url\":\"/manage/logs/list\",\"sort\":1,\"menuType\":1,\"code\":\"UseLogList\",\"hidden\":true,\"permission\":\"/api/box/sys/api/log/**\",\"subMenus\":[]},{\"name\":\"日志详情\",\"checked\":null,\"id\":14,\"parentId\":12,\"open\":null,\"path\":null,\"url\":\"/manage/logs/detail\",\"sort\":1,\"menuType\":1,\"code\":\"DetailUesLog\",\"hidden\":true,\"permission\":null,\"subMenus\":[]}]},{\"name\":\"分类管理\",\"checked\":null,\"id\":30,\"parentId\":4,\"open\":null,\"path\":null,\"url\":\"/manage/classification\",\"sort\":1,\"menuType\":1,\"code\":\"Classification\",\"hidden\":true,\"permission\":\"manage:classification\",\"subMenus\":[]},{\"name\":\"数据源\",\"checked\":null,\"id\":31,\"parentId\":4,\"open\":null,\"path\":null,\"url\":\"/manage/datasource\",\"sort\":1,\"menuType\":1,\"code\":\"DataSource\",\"hidden\":true,\"permission\":\"/api/box/sys/datasource/**;/api/box/sys/user/pubKey\",\"subMenus\":[]},{\"name\":\"密码重置\",\"checked\":null,\"id\":32,\"parentId\":4,\"open\":null,\"path\":null,\"url\":\"/manage/edit-pwd\",\"sort\":1,\"menuType\":1,\"code\":\"ChangePassword\",\"hidden\":true,\"permission\":\"/api/box/sys/user/update/pwd\",\"subMenus\":[]}]}]";
|
||||
try {
|
||||
List<Map<String, Object>> list = objectMapper.readValue(menus, List.class);
|
||||
return Result.success(list);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
+6
-6
@@ -80,11 +80,6 @@ public class BaseApiInfo extends BaseEntity {
|
||||
*/
|
||||
private String sqlScript;
|
||||
|
||||
/**
|
||||
* 是否显示脚本1/0
|
||||
*/
|
||||
private String showSqlScript;
|
||||
|
||||
/**
|
||||
* 分页设置,不分页:0、只分页:page、分页并统计:pageCount
|
||||
*/
|
||||
@@ -93,7 +88,7 @@ public class BaseApiInfo extends BaseEntity {
|
||||
/**
|
||||
* 数据源Id
|
||||
*/
|
||||
private String datasourceId;
|
||||
private Integer datasourceId;
|
||||
|
||||
/**
|
||||
* 数据源名称
|
||||
@@ -104,6 +99,8 @@ public class BaseApiInfo extends BaseEntity {
|
||||
* schema
|
||||
*/
|
||||
private String schemaName;
|
||||
// 表名
|
||||
private String tableName;
|
||||
|
||||
/**
|
||||
* 数据源类型
|
||||
@@ -120,4 +117,7 @@ public class BaseApiInfo extends BaseEntity {
|
||||
*/
|
||||
private Long parentId;
|
||||
|
||||
/** 分组名称 */
|
||||
private String groupName;
|
||||
|
||||
}
|
||||
File renamed without changes.
+2
-2
@@ -40,7 +40,7 @@ public class BaseApiParam {
|
||||
/**
|
||||
* 是否必填
|
||||
*/
|
||||
private Boolean required;
|
||||
private String required;
|
||||
|
||||
/**
|
||||
* 操作
|
||||
@@ -70,7 +70,7 @@ public class BaseApiParam {
|
||||
/**
|
||||
* 数据源Id
|
||||
*/
|
||||
private String datasourceId;
|
||||
private Integer datasourceId;
|
||||
|
||||
/**
|
||||
* schema
|
||||
+3
-1
@@ -33,13 +33,15 @@ public class BaseApiSql extends BaseEntity {
|
||||
/**
|
||||
* 数据源Id
|
||||
*/
|
||||
private String datasourceId;
|
||||
private Integer datasourceId;
|
||||
|
||||
/**
|
||||
* schema
|
||||
*/
|
||||
private String schemaName;
|
||||
|
||||
private String tableName;
|
||||
|
||||
/**
|
||||
* 数据源类型
|
||||
*/
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+5
@@ -3,6 +3,8 @@ package cn.crabc.core.app.entity.param;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class ApiTestParam {
|
||||
@@ -17,10 +19,13 @@ public class ApiTestParam {
|
||||
|
||||
private String sqlParams;
|
||||
|
||||
private Integer pageSetup;
|
||||
|
||||
private String resultType;
|
||||
|
||||
private Object requestParams;
|
||||
|
||||
private String bodyData;
|
||||
|
||||
private Map<String,Object> queryParam;
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-2
@@ -71,8 +71,6 @@ public class BaseApiInfoVO extends BaseEntity {
|
||||
private Integer pageSetup;
|
||||
|
||||
private String sqlScript;
|
||||
|
||||
private String showSqlScript;
|
||||
/**
|
||||
* 发布时间
|
||||
*/
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
@@ -3,6 +3,8 @@ package cn.crabc.core.app.entity.vo;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class RequestParamsVO {
|
||||
@@ -46,4 +48,5 @@ public class RequestParamsVO {
|
||||
*/
|
||||
private String paramDesc;
|
||||
|
||||
private List<RequestParamsVO> children;
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
@@ -79,6 +79,7 @@ public class ExceptionHandler {
|
||||
@org.springframework.web.bind.annotation.ExceptionHandler(value = HttpMessageNotReadableException.class)
|
||||
@ResponseBody
|
||||
public Result exceptionHandler(HttpServletRequest req, HttpMessageNotReadableException e) {
|
||||
e.printStackTrace();
|
||||
return Result.error(50002, "Body参数异常,请检查Body/json是否正确传参");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.crabc.core.app.filter;
|
||||
|
||||
import cn.crabc.core.app.util.ApiThreadLocal;
|
||||
import cn.crabc.core.app.util.UserThreadLocal;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ApiFilter 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;
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
ContentCachingResponseWrapper responseWrapper = null;
|
||||
try {
|
||||
// 包装类
|
||||
if (request.getContentType() == null || request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
|
||||
BaseRequestWrapper requestWrapper = new BaseRequestWrapper(request);
|
||||
responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
filterChain.doFilter(requestWrapper, responseWrapper);
|
||||
} else {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
}finally {
|
||||
ApiThreadLocal.remove();
|
||||
if (responseWrapper != null) {
|
||||
responseWrapper.copyBodyToResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
ApiThreadLocal.remove();
|
||||
Filter.super.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package cn.crabc.core.app.filter;
|
||||
|
||||
import cn.crabc.core.app.entity.BaseApiLog;
|
||||
import cn.crabc.core.app.entity.BaseApp;
|
||||
import cn.crabc.core.app.entity.dto.ApiInfoDTO;
|
||||
import cn.crabc.core.app.service.system.IBaseApiInfoService;
|
||||
import cn.crabc.core.app.service.system.IBaseApiLogService;
|
||||
import cn.crabc.core.app.util.ApiThreadLocal;
|
||||
import cn.crabc.core.app.util.RequestUtils;
|
||||
import cn.crabc.core.app.util.Result;
|
||||
import cn.crabc.core.app.util.SM3Util;
|
||||
import cn.crabc.core.datasource.enums.ErrorStatusEnum;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* API开放接口鉴权过滤 拦截器
|
||||
*
|
||||
* @author yuqf
|
||||
*/
|
||||
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
|
||||
|
||||
// API开放接口前缀
|
||||
private static final String API_PRE = "/api/web/";
|
||||
@Autowired
|
||||
private IBaseApiLogService iBaseApiLogService;
|
||||
@Autowired
|
||||
private IBaseApiInfoService iBaseApiInfoService;
|
||||
@Value("${crabc.auth.expiresTime:10}")
|
||||
private Integer expiresTime;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
@Qualifier("apiCache")
|
||||
private Cache<String, ApiInfoDTO> apiCache;
|
||||
|
||||
private ApiInfoDTO getApiData(String key) {
|
||||
String[] split = key.split("_");
|
||||
return iBaseApiInfoService.getApiInfoCache(split[0], split[1]);
|
||||
}
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String path = request.getRequestURI();
|
||||
String method = request.getMethod();
|
||||
|
||||
ApiInfoDTO apiInfo = apiCache.get(method + "_" + path.replace(API_PRE, ""), this::getApiData);
|
||||
if (apiInfo == null) {
|
||||
setErrorResponse(request, response,ErrorStatusEnum.API_INVALID.getCode(),ErrorStatusEnum.API_INVALID.getMassage());
|
||||
return false;
|
||||
}
|
||||
if (apiInfo.getEnabled() == 0) {
|
||||
setErrorResponse(request, response,ErrorStatusEnum.API_OFFLINE.getCode(),ErrorStatusEnum.API_OFFLINE.getMassage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 存入当前时间,当作是日志的请求时间
|
||||
apiInfo.setRequestDate(new Date());
|
||||
apiInfo.setRequestTime(System.currentTimeMillis());
|
||||
// 放入上下文
|
||||
ApiThreadLocal.set(apiInfo);
|
||||
try {
|
||||
// 应用列表
|
||||
List<BaseApp> appList = apiInfo.getAppList();
|
||||
|
||||
return switch (apiInfo.getAuthType().toUpperCase()) {
|
||||
case "APP_CODE" -> checkAppCode(request,response, appList);
|
||||
case "APP_KEY" -> checkAppKey(request,response, appList);
|
||||
case "APP_SECRET" -> checkSM3(request,response, appList);
|
||||
default -> true;
|
||||
};
|
||||
}catch (Exception e) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.API_UN_AUTH.getCode(),ErrorStatusEnum.API_UN_AUTH.getMassage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
|
||||
addLog(request, response, "");
|
||||
// 清除上下文
|
||||
ApiThreadLocal.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常返回
|
||||
* @param response
|
||||
* @param status
|
||||
* @param message
|
||||
* @throws Exception
|
||||
*/
|
||||
private void setErrorResponse(HttpServletRequest request, HttpServletResponse response, int status, String message)
|
||||
throws Exception {
|
||||
// 指定错误码
|
||||
response.setStatus(400);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
Result result = Result.error(status, message);
|
||||
String json = objectMapper.writeValueAsString(result);
|
||||
// 日记记录
|
||||
addLog(request, response, json);
|
||||
response.getWriter().write(json);
|
||||
}
|
||||
/**
|
||||
* 记录访问日志
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
*/
|
||||
private void addLog(HttpServletRequest request, HttpServletResponse response, String msg) throws Exception {
|
||||
ContentCachingResponseWrapper responseWrapper = null;
|
||||
BaseApiLog apiLog = new BaseApiLog();
|
||||
long endTime = System.currentTimeMillis();
|
||||
ApiInfoDTO apiInfo = ApiThreadLocal.get();
|
||||
if (apiInfo != null) {
|
||||
apiLog.setApiId(apiInfo.getApiId());
|
||||
apiLog.setApiName(apiInfo.getApiName());
|
||||
apiLog.setApiMethod(apiInfo.getApiMethod());
|
||||
apiLog.setAuthType(apiInfo.getAuthType());
|
||||
apiLog.setRequestTime(apiInfo.getRequestDate());
|
||||
apiLog.setCostTime(endTime - apiInfo.getRequestTime());
|
||||
}
|
||||
int status = response.getStatus();
|
||||
apiLog.setAppName(RequestUtils.getAppKey(request));
|
||||
apiLog.setApiPath(request.getRequestURI());
|
||||
apiLog.setRequestIp(RequestUtils.getIp(request));
|
||||
apiLog.setResponseTime(new Date());
|
||||
apiLog.setQueryParam(request.getQueryString());
|
||||
apiLog.setResponseCode(status);
|
||||
apiLog.setRequestStatus(status == 200 ? "success" : "fail");
|
||||
try {
|
||||
if (request instanceof BaseRequestWrapper) {
|
||||
String requestBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
|
||||
apiLog.setRequestBody(requestBody);
|
||||
}
|
||||
if (response instanceof ContentCachingResponseWrapper) {
|
||||
responseWrapper = (ContentCachingResponseWrapper) response;
|
||||
byte[] content = responseWrapper.getContentAsByteArray();
|
||||
String responseBody = new String(content, StandardCharsets.UTF_8);
|
||||
apiLog.setResponseBody(responseBody);
|
||||
}
|
||||
if (status == 400) {
|
||||
apiLog.setResponseBody(msg);
|
||||
}
|
||||
iBaseApiLogService.addLog(apiLog);
|
||||
} catch (Exception e) {
|
||||
log.error("响应结果转换异常", e);
|
||||
} finally {
|
||||
if (responseWrapper != null) {
|
||||
responseWrapper.copyBodyToResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证接口访问权限APP_CODE
|
||||
*
|
||||
* @param request
|
||||
* @param appList
|
||||
* @return
|
||||
*/
|
||||
private boolean checkAppCode(HttpServletRequest request,HttpServletResponse response, List<BaseApp> appList) throws Exception {
|
||||
String appCode = RequestUtils.getAppCode(request);
|
||||
if (appCode == null || appCode.isEmpty()) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.API_UN_AUTH.getCode(),ErrorStatusEnum.API_UN_AUTH.getMassage());
|
||||
return false;
|
||||
}
|
||||
boolean check = appList.stream().anyMatch(app -> app.getAppCode().equals(appCode));
|
||||
if (!check) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.API_UN_AUTH.getCode(),ErrorStatusEnum.API_UN_AUTH.getMassage());
|
||||
}
|
||||
return check;
|
||||
}
|
||||
/**
|
||||
* 验证接口访问权限APP_KEY
|
||||
*
|
||||
* @param request
|
||||
* @param appList
|
||||
* @return
|
||||
*/
|
||||
private boolean checkAppKey(HttpServletRequest request, HttpServletResponse response, List<BaseApp> appList) throws Exception {
|
||||
String appKey = RequestUtils.getAppKey(request);
|
||||
if (appKey == null || appKey.isEmpty()) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.SHA_APPKEY_NOT_FOUNT.getCode(),ErrorStatusEnum.SHA_APPKEY_NOT_FOUNT.getMassage());
|
||||
return false;
|
||||
}
|
||||
boolean check = appList.stream().anyMatch(app -> app.getAppKey().equals(appKey));
|
||||
if (!check) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.APP_UN_AUTH.getCode(),ErrorStatusEnum.APP_UN_AUTH.getMassage());
|
||||
}
|
||||
return check;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国密签名认证
|
||||
*
|
||||
* @param request
|
||||
* @param appList
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public boolean checkSM3(HttpServletRequest request, HttpServletResponse response, List<BaseApp> appList) throws Exception {
|
||||
// 认证参数
|
||||
String sign = Optional.ofNullable(request.getHeader("sign")).orElse(request.getParameter("sign"));
|
||||
String timeStamp = Optional.ofNullable(request.getHeader("timestamp")).orElse(request.getParameter("timestamp"));
|
||||
String appKey = Optional.ofNullable(request.getHeader("appkey")).orElse(request.getParameter("appkey"));
|
||||
if (appKey == null || sign == null || timeStamp == null) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.SHA_PARAM_NOT_FOUNT.getCode(), ErrorStatusEnum.PARAM_NOT_FOUNT.getMassage());
|
||||
return false;
|
||||
}
|
||||
// 校验时间戳,超过10分钟失效
|
||||
long authTime = Long.parseLong(timeStamp);
|
||||
long nowTime = System.currentTimeMillis() - authTime;
|
||||
if (nowTime > expiresTime * 60 * 1000) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.SHA_TIMESTAMP_EXPIRE.getCode(), ErrorStatusEnum.SHA_TIMESTAMP_EXPIRE.getMassage());
|
||||
return false;
|
||||
}
|
||||
|
||||
String appSecret = appList.stream()
|
||||
.filter(app -> app.getAppKey().equals(appKey))
|
||||
.map(BaseApp::getAppSecret)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
|
||||
boolean verify = SM3Util.verify(appSecret + timeStamp, sign);
|
||||
if (!verify) {
|
||||
setErrorResponse(request,response,ErrorStatusEnum.API_UN_AUTH.getCode(),ErrorStatusEnum.API_UN_AUTH.getMassage());
|
||||
}
|
||||
return verify;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+9
-14
@@ -18,30 +18,25 @@ import org.springframework.web.servlet.HandlerInterceptor;
|
||||
*/
|
||||
public class JwtInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Value("${crabc.token.expireTime:36000}")
|
||||
private long expireTime;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
|
||||
if (request.getMethod().toUpperCase().equals("OPTIONS")){
|
||||
return true; // 通过OPTION请求
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 放行OPTIONS请求
|
||||
if ("OPTIONS".equals(request.getMethod().toUpperCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取并校验token
|
||||
String token = JwtUtil.getToken(request);
|
||||
if (token == null) {
|
||||
throw new CustomException(ErrorStatusEnum.JWT_LOGIN_EXPIRE.getCode(), ErrorStatusEnum.JWT_UN_AUTH.getMassage());
|
||||
}
|
||||
|
||||
// 解析token
|
||||
Claims claims = JwtUtil.parseToken(token);
|
||||
if (claims == null) {
|
||||
throw new CustomException(ErrorStatusEnum.JWT_LOGIN_EXPIRE.getCode(), ErrorStatusEnum.JWT_UN_AUTH.getMassage());
|
||||
}
|
||||
long nowTime = System.currentTimeMillis();
|
||||
Long expire = Long.parseLong(claims.get("expireTime").toString());
|
||||
long time = nowTime - expire;
|
||||
if (time/1000 > expireTime) {
|
||||
throw new CustomException(ErrorStatusEnum.JWT_LOGIN_EXPIRE.getCode(), ErrorStatusEnum.JWT_LOGIN_EXPIRE.getMassage());
|
||||
}else{
|
||||
|
||||
}
|
||||
// 设置用户信息到线程上下文
|
||||
UserThreadLocal.set(claims);
|
||||
return true;
|
||||
}
|
||||
+7
@@ -74,6 +74,13 @@ public interface BaseApiInfoMapper {
|
||||
*/
|
||||
List<ApiInfoDTO> selectApiDetail(@Param("apiId") Long apiId);
|
||||
|
||||
/**
|
||||
* 根据apiId查询信息
|
||||
* @param
|
||||
* @return
|
||||
*/
|
||||
ApiInfoDTO getApiDetail(@Param("method") String method, @Param("apiPath") String apiPath);
|
||||
|
||||
/**
|
||||
* 插入API
|
||||
*
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+25
-25
@@ -23,6 +23,7 @@ public class BaseDataServiceImpl implements IBaseDataService {
|
||||
|
||||
@Autowired
|
||||
private DataSourceManager dataSourceManager;
|
||||
|
||||
@Override
|
||||
public String testConnection(BaseDataSource dataSource) {
|
||||
return dataSourceManager.test(dataSource);
|
||||
@@ -40,21 +41,17 @@ public class BaseDataServiceImpl implements IBaseDataService {
|
||||
Object result = this.execute(datasourceId, datasourceType, schema, sql, params);
|
||||
PreviewVO preview = new PreviewVO();
|
||||
if (result instanceof List) {
|
||||
List<Map<String, Object>> list = (List<Map<String, Object>>)result;
|
||||
List<Map<String, Object>> list = (List<Map<String, Object>>) result;
|
||||
if (!list.isEmpty()) {
|
||||
preview.setData(list);
|
||||
Set<String> fieldName = list.get(0).keySet();
|
||||
preview.setMetadata(fieldName);
|
||||
preview.setMetadata(list.get(0).keySet());
|
||||
}
|
||||
}else{
|
||||
Set<String> fieldName = new HashSet<>();
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
} else {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("执行结果", "执行成功,影响条数:"+result);
|
||||
list.add(data);
|
||||
data.put("执行结果", String.format("执行成功,影响条数:%s", result));
|
||||
List<Map<String, Object>> list = Collections.singletonList(data);
|
||||
preview.setData(list);
|
||||
fieldName.add("执行结果");
|
||||
preview.setMetadata(fieldName);
|
||||
preview.setMetadata(Collections.singleton("执行结果"));
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
@@ -62,25 +59,28 @@ public class BaseDataServiceImpl implements IBaseDataService {
|
||||
@Override
|
||||
public Object execute(String datasourceId, String datasourceType, String schema, String sql, Map<String, Object> params) {
|
||||
StatementMapper statementMapper = dataSourceManager.getStatementMapper(datasourceId);
|
||||
// 数据源类型
|
||||
|
||||
if (datasourceType != null && !params.containsKey(BaseConstant.DATA_SOURCE_TYPE)) {
|
||||
params.put(BaseConstant.DATA_SOURCE_TYPE, datasourceType);
|
||||
}
|
||||
String sqlType = SQLUtil.getOperateType(sql);
|
||||
if ("insert".equalsIgnoreCase(sqlType)) {
|
||||
return statementMapper.insert(datasourceId, schema, sql, params);
|
||||
}else if("update".equalsIgnoreCase(sqlType)){
|
||||
return statementMapper.update(datasourceId, schema, sql, params);
|
||||
}else if("delete".equalsIgnoreCase(sqlType)){
|
||||
return statementMapper.delete(datasourceId, schema, sql, params);
|
||||
} else {
|
||||
Object pageNum = params.get(BaseConstant.PAGE_NUM);
|
||||
Object pageSize = params.get(BaseConstant.PAGE_SIZE);
|
||||
if (pageNum != null && pageSize != null) {
|
||||
return statementMapper.selectPage(datasourceId, schema, sql, params, Integer.parseInt(pageNum.toString()), Integer.parseInt(pageSize.toString()));
|
||||
} else {
|
||||
|
||||
String sqlType = SQLUtil.getOperateType(sql).toLowerCase();
|
||||
switch (sqlType) {
|
||||
case "insert":
|
||||
return statementMapper.insert(datasourceId, schema, sql, params);
|
||||
case "update":
|
||||
return statementMapper.update(datasourceId, schema, sql, params);
|
||||
case "delete":
|
||||
return statementMapper.delete(datasourceId, schema, sql, params);
|
||||
default:
|
||||
Object pageNum = params.get(BaseConstant.PAGE_NUM);
|
||||
Object pageSize = params.get(BaseConstant.PAGE_SIZE);
|
||||
if (pageNum != null && pageSize != null) {
|
||||
return statementMapper.selectPage(datasourceId, schema, sql, params,
|
||||
Integer.parseInt(pageNum.toString()),
|
||||
Integer.parseInt(pageSize.toString()));
|
||||
}
|
||||
return statementMapper.selectList(datasourceId, schema, sql, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -18,7 +18,13 @@ import java.util.List;
|
||||
*/
|
||||
public interface IBaseApiInfoService {
|
||||
|
||||
void initApi();
|
||||
/**
|
||||
* 根据接口url获取API关联数据
|
||||
*
|
||||
* @param
|
||||
* @return
|
||||
*/
|
||||
ApiInfoDTO getApiInfoCache(String method, String apiPath);
|
||||
|
||||
/**
|
||||
* 获取API关联数据
|
||||
@@ -153,4 +159,10 @@ public interface IBaseApiInfoService {
|
||||
*/
|
||||
Integer addChooseApi(BaseAppApi appApi);
|
||||
|
||||
/**
|
||||
* 销毁接口
|
||||
* @param apiId
|
||||
* @return
|
||||
*/
|
||||
Integer destroyApiInfo(Long apiId);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+32
-8
@@ -49,16 +49,23 @@ public class BaseApiInfoServiceImpl implements IBaseApiInfoService {
|
||||
private BaseApiParamMapper apiParamMapper;
|
||||
@Autowired
|
||||
@Qualifier("apiCache")
|
||||
Cache<String, Object> apiInfoCache;
|
||||
Cache<String, ApiInfoDTO> apiInfoCache;
|
||||
|
||||
@Scheduled(cron = "${crabc.corn.api:0 0/1 * * * ?}")
|
||||
public void task() {
|
||||
initApi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initApi() {
|
||||
updateCache(null);
|
||||
public ApiInfoDTO getApiInfoCache(String method, String apiPath) {
|
||||
ApiInfoDTO apiInfo = apiInfoMapper.getApiDetail(method, apiPath);
|
||||
if (apiInfo == null) {
|
||||
return null;
|
||||
}
|
||||
// 应用
|
||||
List<BaseApp> appApis = baseAppMapper.selectApiApp(apiInfo.getApiId());
|
||||
// 请求参数
|
||||
List<BaseApiParam> baseApiParams = apiParamMapper.selectReqParams(apiInfo.getApiId());
|
||||
|
||||
apiInfo.setAppList(appApis == null ? new ArrayList<>() : appApis);
|
||||
apiInfo.setRequestParams(baseApiParams == null ? new ArrayList<>() : baseApiParams);
|
||||
return apiInfo;
|
||||
}
|
||||
|
||||
public void updateCache(Long apiId) {
|
||||
@@ -71,7 +78,7 @@ public class BaseApiInfoServiceImpl implements IBaseApiInfoService {
|
||||
@Override
|
||||
public List<ApiInfoDTO> getApiCache(Long apiId) {
|
||||
List<ApiInfoDTO> apiInfos = apiInfoMapper.selectApiDetail(apiId);
|
||||
if (apiInfos.size() == 0) {
|
||||
if (apiInfos.isEmpty()) {
|
||||
return apiInfos;
|
||||
}
|
||||
// 应用
|
||||
@@ -155,6 +162,7 @@ public class BaseApiInfoServiceImpl implements IBaseApiInfoService {
|
||||
api.setApiType("SQL");
|
||||
api.setDatasourceId(sql.getDatasourceId());
|
||||
api.setSchemaName(sql.getSchemaName());
|
||||
api.setTableName(sql.getTableName());
|
||||
api.setDatasourceType(sql.getDatasourceType());
|
||||
api.setSqlScript(sql.getSqlScript());
|
||||
api.setCreateTime(date);
|
||||
@@ -188,6 +196,7 @@ public class BaseApiInfoServiceImpl implements IBaseApiInfoService {
|
||||
api.setEnabled(0);
|
||||
api.setDatasourceId(sql.getDatasourceId());
|
||||
api.setSchemaName(sql.getSchemaName());
|
||||
api.setTableName(sql.getTableName());
|
||||
api.setDatasourceType(sql.getDatasourceType());
|
||||
api.setSqlScript(sql.getSqlScript());
|
||||
api.setUpdateTime(updateTime);
|
||||
@@ -286,6 +295,21 @@ public class BaseApiInfoServiceImpl implements IBaseApiInfoService {
|
||||
return baseAppApiMapper.insert(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer destroyApiInfo(Long apiId) {
|
||||
String userId = UserThreadLocal.getUserId();
|
||||
|
||||
BaseApiInfo baseApiInfo = new BaseApiInfo();
|
||||
baseApiInfo.setUpdateTime(new Date());
|
||||
baseApiInfo.setUpdateBy(userId);
|
||||
baseApiInfo.setApiStatus(ApiStateEnum.DESTROY.getName());
|
||||
baseApiInfo.setApiId(apiId);
|
||||
apiInfoMapper.updateApiState(baseApiInfo);
|
||||
// 更新缓存
|
||||
this.updateCache(apiId);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* api信息历史发布版本存档
|
||||
*
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package cn.crabc.core.app.service.system.impl;
|
||||
|
||||
import cn.crabc.core.app.entity.vo.ApiParamsVO;
|
||||
import cn.crabc.core.app.entity.vo.RequestParamsVO;
|
||||
import cn.crabc.core.app.mapper.BaseApiParamMapper;
|
||||
import cn.crabc.core.app.service.system.IBaseApiParamService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* API参数 服务实现
|
||||
*
|
||||
* @author yuqf
|
||||
*/
|
||||
@Service
|
||||
public class BaseApiParamServiceImpl implements IBaseApiParamService {
|
||||
|
||||
@Autowired
|
||||
private BaseApiParamMapper baseApiParamMapper;
|
||||
@Value("${crabc.result.code:code}")
|
||||
private String resultCode;
|
||||
@Value("${crabc.result.msg:msg}")
|
||||
private String resultMsg;
|
||||
@Value("${crabc.result.data:data}")
|
||||
private String resultData;
|
||||
|
||||
public static final String REQUEST ="request";
|
||||
/**
|
||||
* 响应参数
|
||||
*/
|
||||
public static final String RESPONSE ="response";
|
||||
|
||||
@Override
|
||||
public ApiParamsVO getApiDetailsParams(Long apiId) {
|
||||
ApiParamsVO apiParams = new ApiParamsVO();
|
||||
List<RequestParamsVO> requestParams = baseApiParamMapper.selectApiParams(apiId);
|
||||
Map<String, List<RequestParamsVO>> paramMap = requestParams.stream().collect(Collectors.groupingBy(RequestParamsVO::getParamModel));
|
||||
apiParams.setReqParams(paramMap.get(REQUEST));
|
||||
List<RequestParamsVO> responseParam = paramMap.get(RESPONSE);
|
||||
apiParams.setResParams(this.setResponseParams(responseParam));
|
||||
return apiParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理返回参数
|
||||
* @param responseParams
|
||||
* @return
|
||||
*/
|
||||
public List<RequestParamsVO> setResponseParams(List<RequestParamsVO> responseParams) {
|
||||
List<RequestParamsVO> list = new ArrayList<>();
|
||||
if (responseParams == null || responseParams.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
RequestParamsVO responseCode = new RequestParamsVO();
|
||||
responseCode.setParamName(resultCode);
|
||||
responseCode.setParamDesc("状态码:0 成功");
|
||||
responseCode.setParamType("Int");
|
||||
responseCode.setExample("0");
|
||||
responseCode.setRequired("Y");
|
||||
list.add(responseCode);
|
||||
|
||||
RequestParamsVO responseMsg = new RequestParamsVO();
|
||||
responseMsg.setParamName(resultMsg);
|
||||
responseMsg.setParamDesc("错误信息");
|
||||
responseMsg.setParamType("String");
|
||||
responseMsg.setExample("");
|
||||
responseMsg.setChildren(null);
|
||||
responseCode.setRequired("Y");
|
||||
list.add(responseMsg);
|
||||
|
||||
RequestParamsVO responseData = new RequestParamsVO();
|
||||
responseData.setParamName(resultData);
|
||||
responseData.setParamDesc("数据");
|
||||
responseData.setParamType("Array");
|
||||
responseData.setExample("");
|
||||
responseCode.setRequired("Y");
|
||||
responseData.setChildren(responseParams);
|
||||
list.add(responseData);
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
File renamed without changes.
+3
-2
@@ -61,7 +61,7 @@ public class BaseDataSourceServiceImpl implements IBaseDataSourceService {
|
||||
public void init() {
|
||||
List<BaseDataSource> baseDataSources = this.getList();
|
||||
for (BaseDataSource dataSource : baseDataSources) {
|
||||
DataSource ds = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(dataSource.getDatasourceId());
|
||||
DataSource ds = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(dataSource.getDatasourceId().toString());
|
||||
try {
|
||||
String jdbcUrl = null;
|
||||
String username = null;
|
||||
@@ -113,6 +113,7 @@ public class BaseDataSourceServiceImpl implements IBaseDataSourceService {
|
||||
dataSource.setClassify("jdbc");
|
||||
dataSource.setCreateBy(UserThreadLocal.getUserId());
|
||||
dataSource.setCreateTime(new Date());
|
||||
dataSource.setUpdateTime(new Date());
|
||||
// 判断是否是自定义
|
||||
if ("custom".equals(dataSource.getDatasourceType())){
|
||||
String dataSourceType = SQLUtil.getDataSourceType(dataSource.getJdbcUrl());
|
||||
@@ -176,7 +177,7 @@ public class BaseDataSourceServiceImpl implements IBaseDataSourceService {
|
||||
byte[] decode = Base64.getDecoder().decode(password);
|
||||
pwd = new String(decode);
|
||||
}else if (dataSource.getDatasourceId() != null){
|
||||
BaseDatasource baseDatasource = dataSourceMapper.selectOne(Integer.parseInt(dataSource.getDatasourceId()));
|
||||
BaseDatasource baseDatasource = dataSourceMapper.selectOne(dataSource.getDatasourceId());
|
||||
byte[] decode = Base64.getDecoder().decode(baseDatasource.getPassword());
|
||||
pwd = new String(decode);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+6
-1
@@ -1,6 +1,9 @@
|
||||
package cn.crabc.core.app.util;
|
||||
|
||||
import cn.crabc.core.app.entity.dto.ApiInfoDTO;
|
||||
import cn.crabc.core.app.filter.AuthInterceptor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 上下文 当前用户API信息
|
||||
@@ -8,7 +11,7 @@ import cn.crabc.core.app.entity.dto.ApiInfoDTO;
|
||||
* @author yuqf
|
||||
*/
|
||||
public class ApiThreadLocal {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiThreadLocal.class);
|
||||
private static ThreadLocal<ApiInfoDTO> apiInfo =new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
@@ -16,6 +19,7 @@ public class ApiThreadLocal {
|
||||
* @param api
|
||||
*/
|
||||
public static void set(ApiInfoDTO api){
|
||||
log.info("--> 设置上下文API信息,{}",api.getApiPath());
|
||||
apiInfo.set(api);
|
||||
}
|
||||
|
||||
@@ -37,6 +41,7 @@ public class ApiThreadLocal {
|
||||
* 清除
|
||||
*/
|
||||
public static void remove(){
|
||||
log.info("<-- 清除上下文API信息");
|
||||
apiInfo.remove();
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -5,6 +5,7 @@ import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -17,8 +18,9 @@ import java.util.UUID;
|
||||
public class JwtUtil {
|
||||
private static String header = "Authorization";
|
||||
// 令牌秘钥
|
||||
private static String secret = "crabcjfakdjfaldjflkadjlafjaldkjlaflkalk";
|
||||
private static String secret = "KLSDJIWKD27JKZDFJALKJKD82KM90DL1NWMD";
|
||||
public static final String TOKEN_PREFIX = "bearer ";
|
||||
public static long expirationTime = 1000L * 60 * 60 * 8; // 8小时过期
|
||||
|
||||
/**
|
||||
* 创建令牌
|
||||
@@ -27,7 +29,6 @@ public class JwtUtil {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("userName", userName);
|
||||
claims.put("expireTime", System.currentTimeMillis());
|
||||
return createToken(claims);
|
||||
}
|
||||
|
||||
@@ -36,6 +37,8 @@ public class JwtUtil {
|
||||
*/
|
||||
public static String createToken(Map<String, Object> claims) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
Date now = new Date();
|
||||
Date expiration = new Date(now.getTime() + expirationTime);
|
||||
try {
|
||||
return Jwts.builder()
|
||||
.header()
|
||||
@@ -43,7 +46,7 @@ public class JwtUtil {
|
||||
.add("alg", "HS256")
|
||||
.and()
|
||||
.claims(claims)
|
||||
// 令牌ID
|
||||
.expiration(expiration)
|
||||
.id(uuid)
|
||||
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
|
||||
.compact();
|
||||
File renamed without changes.
File renamed without changes.
+11
-13
@@ -1,13 +1,11 @@
|
||||
package cn.crabc.core.app.util;
|
||||
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import java.security.*;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* RSA加/解密工具类
|
||||
@@ -24,13 +22,13 @@ public class RSAUtils {
|
||||
* @throws Exception
|
||||
*/
|
||||
public static String decryptByPubKey(String pubKey, String text) throws Exception {
|
||||
byte[] bytes = Base64Utils.decodeFromString(pubKey);
|
||||
byte[] bytes = Base64.getDecoder().decode(pubKey);
|
||||
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(bytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
|
||||
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
|
||||
Cipher cipher = Cipher.getInstance(RSA);
|
||||
cipher.init(Cipher.DECRYPT_MODE, publicKey);
|
||||
byte[] result = Base64Utils.decodeFromString(text);
|
||||
byte[] result = Base64.getDecoder().decode(text);
|
||||
return new String(result);
|
||||
}
|
||||
|
||||
@@ -43,14 +41,14 @@ public class RSAUtils {
|
||||
* @throws Exception
|
||||
*/
|
||||
public static String encryptByPriKey(String priKey, String text) throws Exception {
|
||||
byte[] bytes = Base64Utils.decodeFromString(priKey);
|
||||
byte[] bytes = Base64.getDecoder().decode(priKey);
|
||||
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(bytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
|
||||
Cipher cipher = Cipher.getInstance(RSA);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
|
||||
byte[] result = cipher.doFinal(text.getBytes());
|
||||
return Base64Utils.encodeToString(result);
|
||||
return Base64.getEncoder().encodeToString(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,13 +63,13 @@ public class RSAUtils {
|
||||
if (text == null || priKey == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] bytes = Base64Utils.decodeFromString(priKey);
|
||||
byte[] bytes = Base64.getDecoder().decode(priKey);
|
||||
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(bytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
|
||||
Cipher cipher = Cipher.getInstance(RSA);
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
byte[] result = cipher.doFinal(Base64Utils.decodeFromString(text));
|
||||
byte[] result = cipher.doFinal(Base64.getDecoder().decode(text));
|
||||
return new String(result);
|
||||
}
|
||||
|
||||
@@ -86,7 +84,7 @@ public class RSAUtils {
|
||||
if (text == null || pubKey == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] bytes = Base64Utils.decodeFromString(pubKey);
|
||||
byte[] bytes = Base64.getDecoder().decode(pubKey);
|
||||
|
||||
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(bytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
|
||||
@@ -94,7 +92,7 @@ public class RSAUtils {
|
||||
Cipher cipher = Cipher.getInstance(RSA);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
byte[] result = cipher.doFinal(text.getBytes());
|
||||
return Base64Utils.encodeToString(result);
|
||||
return Base64.getEncoder().encodeToString(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,8 +107,8 @@ public class RSAUtils {
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
|
||||
String pubKey = Base64Utils.encodeToString(rsaPublicKey.getEncoded());
|
||||
String priKey = Base64Utils.encodeToString(rsaPrivateKey.getEncoded());
|
||||
String pubKey = Base64.getEncoder().encodeToString(rsaPublicKey.getEncoded());
|
||||
String priKey = Base64.getEncoder().encodeToString(rsaPrivateKey.getEncoded());
|
||||
RSAKeyPair rsaKeyPair = new RSAKeyPair();
|
||||
rsaKeyPair.setPublicKey(pubKey);
|
||||
rsaKeyPair.setPrivateKey(priKey);
|
||||
+24
-1
@@ -81,7 +81,27 @@ public class RequestUtils {
|
||||
public static String getAppCode(HttpServletRequest req) {
|
||||
String appCode = req.getHeader("appCode");
|
||||
if (appCode == null) {
|
||||
String[] codeNames = new String[]{"appCode","AppCode", "appcode", "app_code"};
|
||||
String[] codeNames = new String[]{"appCode","AppCode", "app_code"};
|
||||
for(String codeName : codeNames) {
|
||||
appCode = req.getParameter(codeName);
|
||||
if (appCode != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return appCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头和URL参数中的认证AppKey
|
||||
*
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
public static String getAppKey(HttpServletRequest req) {
|
||||
String appCode = req.getHeader("appKey");
|
||||
if (appCode == null) {
|
||||
String[] codeNames = new String[]{"appKey","AppKey", "app_key"};
|
||||
for(String codeName : codeNames) {
|
||||
appCode = req.getParameter(codeName);
|
||||
if (appCode != null) {
|
||||
@@ -123,6 +143,9 @@ public class RequestUtils {
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip) || "localhost".equals(ip)) {
|
||||
ip ="127.0.0.1";
|
||||
}
|
||||
return ip == null ? "" : ip.trim();
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,52 @@
|
||||
package cn.crabc.core.app.util;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
import java.util.HexFormat;
|
||||
|
||||
/**
|
||||
* 国密SM3加密工具类
|
||||
*
|
||||
* @author yuqf
|
||||
*/
|
||||
public class SM3Util {
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串的SM3算法哈希值
|
||||
* @param data 输入字符串
|
||||
* @return 十六进制格式的哈希值
|
||||
*/
|
||||
public static String hash(String data) {
|
||||
return hash(data.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 节数组的SM3算法哈希值
|
||||
* @param bytes 输入字节数组
|
||||
* @return 十六进制格式的哈希值
|
||||
*/
|
||||
public static String hash(byte[] bytes) {
|
||||
SM3Digest digest = new SM3Digest();
|
||||
digest.update(bytes, 0, bytes.length);
|
||||
byte[] result = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(result, 0);
|
||||
return HexFormat.of().formatHex(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证SM3签名哈希值
|
||||
* @param data 待签名字符串
|
||||
* @param sign 预期哈希值
|
||||
* @return 是否验证通过
|
||||
*/
|
||||
public static boolean verify(String data, String sign) {
|
||||
String hash = hash(data.getBytes(StandardCharsets.UTF_8));
|
||||
return hash.equalsIgnoreCase(sign);
|
||||
}
|
||||
}
|
||||
+51
-4
@@ -2,6 +2,7 @@ package cn.crabc.core.app.util;
|
||||
|
||||
|
||||
import cn.crabc.core.app.entity.BaseApiParam;
|
||||
import cn.crabc.core.app.entity.vo.ColumnParseVo;
|
||||
import cn.crabc.core.datasource.exception.CustomException;
|
||||
import com.alibaba.druid.DbType;
|
||||
import com.alibaba.druid.sql.SQLUtils;
|
||||
@@ -14,7 +15,7 @@ import com.alibaba.druid.sql.ast.statement.SQLSelect;
|
||||
import com.alibaba.druid.sql.ast.statement.SQLSelectItem;
|
||||
import com.alibaba.druid.sql.ast.statement.SQLSelectQueryBlock;
|
||||
import com.alibaba.druid.sql.ast.statement.SQLSelectStatement;
|
||||
import com.alibaba.druid.sql.dialect.clickhouse.visitor.ClickSchemaStatVisitor;
|
||||
import com.alibaba.druid.sql.dialect.clickhouse.visitor.CKStatVisitor;
|
||||
import com.alibaba.druid.sql.dialect.db2.visitor.DB2SchemaStatVisitor;
|
||||
import com.alibaba.druid.sql.dialect.hive.visitor.HiveSchemaStatVisitor;
|
||||
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlSchemaStatVisitor;
|
||||
@@ -195,7 +196,7 @@ public class SQLUtil {
|
||||
visitor = new DB2SchemaStatVisitor();
|
||||
break;
|
||||
case "clickhouse":
|
||||
visitor = new ClickSchemaStatVisitor();
|
||||
visitor = new CKStatVisitor();
|
||||
break;
|
||||
case "hive":
|
||||
visitor = new HiveSchemaStatVisitor();
|
||||
@@ -329,10 +330,10 @@ public class SQLUtil {
|
||||
if (reqParams instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) reqParams;
|
||||
for (BaseApiParam param : paramsConfig) {
|
||||
if (param.getRequired() && !paramMap.containsKey(param.getParamName())) {
|
||||
if ("Y".equals(param.getRequired()) && !paramMap.containsKey(param.getParamName())) {
|
||||
// 必传参数没有传,则报错提示
|
||||
throw new CustomException(52005, "请求参数缺少!");
|
||||
} else if (!param.getRequired() && !paramMap.containsKey(param.getParamName())) {
|
||||
} else if ("N".equals(param.getRequired()) && !paramMap.containsKey(param.getParamName())) {
|
||||
// 非比传参数,去掉SQL中相应的条件
|
||||
sql = regexSql(sql, param.getParamName());
|
||||
}
|
||||
@@ -416,4 +417,50 @@ public class SQLUtil {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static Set<String> extractForeachParams(String sql) {
|
||||
Set<String> params = new HashSet<>();
|
||||
Pattern pattern = Pattern.compile("collection='(.*?)'");
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
params.add(matcher.group(1));
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
public static Set<String> extractIfParams(String sql) {
|
||||
Set<String> params = new HashSet<>();
|
||||
Pattern pattern = Pattern.compile(" test=['\"](\\w+)\\s*(?:!=|>=|<=|==|<|>|<|<=|>|>=)");
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
params.add(matcher.group(1));
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
public static String completeSql(String sql) {
|
||||
sql = sql.trim().toLowerCase();
|
||||
if (sql.endsWith("from")) {
|
||||
return sql + " test";
|
||||
} else if (sql.endsWith("where")) {
|
||||
return sql + " 1=1";
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
public static Set<String> parseResultColumns(String sql, String datasourceType) {
|
||||
Set<String> resNames = SQLUtil.analyzeSQL(sql.trim(), datasourceType);
|
||||
if (resNames.isEmpty()) {
|
||||
sql = SQLUtil.checkTable(sql, " test ");
|
||||
resNames = SQLUtil.analyzeSQL(sql.trim(), datasourceType);
|
||||
}
|
||||
return resNames;
|
||||
}
|
||||
|
||||
public static ColumnParseVo buildColumnInfo(String name) {
|
||||
ColumnParseVo column = new ColumnParseVo();
|
||||
column.setColName(name);
|
||||
column.setColType("String");
|
||||
column.setItemIndex(0);
|
||||
return column;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+41
-16
@@ -23,12 +23,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
i.update_by,
|
||||
i.update_time,
|
||||
i.release_time,
|
||||
i.show_sql_script,
|
||||
d.datasource_name
|
||||
g.group_name
|
||||
FROM
|
||||
base_api_info i
|
||||
left join base_datasource d
|
||||
on i.datasource_id = d.datasource_id
|
||||
left join base_group g
|
||||
on i.group_id = g.group_id
|
||||
<where>
|
||||
<if test="apiName !=null and apiName !=''">
|
||||
(i.api_name like concat('%',#{apiName},'%')
|
||||
@@ -49,7 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
api_status,
|
||||
group_id
|
||||
FROM
|
||||
base_api_info where api_status !='history'
|
||||
base_api_info where api_status in ('edit','release')
|
||||
<if test="groupId !=null">
|
||||
and group_id = #{groupId}
|
||||
</if>
|
||||
@@ -69,8 +68,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
base_app_api aa on
|
||||
ai.api_id = aa.api_id
|
||||
WHERE
|
||||
ai.enabled = 1
|
||||
AND ai.api_status = 'release'
|
||||
ai.api_status = 'release'
|
||||
<if test="appId != null">
|
||||
and aa.app_id = #{appId}
|
||||
</if>
|
||||
@@ -91,7 +89,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
remarks,
|
||||
version,
|
||||
page_setup,
|
||||
show_sql_script,
|
||||
datasource_id,
|
||||
datasource_type,
|
||||
schema_name,
|
||||
@@ -107,6 +104,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getApiDetail" resultType="cn.crabc.core.app.entity.dto.ApiInfoDTO">
|
||||
SELECT
|
||||
api_id,
|
||||
api_name,
|
||||
api_path,
|
||||
api_type,
|
||||
api_method,
|
||||
auth_type,
|
||||
api_status,
|
||||
enabled,
|
||||
group_id,
|
||||
tenant_id,
|
||||
remarks,
|
||||
version,
|
||||
page_setup,
|
||||
datasource_id,
|
||||
datasource_type,
|
||||
schema_name,
|
||||
sql_script,
|
||||
sql_type,
|
||||
result_type
|
||||
FROM
|
||||
base_api_info
|
||||
WHERE
|
||||
parent_id = 0 AND api_status ='release' and api_path =#{apiPath} and api_method =#{method}
|
||||
</select>
|
||||
|
||||
<select id="selectApiById" resultType="cn.crabc.core.app.entity.BaseApiInfo">
|
||||
SELECT
|
||||
api_id,
|
||||
@@ -128,8 +152,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
datasource_id,
|
||||
datasource_type,
|
||||
schema_name,
|
||||
table_name,
|
||||
sql_script,
|
||||
show_sql_script,
|
||||
page_setup,
|
||||
sql_type,
|
||||
result_type
|
||||
@@ -154,7 +178,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
b.page_setup,
|
||||
b.create_time,
|
||||
b.sql_script,
|
||||
b.show_sql_script,
|
||||
b.update_time,
|
||||
b.release_time,
|
||||
b.version
|
||||
@@ -187,14 +210,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="sqlType != null and sqlType != ''">sql_type,</if>
|
||||
<if test="resultType != null and resultType != ''">result_type,</if>
|
||||
<if test="sqlScript != null and sqlScript !=''">sql_script,</if>
|
||||
<if test="showSqlScript != null and showSqlScript !=''">show_sql_script,</if>
|
||||
<if test="datasourceId != null and datasourceId != ''">datasource_id,</if>
|
||||
<if test="datasourceType != null and datasourceType != ''">datasource_type,</if>
|
||||
<if test="schemaName != null and schemaName != ''">schema_name,</if>
|
||||
<if test="tableName != null and tableName != ''">table_name,</if>
|
||||
<if test="pageSetup != null">page_setup,</if>
|
||||
enabled,
|
||||
create_by,
|
||||
create_time
|
||||
create_time,
|
||||
update_time
|
||||
)values(
|
||||
<if test="apiName != null and apiName != ''">#{apiName},</if>
|
||||
<if test="apiPath != null and apiPath != ''">#{apiPath},</if>
|
||||
@@ -210,14 +234,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="sqlType != null and sqlType != ''">#{sqlType},</if>
|
||||
<if test="resultType != null and resultType != ''">#{resultType},</if>
|
||||
<if test="sqlScript != null and sqlScript !=''">#{sqlScript},</if>
|
||||
<if test="showSqlScript != null and showSqlScript !=''">#{showSqlScript},</if>
|
||||
<if test="datasourceId != null and datasourceId != ''">#{datasourceId},</if>
|
||||
<if test="datasourceType != null and datasourceType != ''">#{datasourceType},</if>
|
||||
<if test="schemaName != null and schemaName != ''">#{schemaName},</if>
|
||||
<if test="tableName != null and tableName != ''">#{tableName},</if>
|
||||
<if test="pageSetup != null">#{pageSetup},</if>
|
||||
#{enabled},
|
||||
#{createBy},
|
||||
#{createTime}
|
||||
#{createTime},
|
||||
#{updateTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
@@ -239,10 +264,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="sqlType != null and sqlType != ''">sql_type = #{sqlType},</if>
|
||||
<if test="resultType != null and resultType != ''">result_type = #{resultType},</if>
|
||||
<if test="sqlScript != null and sqlScript !=''">sql_script = #{sqlScript},</if>
|
||||
<if test="showSqlScript != null and showSqlScript !=''">show_sql_script =#{showSqlScript},</if>
|
||||
<if test="datasourceId != null and datasourceId != ''">datasource_id =#{datasourceId},</if>
|
||||
<if test="datasourceType != null and datasourceType != ''">datasource_type =#{datasourceType},</if>
|
||||
<if test="schemaName != null and schemaName != ''">schema_name =#{schemaName},</if>
|
||||
<if test="tableName != null and tableName != ''">table_name =#{tableName},</if>
|
||||
<if test="pageSetup != null">page_setup =#{pageSetup},</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
update_time = #{updateTime}
|
||||
@@ -263,7 +288,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</update>
|
||||
|
||||
<delete id="deleteApiInfo">
|
||||
DELETE FROM base_api_info where api_status ='edit' and api_id =#{apiId} and create_by =#{userId}
|
||||
DELETE FROM base_api_info where api_id =#{apiId} and create_by =#{userId}
|
||||
</delete>
|
||||
|
||||
<select id="countApi" resultType="java.lang.Integer">
|
||||
+2
-2
@@ -33,8 +33,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and request_status =#{result}
|
||||
</if>
|
||||
<if test="startTime !='' and endTime !=''">
|
||||
and request_time >=#{startTime}
|
||||
and request_time <=#{endTime}
|
||||
and DATE_FORMAT(request_time,'%Y-%m-%d') >=#{startTime}
|
||||
and DATE_FORMAT(request_time,'%Y-%m-%d') <=#{endTime}
|
||||
</if>
|
||||
</where>
|
||||
order by log_id desc
|
||||
+1
@@ -22,6 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectList" resultType="cn.crabc.core.app.entity.BaseApiParam">
|
||||
SELECT
|
||||
param_id,
|
||||
api_id,
|
||||
param_name,
|
||||
column_name,
|
||||
File renamed without changes.
File renamed without changes.
+41
-3
@@ -16,7 +16,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
password,
|
||||
remarks,
|
||||
extend,
|
||||
secret_key
|
||||
secret_key,
|
||||
min_idle,
|
||||
max_active,
|
||||
connect_timeout,
|
||||
idle_timeout,
|
||||
max_lifetime,
|
||||
keepalive_time
|
||||
FROM
|
||||
base_datasource ORDER BY datasource_id DESC
|
||||
</select>
|
||||
@@ -33,6 +39,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
'******' as password,
|
||||
remarks,
|
||||
extend,
|
||||
min_idle,
|
||||
max_active,
|
||||
connect_timeout,
|
||||
idle_timeout,
|
||||
max_lifetime,
|
||||
keepalive_time,
|
||||
update_time
|
||||
FROM
|
||||
base_datasource
|
||||
@@ -56,6 +68,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
username,
|
||||
password,
|
||||
extend,
|
||||
min_idle,
|
||||
max_active,
|
||||
connect_timeout,
|
||||
idle_timeout,
|
||||
max_lifetime,
|
||||
keepalive_time,
|
||||
secret_key
|
||||
FROM
|
||||
base_datasource WHERE datasource_id=#{datasourceId}
|
||||
@@ -79,7 +97,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="remarks != null and remarks != ''">remarks,</if>
|
||||
<if test="secretKey != null and secretKey != ''">secret_key,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
create_time
|
||||
<if test="minIdle != null">min_idle,</if>
|
||||
<if test="maxActive != null">max_active,</if>
|
||||
<if test="connectTimeout != null">connect_timeout,</if>
|
||||
<if test="idleTimeout != null">idle_timeout,</if>
|
||||
<if test="maxLifetime != null">max_lifetime,</if>
|
||||
<if test="keepaliveTime != null">keepalive_time,</if>
|
||||
create_time,
|
||||
update_time
|
||||
)values(
|
||||
<if test="datasourceId != null and datasourceId != ''">#{datasourceId},</if>
|
||||
<if test="datasourceName != null and datasourceName != ''">#{datasourceName},</if>
|
||||
@@ -93,7 +118,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="remarks != null and remarks != ''">#{remarks},</if>
|
||||
<if test="secretKey != null and secretKey != ''">#{secretKey},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
#{createTime}
|
||||
<if test="minIdle != null">#{minIdle},</if>
|
||||
<if test="maxActive != null">#{maxActive},</if>
|
||||
<if test="connectTimeout != null">#{connectTimeout},</if>
|
||||
<if test="idleTimeout != null">#{idleTimeout},</if>
|
||||
<if test="maxLifetime != null">#{maxLifetime},</if>
|
||||
<if test="keepaliveTime != null">#{keepaliveTime},</if>
|
||||
#{createTime},
|
||||
#{updateTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
@@ -110,6 +142,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="classify != null and classify != ''">classify = #{classify},</if>
|
||||
<if test="secretKey != null and secretKey != ''">secret_key = #{secretKey},</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
<if test="minIdle != null">min_idle=#{minIdle},</if>
|
||||
<if test="maxActive != null">max_active=#{maxActive},</if>
|
||||
<if test="connectTimeout != null">connect_timeout=#{connectTimeout},</if>
|
||||
<if test="idleTimeout != null">idle_timeout=#{idleTimeout},</if>
|
||||
<if test="maxLifetime != null">max_lifetime=#{maxLifetime},</if>
|
||||
<if test="keepaliveTime != null">keepalive_time=#{keepaliveTime},</if>
|
||||
update_time =#{updateTime},remarks = #{remarks}
|
||||
</set>
|
||||
WHERE datasource_id = #{datasourceId}
|
||||
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user