fix: 修复集成测试中发现的全部 bug — 15 服务端到端验证通过

修复涵盖 6 大类问题:

1. api-gateway
   - 路径前缀剥离 /api 而非 /api/v1,保留下游 /v1/ controller 前缀
   - JWKS URL 默认值修复
   - publicPaths 白名单对齐 /v1/iam/*

2. iam
   - iam.module.ts exports 补充 PermissionCacheService 和 IamRepository
   - main.ts resolveProtoPath() 多路径探测 proto 文件

3. core-edu
   - app.module.ts AuthMiddleware 全局注册

4. BFF 层 GraphQL 端点(teacher-bff / parent-bff / student-bff)
   - teacher-bff: mock dataScope OWN→SELF 对齐 GraphQL enum;WHATWG Request header .get() 提取
   - parent-bff: handleNodeRequestAndResponse 不存在 → 直接 yoga(req,res);WHATWG Request header .get() 提取
   - student-bff: auth.resolver 移除 ActionState 信封返回扁平对象;WHATWG Request header .get() 提取

5. ai
   - Kafka 事务降级 + 10s 超时
   - gRPC 拦截器降级
   - dev mode 禁用事务模式

6. 前端 + 共享包
   - teacher-portal: MF 插件条件实例化 + transpilePackages + extensionAlias
   - ui-components: error-boundary.tsx 添加 use client
   - ui-tokens: tailwind-theme.css 移除 @layer base
   - shared-ts: 导出从源码改为 dist 编译产物;OutboxModule global:true

7. infra
   - .gitignore 补充 keys/ *.pem *.key secrets/ 排除规则
   - infra/init-sql/02-all-services-schema.sql 36 张表 DDL

验证结果:
- TS typecheck: 19 个 workspace 项目全部通过
- Go vet + Ruff: 通过
- 15 服务全部启动成功
- 3 个 BFF GraphQL 端点 + 4 个前端页面全部 200
- Gateway → iam → core-edu 端到端链路验证通过

AI identity: trae-main(集成测试修复会话)
This commit is contained in:
SpecialX
2026-07-11 01:41:46 +08:00
parent c6362f4b04
commit 61d824924a
35 changed files with 1020 additions and 192 deletions

View File

@@ -1,4 +1,5 @@
import { Module } from "@nestjs/common";
import { ClientsModule } from "../clients/clients.module.js";
import { ChildGuard } from "./child-guard.js";
/**
@@ -10,6 +11,7 @@ import { ChildGuard } from "./child-guard.js";
* 依赖 ClientsModule 提供的 IAM_CLIENT。
*/
@Module({
imports: [ClientsModule],
providers: [ChildGuard],
exports: [ChildGuard],
})

View File

@@ -103,6 +103,7 @@ export function callGrpc<TReq, TRes>(
method: string,
request: TReq,
serviceName: string,
timeoutMs = 5000,
): Promise<TRes> {
const rawClient = client as Record<string, unknown>;
const fn = rawClient[method] as (
@@ -117,7 +118,16 @@ export function callGrpc<TReq, TRes>(
}
return new Promise<TRes>((resolvePromise, reject) => {
const timer = setTimeout(() => {
reject(
new Error(
`gRPC call ${serviceName}.${method} timed out after ${timeoutMs}ms`,
),
);
}, timeoutMs);
fn.call(rawClient, request, (err, res) => {
clearTimeout(timer);
if (err) {
reject(err);
} else {

View File

@@ -1,5 +1,6 @@
import { Module } from "@nestjs/common";
import { DataLoaderFactory } from "./dataloader.factory.js";
import { ClientsModule } from "../clients/clients.module.js";
/**
* DataLoader 模块。
@@ -8,6 +9,7 @@ import { DataLoaderFactory } from "./dataloader.factory.js";
* 依赖 ClientsModule 提供的 IAM_CLIENT / CORE_EDU_CLIENT。
*/
@Module({
imports: [ClientsModule],
providers: [DataLoaderFactory],
exports: [DataLoaderFactory],
})

View File

@@ -8,8 +8,7 @@ import type { YogaInstance } from "../graphql/yoga.js";
* - POST /graphql执行 GraphQL query / mutation
* - GET /graphql开发环境返回 GraphiQL playground生产关闭
*
* 由 NestJS 路由到 yoga.handleNodeRequestAndResponse
* Yoga 内部负责解析 body / 构建 context含 ParentSession + DataLoaders/ 执行 schema / 返回结果。
* graphql-yoga v5Yoga 实例本身是 callable handler直接调用 yoga(req, res)。
*/
@Controller("graphql")
export class GraphqlController {
@@ -21,6 +20,6 @@ export class GraphqlController {
@All()
async handle(@Req() req: Request, @Res() res: Response): Promise<void> {
await this.yoga.handleNodeRequestAndResponse(req, res, { req, res });
await this.yoga(req, res);
}
}

View File

@@ -9,6 +9,7 @@ import { DataLoaderModule } from "../dataloader/dataloader.module.js";
import { DataLoaderFactory } from "../dataloader/dataloader.factory.js";
import { AggregationModule } from "../aggregation/aggregation.module.js";
import { ChildGuard } from "../aggregation/child-guard.js";
import { ClientsModule } from "../clients/clients.module.js";
import { createYogaInstance, type YogaInstance } from "./yoga.js";
import { buildResolvers, type ResolverDeps } from "./resolvers/index.js";
@@ -26,7 +27,7 @@ import { buildResolvers, type ResolverDeps } from "./resolvers/index.js";
* P5注入 MSG_CLIENT 用于通知 resolver。
*/
@Module({
imports: [DataLoaderModule, AggregationModule],
imports: [DataLoaderModule, AggregationModule, ClientsModule],
controllers: [GraphqlController],
providers: [
{

View File

@@ -8,7 +8,7 @@ import type { Request, Response } from "express";
import type { IResolvers } from "@graphql-tools/utils";
import { buildSchema } from "./schema.js";
import { buildSession } from "./context.js";
import type { GraphqlContext, ParentSession } from "./context.js";
import type { GraphqlContext } from "./context.js";
import { env } from "../config/env.js";
import { logger } from "../shared/observability/logger.js";
import {
@@ -72,20 +72,29 @@ export function createYogaInstance(
) {
const schema = buildSchema(resolvers);
return createYoga<{
req: Request;
res: Response;
session: ParentSession;
}>({
return createYoga({
schema,
graphqlEndpoint: "/graphql",
graphiql:
env.NODE_ENV === "development" && env.GRAPHQL_INTROSPECTION_ENABLED,
context: ({ req }): GraphqlContext => {
// graphql-yoga v5request 是 WHATWG Requestheaders 需用 .get() 提取
context: ({ request }): GraphqlContext => {
const req = {
headers: {
"x-user-id": request.headers.get("x-user-id") ?? undefined,
"x-request-id": request.headers.get("x-request-id") ?? undefined,
"x-user-roles": request.headers.get("x-user-roles") ?? undefined,
},
} as unknown as Request;
const session = buildSession(req);
const loaders = loaderFactory.createLoaders();
return { session, req, res: req.res as Response, loaders };
return {
session,
req,
res: {} as Response,
loaders,
};
},
plugins: [validationRulesPlugin, metricsPlugin],