feat: 接口管理功能 - 分页查询、详情查看、测试代理
- 后端:OpenAPI spec 解析同步、接口分页查询、代理测试(SSRF防护) - 前端:接口列表页、详情对话框(详情/测试双标签)、Token来源选择 - 服务启动自动同步接口数据,支持手动触发同步 - 测试代理路径修复:自动添加 /api 前缀以匹配后端 SSRF 校验 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ public class ApiEndpointItemResponse {
|
||||
private String id;
|
||||
private String path;
|
||||
private String method;
|
||||
private String operationId;
|
||||
private String summary;
|
||||
private String tags;
|
||||
private Integer deprecated;
|
||||
|
||||
@@ -130,6 +130,7 @@ public class ApiEndpointServiceImpl implements ApiEndpointService {
|
||||
JsonNode paths = root.path("paths");
|
||||
Iterator<Map.Entry<String, JsonNode>> pathEntries = paths.fields();
|
||||
int count = 0;
|
||||
java.util.Set<String> seenOperationIds = new java.util.HashSet<>();
|
||||
|
||||
while (pathEntries.hasNext()) {
|
||||
Map.Entry<String, JsonNode> pathEntry = pathEntries.next();
|
||||
@@ -146,6 +147,11 @@ public class ApiEndpointServiceImpl implements ApiEndpointService {
|
||||
|
||||
String operationId = endpointNode.path("operationId").asText();
|
||||
if (operationId.isEmpty()) continue;
|
||||
if (seenOperationIds.contains(operationId)) {
|
||||
log.warn("跳过重复 operationId: {} ({} {})", operationId, path, method);
|
||||
continue;
|
||||
}
|
||||
seenOperationIds.add(operationId);
|
||||
|
||||
ApiEndpoint apiEndpoint = ApiEndpoint.builder()
|
||||
.path(path)
|
||||
@@ -294,6 +300,7 @@ public class ApiEndpointServiceImpl implements ApiEndpointService {
|
||||
r.setId(e.getId());
|
||||
r.setPath(e.getPath());
|
||||
r.setMethod(e.getMethod());
|
||||
r.setOperationId(e.getOperationId());
|
||||
r.setSummary(e.getSummary());
|
||||
r.setTags(e.getTags());
|
||||
r.setDeprecated(e.getDeprecated());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
---
|
||||
author: Peanut
|
||||
created_at: 2026-05-23
|
||||
purpose: 设计 web-admin 接口管理功能,通过解析 OpenAPI JSON 实现接口发现、管理和测试
|
||||
---
|
||||
|
||||
# 接口管理功能设计文档
|
||||
|
||||
## 1. 架构概览
|
||||
|
||||
Spring Boot 启动时通过 ApplicationRunner 解析本机 `/api/v3/api-docs` 的 OpenAPI JSON,在事务中全量更新数据库(删除旧数据 → 解析 → 批量插入)。前端从数据库查询接口列表,支持分页、按标签/方法/路径/operationId 搜索。接口测试面板支持三种 Token 来源:当前管理员 Token、手动输入、用户端登录获取并自动保存。后端提供代理测试接口,仅限管理员访问,仅允许转发到本地 `/api/*` 路径,避免 SSRF。
|
||||
|
||||
## 2. 数据库设计
|
||||
|
||||
### 接口主表 api_endpoint
|
||||
|
||||
```sql
|
||||
CREATE TABLE api_endpoint (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
path VARCHAR(500) NOT NULL,
|
||||
method VARCHAR(10) NOT NULL,
|
||||
operation_id VARCHAR(200),
|
||||
summary VARCHAR(500),
|
||||
description TEXT,
|
||||
tags VARCHAR(500),
|
||||
deprecated TINYINT(1) DEFAULT 0,
|
||||
request_schema JSON,
|
||||
response_schema JSON,
|
||||
create_by VARCHAR(64),
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) DEFAULT 0,
|
||||
remarks VARCHAR(500),
|
||||
|
||||
UNIQUE INDEX idx_operation_id (operation_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### 参数表 api_param
|
||||
|
||||
```sql
|
||||
CREATE TABLE api_param (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
endpoint_id VARCHAR(64) NOT NULL,
|
||||
param_type VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
required TINYINT(1) DEFAULT 0,
|
||||
param_type_def VARCHAR(50),
|
||||
description VARCHAR(500),
|
||||
default_value VARCHAR(200),
|
||||
enum_values JSON,
|
||||
example VARCHAR(500),
|
||||
|
||||
INDEX idx_endpoint (endpoint_id),
|
||||
FOREIGN KEY (endpoint_id) REFERENCES api_endpoint(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
**说明**:实体类继承项目已有的 `BaseEntity`(含 id、createBy、createTime、updateBy、updateTime、isDeleted、remarks),使用 `IdType.ASSIGN_UUID` 生成 UUID 主键。
|
||||
|
||||
## 3. 后端服务设计
|
||||
|
||||
### 新增文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `entity/ApiEndpoint.java` | 继承 BaseEntity |
|
||||
| `entity/ApiParam.java` | 继承 BaseEntity |
|
||||
| `mapper/ApiEndpointMapper.java` | BaseMapper<ApiEndpoint> |
|
||||
| `mapper/ApiParamMapper.java` | BaseMapper<ApiParam> |
|
||||
| `dto/request/ApiEndpointListRequest.java` | 继承 BasePageRequest,添加 method、tags 过滤 |
|
||||
| `dto/response/ApiEndpointItemResponse.java` | 列表项 |
|
||||
| `dto/response/ApiEndpointDetailResponse.java` | 详情 + 参数列表 |
|
||||
| `dto/request/ApiTestProxyRequest.java` | 代理测试入参 |
|
||||
| `service/ApiEndpointService.java` | 解析 OpenAPI JSON、同步、查询 |
|
||||
| `service/ApiEndpointSyncRunner.java` | @Component + ApplicationRunner,启动时触发 |
|
||||
| `controller/ApiEndpointController.java` | 路由前缀 /endpoint |
|
||||
| `controller/ApiTestProxyController.java` | 路由前缀 /endpoint/test,管理员专用 |
|
||||
|
||||
### 同步逻辑(ApiEndpointService.syncFromOpenApi())
|
||||
|
||||
1. 通过 RestTemplate 请求 `http://127.0.0.1:{port}/api/v3/api-docs`(port 从配置文件读取)
|
||||
2. 若请求失败,记录 WARN 日志,不阻断启动
|
||||
3. `@Transactional` 包裹:DELETE FROM api_param → DELETE FROM api_endpoint
|
||||
4. 遍历 `paths` → `method` → 解析每个 endpoint
|
||||
5. 提取 tags、summary、description、operationId、requestSchema、responseSchema
|
||||
6. 从 `parameters` 数组解析参数列表(query/path/header/cookie)
|
||||
7. 从 `requestBody.content` 解析 body schema
|
||||
8. `$ref` 展开为内联 schema,最大展开深度 10 层
|
||||
9. 批量插入 endpoint → 批量插入 param
|
||||
|
||||
### 后端接口(无 /api 前缀,通过网关转发)
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|---|---|---|---|
|
||||
| POST | /admin/endpoint/list | 分页查询,关键词同时搜索 path/summary/operation_id | 管理员 |
|
||||
| GET | /admin/endpoint/detail | 查询详情 + 参数列表 | 管理员 |
|
||||
| POST | /admin/endpoint/sync | 手动触发同步,同步耗时较长,返回 task ID | 管理员 |
|
||||
| POST | /admin/endpoint/test | 代理测试请求,仅允许 /api/* 路径 | 管理员 |
|
||||
|
||||
### 代理测试安全控制
|
||||
|
||||
- 仅允许转发到 `/api/*` 路径,拒绝其他 URL(SSRF 防护)
|
||||
- 仅管理员可访问(AdminAuthInterceptor 拦截 /admin/**)
|
||||
- 默认 30s 超时,超时返回明确错误信息
|
||||
- 非 JSON 响应限制展示前 2000 字符,HTML 内容做转义处理
|
||||
|
||||
### 同步接口异步化
|
||||
|
||||
手动同步(`/admin/endpoint/sync`)采用 `@Async` 异步执行,返回 `{ taskId: "xxx" }`,前端通过轮询 `/admin/endpoint/sync/status/{taskId}` 获取同步进度和结果。ApplicationRunner 的启动同步同样异步,不阻塞应用启动。
|
||||
|
||||
## 4. 前端页面设计
|
||||
|
||||
### 菜单位置
|
||||
|
||||
在现有 **开发工具** 分组下新增 "接口管理" 子菜单。
|
||||
|
||||
### 接口列表页
|
||||
|
||||
- 搜索条件:关键词(覆盖 path、summary、operation_id)、HTTP 方法下拉、标签下拉
|
||||
- 表格列:方法(颜色标签区分)、路径、简述、标签、更新时间
|
||||
- 操作列:[详情] 按钮
|
||||
- 顶部:[手动同步] 按钮(触发后显示同步进度提示)
|
||||
|
||||
### 接口详情/测试弹窗
|
||||
|
||||
点击"详情"弹出对话框,包含两个标签页:
|
||||
|
||||
**详情标签**:
|
||||
- 路径、方法、描述
|
||||
- 参数列表表格(类型、名称、必填、描述、示例)
|
||||
- 响应结构 JSON 展示
|
||||
|
||||
**测试标签**:
|
||||
- Token 来源选择(单选):
|
||||
- 使用当前管理员 Token(默认)
|
||||
- 手动输入文本框
|
||||
- 用户端登录获取(输入手机号/验证码,成功后自动保存 Token 到 localStorage)
|
||||
- 参数表单:根据接口参数自动生成输入框
|
||||
- [发送请求] 按钮
|
||||
- 响应结果区域:状态码、耗时、JSON 格式化展示
|
||||
|
||||
## 5. 错误处理与边界情况
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|---|---|
|
||||
| `/v3/api-docs` 请求失败 | 记录 WARN 日志,不阻断启动,提供手动同步 |
|
||||
| 同步中途异常 | `@Transactional` 回滚,下次同步全量替换 |
|
||||
| 数据库为空 | 前端显示空状态提示 + 手动同步按钮 |
|
||||
| 测试代理超时 | 返回 `Result.error("代理请求超时(30s),目标接口可能响应过慢或不可达")` |
|
||||
| 非 JSON 响应 | 展示前 2000 字符,HTML 转义,二进制返回类型提示 |
|
||||
| Token 保存 | 管理员 Token 走现有机制,手动 Token 仅内存使用,用户端 Token 存 localStorage |
|
||||
| `$ref` 循环引用 | 最大展开深度 10 层,超限保留 `$ref` 并提示 |
|
||||
| 文件上传接口 | 本期不支持,后续迭代 |
|
||||
@@ -107,7 +107,15 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'ApiTester',
|
||||
component: () => import('@/views/tools/ApiTester.vue'),
|
||||
meta: { title: 'API接口调用' }
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/endpoint',
|
||||
component: Layout,
|
||||
redirect: '/endpoint/list',
|
||||
meta: { title: '接口管理', icon: 'Tools' },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'EndpointList',
|
||||
|
||||
@@ -175,7 +175,7 @@ const handleTest = async () => {
|
||||
|
||||
const res: any = await testEndpoint({
|
||||
method: testForm.value.method,
|
||||
path: testForm.value.path,
|
||||
path: '/api' + testForm.value.path,
|
||||
body: testForm.value.body || undefined,
|
||||
headers,
|
||||
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
||||
|
||||
Reference in New Issue
Block a user