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:
@@ -19,7 +19,7 @@
|
||||
* - volumeThreshold: 10 (最少 10 次调用才评估)
|
||||
* - rollingCountTimeout: 60000 (1 分钟滚动窗口)
|
||||
*/
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Injectable, Optional } from "@nestjs/common";
|
||||
import CircuitBreaker from "opossum";
|
||||
import promClient from "prom-client";
|
||||
import type { DownstreamClient, CallOptions } from "@edu/shared-ts/bff";
|
||||
@@ -92,7 +92,7 @@ export class CircuitBreakerService {
|
||||
private readonly breakers = new Map<string, CircuitBreaker>();
|
||||
private readonly config: BreakerConfig;
|
||||
|
||||
constructor(config?: Partial<BreakerConfig>) {
|
||||
constructor(@Optional() config?: Partial<BreakerConfig>) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
* 集成方式: GraphQL Yoga 作为 Express middleware 挂载到 NestJS HTTP Adapter,
|
||||
* 路径 POST /graphql,开发环境启用 Playground.
|
||||
*/
|
||||
import { promises } from "node:fs";
|
||||
import { promises, existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createYoga, type YogaServerInstance } from "graphql-yoga";
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema";
|
||||
import type { Request, Response } from "express";
|
||||
@@ -54,11 +55,25 @@ export interface StudentBffContext {
|
||||
|
||||
/**
|
||||
* GraphQL schema 文件路径.
|
||||
* 多路径探测:兼容从项目根目录或服务目录运行(pnpm --filter cwd 为服务目录).
|
||||
*/
|
||||
const SCHEMA_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
"packages/shared-ts/contracts/graphql/student-bff.schema.graphql",
|
||||
);
|
||||
const _schemaDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SCHEMA_CANDIDATES = [
|
||||
path.resolve(
|
||||
process.cwd(),
|
||||
"packages/shared-ts/contracts/graphql/student-bff.schema.graphql",
|
||||
),
|
||||
path.resolve(
|
||||
process.cwd(),
|
||||
"../../packages/shared-ts/contracts/graphql/student-bff.schema.graphql",
|
||||
),
|
||||
path.resolve(
|
||||
_schemaDir,
|
||||
"../../../../../packages/shared-ts/contracts/graphql/student-bff.schema.graphql",
|
||||
),
|
||||
];
|
||||
const SCHEMA_PATH: string =
|
||||
SCHEMA_CANDIDATES.find((p) => existsSync(p)) ?? SCHEMA_CANDIDATES[0]!;
|
||||
|
||||
/**
|
||||
* 加载 schema SDL 文本.
|
||||
@@ -107,7 +122,15 @@ export async function createStudentBffYoga(
|
||||
>({
|
||||
schema,
|
||||
graphqlEndpoint: "/graphql",
|
||||
context: ({ req }): StudentBffContext => {
|
||||
// graphql-yoga v5:request 是 WHATWG Request,headers 需用 .get() 提取
|
||||
context: ({ request }): StudentBffContext => {
|
||||
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 userId = extractUserIdFromRequest(req);
|
||||
const traceId = extractTraceIdFromRequest(req);
|
||||
const userRoles = extractUserRolesFromRequest(req);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
*/
|
||||
import type { StudentBffContext } from "../../shared/graphql/yoga.js";
|
||||
import type { DownstreamResponse } from "@edu/shared-ts/bff";
|
||||
import { ok, fail, degraded, DegradedReason } from "../../shared/action-state.js";
|
||||
import { UnauthorizedError } from "../../shared/errors/application-error.js";
|
||||
|
||||
export const authResolvers = {
|
||||
@@ -35,19 +34,28 @@ export const authResolvers = {
|
||||
service: "iam",
|
||||
method: "GetUserInfo",
|
||||
request: { userId: ctx.userId },
|
||||
options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
options: {
|
||||
traceId: ctx.traceId,
|
||||
metadata: { "x-user-id": ctx.userId },
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "iam",
|
||||
method: "GetEffectivePermissions",
|
||||
request: { userId: ctx.userId },
|
||||
options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
options: {
|
||||
traceId: ctx.traceId,
|
||||
metadata: { "x-user-id": ctx.userId },
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "iam",
|
||||
method: "GetViewports",
|
||||
request: { userId: ctx.userId },
|
||||
options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
options: {
|
||||
traceId: ctx.traceId,
|
||||
metadata: { "x-user-id": ctx.userId },
|
||||
},
|
||||
},
|
||||
] as const);
|
||||
|
||||
@@ -59,23 +67,13 @@ export const authResolvers = {
|
||||
|
||||
const degradedFields: string[] = [];
|
||||
if (!userInfoResp.success) degradedFields.push("user");
|
||||
if (!permsResp.success) degradedFields.push("permissions");
|
||||
if (!viewportsResp.success) degradedFields.push("viewport");
|
||||
if (!permsResp.success) degradedFields.push("effectivePermissions");
|
||||
if (!viewportsResp.success) degradedFields.push("viewports");
|
||||
|
||||
// 必需字段失败时返回错误
|
||||
// 必需字段失败时抛错
|
||||
if (!userInfoResp.success) {
|
||||
return fail(
|
||||
"BFF_STUDENT_BAD_GATEWAY",
|
||||
throw new Error(
|
||||
`Failed to fetch user info: ${userInfoResp.error.message}`,
|
||||
{
|
||||
details: {
|
||||
service: userInfoResp.error.service,
|
||||
method: userInfoResp.error.method,
|
||||
traceId: ctx.traceId,
|
||||
},
|
||||
i18nKey: "error.bffStudent.bad_gateway",
|
||||
traceId: ctx.traceId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +91,8 @@ export const authResolvers = {
|
||||
? (viewportsResp.data as { navigation: unknown[]; dataScope: unknown })
|
||||
: { navigation: [], dataScope: {} };
|
||||
|
||||
const data = {
|
||||
// 返回扁平对象,对齐 CurrentUserPayload schema
|
||||
return {
|
||||
user: {
|
||||
id: userInfo.userId,
|
||||
email: userInfo.email,
|
||||
@@ -101,20 +100,14 @@ export const authResolvers = {
|
||||
avatar: userInfo.avatar,
|
||||
roles: userInfo.roles,
|
||||
},
|
||||
permissions,
|
||||
viewport: viewports,
|
||||
viewports,
|
||||
effectivePermissions: permissions,
|
||||
dataScope: "SELF",
|
||||
degraded: degradedFields.length > 0,
|
||||
degradedReason:
|
||||
degradedFields.length > 0 ? "DOWNSTREAM_PARTIAL_FAILURE" : null,
|
||||
degradedFields: degradedFields.length > 0 ? degradedFields : null,
|
||||
};
|
||||
|
||||
if (degradedFields.length > 0) {
|
||||
return degraded(
|
||||
data,
|
||||
DegradedReason.DOWNSTREAM_PARTIAL_FAILURE,
|
||||
degradedFields,
|
||||
{ traceId: ctx.traceId },
|
||||
);
|
||||
}
|
||||
|
||||
return ok(data, { traceId: ctx.traceId });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user