175 lines
4.8 KiB
TypeScript
175 lines
4.8 KiB
TypeScript
export type ErrorType =
|
||
| "validation"
|
||
| "not_found"
|
||
| "permission_denied"
|
||
| "conflict"
|
||
| "business"
|
||
| "database"
|
||
| "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;
|
||
// FIX #1: traceId 改为可写,以便 GlobalErrorFilter 注入请求级 traceId
|
||
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,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
export class ValidationError extends ApplicationError {
|
||
readonly type = "validation" as const;
|
||
readonly statusCode = 400;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_VALIDATION_ERROR", details);
|
||
}
|
||
}
|
||
|
||
export class NotFoundError extends ApplicationError {
|
||
readonly type = "not_found" as const;
|
||
readonly statusCode = 404;
|
||
constructor(resource: string, id: string) {
|
||
super(`${resource} not found: ${id}`, "MSG_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}`, "MSG_PERMISSION_DENIED", {
|
||
permission,
|
||
});
|
||
}
|
||
}
|
||
|
||
export class ConflictError extends ApplicationError {
|
||
readonly type = "conflict" as const;
|
||
readonly statusCode = 409;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_CONFLICT", details);
|
||
}
|
||
}
|
||
|
||
export class BusinessError extends ApplicationError {
|
||
readonly type = "business" as const;
|
||
readonly statusCode = 422;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_BUSINESS_ERROR", details);
|
||
}
|
||
}
|
||
|
||
export class DatabaseError extends ApplicationError {
|
||
readonly type = "database" as const;
|
||
readonly statusCode = 500;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_DATABASE_ERROR", details);
|
||
}
|
||
}
|
||
|
||
export class InternalError extends ApplicationError {
|
||
readonly type = "internal" as const;
|
||
readonly statusCode = 500;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_INTERNAL_ERROR", details);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ES 不可用且无降级路径(503)。
|
||
* 仲裁依据:02-architecture-design.md §6.2
|
||
*/
|
||
export class EsUnavailableError extends ApplicationError {
|
||
readonly type = "internal" as const;
|
||
readonly statusCode = 503;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_ES_UNAVAILABLE", details);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Redis 不可用且无降级路径(503)。
|
||
* 仲裁依据:02-architecture-design.md §6.2
|
||
*/
|
||
export class RedisUnavailableError extends ApplicationError {
|
||
readonly type = "internal" as const;
|
||
readonly statusCode = 503;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_REDIS_UNAVAILABLE", details);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Push Gateway 不可用(503)。
|
||
* 仲裁依据:02-architecture-design.md §6.2 + coord M4(软失败,仅必要时抛出)
|
||
*/
|
||
export class PushGatewayUnavailableError extends ApplicationError {
|
||
readonly type = "internal" as const;
|
||
readonly statusCode = 503;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_PUSH_GATEWAY_UNAVAILABLE", details);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通知模板不存在(404)。
|
||
* 仲裁依据:02-architecture-design.md §6.2
|
||
*/
|
||
export class TemplateNotFoundError extends ApplicationError {
|
||
readonly type = "not_found" as const;
|
||
readonly statusCode = 404;
|
||
constructor(templateCode: string) {
|
||
super(
|
||
`Notification template not found: ${templateCode}`,
|
||
"MSG_TEMPLATE_NOT_FOUND",
|
||
{ templateCode },
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 模板渲染失败(变量缺失等,422)。
|
||
* 仲裁依据:02-architecture-design.md §6.2
|
||
*/
|
||
export class TemplateRenderError extends ApplicationError {
|
||
readonly type = "business" as const;
|
||
readonly statusCode = 422;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_TEMPLATE_RENDER_ERROR", details);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通知频率超限(429)。
|
||
* 仲裁依据:02-architecture-design.md §6.2(NotificationPreference.frequency_limit 预留)
|
||
*/
|
||
export class RateLimitExceededError extends ApplicationError {
|
||
readonly type = "business" as const;
|
||
readonly statusCode = 429;
|
||
constructor(message: string, details?: ErrorDetails) {
|
||
super(message, "MSG_RATE_LIMIT_EXCEEDED", details);
|
||
}
|
||
}
|