From 522f269f594eb927c381b56150a11f489154ff2a Mon Sep 17 00:00:00 2001 From: wsm <2746563060@qq.com> Date: Tue, 11 Aug 2026 15:14:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(extapi):=20=E6=96=B0=E5=A2=9E=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E6=8E=A5=E5=8F=A3=E6=B8=85=E5=8D=95=E6=B3=A8=E8=A7=A3?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E4=B8=8E=E5=B1=95=E7=A4=BA=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 @ExternalApi/@ExternalCall 方法级注解,启动时扫描生成内存注册表。 提供 /sys/extApi/list 展示、exportXls(AutoPoi) 与 exportMarkdown 导出; 给 TaskApiController.countTaskforporter 打 @ExternalApi 示范并补菜单 SQL。 --- .../common/aspect/annotation/ExternalApi.java | 32 +++ .../annotation/ExternalApiAuthType.java | 13 ++ .../aspect/annotation/ExternalCall.java | 38 +++ .../config/extapi/ExternalApiRegistry.java | 30 +++ .../config/extapi/ExternalApiScanner.java | 216 ++++++++++++++++++ .../config/extapi/model/ExternalApiInfo.java | 55 +++++ .../config/extapi/model/ExternalApiType.java | 11 + .../openapi/controller/TaskApiController.java | 3 + .../controller/ExternalApiController.java | 115 ++++++++++ .../dml/20260811_外部接口清单菜单.sql | 30 +++ 10 files changed, 543 insertions(+) create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApi.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApiAuthType.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalCall.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiRegistry.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiScanner.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiInfo.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiType.java create mode 100644 jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/ExternalApiController.java create mode 100644 sql-changelog/dml/20260811_外部接口清单菜单.sql diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApi.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApi.java new file mode 100644 index 0000000..29cfeaf --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApi.java @@ -0,0 +1,32 @@ +package org.jeecg.common.aspect.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 对外提供的接口(供外部系统调用) + *

