|
|
@@ -0,0 +1,485 @@
|
|
|
+# D8D Starter 错误处理规范
|
|
|
+
|
|
|
+## 版本信息
|
|
|
+| 版本 | 日期 | 描述 | 作者 |
|
|
|
+|------|------|------|------|
|
|
|
+| 1.0 | 2025-09-15 | 初始错误处理规范 | Sarah (PO) |
|
|
|
+
|
|
|
+## 1. 错误处理原则
|
|
|
+
|
|
|
+### 1.1 核心原则
|
|
|
+- **一致性**: 所有错误响应格式统一
|
|
|
+- **安全性**: 不泄露敏感信息到客户端
|
|
|
+- **可操作性**: 错误信息应指导用户或开发者
|
|
|
+- **可追溯性**: 错误应包含足够上下文用于调试
|
|
|
+
|
|
|
+### 1.2 错误分类
|
|
|
+
|
|
|
+#### 按严重程度
|
|
|
+- 🔴 **致命错误**: 系统无法继续运行
|
|
|
+- 🟡 **业务错误**: 用户操作失败,可恢复
|
|
|
+- 🔵 **客户端错误**: 用户输入或配置问题
|
|
|
+- ⚪ **信息性错误**: 警告或提示信息
|
|
|
+
|
|
|
+#### 按来源
|
|
|
+- **验证错误**: 输入数据验证失败
|
|
|
+- **业务逻辑错误**: 业务规则违反
|
|
|
+- **系统错误**: 基础设施或第三方服务故障
|
|
|
+- **网络错误**: 连接超时或中断
|
|
|
+
|
|
|
+## 2. 错误响应格式
|
|
|
+
|
|
|
+### 2.1 标准错误响应
|
|
|
+```typescript
|
|
|
+// 成功响应
|
|
|
+{
|
|
|
+ "success": true,
|
|
|
+ "data": { /* 业务数据 */ },
|
|
|
+ "message": "操作成功"
|
|
|
+}
|
|
|
+
|
|
|
+// 错误响应
|
|
|
+{
|
|
|
+ "success": false,
|
|
|
+ "error": {
|
|
|
+ "code": "VALIDATION_ERROR",
|
|
|
+ "message": "输入数据验证失败",
|
|
|
+ "details": [
|
|
|
+ {
|
|
|
+ "field": "email",
|
|
|
+ "message": "邮箱格式不正确"
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "timestamp": "2025-09-15T10:30:00Z",
|
|
|
+ "requestId": "req_1234567890"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 HTTP状态码映射
|
|
|
+
|
|
|
+| 错误类型 | HTTP状态码 | 错误代码 | 描述 |
|
|
|
+|----------|------------|----------|------|
|
|
|
+| 验证错误 | 400 | `VALIDATION_ERROR` | 输入数据验证失败 |
|
|
|
+| 未授权 | 401 | `UNAUTHORIZED` | 需要认证 |
|
|
|
+| 权限不足 | 403 | `FORBIDDEN` | 权限不足 |
|
|
|
+| 资源不存在 | 404 | `NOT_FOUND` | 资源未找到 |
|
|
|
+| 业务错误 | 409 | `BUSINESS_ERROR` | 业务规则冲突 |
|
|
|
+| 系统错误 | 500 | `INTERNAL_ERROR` | 服务器内部错误 |
|
|
|
+| 服务不可用 | 503 | `SERVICE_UNAVAILABLE` | 服务暂时不可用 |
|
|
|
+
|
|
|
+## 3. 错误代码规范
|
|
|
+
|
|
|
+### 3.1 错误代码命名约定
|
|
|
+- 使用 `SCREAMING_SNAKE_CASE`
|
|
|
+- 前缀表示错误类别
|
|
|
+- 后缀表示具体错误
|
|
|
+
|
|
|
+### 3.2 标准错误代码
|
|
|
+
|
|
|
+#### 认证错误 (AUTH_*)
|
|
|
+```typescript
|
|
|
+export const AUTH_ERRORS = {
|
|
|
+ UNAUTHORIZED: 'AUTH_UNAUTHORIZED',
|
|
|
+ INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
|
|
|
+ TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
|
|
|
+ TOKEN_INVALID: 'AUTH_TOKEN_INVALID',
|
|
|
+ PERMISSION_DENIED: 'AUTH_PERMISSION_DENIED'
|
|
|
+} as const;
|
|
|
+```
|
|
|
+
|
|
|
+#### 验证错误 (VALIDATION_*)
|
|
|
+```typescript
|
|
|
+export const VALIDATION_ERRORS = {
|
|
|
+ REQUIRED_FIELD: 'VALIDATION_REQUIRED_FIELD',
|
|
|
+ INVALID_EMAIL: 'VALIDATION_INVALID_EMAIL',
|
|
|
+ INVALID_PASSWORD: 'VALIDATION_INVALID_PASSWORD',
|
|
|
+ UNIQUE_CONSTRAINT: 'VALIDATION_UNIQUE_CONSTRAINT',
|
|
|
+ OUT_OF_RANGE: 'VALIDATION_OUT_OF_RANGE'
|
|
|
+} as const;
|
|
|
+```
|
|
|
+
|
|
|
+#### 业务错误 (BUSINESS_*)
|
|
|
+```typescript
|
|
|
+export const BUSINESS_ERRORS = {
|
|
|
+ USER_NOT_FOUND: 'BUSINESS_USER_NOT_FOUND',
|
|
|
+ INSUFFICIENT_BALANCE: 'BUSINESS_INSUFFICIENT_BALANCE',
|
|
|
+ OPERATION_NOT_ALLOWED: 'BUSINESS_OPERATION_NOT_ALLOWED',
|
|
|
+ RESOURCE_CONFLICT: 'BUSINESS_RESOURCE_CONFLICT'
|
|
|
+} as const;
|
|
|
+```
|
|
|
+
|
|
|
+#### 系统错误 (SYSTEM_*)
|
|
|
+```typescript
|
|
|
+export const SYSTEM_ERRORS = {
|
|
|
+ DATABASE_ERROR: 'SYSTEM_DATABASE_ERROR',
|
|
|
+ NETWORK_ERROR: 'SYSTEM_NETWORK_ERROR',
|
|
|
+ EXTERNAL_SERVICE_ERROR: 'SYSTEM_EXTERNAL_SERVICE_ERROR',
|
|
|
+ CONFIGURATION_ERROR: 'SYSTEM_CONFIGURATION_ERROR'
|
|
|
+} as const;
|
|
|
+```
|
|
|
+
|
|
|
+## 4. 实现指南
|
|
|
+
|
|
|
+### 4.1 后端错误处理
|
|
|
+
|
|
|
+#### 错误类定义
|
|
|
+```typescript
|
|
|
+// src/shared/errors/AppError.ts
|
|
|
+export class AppError extends Error {
|
|
|
+ public readonly code: string;
|
|
|
+ public readonly statusCode: number;
|
|
|
+ public readonly details?: any;
|
|
|
+ public readonly timestamp: Date;
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ code: string,
|
|
|
+ message: string,
|
|
|
+ statusCode: number = 500,
|
|
|
+ details?: any
|
|
|
+ ) {
|
|
|
+ super(message);
|
|
|
+ this.code = code;
|
|
|
+ this.statusCode = statusCode;
|
|
|
+ this.details = details;
|
|
|
+ this.timestamp = new Date();
|
|
|
+
|
|
|
+ // 保持正确的堆栈跟踪
|
|
|
+ Error.captureStackTrace(this, this.constructor);
|
|
|
+ }
|
|
|
+
|
|
|
+ toJSON() {
|
|
|
+ return {
|
|
|
+ code: this.code,
|
|
|
+ message: this.message,
|
|
|
+ details: this.details,
|
|
|
+ timestamp: this.timestamp.toISOString()
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 特定错误类
|
|
|
+export class ValidationError extends AppError {
|
|
|
+ constructor(message: string, details?: any) {
|
|
|
+ super('VALIDATION_ERROR', message, 400, details);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export class NotFoundError extends AppError {
|
|
|
+ constructor(resource: string, id?: string | number) {
|
|
|
+ const message = id
|
|
|
+ ? `${resource} with ID ${id} not found`
|
|
|
+ : `${resource} not found`;
|
|
|
+ super('NOT_FOUND', message, 404);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 错误处理中间件
|
|
|
+```typescript
|
|
|
+// src/server/middleware/errorHandler.ts
|
|
|
+import { Context } from 'hono';
|
|
|
+import { AppError } from '../../shared/errors/AppError';
|
|
|
+
|
|
|
+export async function errorHandler(err: Error, c: Context) {
|
|
|
+ // 记录错误
|
|
|
+ console.error('Error:', {
|
|
|
+ message: err.message,
|
|
|
+ stack: err.stack,
|
|
|
+ url: c.req.url,
|
|
|
+ method: c.req.method,
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ });
|
|
|
+
|
|
|
+ // AppError 实例
|
|
|
+ if (err instanceof AppError) {
|
|
|
+ return c.json(
|
|
|
+ {
|
|
|
+ success: false,
|
|
|
+ error: {
|
|
|
+ code: err.code,
|
|
|
+ message: err.message,
|
|
|
+ details: err.details,
|
|
|
+ timestamp: err.timestamp.toISOString(),
|
|
|
+ requestId: c.get('requestId')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ err.statusCode
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 未知错误(生产环境隐藏细节)
|
|
|
+ const isProduction = process.env.NODE_ENV === 'production';
|
|
|
+
|
|
|
+ return c.json(
|
|
|
+ {
|
|
|
+ success: false,
|
|
|
+ error: {
|
|
|
+ code: 'INTERNAL_ERROR',
|
|
|
+ message: isProduction ? 'Internal server error' : err.message,
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
+ requestId: c.get('requestId')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 500
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 使用示例
|
|
|
+```typescript
|
|
|
+// 在服务中使用
|
|
|
+export class UserService {
|
|
|
+ async getUserById(id: number) {
|
|
|
+ const user = await User.findOne({ where: { id } });
|
|
|
+
|
|
|
+ if (!user) {
|
|
|
+ throw new NotFoundError('User', id);
|
|
|
+ }
|
|
|
+
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+
|
|
|
+ async createUser(data: CreateUserDto) {
|
|
|
+ // 验证输入
|
|
|
+ const errors = validateUserData(data);
|
|
|
+ if (errors.length > 0) {
|
|
|
+ throw new ValidationError('Invalid user data', errors);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查唯一性
|
|
|
+ const existingUser = await User.findOne({
|
|
|
+ where: [{ email: data.email }, { username: data.username }]
|
|
|
+ });
|
|
|
+
|
|
|
+ if (existingUser) {
|
|
|
+ throw new BusinessError('USER_ALREADY_EXISTS', 'User with this email or username already exists', 409);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建用户
|
|
|
+ try {
|
|
|
+ return await User.create(data).save();
|
|
|
+ } catch (error) {
|
|
|
+ throw new DatabaseError('Failed to create user', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 前端错误处理
|
|
|
+
|
|
|
+#### 错误处理钩子
|
|
|
+```typescript
|
|
|
+// src/client/hooks/useErrorHandler.ts
|
|
|
+import { toast } from 'sonner';
|
|
|
+
|
|
|
+export function useErrorHandler() {
|
|
|
+ const handleError = (error: unknown) => {
|
|
|
+ console.error('Application error:', error);
|
|
|
+
|
|
|
+ if (error instanceof Error) {
|
|
|
+ // 显示用户友好的错误消息
|
|
|
+ toast.error(getUserFriendlyMessage(error));
|
|
|
+ } else {
|
|
|
+ toast.error('发生未知错误,请重试');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const getUserFriendlyMessage = (error: Error): string => {
|
|
|
+ const message = error.message.toLowerCase();
|
|
|
+
|
|
|
+ if (message.includes('network')) {
|
|
|
+ return '网络连接失败,请检查网络设置';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (message.includes('validation')) {
|
|
|
+ return '输入数据验证失败,请检查表单';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (message.includes('permission') || message.includes('auth')) {
|
|
|
+ return '权限不足,请重新登录';
|
|
|
+ }
|
|
|
+
|
|
|
+ return error.message || '操作失败,请重试';
|
|
|
+ };
|
|
|
+
|
|
|
+ return { handleError, getUserFriendlyMessage };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### API客户端错误处理
|
|
|
+```typescript
|
|
|
+// src/client/api.ts
|
|
|
+import { toast } from 'sonner';
|
|
|
+
|
|
|
+const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
|
|
|
+ try {
|
|
|
+ const response = await axios.request({
|
|
|
+ url: url.toString(),
|
|
|
+ method: init?.method || 'GET',
|
|
|
+ headers: init?.headers,
|
|
|
+ data: init?.body,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理错误响应
|
|
|
+ if (response.status >= 400) {
|
|
|
+ const errorData = response.data?.error || {};
|
|
|
+ throw new ApiError(
|
|
|
+ errorData.message || '请求失败',
|
|
|
+ response.status,
|
|
|
+ errorData.code,
|
|
|
+ errorData.details
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return createResponse(response);
|
|
|
+ } catch (error) {
|
|
|
+ if (axios.isAxiosError(error)) {
|
|
|
+ const errorData = error.response?.data?.error || {};
|
|
|
+ throw new ApiError(
|
|
|
+ errorData.message || error.message,
|
|
|
+ error.response?.status || 500,
|
|
|
+ errorData.code,
|
|
|
+ errorData.details
|
|
|
+ );
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+class ApiError extends Error {
|
|
|
+ constructor(
|
|
|
+ message: string,
|
|
|
+ public status: number,
|
|
|
+ public code?: string,
|
|
|
+ public details?: any
|
|
|
+ ) {
|
|
|
+ super(message);
|
|
|
+ this.name = 'ApiError';
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 5. 日志和监控
|
|
|
+
|
|
|
+### 5.1 错误日志格式
|
|
|
+```typescript
|
|
|
+// 错误日志示例
|
|
|
+{
|
|
|
+ "level": "error",
|
|
|
+ "timestamp": "2025-09-15T10:30:00.000Z",
|
|
|
+ "message": "User creation failed",
|
|
|
+ "code": "VALIDATION_ERROR",
|
|
|
+ "stack": "Error: Invalid email format...",
|
|
|
+ "context": {
|
|
|
+ "userId": 123,
|
|
|
+ "email": "invalid-email",
|
|
|
+ "requestId": "req_1234567890",
|
|
|
+ "url": "/api/v1/users",
|
|
|
+ "method": "POST"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 监控配置
|
|
|
+```typescript
|
|
|
+// 错误监控集成
|
|
|
+import * as Sentry from '@sentry/node';
|
|
|
+import * as Tracing from '@sentry/tracing';
|
|
|
+
|
|
|
+Sentry.init({
|
|
|
+ dsn: process.env.SENTRY_DSN,
|
|
|
+ environment: process.env.NODE_ENV,
|
|
|
+ tracesSampleRate: 1.0,
|
|
|
+});
|
|
|
+
|
|
|
+// 在错误处理中间件中
|
|
|
+if (process.env.NODE_ENV === 'production') {
|
|
|
+ Sentry.captureException(err, {
|
|
|
+ extra: {
|
|
|
+ url: c.req.url,
|
|
|
+ method: c.req.method,
|
|
|
+ requestId: c.get('requestId')
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 6. 测试错误场景
|
|
|
+
|
|
|
+### 6.1 错误测试示例
|
|
|
+```typescript
|
|
|
+// 测试验证错误
|
|
|
+describe('UserService - Error Handling', () => {
|
|
|
+ it('should throw ValidationError for invalid email', async () => {
|
|
|
+ const invalidData = { email: 'invalid', password: '123' };
|
|
|
+
|
|
|
+ await expect(userService.createUser(invalidData))
|
|
|
+ .rejects
|
|
|
+ .toThrow(ValidationError);
|
|
|
+
|
|
|
+ await expect(userService.createUser(invalidData))
|
|
|
+ .rejects
|
|
|
+ .toMatchObject({
|
|
|
+ code: 'VALIDATION_ERROR',
|
|
|
+ statusCode: 400
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should throw NotFoundError for non-existent user', async () => {
|
|
|
+ await expect(userService.getUserById(999))
|
|
|
+ .rejects
|
|
|
+ .toThrow(NotFoundError);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// 测试错误响应格式
|
|
|
+describe('Error Handler Middleware', () => {
|
|
|
+ it('should return formatted error response', async () => {
|
|
|
+ const server = createTestServer();
|
|
|
+
|
|
|
+ const response = await server.inject({
|
|
|
+ method: 'GET',
|
|
|
+ url: '/api/v1/users/999'
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.statusCode).toBe(404);
|
|
|
+ expect(response.json()).toEqual({
|
|
|
+ success: false,
|
|
|
+ error: {
|
|
|
+ code: 'NOT_FOUND',
|
|
|
+ message: 'User with ID 999 not found',
|
|
|
+ timestamp: expect.any(String),
|
|
|
+ requestId: expect.any(String)
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## 7. 最佳实践清单
|
|
|
+
|
|
|
+### 7.1 错误处理检查清单
|
|
|
+- [ ] 所有错误都有明确的错误代码
|
|
|
+- [ ] 错误消息对用户友好且可操作
|
|
|
+- [ ] 生产环境不泄露敏感信息
|
|
|
+- [ ] 错误包含足够的调试上下文
|
|
|
+- [ ] 错误响应格式统一
|
|
|
+- [ ] 适当的HTTP状态码
|
|
|
+- [ ] 错误日志记录完整
|
|
|
+- [ ] 前端错误处理用户友好
|
|
|
+- [ ] 测试覆盖错误场景
|
|
|
+
|
|
|
+### 7.2 安全注意事项
|
|
|
+- ❌ 不要返回堆栈跟踪到客户端
|
|
|
+- ❌ 不要泄露数据库错误细节
|
|
|
+- ❌ 不要暴露敏感配置信息
|
|
|
+- ✅ 使用通用的错误消息
|
|
|
+- ✅ 记录详细错误到服务器日志
|
|
|
+- ✅ 使用请求ID关联错误
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**最后更新**: 2025-09-15
|
|
|
+**维护者**: 开发团队
|
|
|
+**文档状态**: 正式版
|