Skip to content

Commit 3f907eb

Browse files
committed
chore: add AppError.ts - extract from ErrorService.ts
1 parent 55ffcbb commit 3f907eb

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

src/services/AppError.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* AppError - 应用错误类和错误类型定义
3+
*/
4+
5+
import { t } from 'i18next'
6+
import { logger } from './LoggerService'
7+
8+
// ============================================
9+
// 错误类型枚举
10+
// ============================================
11+
export enum ErrorCategory {
12+
NETWORK = 'NETWORK',
13+
API = 'API',
14+
VALIDATION = 'VALIDATION',
15+
AUTHENTICATION = 'AUTH',
16+
AUTHORIZATION = 'FORBIDDEN',
17+
NOT_FOUND = 'NOT_FOUND',
18+
TIMEOUT = 'TIMEOUT',
19+
FILE_SYSTEM = 'FILE_SYSTEM',
20+
CONFIGURATION = 'CONFIG',
21+
UNKNOWN = 'UNKNOWN',
22+
}
23+
24+
// ============================================
25+
// 错误严重级别
26+
// ============================================
27+
export enum ErrorSeverity {
28+
LOW = 'low',
29+
MEDIUM = 'medium',
30+
HIGH = 'high',
31+
CRITICAL = 'critical',
32+
}
33+
34+
// ============================================
35+
// 错误分类到 i18n key 的映射
36+
// ============================================
37+
export const ERROR_CATEGORY_I18N_KEYS: Record<ErrorCategory, string> = {
38+
[ErrorCategory.NETWORK]: 'error_network',
39+
[ErrorCategory.API]: 'error_api',
40+
[ErrorCategory.VALIDATION]: 'error_validation',
41+
[ErrorCategory.AUTHENTICATION]: 'error_auth',
42+
[ErrorCategory.AUTHORIZATION]: 'error_forbidden',
43+
[ErrorCategory.NOT_FOUND]: 'error_not_found',
44+
[ErrorCategory.TIMEOUT]: 'error_timeout',
45+
[ErrorCategory.FILE_SYSTEM]: 'error_file_system',
46+
[ErrorCategory.CONFIGURATION]: 'error_config',
47+
[ErrorCategory.UNKNOWN]: 'error_unknown',
48+
}
49+
50+
// ============================================
51+
// 应用错误类
52+
// ============================================
53+
export class AppError extends Error {
54+
readonly category: ErrorCategory
55+
readonly severity: ErrorSeverity
56+
readonly code: string
57+
readonly context?: Record<string, unknown>
58+
readonly timestamp: Date
59+
readonly originalError?: Error
60+
61+
constructor(
62+
message: string,
63+
category: ErrorCategory = ErrorCategory.UNKNOWN,
64+
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
65+
code: string = 'UNKNOWN_ERROR',
66+
context?: Record<string, unknown>,
67+
originalError?: Error
68+
) {
69+
super(message)
70+
this.name = 'AppError'
71+
this.category = category
72+
this.severity = severity
73+
this.code = code
74+
this.context = context
75+
this.timestamp = new Date()
76+
this.originalError = originalError
77+
78+
if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {
79+
Error.captureStackTrace(this, AppError)
80+
}
81+
}
82+
83+
getUserMessage(): string {
84+
const i18nKey = `errors.${this.code}`
85+
const i18nMessage = t(i18nKey, { defaultValue: '' })
86+
if (i18nMessage) {
87+
return i18nMessage
88+
}
89+
90+
if (import.meta.env.DEV && !i18nMessage) {
91+
logger.warn(`Missing i18n key: ${i18nKey}`, 'ErrorService', { code: this.code, category: this.category })
92+
}
93+
94+
const i18nKey2 = ERROR_CATEGORY_I18N_KEYS[this.category]
95+
return i18nKey2 ? t(i18nKey2, { defaultValue: this.message }) : this.message
96+
}
97+
98+
shouldShowToUser(): boolean {
99+
return this.severity !== ErrorSeverity.LOW
100+
}
101+
102+
shouldReport(): boolean {
103+
return this.severity === ErrorSeverity.HIGH || this.severity === ErrorSeverity.CRITICAL
104+
}
105+
106+
toJSON(): Record<string, unknown> {
107+
return {
108+
name: this.name,
109+
message: this.message,
110+
category: this.category,
111+
severity: this.severity,
112+
code: this.code,
113+
context: import.meta.env.DEV ? this.context : undefined,
114+
timestamp: this.timestamp.toISOString(),
115+
stack: import.meta.env.DEV ? this.stack : undefined,
116+
}
117+
}
118+
119+
// ==========================================
120+
// 工厂方法
121+
// ==========================================
122+
123+
static network(message: string, original?: Error, context?: Record<string, unknown>): AppError {
124+
return new AppError(message, ErrorCategory.NETWORK, ErrorSeverity.HIGH, 'NETWORK_ERROR', context, original)
125+
}
126+
127+
static api(message: string, code: string = 'API_ERROR', context?: Record<string, unknown>, original?: Error): AppError {
128+
return new AppError(message, ErrorCategory.API, ErrorSeverity.HIGH, code, context, original)
129+
}
130+
131+
static validation(message: string, field?: string, context?: Record<string, unknown>): AppError {
132+
return new AppError(message, ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM, 'VALIDATION_ERROR', { ...context, field })
133+
}
134+
135+
static auth(message: string = t('error_auth_failed')): AppError {
136+
return new AppError(message, ErrorCategory.AUTHENTICATION, ErrorSeverity.HIGH, 'AUTH_ERROR')
137+
}
138+
139+
static forbidden(message: string = t('error_no_permission')): AppError {
140+
return new AppError(message, ErrorCategory.AUTHORIZATION, ErrorSeverity.HIGH, 'FORBIDDEN_ERROR')
141+
}
142+
143+
static notFound(resource: string, context?: Record<string, unknown>): AppError {
144+
return new AppError(
145+
t('error_resource_not_found', { resource }),
146+
ErrorCategory.NOT_FOUND,
147+
ErrorSeverity.MEDIUM,
148+
'NOT_FOUND_ERROR',
149+
context
150+
)
151+
}
152+
153+
static timeout(operation: string, timeoutMs: number): AppError {
154+
return new AppError(
155+
t('error_operation_timeout', { operation }),
156+
ErrorCategory.TIMEOUT,
157+
ErrorSeverity.HIGH,
158+
'TIMEOUT_ERROR',
159+
{ timeout: timeoutMs }
160+
)
161+
}
162+
163+
static fileSystem(message: string, original?: Error): AppError {
164+
return new AppError(message, ErrorCategory.FILE_SYSTEM, ErrorSeverity.HIGH, 'FILE_SYSTEM_ERROR', undefined, original)
165+
}
166+
167+
static config(message: string): AppError {
168+
return new AppError(message, ErrorCategory.CONFIGURATION, ErrorSeverity.CRITICAL, 'CONFIG_ERROR')
169+
}
170+
}

0 commit comments

Comments
 (0)