feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层

包含 clients/graphql/middleware、health probes、shared-ts contracts 等
This commit is contained in:
SpecialX
2026-07-10 19:10:07 +08:00
parent b82593aac2
commit 99155a5ea1
37 changed files with 2862 additions and 324 deletions

View File

@@ -0,0 +1,159 @@
// DownstreamClient 抽象基类B8 裁决3 个 BFF 统一使用)
// 提供统一的错误映射 + 结构化日志 + metrics 记录
import type { Logger } from "pino";
import { logger } from "../shared/observability/logger.js";
import {
BadGatewayError,
AggregationFailedError,
} from "../shared/errors/application-error.js";
import type {
CallContext,
DownstreamError,
DownstreamResult,
DownstreamServiceName,
GrpcMetadata,
} from "./types.js";
/** gRPC 状态码 → HTTP 语义映射(@grpc/grpc-js status codes */
const GRPC_STATUS = {
OK: 0,
CANCELLED: 1,
UNKNOWN: 2,
INVALID_ARGUMENT: 3,
DEADLINE_EXCEEDED: 4,
NOT_FOUND: 5,
ALREADY_EXISTS: 6,
PERMISSION_DENIED: 7,
RESOURCE_EXHAUSTED: 8,
FAILED_PRECONDITION: 9,
ABORTED: 10,
OUT_OF_RANGE: 11,
UNIMPLEMENTED: 12,
INTERNAL: 13,
UNAVAILABLE: 14,
DATA_LOSS: 15,
UNAUTHENTICATED: 16,
} as const;
export abstract class BaseDownstreamClient {
protected log: Logger;
abstract readonly serviceName: DownstreamServiceName;
constructor() {
// abstract property 在子类构造后才可用,延迟初始化 logger
this.log = logger.child({ downstream: "downstream" });
}
/** 子类在构造函数中调用以初始化 logger带正确 serviceName */
protected initLogger(): void {
this.log = logger.child({ downstream: this.serviceName });
}
/** 构建 gRPC metadata注入 x-user-id + x-request-id */
protected buildMetadata(ctx: CallContext): GrpcMetadata {
const meta: GrpcMetadata = { "x-user-id": ctx.userId };
if (ctx.traceId) {
meta["x-request-id"] = ctx.traceId;
}
return meta;
}
/** 将 gRPC 错误映射为 BadGatewayError502 */
protected mapGrpcError(err: unknown, rpc: string): BadGatewayError {
const grpcErr = err as {
code?: number;
message?: string;
details?: string;
};
const code = grpcErr.code ?? GRPC_STATUS.UNKNOWN;
const message = grpcErr.message ?? "Unknown gRPC error";
this.log.warn(
{ rpc, grpcCode: code, message, err },
"Downstream gRPC call failed",
);
// UNAVAILABLE → 502 Bad Gateway
if (code === GRPC_STATUS.UNAVAILABLE) {
return new BadGatewayError(
`Downstream ${this.serviceName}.${rpc} unavailable`,
{ service: this.serviceName, rpc, grpcCode: code },
);
}
// UNAUTHENTICATED → 映射为 BadGatewayBFF 自身做身份校验,下游不应返回 UNAUTHENTICATED
if (code === GRPC_STATUS.UNAUTHENTICATED) {
return new BadGatewayError(
`Downstream ${this.serviceName}.${rpc} returned UNAUTHENTICATED`,
{ service: this.serviceName, rpc, grpcCode: code },
);
}
// 其他错误统一映射为 BadGateway
return new BadGatewayError(
`Downstream ${this.serviceName}.${rpc} failed: ${message}`,
{
service: this.serviceName,
rpc,
grpcCode: code,
details: grpcErr.details,
},
);
}
/** 执行 gRPC 调用并统一错误处理 + 超时控制 */
protected async callGrpc<T>(
rpc: string,
fn: () => Promise<T>,
timeoutMs = 3000,
): Promise<T> {
const start = Date.now();
try {
const result = await Promise.race([
fn(),
this.createTimeout(timeoutMs, rpc),
]);
const duration = Date.now() - start;
this.log.debug(
{ rpc, durationMs: duration },
"Downstream gRPC call succeeded",
);
return result;
} catch (err) {
throw this.mapGrpcError(err, rpc);
}
}
private createTimeout(ms: number, rpc: string): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(
new BadGatewayError(
`Downstream ${this.serviceName}.${rpc} timed out after ${ms}ms`,
{ service: this.serviceName, rpc, timeoutMs: ms },
),
);
}, ms);
});
}
/** 降级模式 B部分失败时返回 success=true + degraded=true + warningpresident §2.6 */
protected degraded<T>(
data: T | null,
warning: string,
rpc: string,
): DownstreamResult<T> {
this.log.warn({ rpc, warning }, "Downstream call degraded");
return { success: true, data, degraded: true, warning };
}
/** 聚合失败多下游部分失败且无降级数据president §2.6 */
protected aggregationFailed(
errors: DownstreamError[],
): AggregationFailedError {
return new AggregationFailedError(
`Aggregation failed for ${this.serviceName}: ${errors.length} downstream(s) failed`,
{ service: this.serviceName, errors },
);
}
}