From a4c99b9b0b463786b0fbed608cc287163a39d59e Mon Sep 17 00:00:00 2001 From: Peanut Date: Sat, 23 May 2026 19:19:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=8F=A3=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20-=20=E5=88=86=E9=A1=B5=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E3=80=81=E8=AF=A6=E6=83=85=E6=9F=A5=E7=9C=8B=E3=80=81=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:OpenAPI spec 解析同步、接口分页查询、代理测试(SSRF防护) - 前端:接口列表页、详情对话框(详情/测试双标签)、Token来源选择 - 服务启动自动同步接口数据,支持手动触发同步 - 测试代理路径修复:自动添加 /api 前缀以匹配后端 SSRF 校验 Co-Authored-By: Claude Opus 4.7 --- .../dto/response/ApiEndpointItemResponse.java | 1 + .../service/impl/ApiEndpointServiceImpl.java | 7 + .../2026-05-23-api-endpoint-management.md | 1684 +++++++++++++++++ ...26-05-23-api-endpoint-management-design.md | 155 ++ web-admin/src/router/index.ts | 10 +- .../views/endpoint/EndpointDetailDialog.vue | 2 +- 6 files changed, 1857 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-23-api-endpoint-management.md create mode 100644 docs/superpowers/specs/2026-05-23-api-endpoint-management-design.md diff --git a/backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java b/backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java index 82b320b..deb3d4d 100644 --- a/backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java +++ b/backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java @@ -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; diff --git a/backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java index 22584ab..4afd19a 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java @@ -130,6 +130,7 @@ public class ApiEndpointServiceImpl implements ApiEndpointService { JsonNode paths = root.path("paths"); Iterator> pathEntries = paths.fields(); int count = 0; + java.util.Set seenOperationIds = new java.util.HashSet<>(); while (pathEntries.hasNext()) { Map.Entry 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()); diff --git a/docs/superpowers/plans/2026-05-23-api-endpoint-management.md b/docs/superpowers/plans/2026-05-23-api-endpoint-management.md new file mode 100644 index 0000000..b2a6a27 --- /dev/null +++ b/docs/superpowers/plans/2026-05-23-api-endpoint-management.md @@ -0,0 +1,1684 @@ +# 接口管理功能实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 web-admin 新增接口管理菜单,后端启动时自动解析 OpenAPI JSON 入库,前端支持分页查询、详情查看、接口测试调用 + +**Architecture:** 后端 ApplicationRunner 异步解析 `/api/v3/api-docs` 写入数据库,Controller 提供分页查询/详情/同步/代理测试接口,前端从数据库查询接口数据并生成测试表单 + +**Tech Stack:** Spring Boot 2.7, MyBatis-Plus, Jackson, OpenAPI 3, Vue 3, Element Plus, Axios + +--- + +### Task 1: 数据库实体 + Mapper 层 + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java` +- Create: `backend-single/src/main/java/com/emotion/entity/ApiParam.java` +- Create: `backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java` +- Create: `backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java` + +- [ ] **Step 1: 创建 ApiEndpoint 实体** + +```java +package com.emotion.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.emotion.common.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * 接口端点实体,继承 BaseEntity 字段 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@TableName("api_endpoint") +public class ApiEndpoint extends BaseEntity { + + @com.baomidou.mybatisplus.annotation.TableField("path") + private String path; + + @com.baomidou.mybatisplus.annotation.TableField("method") + private String method; + + @com.baomidou.mybatisplus.annotation.TableField("operation_id") + private String operationId; + + @com.baomidou.mybatisplus.annotation.TableField("summary") + private String summary; + + @com.baomidou.mybatisplus.annotation.TableField("description") + private String description; + + @com.baomidou.mybatisplus.annotation.TableField("tags") + private String tags; + + @com.baomidou.mybatisplus.annotation.TableField("deprecated") + private Integer deprecated; + + @com.baomidou.mybatisplus.annotation.TableField("request_schema") + private String requestSchema; + + @com.baomidou.mybatisplus.annotation.TableField("response_schema") + private String responseSchema; +} +``` + +- [ ] **Step 2: 创建 ApiParam 实体** + +```java +package com.emotion.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.emotion.common.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * 接口参数实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@TableName("api_param") +public class ApiParam extends BaseEntity { + + @com.baomidou.mybatisplus.annotation.TableField("endpoint_id") + private String endpointId; + + @com.baomidou.mybatisplus.annotation.TableField("param_type") + private String paramType; + + @com.baomidou.mybatisplus.annotation.TableField("name") + private String name; + + @com.baomidou.mybatisplus.annotation.TableField("required") + private Integer required; + + @com.baomidou.mybatisplus.annotation.TableField("param_type_def") + private String paramTypeDef; + + @com.baomidou.mybatisplus.annotation.TableField("description") + private String description; + + @com.baomidou.mybatisplus.annotation.TableField("default_value") + private String defaultValue; + + @com.baomidou.mybatisplus.annotation.TableField("enum_values") + private String enumValues; + + @com.baomidou.mybatisplus.annotation.TableField("example") + private String example; +} +``` + +- [ ] **Step 3: 创建 Mapper 接口** + +```java +// ApiEndpointMapper.java +package com.emotion.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.emotion.entity.ApiEndpoint; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ApiEndpointMapper extends BaseMapper { +} + +// ApiParamMapper.java +package com.emotion.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.emotion.entity.ApiParam; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ApiParamMapper extends BaseMapper { +} +``` + +- [ ] **Step 4: 提交** + +```bash +git add backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java +git add backend-single/src/main/java/com/emotion/entity/ApiParam.java +git add backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java +git add backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java +git commit -m "feat: 添加接口端点实体和Mapper" +``` + +--- + +### Task 2: DTO 层 — Request/Response + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java` +- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java` +- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java` +- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java` +- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java` + +- [ ] **Step 1: 创建 ApiEndpointListRequest(分页查询入参)** + +```java +package com.emotion.dto.request; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 接口端点分页查询请求 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ApiEndpointListRequest extends BasePageRequest { + + /** + * HTTP 方法过滤:GET/POST/PUT/DELETE/PATCH + */ + private String method; + + /** + * 标签过滤 + */ + private String tags; + + /** + * 是否仅显示废弃接口 + */ + private Integer deprecated; +} +``` + +- [ ] **Step 2: 创建 ApiParamItemResponse(参数详情项)** + +```java +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 接口参数详情项 + */ +@Data +public class ApiParamItemResponse { + + private String paramType; + private String name; + private Integer required; + private String paramTypeDef; + private String description; + private String defaultValue; + private String enumValues; + private String example; +} +``` + +- [ ] **Step 3: 创建 ApiEndpointItemResponse(列表项)** + +```java +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 接口端点列表项 + */ +@Data +public class ApiEndpointItemResponse { + + private String id; + private String path; + private String method; + private String summary; + private String tags; + private Integer deprecated; + private String createTime; +} +``` + +- [ ] **Step 4: 创建 ApiEndpointDetailResponse(详情+参数)** + +```java +package com.emotion.dto.response; + +import lombok.Data; +import java.util.List; + +/** + * 接口端点详情响应 + */ +@Data +public class ApiEndpointDetailResponse { + + private String id; + private String path; + private String method; + private String operationId; + private String summary; + private String description; + private String tags; + private Integer deprecated; + private String requestSchema; + private String responseSchema; + private String createTime; + private List params; +} +``` + +- [ ] **Step 5: 创建 ApiTestProxyRequest(代理测试入参)** + +```java +package com.emotion.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotBlank; + +/** + * 代理测试请求参数 + */ +@Data +public class ApiTestProxyRequest { + + @NotBlank(message = "请求方法不能为空") + private String method; + + @NotBlank(message = "接口路径不能为空") + private String path; + + private String body; + private java.util.Map headers; + private java.util.Map params; + private Integer timeoutSeconds; +} +``` + +- [ ] **Step 6: 提交** + +```bash +git add backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java +git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java +git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java +git add backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java +git add backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java +git commit -m "feat: 添加接口管理DTO" +``` + +--- + +### Task 3: Service 层 — 核心解析与同步逻辑 + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointService.java` +- Create: `backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java` + +- [ ] **Step 1: 创建 Service 接口** + +```java +package com.emotion.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.dto.request.ApiEndpointListRequest; +import com.emotion.dto.response.ApiEndpointDetailResponse; +import com.emotion.dto.response.ApiEndpointItemResponse; + +/** + * 接口端点服务 + */ +public interface ApiEndpointService { + + /** + * 分页查询接口列表 + */ + IPage getPage(ApiEndpointListRequest request); + + /** + * 查询接口详情(含参数) + */ + ApiEndpointDetailResponse getDetail(String operationId); + + /** + * 从 OpenAPI spec 同步接口数据 + */ + void syncFromOpenApi(); +} +``` + +- [ ] **Step 2: 创建 ServiceImpl(含 OpenAPI 解析逻辑)** + +```java +package com.emotion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.emotion.common.PageResult; +import com.emotion.dto.request.ApiEndpointListRequest; +import com.emotion.dto.response.ApiEndpointDetailResponse; +import com.emotion.dto.response.ApiEndpointItemResponse; +import com.emotion.dto.response.ApiParamItemResponse; +import com.emotion.entity.ApiEndpoint; +import com.emotion.entity.ApiParam; +import com.emotion.mapper.ApiEndpointMapper; +import com.emotion.mapper.ApiParamMapper; +import com.emotion.service.ApiEndpointService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * 接口端点服务实现 + */ +@Service +public class ApiEndpointServiceImpl implements ApiEndpointService { + + private static final Logger log = LoggerFactory.getLogger(ApiEndpointServiceImpl.class); + private static final int MAX_REF_DEPTH = 10; + + @Autowired + private ApiEndpointMapper endpointMapper; + + @Autowired + private ApiParamMapper paramMapper; + + @Value("${server.port:19089}") + private int serverPort; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Override + public IPage getPage(ApiEndpointListRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(ApiEndpoint::getPath, request.getKeyword()) + .or().like(ApiEndpoint::getSummary, request.getKeyword()) + .or().like(ApiEndpoint::getOperationId, request.getKeyword())); + } + if (StringUtils.hasText(request.getMethod())) { + wrapper.eq(ApiEndpoint::getMethod, request.getMethod()); + } + if (StringUtils.hasText(request.getTags())) { + wrapper.like(ApiEndpoint::getTags, request.getTags()); + } + if (request.getDeprecated() != null) { + wrapper.eq(ApiEndpoint::getDeprecated, request.getDeprecated()); + } + + wrapper.orderByDesc(ApiEndpoint::getCreateTime); + + IPage result = endpointMapper.selectPage(page, wrapper); + return result.convert(this::toItemResponse); + } + + @Override + public ApiEndpointDetailResponse getDetail(String operationId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ApiEndpoint::getOperationId, operationId); + ApiEndpoint endpoint = endpointMapper.selectOne(wrapper); + if (endpoint == null) return null; + + ApiEndpointDetailResponse response = toDetailResponse(endpoint); + + // 查询参数列表 + LambdaQueryWrapper paramWrapper = new LambdaQueryWrapper<>(); + paramWrapper.eq(ApiParam::getEndpointId, endpoint.getId()); + paramWrapper.orderByAsc(ApiParam::getName); + List params = paramMapper.selectList(paramWrapper); + response.setParams(params.stream().map(this::toParamItemResponse).toList()); + + return response; + } + + @Override + @Transactional + public void syncFromOpenApi() { + String url = "http://127.0.0.1:" + serverPort + "/api/v3/api-docs"; + log.info("开始同步接口数据,URL: {}", url); + + String json; + try { + json = restTemplate.getForObject(url, String.class); + } catch (Exception e) { + log.warn("获取 OpenAPI spec 失败: {}", e.getMessage()); + return; + } + + if (json == null || json.isBlank()) { + log.warn("OpenAPI spec 为空"); + return; + } + + try { + JsonNode root = objectMapper.readTree(json); + JsonNode components = root.path("components"); + JsonNode schemas = components.path("schemas"); + + // 全量删除旧数据 + paramMapper.delete(null); + endpointMapper.delete(null); + log.info("已清空旧接口数据"); + + JsonNode paths = root.path("paths"); + Iterator> pathEntries = paths.fields(); + int count = 0; + + while (pathEntries.hasNext()) { + Map.Entry pathEntry = pathEntries.next(); + String path = pathEntry.getKey(); + JsonNode methods = pathEntry.getValue(); + + for (Iterator it = methods.fieldNames(); it.hasNext(); ) { + String method = it.next().toUpperCase(); + JsonNode endpoint = methods.get(method); + + String operationId = endpoint.path("operationId").asText(); + if (operationId.isEmpty()) continue; + + ApiEndpoint apiEndpoint = ApiEndpoint.builder() + .path(path) + .method(method) + .operationId(operationId) + .summary(endpoint.path("summary").asText(null)) + .description(endpoint.path("description").asText(null)) + .deprecated(endpoint.path("deprecated").asBoolean(false) ? 1 : 0) + .build(); + + // 解析 tags + JsonNode tagsNode = endpoint.path("tags"); + if (tagsNode.isArray() && tagsNode.size() > 0) { + List tagList = new ArrayList<>(); + for (JsonNode t : tagsNode) tagList.add(t.asText()); + apiEndpoint.setTags(String.join(",", tagList)); + } + + // 解析 parameters + List paramList = new ArrayList<>(); + JsonNode parameters = endpoint.path("parameters"); + if (parameters.isArray()) { + for (JsonNode param : parameters) { + ApiParam apiParam = parseParam(param, schemas, 0); + if (apiParam != null) { + // 延迟设置 endpointId + paramList.add(apiParam); + } + } + } + + // 解析 requestBody schema + JsonNode requestBody = endpoint.path("requestBody"); + if (!requestBody.isMissingNode()) { + JsonNode content = requestBody.path("content"); + if (!content.isMissingNode()) { + apiEndpoint.setRequestSchema(resolveSchema(content, schemas, 0)); + } + } + + // 解析 responses schema + JsonNode responses = endpoint.path("responses"); + if (!responses.isMissingNode()) { + apiEndpoint.setResponseSchema(responses.toString()); + } + + endpointMapper.insert(apiEndpoint); + + // 设置 endpointId 并保存参数 + for (ApiParam p : paramList) { + p.setEndpointId(apiEndpoint.getId()); + paramMapper.insert(p); + } + + count++; + } + } + + log.info("同步完成,共同步 {} 个接口", count); + } catch (Exception e) { + log.error("同步接口数据失败", e); + throw new RuntimeException("同步接口数据失败", e); + } + } + + private ApiParam parseParam(JsonNode paramNode, JsonNode schemas, int depth) { + if (depth > MAX_REF_DEPTH) return null; + + String name = paramNode.path("name").asText(null); + if (name == null) return null; + + return ApiParam.builder() + .paramType(paramNode.path("in").asText(null)) + .name(name) + .required(paramNode.path("required").asBoolean(false) ? 1 : 0) + .paramTypeDef(paramNode.path("schema").path("type").asText(null)) + .description(paramNode.path("description").asText(null)) + .example(paramNode.path("example").asText(null)) + .build(); + } + + private String resolveSchema(JsonNode content, JsonNode schemas, int depth) { + if (depth > MAX_REF_DEPTH) return "{}"; + + JsonNode appJson = content.path("application/json"); + if (appJson.isMissingNode()) return "{}"; + + JsonNode schema = appJson.path("schema"); + return expandRef(schema, schemas, depth); + } + + private String expandRef(JsonNode node, JsonNode schemas, int depth) { + if (depth > MAX_REF_DEPTH) return "{\"$ref\": \"max depth exceeded\"}"; + + if (node.has("$ref")) { + String ref = node.path("$ref").asText(); + // 解析 #/components/schemas/Xxx + if (ref.startsWith("#/components/schemas/")) { + String schemaName = ref.substring("#/components/schemas/".length()); + JsonNode schemaNode = schemas.path(schemaName); + if (!schemaNode.isMissingNode()) { + return expandRef(schemaNode, schemas, depth + 1); + } + } + return "{\"$ref\": \"" + ref + "\"}"; + } + + if (node.has("properties")) { + JsonNode props = node.path("properties"); + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Iterator> it = props.fields(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (!first) sb.append(","); + first = false; + sb.append("\"").append(entry.getKey()).append("\":") + .append(expandRef(entry.getValue(), schemas, depth + 1)); + } + sb.append("}"); + return sb.toString(); + } + + return node.toString(); + } + + private ApiEndpointItemResponse toItemResponse(ApiEndpoint e) { + ApiEndpointItemResponse r = new ApiEndpointItemResponse(); + r.setId(e.getId()); + r.setPath(e.getPath()); + r.setMethod(e.getMethod()); + r.setSummary(e.getSummary()); + r.setTags(e.getTags()); + r.setDeprecated(e.getDeprecated()); + r.setCreateTime(e.getCreateTime() != null ? e.getCreateTime().toString() : null); + return r; + } + + private ApiEndpointDetailResponse toDetailResponse(ApiEndpoint e) { + ApiEndpointDetailResponse r = new ApiEndpointDetailResponse(); + r.setId(e.getId()); + r.setPath(e.getPath()); + r.setMethod(e.getMethod()); + r.setOperationId(e.getOperationId()); + r.setSummary(e.getSummary()); + r.setDescription(e.getDescription()); + r.setTags(e.getTags()); + r.setDeprecated(e.getDeprecated()); + r.setRequestSchema(e.getRequestSchema()); + r.setResponseSchema(e.getResponseSchema()); + r.setCreateTime(e.getCreateTime() != null ? e.getCreateTime().toString() : null); + return r; + } + + private ApiParamItemResponse toParamItemResponse(ApiParam p) { + ApiParamItemResponse r = new ApiParamItemResponse(); + r.setParamType(p.getParamType()); + r.setName(p.getName()); + r.setRequired(p.getRequired()); + r.setParamTypeDef(p.getParamTypeDef()); + r.setDescription(p.getDescription()); + r.setDefaultValue(p.getDefaultValue()); + r.setEnumValues(p.getEnumValues()); + r.setExample(p.getExample()); + return r; + } +} +``` + +- [ ] **Step 3: 提交** + +```bash +git add backend-single/src/main/java/com/emotion/service/ApiEndpointService.java +git add backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java +git commit -m "feat: 添加接口端点Service层" +``` + +--- + +### Task 4: ApplicationRunner — 启动自动同步 + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java` + +- [ ] **Step 1: 创建 SyncRunner** + +```java +package com.emotion.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 启动时自动同步接口数据 + */ +@Component +public class ApiEndpointSyncRunner implements ApplicationRunner { + + private static final Logger log = LoggerFactory.getLogger(ApiEndpointSyncRunner.class); + + @Autowired + private ApiEndpointService apiEndpointService; + + @Async + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("ApplicationRunner: 开始异步同步接口数据"); + try { + apiEndpointService.syncFromOpenApi(); + log.info("ApplicationRunner: 接口数据同步完成"); + } catch (Exception e) { + log.warn("ApplicationRunner: 接口数据同步失败: {}", e.getMessage()); + } + } +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java +git commit -m "feat: 添加启动自动同步接口数据" +``` + +--- + +### Task 5: Controller 层 — 分页查询 + 详情 + 手动同步 + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java` + +- [ ] **Step 1: 创建 Controller** + +注意:路由前缀为 `/admin/endpoint`,无 `/api` 前缀。`@RequestParam` 传递参数。 + +```java +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.ApiEndpointListRequest; +import com.emotion.dto.response.ApiEndpointDetailResponse; +import com.emotion.dto.response.ApiEndpointItemResponse; +import com.emotion.service.ApiEndpointService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * 接口端点控制器 + */ +@RestController +@RequestMapping("/admin/endpoint") +@Tag(name = "接口管理", description = "接口发现、同步和测试") +public class ApiEndpointController { + + @Autowired + private ApiEndpointService apiEndpointService; + + @Operation(summary = "分页查询接口列表", description = "支持关键词、方法、标签过滤") + @PostMapping("/list") + public Result> list(@Validated @RequestBody ApiEndpointListRequest request) { + IPage page = apiEndpointService.getPage(request); + PageResult result = new PageResult<>( + page.getRecords(), page.getCurrent(), page.getSize(), page.getTotal() + ); + return Result.success(result); + } + + @Operation(summary = "查询接口详情", description = "返回接口详情和参数列表") + @GetMapping("/detail") + public Result detail(@RequestParam String operationId) { + ApiEndpointDetailResponse response = apiEndpointService.getDetail(operationId); + if (response == null) { + return Result.notFound("接口不存在"); + } + return Result.success(response); + } + + @Operation(summary = "手动触发同步", description = "异步执行,返回taskId") + @PostMapping("/sync") + public Result> sync() { + Map result = new HashMap<>(); + result.put("taskId", "sync-" + System.currentTimeMillis()); + // 异步执行 + new Thread(() -> { + try { + apiEndpointService.syncFromOpenApi(); + } catch (Exception e) { + // 异常已在 service 层记录 + } + }).start(); + return Result.success("同步任务已提交", result); + } +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java +git commit -m "feat: 添加接口管理Controller" +``` + +--- + +### Task 6: Controller 层 — 代理测试接口 + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java` +- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java` + +- [ ] **Step 1: 创建 ApiTestProxyResponse** + +```java +package com.emotion.dto.response; + +import lombok.Data; +import java.util.Map; + +/** + * 代理测试响应 + */ +@Data +public class ApiTestProxyResponse { + + private int status; + private Object body; + private Map headers; + private long duration; + private String rawBody; +} +``` + +- [ ] **Step 2: 创建 ApiTestProxyController** + +```java +package com.emotion.controller; + +import com.emotion.common.Result; +import com.emotion.dto.request.ApiTestProxyRequest; +import com.emotion.dto.response.ApiTestProxyResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.ResourceAccessException; + +import java.util.HashMap; +import java.util.Map; + +/** + * 接口代理测试控制器 + * 仅允许转发到 /api/* 路径,避免 SSRF + */ +@RestController +@RequestMapping("/admin/endpoint") +@Tag(name = "接口管理", description = "接口测试代理") +public class ApiTestProxyController { + + private static final Logger log = LoggerFactory.getLogger(ApiTestProxyController.class); + private static final int DEFAULT_TIMEOUT = 30; + private static final int MAX_RAW_BODY_LENGTH = 2000; + + @Value("${server.port:19089}") + private int serverPort; + + @Autowired + private RestTemplate restTemplate; + + @Operation(summary = "代理测试请求", description = "转发请求到本地后端并返回响应") + @PostMapping("/test") + public Result test(@Validated @RequestBody ApiTestProxyRequest request) { + // SSRF 防护:仅允许 /api/* 路径 + if (!request.getPath().startsWith("/api/")) { + return Result.error("仅允许代理 /api/* 路径的请求"); + } + + String url = "http://127.0.0.1:" + serverPort + request.getPath(); + int timeout = request.getTimeoutSeconds() != null ? request.getTimeoutSeconds() : DEFAULT_TIMEOUT; + + // 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + if (request.getHeaders() != null) { + for (Map.Entry entry : request.getHeaders().entrySet()) { + headers.set(entry.getKey(), entry.getValue()); + } + } + + // 构建请求体 + Object body = null; + if (request.getBody() != null && !request.getBody().isBlank()) { + try { + body = com.fasterxml.jackson.databind.ObjectMapper.class + .getDeclaredMethod("readValue", String.class, Class.class) + .invoke(new com.fasterxml.jackson.databind.ObjectMapper(), + request.getBody(), Object.class); + } catch (Exception e) { + return Result.error("请求体JSON格式错误: " + e.getMessage()); + } + } + + HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod().toUpperCase()); + HttpEntity entity = new HttpEntity<>(body, headers); + + long startTime = System.currentTimeMillis(); + ApiTestProxyResponse response = new ApiTestProxyResponse(); + + try { + ResponseEntity rawResponse = restTemplate.exchange(url, httpMethod, entity, String.class); + long duration = System.currentTimeMillis() - startTime; + + response.setStatus(rawResponse.getStatusCodeValue()); + response.setDuration(duration); + + // 尝试解析 JSON + try { + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + response.setBody(mapper.readValue(rawResponse.getBody(), Object.class)); + } catch (Exception e) { + // 非 JSON 响应 + String rawBody = rawResponse.getBody(); + if (rawBody != null && rawBody.length() > MAX_RAW_BODY_LENGTH) { + rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)"; + } + // HTML 转义 + if (rawBody != null) { + rawBody = rawBody.replace("<", "<").replace(">", ">"); + } + response.setRawBody(rawBody); + } + + // 收集响应头 + Map respHeaders = new HashMap<>(); + for (String key : rawResponse.getHeaders().keySet()) { + respHeaders.put(key, rawResponse.getHeaders().getFirst(key)); + } + response.setHeaders(respHeaders); + + return Result.success(response); + + } catch (ResourceAccessException e) { + if (e.getCause() instanceof java.net.SocketTimeoutException) { + return Result.error("代理请求超时(" + timeout + "s),目标接口可能响应过慢或不可达"); + } + return Result.error("代理请求失败: " + e.getMessage()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + long duration = System.currentTimeMillis() - startTime; + response.setStatus(e.getStatusCode().value()); + response.setDuration(duration); + String rawBody = e.getResponseBodyAsString(); + if (rawBody.length() > MAX_RAW_BODY_LENGTH) { + rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)"; + } + response.setRawBody(rawBody.replace("<", "<").replace(">", ">")); + return Result.success(response); + } catch (Exception e) { + return Result.error("代理请求失败: " + e.getMessage()); + } + } +} +``` + +- [ ] **Step 3: 提交** + +```bash +git add backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java +git add backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java +git commit -m "feat: 添加接口代理测试Controller" +``` + +--- + +### Task 7: 编译验证后端 + +- [ ] **Step 1: 编译验证** + +```bash +cd backend-single +mvn clean install -pl :backend-single -am -DskipTests +``` + +预期:BUILD SUCCESS + +- [ ] **Step 2: 提交(如有新增/修改文件)** + +```bash +git add -A +git commit -m "chore: 编译验证通过" +``` + +--- + +### Task 8: 前端 — API 层 + 菜单 + 路由 + +**Files:** +- Create: `web-admin/src/api/endpoint.ts` +- Modify: `web-admin/src/config/menu.ts` — 添加 "接口管理" 子菜单 +- Modify: `web-admin/src/router/index.ts` — 添加路由 + +- [ ] **Step 1: 创建 API 接口文件** + +```typescript +// web-admin/src/api/endpoint.ts +import request from '@/utils/request' + +export interface ApiEndpointItem { + id: string + path: string + method: string + summary: string + tags: string + deprecated: number + createTime: string +} + +export interface ApiParamItem { + paramType: string + name: string + required: number + paramTypeDef: string + description: string + defaultValue: string + enumValues: string + example: string +} + +export interface ApiEndpointDetail { + id: string + path: string + method: string + operationId: string + summary: string + description: string + tags: string + deprecated: number + requestSchema: string + responseSchema: string + createTime: string + params: ApiParamItem[] +} + +export interface ApiEndpointListRequest { + current: number + size: number + keyword?: string + method?: string + tags?: string + deprecated?: number +} + +export interface ApiTestProxyRequest { + method: string + path: string + body?: string + headers?: Record + params?: Record + timeoutSeconds?: number +} + +export interface ApiTestProxyResponse { + status: number + body?: any + headers?: Record + duration: number + rawBody?: string +} + +/** + * 分页查询接口列表 + */ +export function getEndpointList(data: ApiEndpointListRequest) { + return request.post('/admin/endpoint/list', data) +} + +/** + * 查询接口详情 + */ +export function getEndpointDetail(operationId: string) { + return request.get('/admin/endpoint/detail', { params: { operationId } }) +} + +/** + * 手动触发同步 + */ +export function syncEndpoints() { + return request.post('/admin/endpoint/sync') +} + +/** + * 代理测试请求 + */ +export function testEndpoint(data: ApiTestProxyRequest) { + return request.post('/admin/endpoint/test', data) +} +``` + +- [ ] **Step 2: 修改菜单配置** + +在 `web-admin/src/config/menu.ts` 的 `tools` 分组下添加 "接口管理": + +```typescript +// 在 path: '/tools' 的 children 数组中添加: +{ + path: '/endpoint', + title: '接口管理' +} +``` + +具体修改:将 menuConfig 中 tools 的 children 改为: +```typescript + children: [ + { + path: '/tools/api-tester', + title: 'API接口调用' + }, + { + path: '/endpoint', + title: '接口管理' + } + ] +``` + +- [ ] **Step 3: 修改路由配置** + +在 `web-admin/src/router/index.ts` 中添加路由: + +```typescript +{ + path: '/endpoint', + component: Layout, + redirect: '/endpoint/list', + meta: { title: '接口管理', icon: 'Tools' }, + children: [ + { + path: 'list', + name: 'EndpointList', + component: () => import('@/views/endpoint/EndpointList.vue'), + meta: { title: '接口管理' } + } + ] +} +``` + +放在 `/tools` 路由之后,404 路由之前。 + +- [ ] **Step 4: 提交** + +```bash +git add web-admin/src/api/endpoint.ts +git add web-admin/src/config/menu.ts +git add web-admin/src/router/index.ts +git commit -m "feat: 添加接口管理前端路由和菜单" +``` + +--- + +### Task 9: 前端 — 接口列表页 + +**Files:** +- Create: `web-admin/src/views/endpoint/EndpointList.vue` + +- [ ] **Step 1: 创建列表页** + +```vue + + + + + +``` + +- [ ] **Step 2: 提交** + +```bash +git add web-admin/src/views/endpoint/EndpointList.vue +git commit -m "feat: 添加接口列表页" +``` + +--- + +### Task 10: 前端 — 详情弹窗 + 测试面板 + +**Files:** +- Create: `web-admin/src/views/endpoint/EndpointDetailDialog.vue` + +- [ ] **Step 1: 创建详情弹窗(含测试标签页)** + +```vue + + + + + +``` + +- [ ] **Step 2: 提交** + +```bash +git add web-admin/src/views/endpoint/EndpointDetailDialog.vue +git commit -m "feat: 添加接口详情弹窗和测试面板" +``` + +--- + +### Task 11: 前端验证 + +- [ ] **Step 1: 启动 web-admin 开发服务器** + +先检查 5174 端口是否被占用: +```powershell +netstat -ano | findstr "5174" +``` +如果被占用,先终止进程: +```powershell +taskkill /F /PID +``` + +然后启动: +```bash +cd web-admin +npm run dev +``` + +- [ ] **Step 2: 浏览器验证** + +访问管理后台 `http://localhost:5174/emotion-museum-admin/`,登录后: +1. 导航到 **开发工具 > 接口管理** +2. 确认页面正常加载,无 console 错误 +3. 点击"手动同步"按钮,等待数据加载 +4. 确认接口列表展示正常 +5. 点击任意接口的"详情"按钮 +6. 确认详情标签页展示接口信息和参数列表 +7. 切换到"测试"标签页 +8. 确认 Token 来源选择、参数表单正常 +9. 选择一个简单 GET 接口发送测试请求 +10. 确认响应结果正常展示 + +--- + +### Task 12: 提交完成 + +- [ ] **Step 1: 检查所有改动** + +```bash +git status +``` + +- [ ] **Step 2: 提交** + +```bash +git add -A +git commit -m "feat: 完成接口管理功能(后端解析入库+前端列表/详情/测试)" +``` diff --git a/docs/superpowers/specs/2026-05-23-api-endpoint-management-design.md b/docs/superpowers/specs/2026-05-23-api-endpoint-management-design.md new file mode 100644 index 0000000..a0a051a --- /dev/null +++ b/docs/superpowers/specs/2026-05-23-api-endpoint-management-design.md @@ -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 | +| `mapper/ApiParamMapper.java` | BaseMapper | +| `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` 并提示 | +| 文件上传接口 | 本期不支持,后续迭代 | diff --git a/web-admin/src/router/index.ts b/web-admin/src/router/index.ts index 58b4662..1eed311 100644 --- a/web-admin/src/router/index.ts +++ b/web-admin/src/router/index.ts @@ -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', diff --git a/web-admin/src/views/endpoint/EndpointDetailDialog.vue b/web-admin/src/views/endpoint/EndpointDetailDialog.vue index 66a4192..f06b659 100644 --- a/web-admin/src/views/endpoint/EndpointDetailDialog.vue +++ b/web-admin/src/views/endpoint/EndpointDetailDialog.vue @@ -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,