feat: 接口管理功能 - 分页查询、详情查看、测试代理

- 后端:OpenAPI spec 解析同步、接口分页查询、代理测试(SSRF防护)
- 前端:接口列表页、详情对话框(详情/测试双标签)、Token来源选择
- 服务启动自动同步接口数据,支持手动触发同步
- 测试代理路径修复:自动添加 /api 前缀以匹配后端 SSRF 校验

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 19:19:08 +08:00
parent 128e163688
commit a4c99b9b0b
6 changed files with 1857 additions and 2 deletions
@@ -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` 并提示 |
| 文件上传接口 | 本期不支持,后续迭代 |
+8
View File
@@ -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,