Files
Edu/services/teacher-bff/src/clients/base.client.ts
SpecialX 99155a5ea1 feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层
包含 clients/graphql/middleware、health probes、shared-ts contracts 等
2026-07-10 19:10:07 +08:00

160 lines
4.6 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.
// 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 },
);
}
}