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;
+
+/**
+ * 外部接口清单扫描器
+ * 启动时扫描两类注解生成内存注册表:
+ *
+ * - @ExternalApi:扫描 Controller 方法(接口地址由 @RequestMapping 推导)
+ * - @ExternalCall:扫描全部 Spring Bean 方法(出站调用,地址取自注解)
+ *
+ */
+@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
+ );