fix(gateway): route muse api through muse server

This commit is contained in:
lili 2026-06-19 07:46:15 -07:00
parent 9d5430b62c
commit 6217029989
6 changed files with 213 additions and 57 deletions

View File

@ -153,7 +153,7 @@ fork yudao-cloud:保留 `system/infra/framework/gateway/member/pay/bpm/report/mp
| **元治理/MetaSchema** (muse-module-meta) | ✅ | ❌ | ✅ | ⚠ | ✅ | ✅ | **82%** | 未实跑/可编译 | 高度一致 | high | **impact-preview facade 仅 Unavailable→写链路运行期端到端阻断**;facade-api 缺失,跨 BC 投影消费契约未建立 |
| **事件/SSE** (muse-module-events) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | **88%** | 未实跑/可编译 | 后端一致/前端漂移 | high | 前端 connectAIStream 契约漂移(被 mock 掩盖,真实联调会失败);统一事件客户端为孤儿无 UI 消费 |
| **账户/个人中心** (muse-module-member) | ✅ | ❌ | ✅ | ⚠ | ✅ | ✅ | **68%** | 未实跑/可编译 | 端点齐/约2/3运行期降级 | high | 跨 BC facade 全 Unavailable→21/33 端面运行期 `*_UNAVAILABLE`;前端仅约 21% 面且跑 MSW |
| **平台底座** (gateway/system/infra/pay/bpm/mp/report + 两前端接线) | ✅ | ✅ | ❌(设计如此) | ✅ | ❌(沿用 yudao) | ✅ | **80%** | 未实跑/可编译 | 无契约(符合设计) | high | **gateway 路由未接任何 Muse BC + 保留死路由**;pay/bpm/mp/report 显式停用 0 运行覆盖;底座 schema 未 Flyway 化 |
| **平台底座** (gateway/system/infra/pay/bpm/mp/report + 两前端接线) | ✅ | ✅ | ❌(设计如此) | ✅ | ❌(沿用 yudao) | ✅ | **80%** | 未实跑/可编译 | 无契约(符合设计) | high | gateway 配置层已补 Muse BC 路由并清理停用模块死路由(2026-06-19),但运行态转发未首跑;pay/bpm/mp/report 显式停用 0 运行覆盖;底座 schema 未 Flyway 化 |
> 说明:`api ❌` 表示该 BC 的 `-api` 子模块仅 package-info / ErrorCode(meta、account 无对外 facade-api DTO,knowledge 的 api 仅 ErrorCode,market 的 api 刻意薄属 yudao 风格,均非"缺失业务"而是"对外 RPC 契约未建立或无需");`contract ❌` 平台底座属设计上不为 yudao 基座单独定契约。
@ -377,16 +377,15 @@ fork yudao-cloud:保留 `system/infra/framework/gateway/member/pay/bpm/report/mp
### 5.8 平台底座(gateway/system/infra/pay/bpm/mp/report + 两前端接线)— 完成度 80%
**实现摘要:** 平台底座 = 对 yudao-cloud 的干净 fork,8 个核查模块全部真实完整(非空壳):system 32 admin controller/36 Impl/39 测试;infra 13/13/21;pay 19/12(PayOrderServiceImpl 610 行 18 方法);bpm 11/12/18;mp 12/9;report 4 Impl。gateway 17 java(CorsFilter/TokenAuthenticationFilter/GrayLoadBalancer/AccessLogFilter/GlobalExceptionHandler 等真实组件)。`muse-server` 是聚合启动入口,**实际装配 system/infra/content/meta/events/knowledge/market/member/ai;pay/bpm/mp/report 在 pom 中显式注释停用**(故意"默认隐藏",符合后端-02)。包名已整体 fork `cn.iocoder.yudao``cn.iocoder.muse` 无残留。前端两仓均真实接线:muse-studio 212 处 `/app-api` 引用 + 真实 client.ts/sse.ts/indexed-db.ts/typed openapi.ts;muse-admin web-antd 真实 `src/api/muse/{ai,knowledge,market,account,governance,audit,newapi,jobs}``/admin-api/muse/**` 且各带 `__tests__`
**实现摘要:** 平台底座 = 对 yudao-cloud 的干净 fork,8 个核查模块全部真实完整(非空壳):system 32 admin controller/36 Impl/39 测试;infra 13/13/21;pay 19/12(PayOrderServiceImpl 610 行 18 方法);bpm 11/12/18;mp 12/9;report 4 Impl。gateway 17 java(CorsFilter/TokenAuthenticationFilter/GrayLoadBalancer/AccessLogFilter/GlobalExceptionHandler 等真实组件)。`muse-server` 是聚合启动入口,**实际装配 system/infra/content/meta/events/knowledge/market/member/ai;pay/bpm/mp/report 在 pom 中显式注释停用**(故意"默认隐藏",符合后端-02)。2026-06-19 已补 gateway 配置层接线:`/admin-api/muse/**``/app-api/muse/**` 指向 `grayLb://muse-server`,并删除停用模块死路由;`GatewayRouteConfigurationTest` 作为回潮门禁。包名已整体 fork `cn.iocoder.yudao``cn.iocoder.muse` 无残留。前端两仓均真实接线:muse-studio 212 处 `/app-api` 引用 + 真实 client.ts/sse.ts/indexed-db.ts/typed openapi.ts;muse-admin web-antd 真实 `src/api/muse/{ai,knowledge,market,account,governance,audit,newapi,jobs}``/admin-api/muse/**` 且各带 `__tests__`
**桩与缺口:**
- **gateway/application.yaml 未为任何 Muse BC 配置路由;Muse 业务 API 当前不经网关,走 muse-server 单体——网关对 Muse 业务实际未接线**(设计写应承接 /admin-api + /app-api 路由)。
- **gateway 保留 bpm/pay/mp/report 死路由**(/admin-api/bpm 等),而这些模块已从装配注释停用,路由指向不存在的服务。
- **gateway 运行态转发未首跑**:`application.yaml` 已有 Muse 业务路由,但 `muse-server` 默认禁用 Nacos 注册,且本地默认端口与 gateway 同为 48080;当前 P1R-7 单体验收仍走 `muse-server` 直服。
- pay/bpm/mp/report 代码完整但被显式注释,**0 运行覆盖**(设计内"默认隐藏",但意味当前无运行/测试保活,有上游漂移腐化风险)。
- **平台底座 8 模块无 Muse 自有 Flyway 迁移**(sql/muse V1-V21 全是 BC schema),其 schema 仍依赖 yudao 遗留静态 SQL dump,未纳入 Flyway 受控迁移。
- mp 模块测试 0、report 仅 2(继承 yudao 现状)。`MockPayClient``UnsupportedOperationException`(yudao 原生 mock 设计,非 Muse 业务桩)。
**设计 vs 现实差异:** 高度吻合(pay/bpm/report/mp"保留默认隐藏不删依赖"逐条对应),但 **1 处真实缺口=gateway 路由未对齐 Muse**:既保留已停用模块死路由,又完全没有 content/ai/knowledge/market/meta/events 真正 Muse BC 路由。设计写"承接路由",实际未承接 BC
**设计 vs 现实差异:** 高度吻合(pay/bpm/report/mp"保留默认隐藏不删依赖"逐条对应);gateway 已在配置层承接 Muse API 路由,但运行态仍未做网关转发首跑
**"完成"声称 vs 现实:** **平台底座根本没有"完成"声称**,它是被继承的 yudao 基座;海量 P1R 文档与 24244 行 P1r* 测试全部针对 BC,与底座无关。对 BC 层这套机制经核查真实且反 churn:`P1rApiCoverageReportTest` 把 catch_all/generic_persistence/sse_placeholder/missing 列为不可标 completed 并断言 completed 恰为 147/233(其余 86 needs_verification,incomplete/blocked/placeholder 均 0);`P1rContentCoreCompletedApprovalIT` 真跑 Flyway(断言 21 迁移)+ 真实 PG 种子 + MockMvc 打真实控制器,断言分页/owner/租户隔离(跨租户 404 vs 跨 owner 403)/读路径不写 outbox;重 IT 用 `assumeTrue` 门控无 DB 时跳过而非伪绿。
@ -394,7 +393,8 @@ fork yudao-cloud:保留 `system/infra/framework/gateway/member/pay/bpm/report/mp
- `muse-server/muse-module-server/pom.xml`(显式注释停用 report/bpm/pay/mp,启用其余)
- `muse-cloud/pom.xml:1-20`(groupId=cn.iocoder.cloud,modules 含全部)
- `muse-module-pay/.../PayOrderServiceImpl.java:1-610`(真实 18 方法非空壳)
- `muse-gateway/src/main/resources/application.yaml:34-`(routes 仅 system/infra/member/bpm/report/pay/mp;**无 content/ai/knowledge/market/meta/events**;bpm/report/pay/mp 死路由)
- `muse-gateway/src/main/resources/application.yaml`(routes 含 `/admin-api/muse/**``/app-api/muse/** → grayLb://muse-server`;停用模块死路由已清理)
- `muse-gateway/src/test/java/cn/iocoder/muse/gateway/GatewayRouteConfigurationTest.java`(配置回潮门禁)
- `docs/api-contracts/openapi-base.yaml`(paths:{} 为空,仅共享组件)
- `docs/superpowers/reports/p1r-api-coverage.json`(totalOperations=233 completed=147 needsVerification=86,catchAll/genericPersistence/ssePlaceholder/missing/blocked=0;domain 无平台底座)
- `muse-server/.../P1rApiCoverageReportTest.java:42-48,287`(NON_REAL 状态不可标 completed;assertEquals(147))

View File

@ -55,7 +55,7 @@
## 三、最大共性风险(跨模块,来自基线)
1. **前端 muse-studio 严重滞后**:AI 候选闭环断链、知识库/市场生产侧 UI 缺失、个人中心约 21% 面跑 MSW——“先审后入”在用户可见层当前不可用。
2. **跨 BC facade 多为 `Unavailable` 占位** → 运行期 `*_UNAVAILABLE`
3. **gateway 路由未接任何 Muse BC + 死路由**:若误以网关为真实入口部署,Muse BC 全部 404
3. **gateway 运行态转发未首跑**:配置层已接 `/admin-api/muse/**``/app-api/muse/**``muse-server` 并清理停用模块死路由;但 `muse-server` 默认禁用 Nacos 注册,网关真实转发仍需部署/注册发现首跑验证
---
@ -162,11 +162,12 @@
- 🔧 个人中心:**✅ profile/用量/权益 + 我的购买/我的授权 + 安全事件(读+确认写路)(rendered-UI 活体证,2026-06-15)**——PersonalCenter 新增「我的购买/我的授权」渲染 market→account 投影真实记录(`useAccountPurchases`/`useAccountLicenses``/account/{purchases,licenses}`;`live-read.spec.ts` 第 5 例 chromium MSW-off 绿,断言真实资产「活体市场资产·测试」渲染——证后端 account 读端在单体可用即被 UI 消费,见 §五A 活体订正);**新增「安全事件」区**渲染 `GET /account/security-events` 真实摘要(`useAccountSecurityEvents``AccountPageResult<SecurityEventSummaryRespVO>`;eventType/severity 徽标 + 已确认/待确认状态;`live-read.spec.ts` 第 6 例 chromium MSW-off 绿,断言种子事件「活体安全事件·异地登录提醒」渲染)。**确认写路**:未确认事件展示「确认」按钮→`useAcknowledgeSecurityEvent`(每次发新 commandId,后端按 commandId 幂等)真打 `POST /account/security-events/{id}/acknowledge`(action=acknowledged)→列表失活重取翻「已确认」、按钮消失(构成 acked→无动作 UI 不变量;`account-security-ack.spec.ts` chromium MSW-off 绿,种子事件 B「待确认演练」)。后端**正负路 curl 实证**(反假绿):正路 acknowledge 真实事件→`code:0`+列表 acknowledged 翻 true;负路 acknowledge 不存在 eventId→`404 安全事件不存在`、非法 action→`400 参数校验`,均 **fail-closed 非许可桩**。写入 **DB 双证**:`muse_member_security_event.acknowledged` 翻 true + `muse_account_security_event_ack` 追加 1 行(action=acknowledged、commandId 为前端 UUID=权威处理历史,非覆盖)。种子幂等可重置(/tmp/SeedSec.java:事件A读锚点幂等插入 + 事件B写锚点每轮重置为未确认)。本轮实跑:account-security-ack **1/1** + live-read **6/6** + vitest 47/47 + tsc 干净。**仍 ⬜**:exports / downloads / new-api-binding 深页(后端均为**正确 fail-closed 外部依赖**:对象存储 / New-API 未配置,非接线缺口,见 §五A)。
- 收口口径:每切片"关 MSW→真连活体单体→playwright e2e 绿"才算完成(反假绿)。
### D. gateway 接线(B4,**判定:单体部署范围外**)
- ⏸ 单体 muse-server 直服 48080 `/app-api`,**网关不在单体部署关键路径**。`muse-gateway` 现 0 条 Muse BC 路由 + 4 条死路由(bpm/pay/report/mp)。收口动作:要么清理死路由 + 加 `/app-api/muse/**→muse-server` 单体路由,要么文档明确"网关仅微服务拆分期启用"。不阻 P1R-7 单体验收。
### D. gateway 接线(B4,**配置层已收口;运行态后置**)
- ✅ **配置层收口(2026-06-19)**:`muse-gateway` 已新增 `/admin-api/muse/**``/app-api/muse/** → grayLb://muse-server`,删除当前聚合入口未启用的 bpm/pay/report/mp 死路由,Knife4j 聚合入口改为 `muse-server`。新增 `GatewayRouteConfigurationTest` 机械门禁,断言 Muse 路由存在、停用模块路由不回潮、Knife4j 不展示停用服务;本轮 `mvn -pl muse-gateway test -Dtest=GatewayRouteConfigurationTest` **3/3 绿**
- ⏸ **运行态仍后置**:单体 P1R-7 仍以 `muse-server` 直服 48080 `/app-api` 为验收关键路径;`muse-server` 默认禁用 Nacos 注册,且 gateway 与 server 本地默认端口同为 48080,所以本轮不声称网关真实转发已完成。后续若启用网关部署,需补注册发现/端口方案并做 curl/e2e 首跑。
### E. ADR 级 ArchUnit grant/runtime 包隔离(B5,**判定:项目显式后置二期**)
- ⏸ BC 边界门 `BcBoundaryArchTest` 已存在且强制(豁免空)。grant/runtime 包级隔离被项目显式后置二期,严格说不在 P1 范围;当前类命名 + 运行时 Guard 软约束。收口:补 `ArchRule` 强约束 runtime 不得 import grant,或文档确认后置。
### 收口顺序(价值/可验证性优先)
1. ✅ 红测试修复(本轮) 2. ⬜ MetaImpact 真实化(解 meta 发布链,多模块) 3. ⬜ account 投影/归因(解 21 端) 4. ⬜ 验收债按 BC 补真实 PG IT 5. ⬜ 前端逐切片 + playwright 活体 e2e 6. ⏸ gateway/ArchUnit 判定收尾。
1. ✅ 红测试修复(本轮) 2. ⬜ MetaImpact 真实化(解 meta 发布链,多模块) 3. ⬜ account 投影/归因(解 21 端) 4. ⬜ 验收债按 BC 补真实 PG IT 5. ⬜ 前端逐切片 + playwright 活体 e2e 6. ✅ gateway 配置层收口 / ⏸ ArchUnit grant-runtime 判定收尾。

View File

@ -0,0 +1,9 @@
# .agent — muse-gateway(统一入口/路由)
> 模块跨会话记忆。进度权威见 [总账](../../docs/mvp/进度总账.md)。本文件只记目标/边界/out-of-scope/现状指针/TODO,过时即更。
- **目标(owner 职责)**:统一入口、认证透传、灰度负载、访问日志、Knife4j 聚合与 `/admin-api/**`、`/app-api/**` 路由承接。
- **边界**:gateway 只做入口转发与横切过滤,不承载 Muse 业务事实、不写 BC 数据、不替代 `muse-server` 聚合容器。
- **out-of-scope**:Content/Knowledge/AI/Market/Meta/Account 业务规则、单体内 facade 装配、外部服务可用性。
- **现状**:2026-06-19 已完成配置层收口:`/admin-api/muse/**` 与 `/app-api/muse/**` 路由到 `muse-server`,并删除当前聚合入口未启用的 bpm/pay/report/mp 死路由;`GatewayRouteConfigurationTest` 作为回潮门禁。
- **关键风险 / TODO**:配置门禁不等于运行态联调完成。`muse-server` 当前默认禁用 Nacos 注册,网关真实转发仍需微服务部署/注册发现或单独端口方案首跑验证。

View File

@ -75,6 +75,11 @@
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -79,44 +79,19 @@ spring:
- Path=/app-api/member/**
filters:
- RewritePath=/app-api/member/v3/api-docs, /v3/api-docs
## bpm-server 服务
- id: bpm-admin-api # 路由的编号
uri: grayLb://bpm-server
## muse-server 聚合服务:承接 Muse 业务 API。pay/bpm/report/mp 当前在聚合入口默认停用,不在网关保留死路由。
- id: muse-admin-api # 路由的编号
uri: grayLb://muse-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/bpm/**
- Path=/admin-api/muse/**
filters:
- RewritePath=/admin-api/bpm/v3/api-docs, /v3/api-docs
## report-server 服务
- id: report-admin-api # 路由的编号
uri: grayLb://report-server
- RewritePath=/admin-api/muse/v3/api-docs, /v3/api-docs
- id: muse-app-api # 路由的编号
uri: grayLb://muse-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/report/**
- Path=/app-api/muse/**
filters:
- RewritePath=/admin-api/report/v3/api-docs, /v3/api-docs
- id: report-jimu # 路由的编号(积木报表)
uri: grayLb://report-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/jmreport/**, /drag/**, /jimubi/**
## pay-server 服务
- id: pay-admin-api # 路由的编号
uri: grayLb://pay-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/pay/**
filters:
- RewritePath=/admin-api/pay/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
- id: pay-app-api # 路由的编号
uri: grayLb://pay-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/app-api/pay/**
filters:
- RewritePath=/app-api/pay/v3/api-docs, /v3/api-docs
## mp-server 服务
- id: mp-admin-api # 路由的编号
uri: grayLb://mp-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/mp/**
filters:
- RewritePath=/admin-api/mp/v3/api-docs, /v3/api-docs
- RewritePath=/app-api/muse/v3/api-docs, /v3/api-docs
x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
default-filters:
@ -143,18 +118,9 @@ knife4j:
- name: member-server
service-name: member-server
url: /admin-api/member/v3/api-docs
- name: bpm-server
service-name: bpm-server
url: /admin-api/bpm/v3/api-docs
- name: pay-server
service-name: pay-server
url: /admin-api/pay/v3/api-docs
- name: mp-server
service-name: mp-server
url: /admin-api/mp/v3/api-docs
- name: ai-server
service-name: ai-server
url: /admin-api/ai/v3/api-docs
- name: muse-server
service-name: muse-server
url: /admin-api/muse/v3/api-docs
--- #################### 芋道相关配置 ####################

View File

@ -0,0 +1,175 @@
package cn.iocoder.muse.gateway;
import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.StreamSupport;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Gateway 路由配置门禁
*
* <p>本测试只校验静态路由事实: Muse 业务 API 不能再被网关漏掉,当前聚合入口未启用的 pay/bpm/report/mp
* 也不能继续保留死路由它不声明运行态注册发现已经可用,运行态转发仍由部署环境的服务注册负责</p>
*/
class GatewayRouteConfigurationTest {
private static final Set<String> DISABLED_SERVICE_NAMES = Set.of(
"bpm-server", "pay-server", "report-server", "mp-server");
private static final Set<String> DISABLED_PATH_FRAGMENTS = Set.of(
"/admin-api/bpm", "/app-api/bpm",
"/admin-api/pay", "/app-api/pay",
"/admin-api/report", "/app-api/report",
"/admin-api/mp", "/app-api/mp",
"/jmreport", "/drag", "/jimubi");
@Test
void should_route_muse_business_api_to_muse_server() throws IOException {
List<RouteView> routes = gatewayRoutes();
RouteView adminRoute = requireRoute(routes, "muse-admin-api");
assertEquals("grayLb://muse-server", adminRoute.uri());
assertTrue(adminRoute.predicates().contains("Path=/admin-api/muse/**"),
"网关必须承接 Muse 管理端业务 API: " + adminRoute.predicates());
assertTrue(adminRoute.filters().contains("RewritePath=/admin-api/muse/v3/api-docs, /v3/api-docs"),
"网关必须保留 Muse 管理端 OpenAPI 聚合重写: " + adminRoute.filters());
RouteView appRoute = requireRoute(routes, "muse-app-api");
assertEquals("grayLb://muse-server", appRoute.uri());
assertTrue(appRoute.predicates().contains("Path=/app-api/muse/**"),
"网关必须承接 Muse 用户端业务 API: " + appRoute.predicates());
assertTrue(appRoute.filters().contains("RewritePath=/app-api/muse/v3/api-docs, /v3/api-docs"),
"网关必须保留 Muse 用户端 OpenAPI 聚合重写: " + appRoute.filters());
}
@Test
void should_not_keep_routes_for_disabled_optional_modules() throws IOException {
List<RouteView> routes = gatewayRoutes();
for (RouteView route : routes) {
assertFalse(DISABLED_SERVICE_NAMES.stream().anyMatch(route.uri()::contains),
"网关不得继续指向聚合入口未启用的服务: " + route);
String joinedPredicates = String.join("\n", route.predicates());
assertFalse(DISABLED_PATH_FRAGMENTS.stream().anyMatch(joinedPredicates::contains),
"网关不得继续保留停用模块的死路由路径: " + route);
}
}
@Test
void should_publish_only_enabled_services_in_knife4j_gateway_index() throws IOException {
List<Map<String, Object>> routes = knife4jRoutes();
Set<String> serviceNames = routes.stream()
.map(route -> Objects.toString(route.get("service-name"), ""))
.collect(Collectors.toSet());
assertTrue(serviceNames.contains("muse-server"), "Knife4j 聚合入口必须包含 muse-server");
for (String disabledServiceName : DISABLED_SERVICE_NAMES) {
assertFalse(serviceNames.contains(disabledServiceName),
"Knife4j 聚合入口不得继续展示停用模块: " + disabledServiceName);
}
}
private RouteView requireRoute(List<RouteView> routes, String id) {
return routes.stream()
.filter(route -> id.equals(route.id()))
.findFirst()
.orElseThrow(() -> new AssertionError("缺少 Gateway 路由: " + id));
}
@SuppressWarnings("unchecked")
private List<RouteView> gatewayRoutes() throws IOException {
Map<String, Object> root = readApplicationYaml();
Map<String, Object> webflux = nestedMap(root,
"spring", "cloud", "gateway", "server", "webflux");
Object rawRoutes = webflux.get("routes");
assertTrue(rawRoutes instanceof Collection<?>, "Gateway routes 必须是 YAML 列表");
return ((Collection<Object>) rawRoutes).stream()
.map(route -> routeView((Map<String, Object>) route))
.toList();
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> knife4jRoutes() throws IOException {
Map<String, Object> root = readApplicationYaml();
Map<String, Object> gateway = nestedMap(root, "knife4j", "gateway");
Object rawRoutes = gateway.get("routes");
assertTrue(rawRoutes instanceof Collection<?>, "Knife4j gateway.routes 必须是 YAML 列表");
return ((Collection<Map<String, Object>>) rawRoutes).stream().toList();
}
private RouteView routeView(Map<String, Object> route) {
return new RouteView(
Objects.toString(route.get("id"), ""),
Objects.toString(route.get("uri"), ""),
stringList(route.get("predicates")),
stringList(route.get("filters")));
}
@SuppressWarnings("unchecked")
private List<String> stringList(Object value) {
if (value == null) {
return List.of();
}
assertTrue(value instanceof Collection<?>, "YAML 节点必须是列表: " + value);
return ((Collection<Object>) value).stream()
.map(Objects::toString)
.toList();
}
@SuppressWarnings("unchecked")
private Map<String, Object> nestedMap(Map<String, Object> root, String... keys) {
Map<String, Object> current = root;
for (String key : keys) {
Object value = current.get(key);
assertTrue(value instanceof Map<?, ?>, "YAML 缺少映射节点: " + String.join(".", keys));
current = (Map<String, Object>) value;
}
return current;
}
@SuppressWarnings("unchecked")
private Map<String, Object> readApplicationYaml() throws IOException {
Path applicationYaml = findRepoRoot()
.resolve("muse-cloud/muse-gateway/src/main/resources/application.yaml");
assertTrue(Files.exists(applicationYaml), "Gateway application.yaml 必须存在: " + applicationYaml);
try (InputStream inputStream = Files.newInputStream(applicationYaml)) {
Object loaded = StreamSupport.stream(new Yaml().loadAll(inputStream).spliterator(), false)
.filter(LinkedHashMap.class::isInstance)
.findFirst()
.orElseThrow(() -> new AssertionError("Gateway application.yaml 必须至少包含一个 YAML 映射文档"));
return (Map<String, Object>) loaded;
}
}
/**
* surefire 工作目录逐级上溯到仓库根,避免模块内/根目录执行时相对路径漂移
*/
private Path findRepoRoot() {
Path current = Path.of("").toAbsolutePath();
for (Path candidate = current; candidate != null; candidate = candidate.getParent()) {
if (Files.exists(candidate.resolve("muse-cloud/muse-gateway/src/main/resources/application.yaml"))) {
return candidate;
}
}
return current;
}
private record RouteView(String id, String uri, List<String> predicates, List<String> filters) {
}
}