143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
// 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"
|
||
| "unauthorized"
|
||
| "forbidden_resource"
|
||
| "identity_mismatch"
|
||
| "conflict"
|
||
| "business"
|
||
| "bad_gateway"
|
||
| "aggregation_failed"
|
||
| "internal";
|
||
|
||
export interface ErrorDetails {
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
export abstract class ApplicationError extends Error {
|
||
abstract readonly type: ErrorType;
|
||
abstract readonly statusCode: number;
|
||
readonly code: string;
|
||
readonly details?: ErrorDetails;
|
||
traceId?: string;
|
||
|
||
constructor(message: string, code: string, details?: ErrorDetails) {
|
||
super(message);
|
||
this.name = this.constructor.name;
|
||
this.code = code;
|
||
this.details = details;
|
||
}
|
||
|
||
toJSON(): Record<string, unknown> {
|
||
return {
|
||
success: false,
|
||
error: {
|
||
code: this.code,
|
||
message: this.message,
|
||
details: this.details,
|
||
traceId: this.traceId,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
/** 输入参数校验失败(400) */
|
||
export class ValidationError extends ApplicationError {
|
||
readonly type = "validation" as const;
|
||
readonly statusCode = 400;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
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}`, "BFF_TEACHER_NOT_FOUND", {
|
||
resource,
|
||
id,
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 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, "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, "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, "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, "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, "BFF_TEACHER_INTERNAL_ERROR", details);
|
||
}
|
||
}
|