feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层
包含 clients/graphql/middleware、health probes、shared-ts contracts 等
This commit is contained in:
@@ -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 缺失或无效(401,president §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 场景 A,president §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 场景 B,president §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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user