标注在 Controller 方法上;接口地址与 HTTP 方法由框架从 @RequestMapping 自动推导。 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ExternalApi { + + /** 接口名称/用途 */ + String name(); + + /** 调用方系统(谁在调我) */ + String caller() default ""; + + /** 认证机制 */ + ExternalApiAuthType auth() default ExternalApiAuthType.NONE; + + /** 数据范围说明(返回/涉及什么数据) */ + String dataScope() default ""; + + /** 备注 */ + String remark() default ""; +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApiAuthType.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApiAuthType.java new file mode 100644 index 0000000..2c4105e --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalApiAuthType.java @@ -0,0 +1,13 @@ +package org.jeecg.common.aspect.annotation; + +/** + * 外部接口认证机制 + */ +public enum ExternalApiAuthType { + /** 无需认证(裸调用) */ + NONE, + /** 共享密钥签名(X-Sign / X-TIMESTAMP) */ + SIGN, + /** JWT 登录态 */ + JWT +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalCall.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalCall.java new file mode 100644 index 0000000..5c16d6d --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/ExternalCall.java @@ -0,0 +1,38 @@ +package org.jeecg.common.aspect.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 调用外部系统的接口(本系统调用其他系统) + *

标注在发起出站调用的方法上(Service、客户端等)。 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ExternalCall { + + /** 用途 */ + String name(); + + /** 被调用的外部系统 */ + String system(); + + /** 被调接口地址 */ + String url(); + + /** HTTP 方法 */ + String httpMethod() default "GET"; + + /** 对方要求的认证机制 */ + ExternalApiAuthType auth() default ExternalApiAuthType.NONE; + + /** 交互的数据范围说明 */ + String dataScope() default ""; + + /** 备注 */ + String remark() default ""; +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiRegistry.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiRegistry.java new file mode 100644 index 0000000..9ee02f2 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiRegistry.java @@ -0,0 +1,30 @@ +package org.jeecg.config.extapi; + +import org.jeecg.config.extapi.model.ExternalApiInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 外部接口清单内存注册表 + *

启动时由 {@link ExternalApiScanner} 填充,运行时只读。 + */ +public class ExternalApiRegistry { + + private static final List REGISTRY = new ArrayList<>(); + + private ExternalApiRegistry() { + } + + public static synchronized void set(List list) { + REGISTRY.clear(); + if (list != null) { + REGISTRY.addAll(list); + } + } + + public static List get() { + return Collections.unmodifiableList(REGISTRY); + } +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiScanner.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiScanner.java new file mode 100644 index 0000000..39902d3 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/ExternalApiScanner.java @@ -0,0 +1,216 @@ +package org.jeecg.config.extapi; + +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.aspect.annotation.ExternalApi; +import org.jeecg.common.aspect.annotation.ExternalApiAuthType; +import org.jeecg.common.aspect.annotation.ExternalCall; +import org.jeecg.config.extapi.model.ExternalApiInfo; +import org.jeecg.config.extapi.model.ExternalApiType; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * 外部接口清单扫描器 + *

启动时扫描两类注解生成内存注册表: + *

+ */ +@Slf4j +@Component +public class ExternalApiScanner implements InitializingBean { + + private final RequestMappingHandlerMapping requestMappingHandlerMapping; + private final ApplicationContext applicationContext; + + public ExternalApiScanner(RequestMappingHandlerMapping requestMappingHandlerMapping, + ApplicationContext applicationContext) { + this.requestMappingHandlerMapping = requestMappingHandlerMapping; + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() { + long start = System.currentTimeMillis(); + List list = new ArrayList<>(); + scanExposeApis(list); + scanCallApis(list); + ExternalApiRegistry.set(list); + long cost = System.currentTimeMillis() - start; + log.info("【外部接口清单-扫描器】初始化完成, 对外提供 {} 条, 对外调用 {} 条, 耗时 {} 毫秒", + list.stream().filter(i -> ExternalApiType.EXPOSE.name().equals(i.getType())).count(), + list.stream().filter(i -> ExternalApiType.CALL.name().equals(i.getType())).count(), + cost); + } + + /** + * 扫描对外提供接口(Controller 方法) + */ + private void scanExposeApis(List list) { + Set> controllers = requestMappingHandlerMapping.getHandlerMethods().values().stream() + .map(HandlerMethod::getBeanType) + .collect(java.util.stream.Collectors.toSet()); + for (Class controller : controllers) { + RequestMapping base = controller.getAnnotation(RequestMapping.class); + String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{}; + for (Method method : controller.getDeclaredMethods()) { + ExternalApi ann = method.getAnnotation(ExternalApi.class); + if (ann == null) { + continue; + } + RequestMappingInfo info = resolveRequestMapping(method); + if (info == null) { + log.warn("【外部接口清单-扫描器】@ExternalApi 方法未找到 HTTP 映射, 跳过, method: {}.{}", + controller.getSimpleName(), method.getName()); + continue; + } + for (String url : rebuildUrl(baseUrl, info.uris)) { + ExternalApiInfo item = new ExternalApiInfo(); + item.setType(ExternalApiType.EXPOSE.name()); + item.setName(ann.name()); + item.setUrl(url); + item.setHttpMethod(info.httpMethod); + item.setCallerSystem(ann.caller()); + item.setTargetSystem("本系统"); + item.setDataFlow("外部系统→本系统(请求入站);本系统→外部系统(数据出站)"); + item.setDataScope(ann.dataScope()); + item.setAuth(authLabel(ann.auth())); + item.setSource(controller.getName() + "#" + method.getName()); + item.setRemark(ann.remark()); + list.add(item); + } + } + } + } + + /** + * 扫描对外调用接口(全部 Spring Bean 方法) + */ + private void scanCallApis(List list) { + Set visited = new HashSet<>(); + String[] beanNames = applicationContext.getBeanDefinitionNames(); + for (String beanName : beanNames) { + Class type = applicationContext.getType(beanName); + if (type == null || type.getName().startsWith("org.springframework") + || type.getName().startsWith("org.apache") + || type.getName().startsWith("java.") + || type.getName().startsWith("lombok")) { + continue; + } + Class target = type.getName().contains("$$") && type.getSuperclass() != null + ? type.getSuperclass() : type; + for (Method method : target.getDeclaredMethods()) { + ExternalCall ann = method.getAnnotation(ExternalCall.class); + if (ann == null) { + continue; + } + String key = target.getName() + "#" + method.getName(); + if (!visited.add(key)) { + continue; + } + ExternalApiInfo item = new ExternalApiInfo(); + item.setType(ExternalApiType.CALL.name()); + item.setName(ann.name()); + item.setUrl(ann.url()); + item.setHttpMethod(ann.httpMethod()); + item.setCallerSystem("本系统"); + item.setTargetSystem(ann.system()); + item.setDataFlow("本系统→外部系统(数据出站)"); + item.setDataScope(ann.dataScope()); + item.setAuth(authLabel(ann.auth())); + item.setSource(target.getName() + "#" + method.getName()); + item.setRemark(ann.remark()); + list.add(item); + } + } + } + + /** + * 从方法的 HTTP 映射注解解析 URL 与 HTTP 方法 + */ + private RequestMappingInfo resolveRequestMapping(Method method) { + RequestMapping rm = method.getAnnotation(RequestMapping.class); + if (rm != null) { + return new RequestMappingInfo(rm.value(), rm.method().length > 0 ? rm.method()[0].name() : "ALL"); + } + GetMapping get = method.getAnnotation(GetMapping.class); + if (get != null) { + return new RequestMappingInfo(get.value(), "GET"); + } + PostMapping post = method.getAnnotation(PostMapping.class); + if (post != null) { + return new RequestMappingInfo(post.value(), "POST"); + } + PutMapping put = method.getAnnotation(PutMapping.class); + if (put != null) { + return new RequestMappingInfo(put.value(), "PUT"); + } + DeleteMapping del = method.getAnnotation(DeleteMapping.class); + if (del != null) { + return new RequestMappingInfo(del.value(), "DELETE"); + } + PatchMapping patch = method.getAnnotation(PatchMapping.class); + if (patch != null) { + return new RequestMappingInfo(patch.value(), "PATCH"); + } + return null; + } + + private List rebuildUrl(String[] bases, String[] uris) { + List urls = new ArrayList<>(); + if (bases.length > 0) { + for (String base : bases) { + for (String uri : uris) { + urls.add(prefix(base) + prefix(uri.replaceAll("\\{.*?}", "*"))); + } + } + } else { + for (String uri : uris) { + urls.add(prefix(uri.replaceAll("\\{.*?}", "*"))); + } + } + return urls; + } + + private String prefix(String seg) { + return seg.startsWith("/") ? seg : "/" + seg; + } + + private String authLabel(ExternalApiAuthType auth) { + switch (auth) { + case SIGN: + return "签名"; + case JWT: + return "JWT"; + default: + return "无"; + } + } + + private static class RequestMappingInfo { + private final String[] uris; + private final String httpMethod; + + private RequestMappingInfo(String[] uris, String httpMethod) { + this.uris = uris; + this.httpMethod = httpMethod; + } + } +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiInfo.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiInfo.java new file mode 100644 index 0000000..93872bb --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiInfo.java @@ -0,0 +1,55 @@ +package org.jeecg.config.extapi.model; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * 外部接口清单条目 + */ +@Data +public class ExternalApiInfo { + + /** 方向类型(对外提供 / 对外调用) */ + @Excel(name = "方向", width = 12) + private String type; + + /** 接口名称/用途 */ + @Excel(name = "接口名称", width = 28) + private String name; + + /** 接口地址 */ + @Excel(name = "接口地址", width = 50) + private String url; + + /** HTTP 方法 */ + @Excel(name = "HTTP方法", width = 10) + private String httpMethod; + + /** 调用方系统(谁在调) */ + @Excel(name = "调用方系统", width = 20) + private String callerSystem; + + /** 被调系统(调谁) */ + @Excel(name = "被调系统", width = 20) + private String targetSystem; + + /** 数据流向 */ + @Excel(name = "数据流向", width = 45) + private String dataFlow; + + /** 数据范围 */ + @Excel(name = "数据范围", width = 45) + private String dataScope; + + /** 认证机制 */ + @Excel(name = "认证机制", width = 12) + private String auth; + + /** 代码位置(类#方法) */ + @Excel(name = "代码位置", width = 70) + private String source; + + /** 备注 */ + @Excel(name = "备注", width = 30) + private String remark; +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiType.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiType.java new file mode 100644 index 0000000..0f5b050 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/extapi/model/ExternalApiType.java @@ -0,0 +1,11 @@ +package org.jeecg.config.extapi.model; + +/** + * 外部接口方向类型 + */ +public enum ExternalApiType { + /** 对外提供(外部系统调用本系统) */ + EXPOSE, + /** 对外调用(本系统调用外部系统) */ + CALL +} diff --git a/jeecg-module-system/jeecg-system-api/jeecg-system-openapi/src/main/java/org/jeecg/openapi/controller/TaskApiController.java b/jeecg-module-system/jeecg-system-api/jeecg-system-openapi/src/main/java/org/jeecg/openapi/controller/TaskApiController.java index 8b739f7..21d36ad 100644 --- a/jeecg-module-system/jeecg-system-api/jeecg-system-openapi/src/main/java/org/jeecg/openapi/controller/TaskApiController.java +++ b/jeecg-module-system/jeecg-system-api/jeecg-system-openapi/src/main/java/org/jeecg/openapi/controller/TaskApiController.java @@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.api.vo.Result; +import org.jeecg.common.aspect.annotation.ExternalApi; +import org.jeecg.common.aspect.annotation.ExternalApiAuthType; import org.jeecg.common.util.oConvertUtils; import org.jeecg.openapi.service.ITaskApi; import org.springframework.web.bind.annotation.GetMapping; @@ -32,6 +34,7 @@ public class TaskApiController { *

调用方(门户)通过 username 参数传入 sys_user.username(登录账号)。 */ @Operation(summary = "门户查询待办数", description = "门户查询待办数") + @ExternalApi(name = "门户查询待办数", caller = "门户系统", auth = ExternalApiAuthType.NONE, dataScope = "仅返回待办数量,不涉及业务明细") @GetMapping("/countTaskforporter") public Result countTaskforporter(HttpServletRequest request) { String username = request.getParameter("username"); diff --git a/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/ExternalApiController.java b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/ExternalApiController.java new file mode 100644 index 0000000..5c6fcc2 --- /dev/null +++ b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/ExternalApiController.java @@ -0,0 +1,115 @@ +package org.jeecg.modules.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.config.extapi.ExternalApiRegistry; +import org.jeecg.config.extapi.model.ExternalApiInfo; +import org.jeecg.config.extapi.model.ExternalApiType; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * 外部接口清单(对外提供 / 对外调用)展示与导出 + *

数据来自 {@link ExternalApiRegistry} 内存注册表(启动时由注解扫描生成)。 + */ +@Tag(name = "外部接口清单") +@RestController +@RequestMapping("/sys/extApi") +@Slf4j +public class ExternalApiController { + + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Operation(summary = "外部接口清单列表") + @GetMapping("/list") + public Result> list() { + return Result.OK(ExternalApiRegistry.get()); + } + + @Operation(summary = "导出外部接口清单Excel") + @GetMapping("/exportXls") + public ModelAndView exportXls(HttpServletRequest request) { + // AutoPoi 导出时会 remove 已处理行(多sheet分页),必须传可变集合 + List data = new ArrayList<>(ExternalApiRegistry.get()); + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + String title = "外部接口清单"; + mv.addObject(NormalExcelConstants.FILE_NAME, title); + mv.addObject(NormalExcelConstants.CLASS, ExternalApiInfo.class); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + (sysUser == null ? "" : sysUser.getRealname()), title); + mv.addObject(NormalExcelConstants.PARAMS, exportParams); + mv.addObject(NormalExcelConstants.DATA_LIST, data); + return mv; + } + + @Operation(summary = "导出外部接口清单Markdown") + @GetMapping("/exportMarkdown") + public void exportMarkdown(HttpServletResponse response) throws IOException { + String content = buildMarkdown(); + String fileName = URLEncoder.encode("外部接口清单.md", StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20"); + response.setContentType("text/markdown;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); + OutputStream os = response.getOutputStream(); + os.write(content.getBytes(StandardCharsets.UTF_8)); + os.flush(); + os.close(); + } + + private String buildMarkdown() { + List all = ExternalApiRegistry.get(); + StringBuilder sb = new StringBuilder(); + sb.append("# 应用系统外部接口清单\n\n"); + sb.append("> 生成时间:" + LocalDateTime.now().format(FMT) + " | 认证机制:无=裸调用 / 签名=X-Sign / JWT=登录态\n\n"); + sb.append("## 一、对外提供的接口(外部系统调用本系统)\n\n"); + sb.append(tableHeader()); + all.stream().filter(i -> ExternalApiType.EXPOSE.name().equals(i.getType())).forEach(i -> sb.append(tableRow(i))); + sb.append("\n## 二、对外调用的接口(本系统调用外部系统)\n\n"); + sb.append(tableHeader()); + all.stream().filter(i -> ExternalApiType.CALL.name().equals(i.getType())).forEach(i -> sb.append(tableRow(i))); + return sb.toString(); + } + + private String tableHeader() { + return "| 接口名称 | 地址 | HTTP方法 | 调用方系统 | 被调系统 | 数据流向 | 数据范围 | 认证机制 | 代码位置 | 备注 |\n" + + "|---|---|---|---|---|---|---|---|---|---|\n"; + } + + private String tableRow(ExternalApiInfo i) { + return String.join(" | ", + cell(i.getName()), + cell(i.getUrl()), + cell(i.getHttpMethod()), + cell(i.getCallerSystem()), + cell(i.getTargetSystem()), + cell(i.getDataFlow()), + cell(i.getDataScope()), + cell(i.getAuth()), + cell(i.getSource()), + cell(i.getRemark())) + "\n"; + } + + private String cell(String v) { + return StringUtils.hasText(v) ? v.replace("|", "\\|").replace("\n", " ") : " "; + } +} diff --git a/sql-changelog/dml/20260811_外部接口清单菜单.sql b/sql-changelog/dml/20260811_外部接口清单菜单.sql new file mode 100644 index 0000000..7c2fb6d --- /dev/null +++ b/sql-changelog/dml/20260811_外部接口清单菜单.sql @@ -0,0 +1,30 @@ +-- ============================================ +-- 外部接口清单 菜单 + 角色授权(幂等) +-- 页面: src/views/super/apiList/index.vue +-- 后端: /sys/extApi/* (ExternalApiController) +-- ============================================ + +SET @menu_id = REPLACE(UUID(), '-', ''); + +INSERT INTO sys_permission + (id, parent_id, name, url, component, is_route, component_name, redirect, + menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, + keep_alive, hidden, hide_tab, description, create_by, create_time, + update_by, update_time, del_flag, rule_flag, status, internal_or_external) +SELECT + @menu_id, '', '外部接口清单', '/super/apiList', 'super/apiList/index', 1, NULL, NULL, + 1, NULL, 1, 100.00, 0, NULL, 1, + 0, 0, 0, '应用系统间接口清单(对外提供/对外调用),注解自动采集', 'admin', NOW(), + 'admin', NOW(), 0, 0, 1, 0 +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM sys_permission WHERE name = '外部接口清单' AND url = '/super/apiList'); + +-- 授权给 admin 角色 +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids) +SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', @menu_id, NULL +FROM DUAL +WHERE EXISTS (SELECT 1 FROM sys_permission WHERE id = @menu_id) + AND NOT EXISTS ( + SELECT 1 FROM sys_role_permission r + WHERE r.role_id = 'f6817f48af4fb3af11b9e8bf182f618b' AND r.permission_id = @menu_id + );