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

@@ -1,12 +1,20 @@
// BFF_TEACHER_ 错误码B5 + G14 裁决,统一 BFF_ 前缀)
// 越权防御 3 类错误码president §2.7 裁决):
// - BFF_TEACHER_UNAUTHORIZED (401): x-user-id 缺失或无效
// - BFF_TEACHER_FORBIDDEN_RESOURCE (403): teacherId 与资源无归属关系(场景 A
// - BFF_TEACHER_IDENTITY_MISMATCH (403): JWT teacherId 与 body teacherId 不一致(场景 B
// 其他聚合层错误码contract §1.5
export type ErrorType =
| "validation"
| "not_found"
| "permission_denied"
| "unauthorized"
| "forbidden_resource"
| "identity_mismatch"
| "conflict"
| "business"
| "database"
| "bad_gateway"
| "aggregation_failed"
| "internal";
export interface ErrorDetails {
@@ -40,79 +48,95 @@ export abstract class ApplicationError extends Error {
}
}
/** 输入参数校验失败400 */
export class ValidationError extends ApplicationError {
readonly type = "validation" as const;
readonly statusCode = 400;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_VALIDATION_ERROR", details);
super(message, "BFF_TEACHER_VALIDATION_FAILED", details);
}
}
/** 资源不存在404 */
export class NotFoundError extends ApplicationError {
readonly type = "not_found" as const;
readonly statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, "TEACHER_BFF_NOT_FOUND", {
super(`${resource} not found: ${id}`, "BFF_TEACHER_NOT_FOUND", {
resource,
id,
});
}
}
export class PermissionDeniedError extends ApplicationError {
readonly type = "permission_denied" as const;
readonly statusCode = 403;
constructor(permission: string) {
super(`Permission denied: ${permission}`, "TEACHER_BFF_PERMISSION_DENIED", {
permission,
});
}
}
/** x-user-id 缺失或无效401president §2.7 */
export class UnauthorizedError extends ApplicationError {
readonly type = "unauthorized" as const;
readonly statusCode = 401;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_UNAUTHORIZED", details);
super(message, "BFF_TEACHER_UNAUTHORIZED", details);
}
}
/** teacherId 与资源无归属关系403 场景 Apresident §2.7 */
export class ForbiddenResourceError extends ApplicationError {
readonly type = "forbidden_resource" as const;
readonly statusCode = 403;
constructor(message: string, details?: ErrorDetails) {
super(message, "BFF_TEACHER_FORBIDDEN_RESOURCE", details);
}
}
/** JWT teacherId 与请求 body teacherId 不一致403 场景 Bpresident §2.7 */
export class IdentityMismatchError extends ApplicationError {
readonly type = "identity_mismatch" as const;
readonly statusCode = 403;
constructor(message: string, details?: ErrorDetails) {
super(message, "BFF_TEACHER_IDENTITY_MISMATCH", details);
}
}
/** 并发冲突409 */
export class ConflictError extends ApplicationError {
readonly type = "conflict" as const;
readonly statusCode = 409;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_CONFLICT", details);
super(message, "BFF_TEACHER_CONFLICT", details);
}
}
/** 业务规则违反422 */
export class BusinessError extends ApplicationError {
readonly type = "business" as const;
readonly statusCode = 422;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_BUSINESS_ERROR", details);
}
}
export class DatabaseError extends ApplicationError {
readonly type = "database" as const;
readonly statusCode = 500;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_DATABASE_ERROR", details);
super(message, "BFF_TEACHER_BUSINESS_ERROR", details);
}
}
/** 下游 gRPC 不可达502 */
export class BadGatewayError extends ApplicationError {
readonly type = "bad_gateway" as const;
readonly statusCode = 502;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_BAD_GATEWAY", details);
super(message, "BFF_TEACHER_UPSTREAM_UNAVAILABLE", details);
}
}
/** 聚合多下游时部分失败且无降级数据500 */
export class AggregationFailedError extends ApplicationError {
readonly type = "aggregation_failed" as const;
readonly statusCode = 500;
constructor(message: string, details?: ErrorDetails) {
super(message, "BFF_TEACHER_AGGREGATION_FAILED", details);
}
}
/** 兜底内部错误500 */
export class InternalError extends ApplicationError {
readonly type = "internal" as const;
readonly statusCode = 500;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_INTERNAL_ERROR", details);
super(message, "BFF_TEACHER_INTERNAL_ERROR", details);
}
}