@xilil/error-codes
Version:
统一错误码管理库 - 支持多语言、类型安全的错误处理工具
670 lines (523 loc) • 23.6 kB
Markdown
这是一个用于统一管理 Express 项目集合中错误码的 TypeScript 库。该库提供了标准化的错误码定义、多语言支持和完整的错误处理工具。
- 🌐 **多语言支持**: 支持中文和英文错误消息
- 📋 **分类管理**: 按错误性质分类管理错误码(而非业务类型)
- 🔧 **类型安全**: 完整的 TypeScript 类型定义
- 🚀 **零依赖**: 不依赖外部库
- 📊 **HTTP 状态码映射**: 错误码与 HTTP 状态码的智能映射
- 🛠️ **工具函数**: 丰富的错误处理工具函数
- 🎯 **Express 集成**: 提供 Express 错误处理中间件
传统的按业务分类(如 CHAT_ERROR_CODES、FILE_ERROR_CODES)存在以下问题:
- **业务耦合严重**:新业务需要创建新的错误码分类
- **错误码重复**:相似错误在不同业务中被重复定义
- **维护成本高**:业务变更时需要修改错误码结构
- **复用性差**:错误码难以跨业务使用
1. **按错误性质分类**:根据错误的本质特征进行分类
2. **高度可复用**:同一错误码可在多个业务中使用
3. **易于维护**:错误码结构稳定,不随业务变化
4. **覆盖全面**:预先定义常见的错误场景
用于系统级别的错误:
- `1001` **INTERNAL_ERROR** - 内部服务器错误
- `1002` **SERVICE_UNAVAILABLE** - 服务暂时不可用
- `1003` **CONFIGURATION_ERROR** - 系统配置错误
- `1004` **EXTERNAL_SERVICE_ERROR** - 外部服务调用失败
用于请求参数验证相关的错误:
- `2001` **INVALID_PARAMETER** - 参数无效
- `2002` **MISSING_PARAMETER** - 缺少必需参数
- `2003` **PARAMETER_OUT_OF_RANGE** - 参数超出范围
- `2004` **INVALID_FORMAT** - 格式不正确
- `2005` **CONTENT_TOO_LONG** - 内容过长
- `2006` **UNSUPPORTED_TYPE** - 不支持的类型
用于身份认证和权限相关的错误:
- `4001` **UNAUTHORIZED** - 未授权访问
- `4002` **INVALID_TOKEN** - 无效的令牌
- `4003` **TOKEN_EXPIRED** - 令牌已过期
- `4004` **INSUFFICIENT_PERMISSIONS** - 权限不足
- `4005` **ACCOUNT_DISABLED** - 账户已被禁用
- `4006` **INVALID_CREDENTIALS** - 认证信息无效
- `4007` **ACCOUNT_LOCKED** - 账户已锁定
- `4008` **SESSION_EXPIRED** - 会话已过期
- `4009` **ACCOUNT_NOT_FOUND** - 账户不存在
- `4010` **INSUFFICIENT_BALANCE** - 余额不足
- `4011` **INSUFFICIENT_INTEGRAL** - 积分不足
用于用户相关的错误
- `4001` **INVALID_INVITE_CODE** - 邀请码无效
用于资源相关的错误:
- `5001` **RESOURCE_NOT_FOUND** - 资源不存在
- `5002` **RESOURCE_ALREADY_EXISTS** - 资源已存在
- `5003` **RESOURCE_CREATION_FAILED** - 资源创建失败
- `5004` **RESOURCE_UPDATE_FAILED** - 资源更新失败
- `5005` **RESOURCE_DELETE_FAILED** - 资源删除失败
- `5006` **RESOURCE_IN_USE** - 资源正在使用中
- `5007` **RESOURCE_LOCKED** - 资源已被锁定
用于各种限制和配额相关的错误:
- `6001` **RATE_LIMIT_EXCEEDED** - 请求频率超出限制
- `6002` **QUOTA_EXCEEDED** - 配额已用完
- `6003` **STORAGE_QUOTA_EXCEEDED** - 存储空间不足
- `6004` **CONCURRENT_LIMIT_EXCEEDED** - 并发数超出限制
- `6005` **DAILY_LIMIT_EXCEEDED** - 日使用量超出限制
- `6006` **SIZE_LIMIT_EXCEEDED** - 大小超出限制
用于数据处理相关的错误:
- `7001` **DATA_CORRUPTION** - 数据已损坏
- `7002` **SERIALIZATION_ERROR** - 数据序列化失败
- `7003` **DESERIALIZATION_ERROR** - 数据反序列化失败
- `7004` **DATA_TRANSFORMATION_ERROR** - 数据转换失败
- `7005` **CHECKSUM_MISMATCH** - 校验和不匹配
用于业务逻辑相关的错误:
- `8001` **BUSINESS_RULE_VIOLATION** - 违反业务规则
- `8002` **OPERATION_NOT_ALLOWED** - 操作不被允许
- `8003` **PREREQUISITE_NOT_MET** - 前置条件未满足
- `8004` **WORKFLOW_ERROR** - 工作流错误
- `8005` **STATE_CONFLICT** - 状态冲突
用于第三方服务集成相关的错误:
- `9001` **AI_SERVICE_ERROR** - AI 服务调用失败
- `9002` **AI_MODEL_UNAVAILABLE** - AI 模型不可用
- `9003` **PAYMENT_SERVICE_ERROR** - 支付服务错误
- `9004` **SMS_SERVICE_ERROR** - 短信服务错误
- `9005` **EMAIL_SERVICE_ERROR** - 邮件服务错误
- `9006` **CLOUD_STORAGE_ERROR** - 云存储服务错误
- `9007` **TRANSLATION_SERVICE_ERROR** - 翻译服务错误
- `9008` **WECHAT_API_ERROR** - 微信接口调用失败
用于 AI 服务集成相关的错误:
- `9001` **MODEL_NOT_AVAILABLE** - 模型不可用
```bash
npm install @aqcq/error-codes
```
```typescript
import { getErrorCode, createErrorResponse, AppError } from '@sqcq/error-codes';
// 获取错误码信息
const error = getErrorCode('RESOURCE_NOT_FOUND', 'zh-CN');
console.log(error);
// { code: 5001, message: '资源不存在', statusCode: 404, description: '请求的资源不存在' }
// 创建错误响应
const response = createErrorResponse('UNAUTHORIZED', 'zh-CN', '/api/user');
console.log(response);
// { success: false, code: 4001, message: '未授权访问', timestamp: 1699123456789, path: '/api/user' }
// 抛出应用错误
throw new AppError('AI_SERVICE_ERROR', 'zh-CN');
```
```typescript
import {
RESOURCE_ERROR_CODES,
VALIDATION_ERROR_CODES,
AUTH_ERROR_CODES,
createErrorResponse,
} from '@sqcq/error-codes';
// 聊天业务中使用
function getChatConversation(id: string) {
if (!id) {
return createErrorResponse('MISSING_PARAMETER', 'zh-CN');
}
if (!conversation) {
return createErrorResponse('RESOURCE_NOT_FOUND', 'zh-CN');
}
if (!hasPermission) {
return createErrorResponse('INSUFFICIENT_PERMISSIONS', 'zh-CN');
}
}
// 文件业务中使用相同的错误码
function getFile(id: string) {
if (!id) {
return createErrorResponse('MISSING_PARAMETER', 'zh-CN');
}
if (!file) {
return createErrorResponse('RESOURCE_NOT_FOUND', 'zh-CN');
}
if (!hasPermission) {
return createErrorResponse('INSUFFICIENT_PERMISSIONS', 'zh-CN');
}
}
```
不同业务场景可以使用相同的通用错误码:
```typescript
// 聊天业务错误映射
const ChatServiceErrors = {
CONVERSATION_NOT_FOUND: 'RESOURCE_NOT_FOUND',
MESSAGE_TOO_LONG: 'CONTENT_TOO_LONG',
AI_MODEL_UNAVAILABLE: 'AI_MODEL_UNAVAILABLE',
DAILY_LIMIT_EXCEEDED: 'DAILY_LIMIT_EXCEEDED',
} as const;
// 文件业务错误映射
const FileServiceErrors = {
FILE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
FILE_TOO_LARGE: 'SIZE_LIMIT_EXCEEDED',
INVALID_FILE_TYPE: 'UNSUPPORTED_TYPE',
STORAGE_QUOTA_EXCEEDED: 'STORAGE_QUOTA_EXCEEDED',
} as const;
// 用户业务错误映射
const UserServiceErrors = {
USER_NOT_FOUND: 'RESOURCE_NOT_FOUND',
USER_ALREADY_EXISTS: 'RESOURCE_ALREADY_EXISTS',
INVALID_PASSWORD: 'INVALID_CREDENTIALS',
ACCOUNT_LOCKED: 'ACCOUNT_LOCKED',
} as const;
```
我们提供了专门为 Express 设计的错误处理中间件,让你可以直接通过 `throw` 抛出错误,自动返回标准化的错误响应给前端。
```typescript
import express from 'express';
import {
AppError,
expressErrorHandler,
asyncHandler,
throwError,
throwIf,
assert,
createSuccessResponse,
} from '@aqcq/error-codes';
const app = express();
app.use(express.json());
// 使用错误处理中间件(必须在所有路由之后)
app.use(expressErrorHandler('zh-CN', true));
// 路由示例
app.get(
'/api/users/:id',
asyncHandler(async (req, res) => {
const id = parseInt(req.params.id);
// 方式1: 使用传统的 throw new AppError()
if (isNaN(id)) {
throw new AppError('INVALID_PARAMETER', 'zh-CN', true, { field: 'id' });
}
const user = await findUser(id);
if (!user) {
throw new AppError('RESOURCE_NOT_FOUND', 'zh-CN', true, { userId: id });
}
res.json(createSuccessResponse(user, '获取用户成功'));
})
);
app.post(
'/api/users',
asyncHandler(async (req, res) => {
const { name, email } = req.body;
// 方式2: 使用 throwError() 简化抛出错误
if (!name) throwError('MISSING_PARAMETER', { field: 'name' });
if (!email) throwError('MISSING_PARAMETER', { field: 'email' });
// 方式3: 使用 throwIf() 条件抛出错误
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
throwIf(!emailRegex.test(email), 'INVALID_FORMAT', { field: 'email' });
const existingUser = await findUserByEmail(email);
throwIf(!!existingUser, 'RESOURCE_ALREADY_EXISTS', { email });
const newUser = await createUser({ name, email });
res.status(201).json(createSuccessResponse(newUser, '创建用户成功'));
})
);
app.delete(
'/api/users/:id',
asyncHandler(async (req, res) => {
const id = parseInt(req.params.id);
// 方式4: 使用 assert() 断言函数
assert(!isNaN(id), 'INVALID_PARAMETER', { field: 'id' });
const user = await findUser(id);
assert(!!user, 'RESOURCE_NOT_FOUND', { userId: id });
await deleteUser(id);
res.json(createSuccessResponse(null, '删除用户成功'));
})
);
app.listen(3000);
```
所有错误都会返回统一的 JSON 格式:
```json
{
"success": false,
"code": 5001,
"message": "资源不存在",
"timestamp": 1699123456789,
"path": "/api/users/123",
"data": {
"userId": 123
}
}
```
成功响应格式:
```json
{
"success": true,
"data": { "id": 1, "name": "张三" },
"message": "获取用户成功",
"timestamp": 1699123456789
}
```
```typescript
interface ErrorCode {
code: number;
message: string;
statusCode: number;
description?: string;
}
interface ErrorCodeConfig {
code: number;
messages: {
'zh-CN': string;
'en-US': string;
};
statusCode: number;
description?: string;
}
interface ErrorResponse {
success: false;
code: number;
data?: any;
message: string;
timestamp: number;
path?: string;
}
interface SuccessResponse<T = any> {
success: true;
code: number;
data: T;
message?: string;
timestamp: number;
}
type SupportedLanguage = 'zh-CN' | 'en-US';
```
获取指定错误键的错误信息。
```typescript
const error = getErrorCode('RESOURCE_NOT_FOUND', 'zh-CN');
// { code: 5001, message: '资源不存在', statusCode: 404, description: '请求的资源不存在' }
```
创建标准错误响应对象。
```typescript
const response = createErrorResponse('UNAUTHORIZED', 'zh-CN', '/api/user', {
reason: 'token_expired',
});
```
创建标准成功响应对象。
```typescript
const response = createSuccessResponse({ id: 1, name: '张三' }, '获取成功');
```
自定义错误类,继承自 Error。
```typescript
throw new AppError('RESOURCE_NOT_FOUND', 'zh-CN', true, { userId: 123 });
```
- `expressErrorHandler(defaultLanguage?, enableStackTrace?)` - Express 错误处理中间件
- `asyncHandler(fn)` - 异步错误包装器
- `throwError(errorKey, details?, language?)` - 快速抛出错误
- `throwIf(condition, errorKey, details?, language?)` - 条件抛出错误
- `assert(condition, errorKey, details?, language?)` - 断言函数
- `isOperationalError(error: Error): boolean` - 判断是否为操作性错误
- `isValidErrorKey(key: string): boolean` - 判断错误键是否有效
- `findErrorKeyByCode(code: number): string | undefined` - 根据错误码查找对应的错误键
- `getStatusCode(errorKey: string): number` - 获取错误键对应的 HTTP 状态码
- `getAllErrorCodes(): Record<string, ErrorCodeConfig>` - 获取所有错误码配置
| 错误码 | 错误名称 | 中文描述 | 英文描述 | HTTP 状态码 |
| -------------------- | ------------------------- | ---------------- | ------------------------------- | ----------- |
| **系统内部错误码** |
| 1001 | INTERNAL_ERROR | 内部服务器错误 | Internal server error | 500 |
| 1002 | SERVICE_UNAVAILABLE | 服务暂时不可用 | Service temporarily unavailable | 503 |
| 1003 | CONFIGURATION_ERROR | 系统配置错误 | System configuration error | 500 |
| 1004 | EXTERNAL_SERVICE_ERROR | 外部服务调用失败 | External service call failed | 502 |
| **参数验证错误码** |
| 2001 | INVALID_PARAMETER | 参数无效 | Invalid parameter | 400 |
| 2002 | MISSING_PARAMETER | 缺少必需参数 | Missing required parameter | 400 |
| 2003 | PARAMETER_OUT_OF_RANGE | 参数超出范围 | Parameter out of range | 400 |
| 2004 | INVALID_FORMAT | 格式不正确 | Invalid format | 400 |
| 2005 | CONTENT_TOO_LONG | 内容过长 | Content too long | 400 |
| 2006 | UNSUPPORTED_TYPE | 不支持的类型 | Unsupported type | 400 |
| **认证授权错误码** |
| 4001 | UNAUTHORIZED | 未授权访问 | Unauthorized access | 401 |
| 4002 | INVALID_TOKEN | 无效的令牌 | Invalid token | 401 |
| 4003 | TOKEN_EXPIRED | 令牌已过期 | Token expired | 401 |
| 4004 | INSUFFICIENT_PERMISSIONS | 权限不足 | Insufficient permissions | 403 |
| 4005 | ACCOUNT_DISABLED | 账户已被禁用 | Account disabled | 403 |
| 4006 | INVALID_CREDENTIALS | 认证信息无效 | Invalid credentials | 401 |
| 4007 | ACCOUNT_LOCKED | 账户已锁定 | Account locked | 423 |
| 4008 | SESSION_EXPIRED | 会话已过期 | Session expired | 401 |
| 4009 | ACCOUNT_NOT_FOUND | 用户不存在 | Account not found | 404 |
| 4010 | INSUFFICIENT_BALANCE | 余额不足 | Insufficient balance | 400 |
| 4011 | INSUFFICIENT_INTEGRAL | 积分不足 | Insufficient integral | 400 |
| **用户错误码** |
| 4101 | INVALID_INVITE_CODE | 邀请码无效 | Invalid invite code | 400 |
| **资源管理错误码** |
| 5001 | RESOURCE_NOT_FOUND | 资源不存在 | Resource not found | 404 |
| 5002 | RESOURCE_ALREADY_EXISTS | 资源已存在 | Resource already exists | 409 |
| 5003 | RESOURCE_CREATION_FAILED | 资源创建失败 | Resource creation failed | 500 |
| 5004 | RESOURCE_UPDATE_FAILED | 资源更新失败 | Resource update failed | 500 |
| 5005 | RESOURCE_DELETE_FAILED | 资源删除失败 | Resource deletion failed | 500 |
| 5006 | RESOURCE_IN_USE | 资源正在使用中 | Resource is in use | 409 |
| 5007 | RESOURCE_LOCKED | 资源已被锁定 | Resource is locked | 423 |
| **配额限制错误码** |
| 6001 | RATE_LIMIT_EXCEEDED | 请求频率超出限制 | Rate limit exceeded | 429 |
| 6002 | QUOTA_EXCEEDED | 配额已用完 | Quota exceeded | 429 |
| 6003 | STORAGE_QUOTA_EXCEEDED | 存储空间不足 | Storage quota exceeded | 507 |
| 6004 | CONCURRENT_LIMIT_EXCEEDED | 并发数超出限制 | Concurrent limit exceeded | 429 |
| 6005 | DAILY_LIMIT_EXCEEDED | 日使用量超出限制 | Daily limit exceeded | 429 |
| 6006 | SIZE_LIMIT_EXCEEDED | 大小超出限制 | Size limit exceeded | 413 |
| **数据处理错误码** |
| 7001 | DATA_CORRUPTION | 数据已损坏 | Data corruption detected | 500 |
| 7002 | SERIALIZATION_ERROR | 数据序列化失败 | Serialization failed | 500 |
| 7003 | DESERIALIZATION_ERROR | 数据反序列化失败 | Deserialization failed | 400 |
| 7004 | DATA_TRANSFORMATION_ERROR | 数据转换失败 | Data transformation failed | 500 |
| 7005 | CHECKSUM_MISMATCH | 校验和不匹配 | Checksum mismatch | 400 |
| **业务逻辑错误码** |
| 8001 | BUSINESS_RULE_VIOLATION | 违反业务规则 | Business rule violation | 400 |
| 8002 | OPERATION_NOT_ALLOWED | 操作不被允许 | Operation not allowed | 403 |
| 8003 | PREREQUISITE_NOT_MET | 前置条件未满足 | Prerequisite not met | 412 |
| 8004 | WORKFLOW_ERROR | 工作流错误 | Workflow error | 400 |
| 8005 | STATE_CONFLICT | 状态冲突 | State conflict | 409 |
| **第三方服务错误码** |
| 9001 | AI_SERVICE_ERROR | AI 服务调用失败 | AI service error | 502 |
| 9002 | AI_MODEL_UNAVAILABLE | AI 模型不可用 | AI model unavailable | 503 |
| 9003 | PAYMENT_SERVICE_ERROR | 支付服务错误 | Payment service error | 502 |
| 9004 | SMS_SERVICE_ERROR | 短信服务错误 | SMS service error | 502 |
| 9005 | EMAIL_SERVICE_ERROR | 邮件服务错误 | Email service error | 502 |
| 9006 | CLOUD_STORAGE_ERROR | 云存储服务错误 | Cloud storage error | 502 |
| 9007 | TRANSLATION_SERVICE_ERROR | 翻译服务错误 | Translation service error | 502 |
| 9008 | WECHAT_API_ERROR | 微信接口调用失败 | WeChat API error | 502 |
| **AI 服务错误码** |
| 11001 | MODEL_NOT_AVAILABLE | 模型不可用 | Model not available | 400 |
```
error-codes/
├── src/
│ ├── constants/
│ │ ├── common.ts
│ │ ├── business.ts
│ │ └── index.ts
│ ├── types/
│ │ └── index.ts
│ ├── utils/
│ │ └── index.ts
│ └── index.ts
├── examples/
├── __tests__/
├── package.json
└── README.md
```
```typescript
// ✅ 推荐 - 使用通用错误码
return createErrorResponse('RESOURCE_NOT_FOUND');
// ❌ 不推荐 - 创建业务特定错误码
return createErrorResponse('CHAT_CONVERSATION_NOT_FOUND');
```
```typescript
// 参数问题 -> 使用 2xxx 验证错误码
if (!isValidEmail(email)) {
return createErrorResponse('INVALID_FORMAT');
}
// 资源问题 -> 使用 5xxx 资源错误码
if (!user) {
return createErrorResponse('RESOURCE_NOT_FOUND');
}
// 权限问题 -> 使用 4xxx 认证错误码
if (!hasPermission) {
return createErrorResponse('INSUFFICIENT_PERMISSIONS');
}
```
```typescript
// 为特定业务创建错误码映射,而不是创建新的错误码
export const ChatServiceErrors = {
CONVERSATION_NOT_FOUND: 'RESOURCE_NOT_FOUND',
MESSAGE_TOO_LONG: 'CONTENT_TOO_LONG',
AI_SERVICE_ERROR: 'AI_SERVICE_ERROR',
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
} as const;
```
```typescript
// ✅ 推荐 - 使用中间件和便捷函数
app.get(
'/api/users/:id',
asyncHandler(async (req, res) => {
const id = parseInt(req.params.id);
throwIf(isNaN(id), 'INVALID_PARAMETER', { field: 'id' });
const user = await findUser(id);
throwIf(!user, 'RESOURCE_NOT_FOUND', { userId: id });
res.json(createSuccessResponse(user));
})
);
// ❌ 不推荐 - 手动处理每个错误
app.get('/api/users/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({
success: false,
message: '参数无效',
// ... 重复的错误处理代码
});
}
// ... 更多重复代码
} catch (error) {
// ... 手动错误处理
}
});
```
当需要添加新的错误码时:
1. **首先考虑现有错误码是否适用**
2. **如果需要新错误码,按错误性质归类**
3. **避免创建业务特定的错误码分类**
4. **保持错误码的通用性和可复用性**
例如,如果需要添加新的限制类型错误:
```typescript
// ✅ 添加到 QUOTA_ERROR_CODES
MONTHLY_LIMIT_EXCEEDED: {
code: 6007,
messages: {
'zh-CN': '月使用量超出限制',
'en-US': 'Monthly limit exceeded'
},
statusCode: 429,
description: '月使用量已达到上限'
}
// ❌ 不要创建新的业务分类
VIDEO_SERVICE_ERROR_CODES: {
// ...
}
```
```bash
npm install
npm test
npm run build
npm run demo
```
MIT License
---
通过按错误性质而非业务类型进行分类,我们实现了:
- **高复用性**:同一错误码可在多个业务中使用
- **易维护性**:错误码结构稳定,不随业务变化
- **标准化**:统一的错误处理方式
- **可扩展性**:易于添加新的错误类型
这种设计让错误码系统更加健壮和可持续发展。