Files
Edu/services/parent-bff/src/clients/grpc/grpc.factory.ts
SpecialX 61d824924a 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(集成测试修复会话)
2026-07-11 01:41:46 +08:00

139 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import {
credentials,
loadPackageDefinition,
type ChannelCredentials,
} from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
import { logger } from "../../shared/observability/logger.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROTO_ROOT = resolve(
__dirname,
"../../../../packages/shared-proto/proto",
);
const LOADER_OPTIONS: protoLoader.Options = {
keepCase: false,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};
/**
* gRPC client 工厂(对齐 02-architecture-design.md §1.4 clients/grpc/)。
*
* 使用 @grpc/proto-loader 动态加载 proto 文件,
* 通过 @grpc/grpc-js 创建 Channel + Client 实例。
*
* proto-loader keepCase: false → snake_case → camelCase
* longs: String → int64 → string
* enums: String → enum → string
*
* 返回的 client 为动态类型proto-loader 不生成静态 TS 类型),
* 调用方通过 `as unknown as SpecificClient` 断言为具体接口。
*/
export function createGrpcClient<TService>(options: {
protoFile: string;
packageName: string;
serviceName: string;
target: string;
}): TService {
const protoPath = resolve(PROTO_ROOT, options.protoFile);
logger.info(
{
protoFile: options.protoFile,
packageName: options.packageName,
serviceName: options.serviceName,
target: options.target,
},
"Creating gRPC client",
);
const packageDefinition = protoLoader.loadSync(protoPath, LOADER_OPTIONS);
const protoDescriptor = loadPackageDefinition(packageDefinition);
// Navigate: protoDescriptor.next_edu_cloud.iam.v1.IamService
const packages = options.packageName.split(".");
let current: unknown = protoDescriptor;
for (const pkg of packages) {
current = (current as Record<string, unknown>)[pkg];
if (current === undefined) {
throw new Error(
`gRPC package "${options.packageName}" not found in proto ${options.protoFile}`,
);
}
}
const ServiceConstructor = (current as Record<string, unknown>)[
options.serviceName
] as {
new (target: string, creds: ChannelCredentials): TService;
};
if (!ServiceConstructor) {
throw new Error(
`gRPC service "${options.serviceName}" not found in package "${options.packageName}"`,
);
}
// proto-loader 动态加载的 client 构造函数类型未知,
// 从 unknown 断言为目标接口(项目规则允许从 unknown 转换)
return new ServiceConstructor(
options.target,
credentials.createInsecure(),
) as unknown as TService;
}
/**
* 将 gRPC callback 风格调用转为 Promise同时记录 metrics。
*
* proto-loader 加载的 client 方法签名为:
* client.Method(request, metadata?, callback)
*
* 此辅助函数省略 metadata 参数,仅处理 request → callback → Promise。
* 需要 metadata 的调用(如传递 trace context应直接操作 raw client。
*/
export function callGrpc<TReq, TRes>(
client: unknown,
method: string,
request: TReq,
serviceName: string,
timeoutMs = 5000,
): Promise<TRes> {
const rawClient = client as Record<string, unknown>;
const fn = rawClient[method] as (
req: TReq,
callback: (err: Error | null, res: TRes) => void,
) => void;
if (typeof fn !== "function") {
return Promise.reject(
new Error(`gRPC method "${method}" not found on ${serviceName} client`),
);
}
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 {
resolvePromise(res);
}
});
});
}