feat: 完成健康检查接口和Swagger文档完善

 健康检查功能:
- 实现完整的健康检查接口(/api/health, /api/health/detailed)
- 支持MySQL和Redis依赖状态检查
- 包含系统信息、性能指标监控
- 修复this上下文问题,确保服务方法正常调用
- 添加全面的健康检查测试用例

📝 Swagger文档优化:
- 创建全局响应Schema定义和错误码说明
- 完善API文档,包含详细的错误码表格
- 添加JWT认证说明和响应格式示例
- 增加全局组件、响应模板和示例
- 创建Swagger文档功能测试

🎯 任务完成:
-  5.0 健康检查接口 - 实现系统和依赖健康状态监控
-  7.0 Swagger文档完善 - 增加全局响应示例和错误码说明

📁 新增文件:
- src/controllers/health.controller.ts - 健康检查控制器
- src/services/health.service.ts - 健康检查服务层
- src/type/health.type.ts - 健康检查类型定义
- src/validators/health.response.ts - 健康检查响应验证
- src/validators/global.response.ts - 全局响应Schema定义
- src/tests/health.test.ts - 健康检查功能测试
- src/tests/redis.test.ts - Redis连接测试
- src/tests/swagger.test.ts - Swagger文档功能测试
This commit is contained in:
expressgy 2025-06-28 22:09:02 +08:00
parent 621963f82c
commit 2ee70e5d42
48 changed files with 11376 additions and 3279 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules/
dist/
.env
bun.lockb
bun.lockb
/logs

File diff suppressed because it is too large Load Diff

3254
aiChat/002-cursor_redis.md Normal file

File diff suppressed because it is too large Load Diff

470
docs/chalk-guide.md Normal file
View File

@ -0,0 +1,470 @@
# Chalk 使用指南
> 最流行的终端字符串样式库,支持链式调用,功能强大
## 📦 安装
```bash
# 使用 Bun
bun add chalk
# 使用 npm
npm install chalk
# 使用 yarn
yarn add chalk
```
## 🚀 快速开始
```typescript
import chalk from 'chalk';
console.log(chalk.blue('Hello world!'));
console.log(chalk.red.bold('Error message'));
console.log(chalk.green.underline('Success!'));
```
## 🎨 完整 API 参考
### 基本文字颜色
```typescript
chalk.black('黑色文字'); // 黑色
chalk.red('红色文字'); // 红色
chalk.green('绿色文字'); // 绿色
chalk.yellow('黄色文字'); // 黄色
chalk.blue('蓝色文字'); // 蓝色
chalk.magenta('洋红色文字'); // 洋红色
chalk.cyan('青色文字'); // 青色
chalk.white('白色文字'); // 白色
chalk.gray('灰色文字'); // 灰色blackBright 的别名)
chalk.grey('灰色文字'); // 灰色gray 的别名)
```
### 明亮文字颜色
```typescript
chalk.blackBright('明亮黑色'); // 明亮黑色(灰色)
chalk.redBright('明亮红色'); // 明亮红色
chalk.greenBright('明亮绿色'); // 明亮绿色
chalk.yellowBright('明亮黄色'); // 明亮黄色
chalk.blueBright('明亮蓝色'); // 明亮蓝色
chalk.magentaBright('明亮洋红'); // 明亮洋红
chalk.cyanBright('明亮青色'); // 明亮青色
chalk.whiteBright('明亮白色'); // 明亮白色
```
### 背景颜色
```typescript
chalk.bgBlack('黑色背景');
chalk.bgRed('红色背景');
chalk.bgGreen('绿色背景');
chalk.bgYellow('黄色背景');
chalk.bgBlue('蓝色背景');
chalk.bgMagenta('洋红背景');
chalk.bgCyan('青色背景');
chalk.bgWhite('白色背景');
chalk.bgGray('灰色背景');
chalk.bgGrey('灰色背景');
```
### 明亮背景颜色
```typescript
chalk.bgBlackBright('明亮黑色背景');
chalk.bgRedBright('明亮红色背景');
chalk.bgGreenBright('明亮绿色背景');
chalk.bgYellowBright('明亮黄色背景');
chalk.bgBlueBright('明亮蓝色背景');
chalk.bgMagentaBright('明亮洋红背景');
chalk.bgCyanBright('明亮青色背景');
chalk.bgWhiteBright('明亮白色背景');
```
### 文字样式
```typescript
chalk.bold('粗体文字'); // 粗体
chalk.dim('暗淡文字'); // 暗淡
chalk.italic('斜体文字'); // 斜体
chalk.underline('下划线文字'); // 下划线
chalk.strikethrough('删除线文字'); // 删除线
chalk.inverse('反转颜色'); // 反转前景色和背景色
chalk.hidden('隐藏文字'); // 隐藏文字
chalk.overline('上划线文字'); // 上划线(某些终端支持)
```
## 🔗 链式调用Chalk 的特色)
```typescript
// 单一样式
chalk.blue('蓝色文字');
// 链式组合样式
chalk.red.bold('红色粗体');
chalk.green.underline('绿色下划线');
chalk.yellow.bgBlue('黄字蓝底');
// 复杂组合
chalk.red.bold.underline('红色粗体下划线');
chalk.blue.bgYellow.italic('蓝字黄底斜体');
chalk.green.dim.strikethrough('绿色暗淡删除线');
// 多重嵌套
chalk.red('红色 ' + chalk.blue('蓝色') + ' 继续红色');
chalk.bold('粗体 ' + chalk.italic('斜体') + ' 继续粗体');
```
## 🔧 高级功能
### RGB 和 HEX 颜色
```typescript
// RGB 颜色
chalk.rgb(255, 136, 0)('橙色文字'); // RGB
chalk.bgRgb(255, 136, 0)('橙色背景'); // RGB 背景
// HEX 颜色
chalk.hex('#FF8800')('橙色文字'); // HEX
chalk.bgHex('#FF8800')('橙色背景'); // HEX 背景
// HSL 颜色
chalk.hsl(32, 100, 50)('橙色文字'); // HSL
chalk.bgHsl(32, 100, 50)('橙色背景'); // HSL 背景
// HSV 颜色
chalk.hsv(32, 100, 100)('橙色文字'); // HSV
chalk.bgHsv(32, 100, 100)('橙色背景'); // HSV 背景
// Keyword 颜色
chalk.keyword('orange')('橙色文字'); // 关键字颜色
chalk.bgKeyword('orange')('橙色背景'); // 关键字背景色
```
### ANSI 256 颜色
```typescript
// ANSI 256 颜色0-255
chalk.ansi256(208)('橙色文字'); // ANSI 256 前景色
chalk.bgAnsi256(208)('橙色背景'); // ANSI 256 背景色
// ANSI 16 颜色0-15
chalk.ansi(9)('明亮红色'); // ANSI 16 前景色
chalk.bgAnsi(9)('明亮红色背景'); // ANSI 16 背景色
```
### 颜色支持检测
```typescript
// 检测颜色支持级别
console.log(chalk.level); // 0, 1, 2, 3
console.log(chalk.supportsColor); // ColorSupport 对象
// 强制设置颜色级别
chalk.level = 3; // 强制支持真彩色
```
### 创建自定义实例
```typescript
import { Chalk } from 'chalk';
// 创建无颜色实例
const noColors = new Chalk({ level: 0 });
console.log(noColors.red('不会显示颜色'));
// 创建强制颜色实例
const forceColors = new Chalk({ level: 3 });
console.log(forceColors.rgb(255, 0, 0)('强制显示红色'));
```
## 💡 实用示例
### 日志级别颜色化
```typescript
import chalk from 'chalk';
const logger = {
error: (msg: string) => console.log(chalk.red.bold(`❌ ERROR: ${msg}`)),
warn: (msg: string) => console.log(chalk.yellow(`⚠️ WARN: ${msg}`)),
info: (msg: string) => console.log(chalk.blue(` INFO: ${msg}`)),
success: (msg: string) => console.log(chalk.green.bold(`✅ SUCCESS: ${msg}`)),
debug: (msg: string) => console.log(chalk.gray(`🐛 DEBUG: ${msg}`)),
};
// 使用
logger.error('数据库连接失败');
logger.warn('配置文件缺少某些字段');
logger.info('服务器启动中...');
logger.success('用户登录成功');
logger.debug('变量值: user_id = 123');
```
### 进度条和状态
```typescript
// 进度条
const progress = (percent: number) => {
const filled = Math.floor(percent / 5);
const empty = 20 - filled;
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
return `${bar} ${percent}%`;
};
console.log(progress(75));
// 状态标签
const status = {
running: chalk.yellow('⏳ RUNNING'),
success: chalk.white.bgGreen(' SUCCESS '),
error: chalk.white.bgRed(' ERROR '),
warning: chalk.black.bgYellow(' WARNING '),
info: chalk.white.bgBlue(' INFO '),
};
console.log(status.running + ' 正在处理...');
console.log(status.success + ' 操作完成!');
console.log(status.error + ' 操作失败!');
```
### 表格美化
```typescript
const table = [
['姓名', '年龄', '状态', '分数'],
['张三', '25', '在线', '95'],
['李四', '30', '离线', '87'],
['王五', '28', '在线', '92'],
];
// 表头
console.log(chalk.bold.blue(table[0].join(' | ')));
console.log(chalk.gray('─'.repeat(30)));
// 数据行
table.slice(1).forEach((row) => {
const [name, age, status, score] = row;
const statusColor = status === '在线' ? chalk.green : chalk.red;
const scoreColor = parseInt(score) >= 90 ? chalk.green.bold : parseInt(score) >= 80 ? chalk.yellow : chalk.red;
console.log(`${name} | ${age} | ${statusColor(status)} | ${scoreColor(score)}`);
});
```
### 代码语法高亮
```typescript
const highlightCode = (code: string) => {
return code
.replace(/\b(function|const|let|var|if|else|for|while|return)\b/g, (match) => chalk.blue.bold(match))
.replace(/\b(true|false|null|undefined)\b/g, (match) => chalk.magenta(match))
.replace(/(['"`])(.*?)\1/g, (match) => chalk.green(match))
.replace(/\/\/.*/g, (match) => chalk.gray(match))
.replace(/\b\d+\b/g, (match) => chalk.cyan(match));
};
const code = `
function hello(name) {
// 打印问候语
const message = "Hello, " + name;
return message;
}
`;
console.log(highlightCode(code));
```
### 彩虹文字效果
```typescript
const rainbow = (text: string) => {
const colors = [chalk.red, chalk.yellow, chalk.green, chalk.cyan, chalk.blue, chalk.magenta];
return text
.split('')
.map((char, i) => colors[i % colors.length](char))
.join('');
};
console.log(rainbow('Rainbow Text!'));
```
### 渐变效果
```typescript
const gradient = (text: string, startColor: [number, number, number], endColor: [number, number, number]) => {
const length = text.length;
return text
.split('')
.map((char, i) => {
const ratio = i / (length - 1);
const r = Math.round(startColor[0] + (endColor[0] - startColor[0]) * ratio);
const g = Math.round(startColor[1] + (endColor[1] - startColor[1]) * ratio);
const b = Math.round(startColor[2] + (endColor[2] - startColor[2]) * ratio);
return chalk.rgb(r, g, b)(char);
})
.join('');
};
console.log(gradient('Gradient Text!', [255, 0, 0], [0, 0, 255])); // 红到蓝渐变
```
## 🚀 在项目中集成
### 与 Winston 日志器集成
```typescript
import winston from 'winston';
import chalk from 'chalk';
const levelStyles = {
error: chalk.red.bold,
warn: chalk.yellow,
info: chalk.blue,
http: chalk.green,
verbose: chalk.cyan,
debug: chalk.gray,
silly: chalk.magenta,
};
const logger = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp({ format: 'HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp }) => {
const styleLevel = levelStyles[level as keyof typeof levelStyles] || chalk.white;
return `[${chalk.gray(timestamp)}] ${styleLevel(level.toUpperCase())}: ${message}`;
}),
),
}),
],
});
```
### CLI 工具美化
```typescript
// 命令行工具的美化输出
class CLI {
static title(text: string) {
console.log(chalk.bold.blue(`\n🚀 ${text}\n`));
}
static section(text: string) {
console.log(chalk.bold.yellow(`📋 ${text}`));
}
static item(text: string, status?: 'success' | 'error' | 'warning') {
const icon = status === 'success' ? '✅' : status === 'error' ? '❌' : status === 'warning' ? '⚠️' : '•';
const color =
status === 'success'
? chalk.green
: status === 'error'
? chalk.red
: status === 'warning'
? chalk.yellow
: chalk.white;
console.log(` ${icon} ${color(text)}`);
}
static divider() {
console.log(chalk.gray('─'.repeat(50)));
}
}
// 使用
CLI.title('项目构建工具');
CLI.section('检查依赖');
CLI.item('Node.js 版本检查', 'success');
CLI.item('npm 包完整性检查', 'success');
CLI.item('TypeScript 编译检查', 'warning');
CLI.divider();
```
### 创建主题系统
```typescript
// 定义主题
const themes = {
default: {
primary: chalk.blue,
secondary: chalk.cyan,
success: chalk.green,
warning: chalk.yellow,
error: chalk.red,
muted: chalk.gray,
highlight: chalk.yellow.bgBlack,
badge: (text: string) => chalk.white.bgBlue(` ${text} `),
},
dark: {
primary: chalk.blueBright,
secondary: chalk.cyanBright,
success: chalk.greenBright,
warning: chalk.yellowBright,
error: chalk.redBright,
muted: chalk.gray,
highlight: chalk.black.bgYellow,
badge: (text: string) => chalk.black.bgWhite(` ${text} `),
},
};
// 使用主题
const theme = themes.default;
console.log(theme.primary('主要信息'));
console.log(theme.success('操作成功'));
console.log(theme.error('发生错误'));
console.log(theme.highlight('重要提示'));
console.log(theme.badge('新功能'));
```
## 📊 完整方法列表
| 类型 | 方法 | 描述 |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ |
| **基本颜色** | `black, red, green, yellow, blue, magenta, cyan, white, gray, grey` | 10种基本文字颜色 |
| **明亮颜色** | `blackBright, redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, whiteBright` | 8种明亮文字颜色 |
| **基本背景** | `bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite, bgGray, bgGrey` | 10种基本背景颜色 |
| **明亮背景** | `bgBlackBright, bgRedBright, bgGreenBright, bgYellowBright, bgBlueBright, bgMagentaBright, bgCyanBright, bgWhiteBright` | 8种明亮背景颜色 |
| **文字样式** | `bold, dim, italic, underline, strikethrough, inverse, hidden, overline` | 8种文字样式 |
| **真彩色** | `rgb(r, g, b), bgRgb(r, g, b), hex(color), bgHex(color), hsl(h, s, l), bgHsl(h, s, l), hsv(h, s, v), bgHsv(h, s, v), keyword(color), bgKeyword(color)` | 真彩色支持 |
| **ANSI 颜色** | `ansi(code), bgAnsi(code), ansi256(code), bgAnsi256(code)` | ANSI 颜色码支持 |
| **工具方法** | `level, supportsColor, Chalk` | 颜色支持检测和自定义实例 |
## ⚡ 性能和特性
### 优势
- **链式调用**:支持优雅的链式语法
- **真彩色支持**:支持 1600万种颜色
- **自动检测**:自动检测终端颜色支持
- **零依赖**:不依赖其他库
- **TypeScript支持**:完整的类型定义
### 性能对比
- **功能最全**:支持最多的颜色格式和样式
- **生态丰富**:最多项目使用,社区支持好
- **文件大小**:相对较大(~25KB但功能更丰富
## 🔗 相关资源
- [GitHub 仓库](https://github.com/chalk/chalk)
- [npm 页面](https://www.npmjs.com/package/chalk)
- [官方文档](https://github.com/chalk/chalk#readme)
- [颜色支持检测](https://github.com/chalk/supports-color)
## 💡 最佳实践
1. **链式调用**:充分利用 Chalk 的链式语法
2. **颜色一致性**:在项目中保持颜色使用的一致性
3. **主题系统**:为复杂项目创建统一的主题系统
4. **性能考虑**:在高频调用时考虑缓存样式函数
5. **可读性**:不要过度使用颜色,保持输出的可读性
---
**Chalk 是功能最全面的终端颜色库,适合需要丰富颜色功能的项目!**

622
docs/http-status-codes.md Normal file
View File

@ -0,0 +1,622 @@
# HTTP 状态码完整指南
## 概述
HTTP 状态码是服务器响应客户端请求时返回的三位数字代码用于表示请求的处理结果。本文档详细列举了所有常见的HTTP状态码以及在API开发中的最佳实践。
## 状态码分类
- **1xx (信息性)**: 请求已接收,继续处理
- **2xx (成功)**: 请求已成功被服务器接收、理解、并接受
- **3xx (重定向)**: 需要后续操作才能完成这一请求
- **4xx (客户端错误)**: 请求包含错误语法或无法被执行
- **5xx (服务器错误)**: 服务器无法完成明显有效的请求
---
## 1xx 信息性状态码
### 100 Continue
- **说明**: 继续,客户端应继续其请求
- **使用场景**: 大文件上传时的初始确认
- **处理建议**: 通常由HTTP库自动处理开发者无需特殊处理
### 101 Switching Protocols
- **说明**: 切换协议,服务器根据客户端的请求切换协议
- **使用场景**: WebSocket握手、HTTP升级到HTTPS
- **处理建议**: 确保客户端支持新协议
### 102 Processing
- **说明**: 处理中,服务器已接收并正在处理请求
- **使用场景**: 长时间运行的请求
- **处理建议**: 告知客户端请求正在处理,避免超时
---
## 2xx 成功状态码
### 200 OK
- **说明**: 请求成功
- **使用场景**: GET、PUT、PATCH请求成功
- **返回内容**: 请求的资源或操作结果
- **示例响应**:
```json
{
"code": 0,
"message": "操作成功",
"data": { "id": 1, "name": "用户" }
}
```
### 201 Created
- **说明**: 创建成功
- **使用场景**: POST请求成功创建资源
- **返回内容**: 新创建的资源信息
- **Header建议**: 添加`Location`头指向新资源
- **示例响应**:
```json
{
"code": 0,
"message": "创建成功",
"data": { "id": 123, "name": "新用户" }
}
```
### 202 Accepted
- **说明**: 已接受,请求已被接受,但尚未处理完成
- **使用场景**: 异步任务、批量操作
- **返回内容**: 任务状态或追踪信息
- **示例响应**:
```json
{
"code": 0,
"message": "任务已提交",
"data": { "taskId": "task-123", "status": "processing" }
}
```
### 204 No Content
- **说明**: 无内容,请求成功但无返回内容
- **使用场景**: DELETE请求成功、PUT更新成功
- **返回内容**: 空响应体
- **注意**: 不应返回JSON响应体
### 206 Partial Content
- **说明**: 部分内容
- **使用场景**: 文件断点续传、大文件分块下载
- **Header要求**: 必须包含`Content-Range`
- **处理建议**: 实现Range请求支持
---
## 3xx 重定向状态码
### 301 Moved Permanently
- **说明**: 永久重定向
- **使用场景**: URL结构永久性改变、API版本升级
- **Header要求**: 必须包含`Location`头
- **SEO影响**: 搜索引擎会更新索引
### 302 Found
- **说明**: 临时重定向
- **使用场景**: 临时维护、负载均衡
- **Header要求**: 必须包含`Location`头
- **注意**: 不会影响搜索引擎索引
### 304 Not Modified
- **说明**: 未修改,资源未发生变化
- **使用场景**: 缓存验证、条件请求
- **Header要求**: 配合`ETag`或`Last-Modified`
- **性能优化**: 减少带宽消耗
### 307 Temporary Redirect
- **说明**: 临时重定向(保持请求方法)
- **使用场景**: 确保POST/PUT等方法不被改变
- **与302区别**: 严格保持原始HTTP方法
### 308 Permanent Redirect
- **说明**: 永久重定向(保持请求方法)
- **使用场景**: API端点永久迁移需保持HTTP方法
- **与301区别**: 严格保持原始HTTP方法
---
## 4xx 客户端错误状态码
### 400 Bad Request
- **说明**: 请求语法错误或参数无效
- **常见原因**:
- JSON格式错误
- 必需参数缺失
- 参数类型不正确
- 参数值超出范围
- **解决方法**:
- 验证请求格式
- 检查参数完整性
- 使用参数验证库
- **示例响应**:
```json
{
"code": 400,
"message": "参数校验失败",
"data": null,
"errors": [
{ "field": "email", "message": "邮箱格式不正确" },
{ "field": "age", "message": "年龄必须大于0" }
]
}
```
### 401 Unauthorized
- **说明**: 未认证,需要身份验证
- **常见原因**:
- 缺少认证信息
- Token已过期
- Token格式错误
- 认证信息无效
- **解决方法**:
- 提供有效的认证信息
- 刷新过期的Token
- 重新登录
- **Header建议**: 添加`WWW-Authenticate`头
- **示例响应**:
```json
{
"code": 401,
"message": "认证失败,请重新登录",
"data": null
}
```
### 403 Forbidden
- **说明**: 禁止访问,已认证但无权限
- **常见原因**:
- 权限不足
- 资源被禁止访问
- IP被封禁
- 账户被冻结
- **解决方法**:
- 联系管理员获取权限
- 检查用户角色设置
- 验证访问策略
- **示例响应**:
```json
{
"code": 403,
"message": "权限不足,无法访问该资源",
"data": null
}
```
### 404 Not Found
- **说明**: 资源不存在
- **常见原因**:
- URL路径错误
- 资源已被删除
- 资源ID不存在
- API端点不存在
- **解决方法**:
- 检查URL拼写
- 验证资源是否存在
- 确认API版本
- **示例响应**:
```json
{
"code": 404,
"message": "请求的资源不存在",
"data": null
}
```
### 405 Method Not Allowed
- **说明**: 请求方法不被允许
- **常见原因**:
- 使用了不支持的HTTP方法
- GET请求用于需要POST的接口
- **Header要求**: 必须包含`Allow`头列出支持的方法
- **示例响应**:
```json
{
"code": 405,
"message": "不支持的请求方法",
"data": null,
"allowedMethods": ["GET", "POST"]
}
```
### 406 Not Acceptable
- **说明**: 不可接受,服务器无法返回客户端可接受的内容格式
- **常见原因**:
- Accept头指定了不支持的格式
- 内容协商失败
- **解决方法**: 调整Accept头或支持更多格式
### 409 Conflict
- **说明**: 冲突,请求与当前资源状态冲突
- **常见原因**:
- 资源已存在
- 并发修改冲突
- 业务逻辑冲突
- **解决方法**:
- 重新获取最新状态
- 解决冲突后重试
- **示例响应**:
```json
{
"code": 409,
"message": "用户名已存在",
"data": null
}
```
### 410 Gone
- **说明**: 资源已永久删除
- **使用场景**: 资源被主动删除且不会恢复
- **与404区别**: 明确表示资源曾经存在但已删除
### 411 Length Required
- **说明**: 需要Content-Length头
- **常见原因**: POST/PUT请求缺少Content-Length
- **解决方法**: 添加Content-Length头
### 412 Precondition Failed
- **说明**: 前置条件失败
- **使用场景**: 条件请求If-Match、If-None-Match等失败
- **常见原因**: ETag不匹配、条件不满足
### 413 Payload Too Large
- **说明**: 请求体过大
- **常见原因**:
- 上传文件超出限制
- JSON数据过大
- **解决方法**:
- 减小文件大小
- 分块上传
- 调整服务器限制
- **示例响应**:
```json
{
"code": 413,
"message": "上传文件大小超出限制最大10MB",
"data": null
}
```
### 414 URI Too Long
- **说明**: URI过长
- **常见原因**: GET请求参数过多
- **解决方法**: 使用POST请求或减少参数
### 415 Unsupported Media Type
- **说明**: 不支持的媒体类型
- **常见原因**:
- Content-Type不正确
- 上传了不支持的文件格式
- **解决方法**: 检查Content-Type头
- **示例响应**:
```json
{
"code": 415,
"message": "不支持的文件格式,仅支持图片文件",
"data": null,
"supportedTypes": ["image/jpeg", "image/png", "image/gif"]
}
```
### 422 Unprocessable Entity
- **说明**: 无法处理的实体,语法正确但语义错误
- **常见原因**:
- 业务逻辑验证失败
- 数据关联性错误
- 状态不允许操作
- **与400区别**: 语法正确但业务逻辑错误
- **示例响应**:
```json
{
"code": 422,
"message": "业务逻辑验证失败",
"data": null,
"errors": [{ "field": "endDate", "message": "结束日期不能早于开始日期" }]
}
```
### 423 Locked
- **说明**: 资源被锁定
- **使用场景**: WebDAV、文件编辑锁定
- **解决方法**: 等待锁定释放或强制解锁
### 429 Too Many Requests
- **说明**: 请求过于频繁
- **常见原因**:
- 超出API调用频率限制
- 防暴力破解保护
- **Header建议**:
- `Retry-After`: 建议重试时间
- `X-RateLimit-*`: 频率限制信息
- **解决方法**:
- 降低请求频率
- 实现指数退避重试
- **示例响应**:
```json
{
"code": 429,
"message": "请求过于频繁,请稍后重试",
"data": null,
"retryAfter": 60
}
```
---
## 5xx 服务器错误状态码
### 500 Internal Server Error
- **说明**: 服务器内部错误
- **常见原因**:
- 代码异常未捕获
- 数据库连接失败
- 第三方服务异常
- 配置错误
- **解决方法**:
- 检查服务器日志
- 修复代码bug
- 验证配置正确性
- **开发环境响应**:
```json
{
"code": 500,
"message": "服务器内部错误",
"data": null,
"traceId": "req-123456",
"details": "NullPointerException at line 42",
"stack": "详细堆栈信息..."
}
```
- **生产环境响应**:
```json
{
"code": 500,
"message": "服务器内部错误,请稍后重试",
"data": null,
"traceId": "req-123456"
}
```
### 501 Not Implemented
- **说明**: 功能未实现
- **使用场景**: API端点规划但未开发
- **解决方法**: 等待功能开发完成
### 502 Bad Gateway
- **说明**: 网关错误
- **常见原因**:
- 上游服务器响应无效
- 代理服务器配置错误
- 负载均衡器问题
- **解决方法**:
- 检查上游服务状态
- 验证代理配置
- 重启相关服务
### 503 Service Unavailable
- **说明**: 服务不可用
- **常见原因**:
- 服务器维护
- 系统过载
- 依赖服务不可用
- **Header建议**: 添加`Retry-After`头
- **解决方法**:
- 等待维护完成
- 扩容服务器
- 修复依赖服务
- **示例响应**:
```json
{
"code": 503,
"message": "服务暂时不可用,系统维护中",
"data": null,
"retryAfter": 3600
}
```
### 504 Gateway Timeout
- **说明**: 网关超时
- **常见原因**:
- 上游服务响应超时
- 网络延迟过高
- 处理时间过长
- **解决方法**:
- 优化上游服务性能
- 增加超时时间
- 实现异步处理
### 505 HTTP Version Not Supported
- **说明**: HTTP版本不支持
- **解决方法**: 使用支持的HTTP版本
---
## API 错误处理最佳实践
### 1. 统一错误响应格式
```json
{
"code": 400,
"message": "用户友好的错误描述",
"data": null,
"traceId": "req-uuid-123",
"timestamp": "2024-01-01T12:00:00Z",
"errors": [
{
"field": "email",
"message": "邮箱格式不正确",
"code": "INVALID_EMAIL_FORMAT"
}
]
}
```
### 2. 错误码设计原则
- **HTTP状态码**: 表示HTTP层面的处理结果
- **业务错误码**: 表示具体的业务错误类型
- **错误消息**: 提供用户友好的错误描述
- **错误详情**: 开发环境提供详细信息,生产环境保护敏感信息
### 3. 常见业务错误码映射
| 业务场景 | HTTP状态码 | 业务错误码 | 错误消息 |
| ---------------- | ---------- | ---------- | -------------------- |
| 参数校验失败 | 400 | 1001 | 参数校验失败 |
| 用户名或密码错误 | 400 | 1002 | 用户名或密码错误 |
| Token无效 | 401 | 1003 | 认证失败,请重新登录 |
| 权限不足 | 403 | 1004 | 权限不足 |
| 资源不存在 | 404 | 1005 | 请求的资源不存在 |
| 资源已存在 | 409 | 1006 | 资源已存在 |
| 请求频率限制 | 429 | 1007 | 请求过于频繁 |
| 系统错误 | 500 | 2001 | 系统内部错误 |
| 数据库错误 | 500 | 2002 | 数据存储错误 |
| 第三方服务错误 | 502 | 2003 | 外部服务错误 |
### 4. 错误日志记录
```typescript
// 错误日志应包含的信息
{
timestamp: "2024-01-01T12:00:00Z",
level: "ERROR",
message: "用户登录失败",
traceId: "req-uuid-123",
userId: "user-456",
ip: "192.168.1.100",
userAgent: "Mozilla/5.0...",
path: "/api/login",
method: "POST",
statusCode: 400,
errorCode: 1002,
errorType: "BUSINESS_ERROR",
stack: "错误堆栈信息...",
requestBody: { /* 脱敏后的请求数据 */ },
context: { /* 其他上下文信息 */ }
}
```
### 5. 客户端错误处理建议
```typescript
// 客户端错误处理示例
async function handleApiError(error: ApiError) {
const { status, data } = error.response;
switch (status) {
case 400:
// 显示参数错误信息
showValidationErrors(data.errors);
break;
case 401:
// 清除本地token跳转登录页
clearToken();
redirectToLogin();
break;
case 403:
// 显示权限不足提示
showPermissionDenied();
break;
case 404:
// 显示资源不存在
showNotFound();
break;
case 429:
// 实现指数退避重试
await retryWithBackoff();
break;
case 500:
case 502:
case 503:
case 504:
// 显示系统错误,建议稍后重试
showSystemError(data.message);
break;
default:
// 显示通用错误信息
showGenericError();
}
}
```
### 6. 监控和告警
- **4xx错误**: 监控客户端错误趋势可能表示API设计问题
- **5xx错误**: 立即告警,表示系统问题需要紧急处理
- **特定错误码**: 针对业务关键错误设置专门监控
- **错误率阈值**: 设置错误率阈值告警
---
## 总结
正确使用HTTP状态码是RESTful API设计的重要组成部分。遵循标准的状态码使用规范结合清晰的错误消息和完善的错误处理机制能够显著提升API的可用性和开发体验。
### 关键原则
1. **语义明确**: 状态码应准确反映请求的处理结果
2. **一致性**: 相同类型的错误应使用相同的状态码
3. **用户友好**: 错误消息应易于理解且可操作
4. **安全性**: 生产环境不暴露敏感系统信息
5. **可追踪**: 每个错误都应有唯一标识便于追踪
6. **可监控**: 重要错误应有相应的监控和告警机制
正确实施这些错误处理策略将大大提升系统的健壮性和可维护性。

75
docs/life.md Normal file
View File

@ -0,0 +1,75 @@
您询问的是 Elysia 的完整生命周期。
## 答案
Elysia 的完整生命周期包含以下阶段,按执行顺序排列:
### 1. start (服务器启动)
服务器启动时执行的钩子 [1](#3-0)
### 2. request (请求接收)
请求到达时最早执行的钩子,在路由匹配之前 [2](#3-1)
### 3. parse (请求体解析)
解析请求体内容,支持多种内容类型 [3](#3-2)
### 4. transform (上下文转换)
转换请求上下文,可以修改请求数据 [4](#3-3)
### 5. derive (派生上下文)
派生新的上下文属性,实际上是 transform 的子类型 [5](#3-4)
### 6. resolve (解析依赖)
解析依赖注入,实际上转换为 beforeHandle 钩子 [6](#3-5)
### 7. beforeHandle (处理前)
在主处理器执行前运行,可以进行验证和预处理 [7](#3-6)
### 8. handler (主处理器)
执行路由的主处理函数
### 9. afterHandle (处理后)
在主处理器执行后运行,可以修改响应 [8](#3-7)
### 10. mapResponse (响应映射)
将处理器返回值映射为 HTTP 响应 [9](#3-8)
### 11. afterResponse (响应后)
响应发送后执行,用于日志记录和清理 [10](#3-9)
### 12. error (错误处理)
在任何阶段发生错误时执行 [11](#3-10)
### 13. stop (服务器停止)
服务器关闭时执行的钩子 [12](#3-11)
### 生命周期执行流程
在 AOT 编译模式下,这些钩子会被编译成优化的 JavaScript 代码 [13](#3-12) 。在动态模式下,则通过运行时解释执行 [14](#3-13) 。
### trace (追踪)
贯穿整个生命周期的追踪系统,用于调试和监控 [15](#3-14)
## Notes
生命周期钩子支持不同的作用域global、scoped、local影响它们在插件间的继承行为。每个钩子都可以返回早期响应来短路后续处理流程。这个设计使得 Elysia 具有高度的可扩展性和灵活性。
Wiki pages you might want to explore:
- [Request Pipeline (elysiajs/elysia)](/wiki/elysiajs/elysia#2)
- [Routing (elysiajs/elysia)](/wiki/elysiajs/elysia#2.2)
- [Lifecycle Hooks (elysiajs/elysia)](/wiki/elysiajs/elysia#2.3)

294
docs/picocolors-guide.md Normal file
View File

@ -0,0 +1,294 @@
# Picocolors 使用指南
> 轻量级终端颜色库,仅 2KB性能优秀API 简洁
## 📦 安装
```bash
# 使用 Bun
bun add picocolors
# 使用 npm
npm install picocolors
# 使用 yarn
yarn add picocolors
```
## 🚀 快速开始
```typescript
import pc from 'picocolors';
console.log(pc.red('红色文字'));
console.log(pc.green('绿色文字'));
console.log(pc.blue('蓝色文字'));
```
## 🎨 完整 API 参考
### 基本文字颜色
```typescript
pc.black('黑色文字'); // 黑色
pc.red('红色文字'); // 红色
pc.green('绿色文字'); // 绿色
pc.yellow('黄色文字'); // 黄色
pc.blue('蓝色文字'); // 蓝色
pc.magenta('洋红色文字'); // 洋红色
pc.cyan('青色文字'); // 青色
pc.white('白色文字'); // 白色
```
### 明亮文字颜色
```typescript
pc.blackBright('明亮黑色'); // 灰色
pc.redBright('明亮红色'); // 明亮红色
pc.greenBright('明亮绿色'); // 明亮绿色
pc.yellowBright('明亮黄色'); // 明亮黄色
pc.blueBright('明亮蓝色'); // 明亮蓝色
pc.magentaBright('明亮洋红'); // 明亮洋红
pc.cyanBright('明亮青色'); // 明亮青色
pc.whiteBright('明亮白色'); // 明亮白色
```
### 背景颜色
```typescript
pc.bgBlack('黑色背景');
pc.bgRed('红色背景');
pc.bgGreen('绿色背景');
pc.bgYellow('黄色背景');
pc.bgBlue('蓝色背景');
pc.bgMagenta('洋红背景');
pc.bgCyan('青色背景');
pc.bgWhite('白色背景');
```
### 明亮背景颜色
```typescript
pc.bgBlackBright('明亮黑色背景');
pc.bgRedBright('明亮红色背景');
pc.bgGreenBright('明亮绿色背景');
pc.bgYellowBright('明亮黄色背景');
pc.bgBlueBright('明亮蓝色背景');
pc.bgMagentaBright('明亮洋红背景');
pc.bgCyanBright('明亮青色背景');
pc.bgWhiteBright('明亮白色背景');
```
### 文字样式
```typescript
pc.bold('粗体文字'); // 粗体
pc.dim('暗淡文字'); // 暗淡
pc.italic('斜体文字'); // 斜体
pc.underline('下划线文字'); // 下划线
pc.strikethrough('删除线文字'); // 删除线
pc.inverse('反转颜色'); // 反转前景色和背景色
pc.hidden('隐藏文字'); // 隐藏文字
pc.reset('重置样式'); // 重置所有样式
```
## 🔧 特殊功能
### 颜色支持检测
```typescript
// 检测终端是否支持颜色
if (pc.isColorSupported) {
console.log(pc.green('终端支持颜色!'));
} else {
console.log('终端不支持颜色');
}
```
### 自定义颜色支持
```typescript
// 强制启用颜色
const forceColors = pc.createColors(true);
console.log(forceColors.red('强制显示红色'));
// 强制禁用颜色
const noColors = pc.createColors(false);
console.log(noColors.red('不会显示颜色'));
```
## 🎯 组合使用
⚠️ **重要**Picocolors 不支持链式调用,需要使用嵌套方式
```typescript
// ❌ 错误用法 - 不支持链式调用
// pc.red.bold('文字')
// ✅ 正确用法 - 嵌套调用
pc.red(pc.bold('红色粗体文字'));
pc.blue(pc.underline('蓝色下划线文字'));
pc.bgYellow(pc.black('黄底黑字'));
pc.green(pc.italic(pc.bold('绿色粗斜体')));
// 复杂组合
pc.bgRed(pc.white(pc.bold(' ERROR '))) + ' ' + pc.red('错误信息');
```
## 💡 实用示例
### 日志级别颜色化
```typescript
import pc from 'picocolors';
const logger = {
error: (msg: string) => console.log(pc.red(pc.bold(`❌ ERROR: ${msg}`))),
warn: (msg: string) => console.log(pc.yellow(`⚠️ WARN: ${msg}`)),
info: (msg: string) => console.log(pc.blue(` INFO: ${msg}`)),
success: (msg: string) => console.log(pc.green(pc.bold(`✅ SUCCESS: ${msg}`))),
debug: (msg: string) => console.log(pc.gray(`🐛 DEBUG: ${msg}`)),
};
// 使用
logger.error('数据库连接失败');
logger.warn('配置文件缺少某些字段');
logger.info('服务器启动中...');
logger.success('用户登录成功');
logger.debug('变量值: user_id = 123');
```
### 进度提示
```typescript
// 加载状态
console.log(pc.yellow('⏳ 正在处理...'));
// 成功状态
console.log(pc.bgGreen(pc.white(' SUCCESS ')) + ' 操作完成!');
// 错误状态
console.log(pc.bgRed(pc.white(' ERROR ')) + ' 操作失败!');
// 警告状态
console.log(pc.bgYellow(pc.black(' WARNING ')) + ' 注意事项');
```
### 表格美化
```typescript
const table = [
['姓名', '年龄', '状态'],
['张三', '25', '在线'],
['李四', '30', '离线'],
['王五', '28', '在线'],
];
// 表头
console.log(pc.bold(pc.blue(table[0].join(' | '))));
console.log(pc.gray('─'.repeat(20)));
// 数据行
table.slice(1).forEach((row) => {
const [name, age, status] = row;
const statusColor = status === '在线' ? pc.green : pc.red;
console.log(`${name} | ${age} | ${statusColor(status)}`);
});
```
### 代码高亮
```typescript
const code = `
function ${pc.cyan('hello')}(${pc.yellow('name')}) {
${pc.gray('// 打印问候语')}
console.log(${pc.green('"Hello, "')} + ${pc.yellow('name')});
}
`;
console.log(code);
```
## 📊 完整方法列表
| 类型 | 方法 | 描述 |
| ------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------- |
| **基本颜色** | `black, red, green, yellow, blue, magenta, cyan, white` | 8种基本文字颜色 |
| **明亮颜色** | `blackBright, redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, whiteBright` | 8种明亮文字颜色 |
| **基本背景** | `bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite` | 8种基本背景颜色 |
| **明亮背景** | `bgBlackBright, bgRedBright, bgGreenBright, bgYellowBright, bgBlueBright, bgMagentaBright, bgCyanBright, bgWhiteBright` | 8种明亮背景颜色 |
| **文字样式** | `bold, dim, italic, underline, strikethrough, inverse, hidden, reset` | 8种文字样式 |
| **工具方法** | `isColorSupported, createColors(enabled)` | 颜色支持检测和自定义 |
## 🚀 在项目中集成
### 与 Winston 日志器集成
```typescript
import winston from 'winston';
import pc from 'picocolors';
const levelColors = {
error: pc.red,
warn: pc.yellow,
info: pc.blue,
http: pc.green,
verbose: pc.cyan,
debug: pc.gray,
silly: pc.magenta,
};
const logger = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp({ format: 'HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp }) => {
const colorize = levelColors[level as keyof typeof levelColors] || pc.white;
return `[${pc.gray(timestamp)}] ${colorize(level.toUpperCase())}: ${message}`;
}),
),
}),
],
});
```
### 创建主题
```typescript
// 定义主题
const theme = {
primary: pc.blue,
secondary: pc.cyan,
success: pc.green,
warning: pc.yellow,
error: pc.red,
muted: pc.gray,
highlight: (text: string) => pc.bgYellow(pc.black(text)),
badge: (text: string) => pc.bgBlue(pc.white(` ${text} `)),
};
// 使用主题
console.log(theme.primary('主要信息'));
console.log(theme.success('操作成功'));
console.log(theme.error('发生错误'));
console.log(theme.highlight('重要提示'));
console.log(theme.badge('新功能'));
```
## ⚡ 性能优势
- **轻量级**:仅 2KB比 chalk 小 14 倍
- **零依赖**:不依赖其他包
- **高性能**:比其他颜色库快 2-3 倍
- **兼容性**:支持 Node.js、Deno、浏览器
## 🔗 相关资源
- [GitHub 仓库](https://github.com/alexeyraspopov/picocolors)
- [npm 页面](https://www.npmjs.com/package/picocolors)
- [性能对比](https://github.com/alexeyraspopov/picocolors#benchmarks)
---
**推荐在所有需要终端颜色输出的项目中使用 Picocolors**

78
docs/winston.md Normal file
View File

@ -0,0 +1,78 @@
Winston 支持以下 7 个日志等级(按优先级从高到低排序):
## 📊 Winston 日志级别
| 级别 | 优先级 | 描述 | 使用场景 |
| ----------- | ------ | ------ | -------------------- |
| **error** | 0 | 错误 | 系统错误、异常、崩溃 |
| **warn** | 1 | 警告 | 潜在问题、性能警告 |
| **info** | 2 | 信息 | 一般信息、重要事件 |
| **http** | 3 | HTTP | HTTP 请求/响应日志 |
| **verbose** | 4 | 详细 | 详细操作信息 |
| **debug** | 5 | 调试 | 调试信息、开发阶段 |
| **silly** | 6 | 最详细 | 极其详细的调试信息 |
## 🎯 级别过滤机制
当你设置日志级别时Winston 会记录**该级别及更高优先级**的所有日志:
```typescript
// 如果设置 level: 'warn'
logger.level = 'warn';
logger.error('会被记录'); // ✅ 优先级 0
logger.warn('会被记录'); // ✅ 优先级 1
logger.info('不会被记录'); // ❌ 优先级 2
logger.debug('不会被记录'); // ❌ 优先级 5
```
## 💡 在你的日志器中使用
你可以更新日志器类来支持所有级别:
```typescript
export class Logger {
static error(message: string): void {
logger.error(message);
}
static warn(message: string): void {
logger.warn(message);
}
static info(message: string): void {
logger.info(message);
}
static http(message: string): void {
logger.http(message);
}
static verbose(message: string): void {
logger.verbose(message);
}
static debug(message: string): void {
logger.debug(message);
}
static silly(message: string): void {
logger.silly(message);
}
}
```
## 🔧 常用配置
```typescript
// 生产环境:只记录重要信息
level: 'warn'; // 记录 error, warn
// 开发环境:记录详细信息
level: 'debug'; // 记录 error, warn, info, http, verbose, debug
// 调试模式:记录所有信息
level: 'silly'; // 记录所有级别
```
这样你就可以根据不同环境灵活控制日志的详细程度了!

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
// ESLint 9.x Flat Config for Elysia + TypeScript 项目
// 详细注释,含风格规范(四空格缩进、分号、单引号等)
@ -29,6 +29,10 @@ export default [
console: 'readonly',
process: 'readonly',
Bun: 'readonly',
Response: 'readonly',
Blob: 'readonly',
File: 'readonly',
TextEncoder: 'readonly',
},
},
plugins: {
@ -45,22 +49,29 @@ export default [
'@typescript-eslint/no-explicit-any': 'off',
// 允许ts-ignore等注释
'@typescript-eslint/ban-ts-comment': 'off',
// 强制四空格缩进
indent: ['error', 4],
// 强制分号
// ===== 禁用与Prettier冲突的格式化规则 =====
indent: 'off', // 让Prettier处理缩进
'max-len': 'off', // 让Prettier处理行长度
'comma-spacing': 'off', // 让Prettier处理逗号空格
'object-curly-spacing': 'off', // 让Prettier处理对象空格
'array-bracket-spacing': 'off', // 让Prettier处理数组空格
// ===== 与Prettier兼容的规则 =====
// 强制分号与Prettier一致
semi: ['error', 'always'],
// 强制单引号
// 强制单引号与Prettier一致
quotes: ['error', 'single'],
// 末尾逗号(多行对象/数组)
// 末尾逗号(与Prettier一致
'comma-dangle': ['error', 'always-multiline'],
// 对象key统一加引号
// 对象key统一加引号与Prettier一致
'quote-props': ['error', 'as-needed'],
// ===== 其他代码质量规则 =====
// 关键字前后空格
'keyword-spacing': ['error', { before: true, after: true }],
// 大括号风格
'brace-style': ['error', '1tbs'],
// 禁止多余空行
'no-multiple-empty-lines': ['error', { max: 1 }],
'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1 }],
},
},
{

View File

@ -15,6 +15,8 @@
},
"devDependencies": {
"@types/bun": "^1.0.25",
"@types/redis": "^4.0.11",
"@types/winston": "^2.4.4",
"@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/parser": "^8.35.0",
"eslint": "^9.29.0",
@ -26,20 +28,30 @@
"dependencies": {
"@elysiajs/jwt": "^1.3.1",
"@elysiajs/swagger": "^1.3.0",
"@types/ua-parser-js": "^0.7.39",
"chalk": "^5.4.1",
"mysql2": "^3.14.1",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"pino-roll": "^3.1.0",
"undici": "^7.11.0"
"nanoid": "^5.1.5",
"picocolors": "^1.1.1",
"redis": "^5.5.6",
"ua-parser-js": "^2.0.4",
"undici": "^7.11.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"scripts": {
"dev": "bun --env-file=.env --hot src/server.ts",
"dev": "bun --watch --env-file=.env --hot src/server.ts",
"start": "bun --env-file=.env.prod src/server.ts",
"test": "bun test",
"vitest": "bun --env-file=.env x vitest run",
"test:watch": "bun test --watch",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"format": "prettier --write ."
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "bun run lint && bun run format:check",
"fix": "bun run lint:fix && bun run format",
"demo:logger": "bun src/demo/logger-demo.ts",
"demo:logger:prod": "NODE_ENV=production bun src/demo/logger-demo.ts"
}
}

View File

@ -4,22 +4,38 @@
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Elysia API服务应用入口文件
* @description Elysia API服务应用入口文件Winston日志系统
*/
import { Elysia } from 'elysia';
import { swaggerPlugin } from '@/plugins/swagger';
import { swaggerPlugin } from '@/plugins/swagger.plugins';
import { authController } from '@/controllers/try/auth.controller';
import { protectedController } from '@/controllers/try/protected.controller';
import { healthController } from '@/controllers/health.controller';
import * as config from '@/config/logger.config';
import loggerPlugin from '@/plugins/logger.plugins';
import { errorHandlerPlugin } from '@/plugins/errorHandler.plugins';
class AuthenticationError extends Error {
constructor(message: string, code = 500) {
super(message);
this.name = 'AuthenticationError';
if (code === 401) {
this.name = 'Unauthorized';
}
}
}
/**
* Elysia应用实例
* @type {Elysia}
*/
export const app = new Elysia()
.state('config', config)
.use(loggerPlugin)
.use(errorHandlerPlugin)
.use(swaggerPlugin)
.use(authController)
.use(protectedController)
.use(healthController)
.state('counter', 0) // 定义名为 counter 的初始值
// 批量定义 不会覆盖前面单独定义的
@ -27,14 +43,12 @@ export const app = new Elysia()
version: '1.0',
server: 'Bun',
})
.state('Error', (message: string) => {
console.log('message', message);
return new AuthenticationError(message);
})
.state('db', '一个方法')
.decorate('closeDB', () => console.log('关闭方法')); // 添加关闭方法
// 健康检查接口
app.get('/api/health', () => ({
code: 0,
message: '服务运行正常',
data: null,
}));
// app.closeDB() 可以以在路由中调用

6
src/config/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './db.config';
export * from './jwt.config';
export * from './logger.config';
export * from './redis.config';
export const ENV = process.env.NODE_ENV || process.env.BUN_ENV || 'development';

View File

@ -0,0 +1,18 @@
/**
* @file Winston日志器配置文件
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Winston日志器配置
*/
import type { LogConfigType } from '@/type/logger.type';
export const loggerConfig: LogConfigType = {
level: process.env.LOG_LEVEL || 'debug',
maxFiles: process.env.LOG_MAX_FILES || '30d',
maxSize: process.env.LOG_MAX_SIZE || '70k',
directory: process.env.LOG_DIRECTORY || 'logs',
console: (process.env.LOG_CONSOLE || 'true') === 'true',
};

View File

@ -0,0 +1,45 @@
/**
* @file Redis数据库配置
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Redis连接配置
*/
/**
* Redis数据库连接配置
* @property {string} connectName - Redis连接名称
* @property {string} host - Redis服务器主机地址
* @property {number} port - Redis服务器端口号
* @property {string} username - Redis用户名
* @property {string} password - Redis密码
* @property {number} database - Redis数据库索引
* @property {string} url - Redis连接URL
*/
export const redisConfig = {
/** Redis连接名称 */
connectName: process.env.REDIS_CONNECT_NAME || 'cursor-init-redis',
/** Redis服务器主机地址 */
host: process.env.REDIS_HOST || '172.16.1.3',
/** Redis服务器端口号 */
port: Number(process.env.REDIS_PORT) || 6379,
/** Redis用户名 */
username: process.env.REDIS_USERNAME || 'default',
/** Redis密码 */
password: process.env.REDIS_PASSWORD || 'docker',
/** Redis数据库索引 */
database: Number(process.env.REDIS_DATABASE) || 0,
};
/**
* Redis连接URL
* @returns Redis连接URL字符串
*/
export const getRedisUrl = (): string => {
const { username, password, host, port, database } = redisConfig;
if (username && password) {
return `redis://${username}:${password}@${host}:${port}/${database}`;
}
return `redis://${host}:${port}/${database}`;
};

View File

@ -0,0 +1,34 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Redis等依赖检查
*/
import { Elysia } from 'elysia';
import { healthService } from '@/services/health.service';
import { healthResponse } from '@/validators/health.response';
/**
*
*
*/
export const healthController = new Elysia({ prefix: '/api' })
.get('/health', async (ctx) => await healthService.getHealthStatus(ctx), {
detail: {
tags: ['健康检查'],
summary: '获取系统健康状态',
description: '检查系统及各依赖服务的健康状态包括数据库、Redis等',
},
response: healthResponse,
})
.get('/health/detailed', async (ctx) => await healthService.getDetailedHealthStatus(ctx), {
detail: {
tags: ['健康检查'],
summary: '获取详细健康状态',
description: '获取系统详细健康状态,包括性能指标、资源使用情况等',
},
response: healthResponse,
});

View File

@ -8,7 +8,7 @@
*/
import { Elysia } from 'elysia';
import { jwtPlugin } from '@/plugins/jwt';
import { jwtPlugin } from '@/plugins/jwt.plugins';
import { loginBodySchema, type LoginBody } from '@/validators/try/auth.validator';
import { loginResponse200Schema, loginResponse400Schema } from '@/validators/try/auth.response';
import { loginService } from '@/services/try/auth.service';

View File

@ -8,7 +8,7 @@
*/
import { Elysia } from 'elysia';
import { jwtAuthPlugin } from '@/plugins/jwt-auth';
import { jwtAuthPlugin } from '@/plugins/jwt.plugins';
import { protectedResponse200Schema, protectedResponse401Schema } from '@/validators/try/protected.response';
import { protectedService } from '@/services/try/protected.service';

View File

@ -0,0 +1,84 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description
*/
import { Elysia } from 'elysia';
import { ENV } from '@/config';
const isDevelopment = ENV === 'development';
/**
*
*/
export const errorHandlerPlugin = (app: Elysia) =>
app.onError(({ error, set, code, log }) => {
switch (code) {
case 'VALIDATION': {
set.status = 400;
let errors = null as any;
if (isDevelopment) {
errors = error.all.map((err) => ({
field: err.path?.slice(1) || 'root',
message: err.message,
expected: err.schema,
received: err.value,
}));
} else {
errors = error.all.map((err) => err.message);
}
return {
code: 400,
message: '参数验证失败',
errors,
};
}
case 'NOT_FOUND': {
set.status = 404;
return {
code: 404,
message: '找不到资源',
};
}
case 'INTERNAL_SERVER_ERROR': {
set.status = 500;
log.error(error);
return {
code: 500,
message: '服务器内部错误',
errors: error.message,
};
}
case 'PARSE': {
set.status = 400;
return {
code: 400,
message: '解析错误',
errors: error.message,
};
}
default: {
// 处理 ElysiaCustomStatusResponse status抛出的异常
if (error?.constructor?.name === 'ElysiaCustomStatusResponse') {
set.status = error.code;
return {
code: error.code,
message: error.response.message || '服务器内部错误',
errors: error,
};
}
console.log('error', error);
set.status = 500;
log.error(error);
return {
code: 500,
message: '服务器内部错误',
errors: error.message,
};
}
}
});

View File

@ -1,49 +0,0 @@
/**
* @file JWT认证插件Elysia官方推荐链式插件
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Elysia官方链式插件写法实现JWT校验401
*/
import { Elysia } from 'elysia';
import { jwtPlugin } from './jwt';
export const jwtAuthPlugin = (app: Elysia) =>
app
.use(jwtPlugin)
.derive(async ({ jwt, headers, status }) => {
const authHeader = headers['authorization'];
if (!authHeader?.startsWith('Bearer ')) {
return status(401, {
code: 401,
message: '未携带Token',
data: null,
});
}
const token = authHeader.replace('Bearer ', '');
try {
const user = await jwt.verify(token);
if (!user)
return status(401, {
code: 401,
message: 'Token无效',
data: null,
});
console.log('USER', user);
return { user };
} catch {
return {};
}
})
.onBeforeHandle(({ user, set }) => {
if (!user) {
set.status = 401;
return {
code: 401 as const,
message: '未授权',
data: null,
};
}
});

View File

@ -0,0 +1,40 @@
/**
* @file JWT插件封装
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Elysia JWT插件
*/
import { Elysia } from 'elysia';
import { jwt } from '@elysiajs/jwt';
import { jwtConfig } from '@/config/jwt.config';
export const jwtPlugin = jwt({
name: 'jwt',
secret: jwtConfig.secret,
exp: jwtConfig.exp,
});
export const jwtAuthPlugin = (app: Elysia) =>
app
.use(jwtPlugin)
.derive(async ({ jwt, headers, status }) => {
const authHeader = headers['authorization'];
if (!authHeader?.startsWith('Bearer ')) {
return status(401, '未携带Token');
}
const token = authHeader.replace('Bearer ', '');
try {
const user = await jwt.verify(token);
if (!user) return status(401, 'Token无效');
return { user };
} catch {
return {};
}
})
.onBeforeHandle(({ user, status }) => {
if (!user) {
return status(401, '未授权');
}
});

View File

@ -1,17 +0,0 @@
/**
* @file JWT插件封装
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Elysia JWT插件
*/
import { jwt } from '@elysiajs/jwt';
import { jwtConfig } from '@/config/jwt.config';
export const jwtPlugin = jwt({
name: 'jwt',
secret: jwtConfig.secret,
exp: jwtConfig.exp,
});

View File

@ -0,0 +1,95 @@
/**
* @file HTTP请求日志插件
* @author hotok
* @date 2024-01-15
* @lastEditor hotok
* @lastEditTime 2024-01-15
* @description HTTP请求和响应的详细信息
*/
import { Elysia } from 'elysia';
import Logger from '@/utils/logger';
import { browserInfo } from '@/utils/deviceInfo';
import { formatFileSize } from '@/utils/formatFileSize';
import { getRandomBackgroundColor } from '@/utils/randomChalk';
/**
* HTTP请求日志插件
* @description Elysia应用添加请求和响应日志功能
* @param app Elysia应用实例
* @returns Elysia应用
* @example
* const app = new Elysia().use(loggerPlugin)
*/
const loggerPlugin = (app: Elysia) =>
app
/** 注册日志实例到应用状态 */
.decorate('log', Logger)
/** 注册请求开始时间到应用状态,用于计算响应时间 */
.state('requestStart', null as [number, number] | null)
.state('color', null as string | null)
/** 请求拦截器 - 记录请求信息 */
.onRequest(({ store: { requestStart, color }, request, server, path, log }) => {
/** 记录请求开始时间 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requestStart = process.hrtime();
/** 获取客户端IP信息 */
const clientIP = server?.requestIP(request);
color = getRandomBackgroundColor()(' ');
/** 记录请求日志 */
log.http({
type: 'request',
method: request.method,
path,
color: color,
ip: `${clientIP?.family}: ${clientIP?.address}:${clientIP?.port}`,
browser: browserInfo(request.headers.get('user-agent')),
});
})
/** 响应拦截器 - 记录响应信息 */
.onAfterResponse(({ log, store: { requestStart, color }, request, set, response, path }) => {
if (requestStart) {
/** 计算请求处理时间 */
const [seconds, nanoseconds] = process.hrtime(requestStart);
const duration = seconds * 1000 + nanoseconds / 1000000;
/** 记录响应日志 */
log.http({
type: 'response',
method: request.method,
path,
color: color,
statusCode: set.status || 200,
requestTime: `${duration.toFixed(2)}ms`,
responseSize: getResponseSize(response),
});
}
});
function getResponseSize(response: Response) {
let responseSize = 0;
if (response instanceof Response) {
// 对于 Response 对象,可以通过 headers 获取 content-length
const contentLength = response.headers.get('content-length');
if (contentLength) {
responseSize = parseInt(contentLength, 10);
} else if (response.body) {
// 如果没有 content-length可以尝试读取 body 大小
// 注意:这可能会消耗 stream需要谨慎使用
responseSize = new Blob([response.body]).size;
}
} else if (typeof response === 'string') {
// 对于字符串响应,计算字节大小
responseSize = new TextEncoder().encode(response).length;
} else if (response && typeof response === 'object') {
// 对于对象响应,先序列化再计算大小
responseSize = new TextEncoder().encode(JSON.stringify(response)).length;
} else if (response instanceof File || response instanceof Blob) {
// 对于文件响应,可以直接访问 size 属性
responseSize = response.size;
}
return formatFileSize(responseSize);
}
export default loggerPlugin;

View File

@ -0,0 +1,460 @@
/**
* @file Swagger插件封装
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Elysia Swagger插件API文档配置
*/
import { swagger } from '@elysiajs/swagger';
import { ERROR_CODES, ERROR_CODE_DESCRIPTIONS } from '@/validators/global.response';
/**
* Swagger插件实例
* @description API文档配置
*/
export const swaggerPlugin = swagger({
path: '/docs',
documentation: {
info: {
title: 'Cursor Init API服务',
version: '1.0.0',
description: `
# Cursor Init API服务
**Elysia + Bun.js** API服务
## 🚀
- **JWT认证**:
- **MySQL数据库**:
- **Redis缓存**:
- **Winston日志**:
- ****:
- ****: API响应格式
- ****:
- ****:
## 📋
| | | |
|--------|------|----------|
| 0 | | |
| 400 | | |
| 401 | | Token无效 |
| 403 | | |
| 404 | | |
| 422 | | |
| 500 | | |
| 503 | | |
## 🔐
JWT认证
\`\`\`
Authorization: Bearer <your-jwt-token>
\`\`\`
## 📝
API响应均采用统一格式
\`\`\`json
{
"code": 0,
"message": "操作成功",
"data": {
// 具体数据
}
}
\`\`\`
## 🔗
- [](/api/health) -
- [](/api/health/detailed) -
`,
contact: {
name: 'API支持',
email: 'support@example.com',
url: 'https://github.com/your-org/cursor-init',
},
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT',
},
termsOfService: 'https://example.com/terms',
},
servers: [
{
url: 'http://localhost:3000',
description: '开发环境',
},
{
url: 'https://api.example.com',
description: '生产环境',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: `
JWT认证说明
1. JWT token
2. Authorization: Bearer <token>
3. Token有效期为24小时
4. Token格式eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
****
\`\`\`
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
\`\`\`
`,
},
},
schemas: {
ErrorCodes: {
type: 'object',
description: '系统错误码定义',
properties: Object.fromEntries(
Object.entries(ERROR_CODES).map(([key, value]) => [
key,
{
type: 'number',
enum: [value],
description: ERROR_CODE_DESCRIPTIONS[value],
example: value,
},
])
),
},
BaseResponse: {
type: 'object',
description: '基础响应结构',
required: ['code', 'message', 'data'],
properties: {
code: {
type: 'number',
description: '响应码0表示成功其他表示错误',
example: 0,
},
message: {
type: 'string',
description: '响应消息,描述操作结果',
example: '操作成功',
},
data: {
description: '响应数据成功时包含具体数据失败时通常为null',
},
},
},
SuccessResponse: {
type: 'object',
description: '成功响应',
required: ['code', 'message', 'data'],
properties: {
code: {
type: 'number',
enum: [0],
description: '成功响应码',
},
message: {
type: 'string',
description: '成功消息',
example: '操作成功',
},
data: {
description: '成功时返回的数据',
},
},
},
ErrorResponse: {
type: 'object',
description: '错误响应',
required: ['code', 'message', 'data'],
properties: {
code: {
type: 'number',
description: '错误响应码',
example: 400,
},
message: {
type: 'string',
description: '错误消息',
example: '参数验证失败',
},
data: {
type: 'null',
description: '错误时数据字段为null',
},
},
},
PaginationResponse: {
type: 'object',
description: '分页响应',
required: ['code', 'message', 'data'],
properties: {
code: {
type: 'number',
enum: [0],
},
message: {
type: 'string',
},
data: {
type: 'object',
required: ['list', 'pagination'],
properties: {
list: {
type: 'array',
description: '数据列表',
items: {},
},
pagination: {
type: 'object',
description: '分页信息',
required: ['page', 'pageSize', 'total', 'totalPages', 'hasNext', 'hasPrev'],
properties: {
page: {
type: 'number',
description: '当前页码从1开始',
minimum: 1,
examples: [1, 2, 3],
},
pageSize: {
type: 'number',
description: '每页条数',
minimum: 1,
maximum: 100,
examples: [10, 20, 50],
},
total: {
type: 'number',
description: '总条数',
minimum: 0,
examples: [0, 100, 1500],
},
totalPages: {
type: 'number',
description: '总页数',
minimum: 0,
examples: [0, 5, 75],
},
hasNext: {
type: 'boolean',
description: '是否有下一页',
},
hasPrev: {
type: 'boolean',
description: '是否有上一页',
},
},
},
},
},
},
},
},
responses: {
Success: {
description: '操作成功',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/SuccessResponse',
},
examples: {
default: {
summary: '成功示例',
value: {
code: 0,
message: '操作成功',
data: {
id: 1,
name: '示例数据',
},
},
},
},
},
},
},
BadRequest: {
description: '业务逻辑错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse',
},
examples: {
default: {
summary: '业务错误示例',
value: {
code: 400,
message: '用户名已存在',
data: null,
},
},
},
},
},
},
Unauthorized: {
description: '身份认证失败',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse',
},
examples: {
tokenExpired: {
summary: 'Token过期',
value: {
code: 401,
message: 'Token已过期请重新登录',
data: null,
},
},
tokenInvalid: {
summary: 'Token无效',
value: {
code: 401,
message: 'Token格式错误或无效',
data: null,
},
},
notLoggedIn: {
summary: '未登录',
value: {
code: 401,
message: '请先登录',
data: null,
},
},
},
},
},
},
Forbidden: {
description: '权限不足',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse',
},
examples: {
default: {
summary: '权限不足示例',
value: {
code: 403,
message: '权限不足,无法访问该资源',
data: null,
},
},
},
},
},
},
NotFound: {
description: '资源未找到',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse',
},
examples: {
default: {
summary: '资源不存在示例',
value: {
code: 404,
message: '请求的资源不存在',
data: null,
},
},
},
},
},
},
ValidationError: {
description: '参数验证失败',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse',
},
examples: {
default: {
summary: '参数验证失败示例',
value: {
code: 422,
message: '邮箱格式不正确',
data: null,
},
},
},
},
},
},
InternalError: {
description: '服务器内部错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse',
},
examples: {
default: {
summary: '服务器错误示例',
value: {
code: 500,
message: '服务器内部错误,请稍后重试',
data: null,
},
},
},
},
},
},
ServiceUnavailable: {
description: '服务不可用',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse',
},
examples: {
default: {
summary: '服务不可用示例',
value: {
code: 503,
message: '服务暂时不可用,请稍后重试',
data: null,
},
},
},
},
},
},
},
},
security: [{ bearerAuth: [] }],
tags: [
{
name: '认证管理',
description: '用户认证相关接口包括登录、注册、Token验证等',
},
{
name: '用户管理',
description: '用户信息管理接口',
},
{
name: '健康检查',
description: '系统健康状态监控接口',
},
],
},
});

View File

@ -1,35 +0,0 @@
/**
* @file Swagger插件封装
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Elysia Swagger插件API文档配置
*/
import { swagger } from '@elysiajs/swagger';
/**
* Swagger插件实例
* @description API文档配置便
*/
export const swaggerPlugin = swagger({
documentation: {
info: {
title: 'API服务',
version: '1.0.0',
description: '基于Elysia的API服务集成JWT、MySQL等功能',
},
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT认证需在header中携带Authorization: Bearer <token>',
},
},
},
security: [{ bearerAuth: [] }],
},
});

View File

@ -0,0 +1,300 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Redis等依赖检查
*/
import type { Context } from 'elysia';
import { Redis } from '@/utils/redis';
import { pool } from '@/utils/mysql';
import { Logger } from '@/utils/logger';
// 临时内联类型定义
interface ComponentStatus {
status: 'healthy' | 'unhealthy' | 'degraded';
responseTime?: number;
error?: string;
details?: Record<string, any>;
}
interface HealthStatus {
code: number;
message: string;
data: {
status: 'healthy' | 'unhealthy' | 'degraded';
timestamp: string;
uptime: number;
responseTime: number;
version: string;
environment: string;
error?: string;
components: {
mysql?: ComponentStatus;
redis?: ComponentStatus;
[key: string]: ComponentStatus | undefined;
};
};
}
interface DetailedHealthStatus extends HealthStatus {
data: HealthStatus['data'] & {
system?: {
platform: string;
arch: string;
nodeVersion: string;
runtime: string;
pid: number;
cwd: string;
};
performance?: {
cpuUsage: {
user: number;
system: number;
};
memoryUsage: {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
arrayBuffers: number;
};
uptime: number;
};
};
}
/**
*
*
*/
class HealthService {
/**
* Redis实例
*/
private redis: Redis;
constructor() {
this.redis = new Redis();
}
/**
*
* @param ctx Elysia上下文
* @returns
*/
async getHealthStatus(ctx: Context): Promise<HealthStatus> {
const startTime = Date.now();
const timestamp = new Date().toISOString();
try {
// 并行检查所有依赖
const [mysqlStatus, redisStatus] = await Promise.allSettled([
this.checkMysqlHealth(),
this.checkRedisHealth(),
]);
/** 系统整体状态 */
const overallStatus = this.determineOverallStatus([
mysqlStatus.status === 'fulfilled' ? mysqlStatus.value : { status: 'unhealthy', error: 'Connection failed' },
redisStatus.status === 'fulfilled' ? redisStatus.value : { status: 'unhealthy', error: 'Connection failed' },
]);
const responseTime = Date.now() - startTime;
return {
code: overallStatus === 'healthy' ? 0 : 1,
message: overallStatus === 'healthy' ? '所有服务运行正常' : '部分服务异常',
data: {
status: overallStatus,
timestamp,
uptime: process.uptime(),
responseTime,
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
components: {
mysql: mysqlStatus.status === 'fulfilled' ? mysqlStatus.value : { status: 'unhealthy', error: 'Connection failed' },
redis: redisStatus.status === 'fulfilled' ? redisStatus.value : { status: 'unhealthy', error: 'Connection failed' },
},
},
};
} catch (error) {
Logger.error(error as Error);
return {
code: 1,
message: '健康检查异常',
data: {
status: 'unhealthy',
timestamp,
uptime: process.uptime(),
responseTime: Date.now() - startTime,
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
error: 'Health check failed',
components: {},
},
};
}
}
/**
*
* @param ctx Elysia上下文
* @returns
*/
async getDetailedHealthStatus(ctx: Context): Promise<DetailedHealthStatus> {
const startTime = Date.now();
const timestamp = new Date().toISOString();
try {
// 获取基本健康状态
const basicHealth = await this.getHealthStatus(ctx);
// 获取系统资源信息
const systemInfo = this.getSystemInfo();
return {
...basicHealth,
data: {
...basicHealth.data,
system: systemInfo,
performance: {
cpuUsage: process.cpuUsage(),
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
},
},
};
} catch (error) {
Logger.error(error as Error);
return {
code: 1,
message: '详细健康检查异常',
data: {
status: 'unhealthy',
timestamp,
uptime: process.uptime(),
responseTime: Date.now() - startTime,
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
error: 'Detailed health check failed',
components: {},
},
};
}
}
/**
* MySQL健康状态
* @returns MySQL组件状态
*/
private async checkMysqlHealth(): Promise<ComponentStatus> {
try {
const startTime = Date.now();
await pool.execute('SELECT 1');
const responseTime = Date.now() - startTime;
return {
status: 'healthy',
responseTime,
details: {
connection: 'active',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || '3306',
},
};
} catch (error) {
Logger.error(error as Error);
return {
status: 'unhealthy',
error: (error as Error).message,
details: {
connection: 'failed',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || '3306',
},
};
}
}
/**
* Redis健康状态
* @returns Redis组件状态
*/
private async checkRedisHealth(): Promise<ComponentStatus> {
try {
const startTime = Date.now();
const isHealthy = await this.redis.checkRedisHealth();
const responseTime = Date.now() - startTime;
if (isHealthy) {
const redisStatus = this.redis.getRedisStatus();
return {
status: 'healthy',
responseTime,
details: {
connection: 'active',
...redisStatus.config,
},
};
} else {
return {
status: 'unhealthy',
error: 'Redis ping failed',
details: {
connection: 'failed',
},
};
}
} catch (error) {
Logger.error(error as Error);
return {
status: 'unhealthy',
error: (error as Error).message,
details: {
connection: 'failed',
},
};
}
}
/**
*
* @param components
* @returns
*/
private determineOverallStatus(components: ComponentStatus[]): 'healthy' | 'unhealthy' | 'degraded' {
const healthyCount = components.filter(c => c.status === 'healthy').length;
const totalCount = components.length;
if (healthyCount === totalCount) {
return 'healthy';
} else if (healthyCount === 0) {
return 'unhealthy';
} else {
return 'degraded';
}
}
/**
*
* @returns
*/
private getSystemInfo() {
return {
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
runtime: 'Bun',
pid: process.pid,
cwd: process.cwd(),
};
}
}
/**
*
*/
export const healthService = new HealthService();

View File

@ -17,6 +17,7 @@ export const protectedService = (user: any) => {
* @type {any} user - JWT解码后的用户信息
* @description jwtAuthPlugin中间件注入
*/
console.log('user', user);
return {
code: 0,
message: '受保护资源访问成功',

View File

@ -9,7 +9,7 @@
*/
import { describe, it, expect } from 'vitest';
import { app } from './app';
import { app } from '../app';
let token = '';

View File

@ -0,0 +1,51 @@
import Logger from '@/utils/logger';
// 测试错误日志记录
try {
throw new Error('这是一个测试错误');
} catch (error) {
if (error instanceof Error) {
Logger.error(error);
}
}
// 测试各种日志类型
Logger.debug('这是字符串调试信息');
Logger.debug({
action: 'debug',
data: { userId: 123, action: 'login' },
timestamp: new Date().toISOString(),
});
Logger.info('这是字符串信息');
Logger.info({
event: 'user_login',
userId: 123,
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
});
Logger.warn('这是字符串警告');
Logger.warn({
warning: 'high_memory_usage',
currentUsage: '85%',
threshold: '80%',
recommendation: 'Consider scaling up',
});
Logger.http({
method: 'POST',
url: '/api/users',
statusCode: 201,
responseTime: 150,
bodySize: '1.2KB',
});
Logger.verbose({
module: 'database',
operation: 'SELECT',
table: 'users',
query: 'SELECT * FROM users WHERE active = true',
executionTime: '45ms',
rowCount: 156,
});

305
src/tests/health.test.ts Normal file
View File

@ -0,0 +1,305 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { setTimeout } from 'node:timers';
import { app } from '@/app';
describe('健康检查接口测试', () => {
beforeAll(async () => {
// 等待应用启动
await new Promise(resolve => setTimeout(resolve, 1000));
});
describe('GET /api/health', () => {
it('应该返回基本健康状态', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}),
);
const body = (await res.json()) as any;
expect(res.status).toBe(200);
expect(body.code).toBeTypeOf('number');
expect(body.message).toBeTypeOf('string');
expect(body.data).toBeTypeOf('object');
// 检查基本数据结构
expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/);
expect(body.data.timestamp).toBeTypeOf('string');
expect(body.data.uptime).toBeTypeOf('number');
expect(body.data.responseTime).toBeTypeOf('number');
expect(body.data.version).toBeTypeOf('string');
expect(body.data.environment).toBeTypeOf('string');
expect(body.data.components).toBeTypeOf('object');
// 检查组件状态
if (body.data.components.mysql) {
expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/);
}
if (body.data.components.redis) {
expect(body.data.components.redis.status).toMatch(/^(healthy|unhealthy|degraded)$/);
}
});
it('应该包含正确的时间戳格式', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
);
const body = (await res.json()) as any;
// 验证ISO时间戳格式
const timestamp = new Date(body.data.timestamp);
expect(timestamp.toISOString()).toBe(body.data.timestamp);
});
it('应该返回合理的响应时间', async () => {
const startTime = Date.now();
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
);
const endTime = Date.now();
const body = (await res.json()) as any;
// 响应时间应该在合理范围内
expect(body.data.responseTime).toBeGreaterThan(0);
expect(body.data.responseTime).toBeLessThan(endTime - startTime + 100); // 允许一定误差
});
it('应该返回正确的环境信息', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
);
const body = (await res.json()) as any;
expect(body.data.environment).toMatch(/^(development|production|test)$/);
expect(body.data.uptime).toBeGreaterThan(0);
});
});
describe('GET /api/health/detailed', () => {
it('应该返回详细健康状态', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health/detailed', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}),
);
const body = (await res.json()) as any;
expect(res.status).toBe(200);
expect(body.code).toBeTypeOf('number');
expect(body.message).toBeTypeOf('string');
expect(body.data).toBeTypeOf('object');
// 检查基本健康检查数据
expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/);
expect(body.data.timestamp).toBeTypeOf('string');
expect(body.data.uptime).toBeTypeOf('number');
expect(body.data.responseTime).toBeTypeOf('number');
expect(body.data.components).toBeTypeOf('object');
// 检查详细信息
if (body.data.system) {
expect(body.data.system.platform).toBeTypeOf('string');
expect(body.data.system.arch).toBeTypeOf('string');
expect(body.data.system.nodeVersion).toBeTypeOf('string');
expect(body.data.system.runtime).toBeTypeOf('string');
expect(body.data.system.pid).toBeTypeOf('number');
expect(body.data.system.cwd).toBeTypeOf('string');
}
if (body.data.performance) {
expect(body.data.performance.cpuUsage).toBeTypeOf('object');
expect(body.data.performance.memoryUsage).toBeTypeOf('object');
expect(body.data.performance.uptime).toBeTypeOf('number');
// 检查CPU使用情况
expect(body.data.performance.cpuUsage.user).toBeTypeOf('number');
expect(body.data.performance.cpuUsage.system).toBeTypeOf('number');
// 检查内存使用情况
expect(body.data.performance.memoryUsage.rss).toBeTypeOf('number');
expect(body.data.performance.memoryUsage.heapTotal).toBeTypeOf('number');
expect(body.data.performance.memoryUsage.heapUsed).toBeTypeOf('number');
expect(body.data.performance.memoryUsage.external).toBeTypeOf('number');
expect(body.data.performance.memoryUsage.arrayBuffers).toBeTypeOf('number');
}
});
it('详细健康检查应该包含系统信息', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health/detailed', {
method: 'GET',
}),
);
const body = (await res.json()) as any;
if (body.data.system) {
expect(body.data.system.runtime).toBe('Bun');
expect(body.data.system.pid).toBe(process.pid);
expect(body.data.system.platform).toBe(process.platform);
expect(body.data.system.arch).toBe(process.arch);
}
});
});
describe('健康检查依赖服务测试', () => {
it('MySQL组件状态应该包含连接信息', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
);
const body = (await res.json()) as any;
if (body.data.components.mysql) {
expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/);
if (body.data.components.mysql.details) {
expect(body.data.components.mysql.details.connection).toMatch(/^(active|failed)$/);
expect(body.data.components.mysql.details.host).toBeTypeOf('string');
expect(body.data.components.mysql.details.port).toBeTypeOf('string');
}
if (body.data.components.mysql.responseTime) {
expect(body.data.components.mysql.responseTime).toBeGreaterThan(0);
}
}
});
it('Redis组件状态应该包含连接信息', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
);
const body = (await res.json()) as any;
if (body.data.components.redis) {
expect(body.data.components.redis.status).toMatch(/^(healthy|unhealthy|degraded)$/);
if (body.data.components.redis.details) {
expect(body.data.components.redis.details.connection).toMatch(/^(active|failed)$/);
}
if (body.data.components.redis.responseTime) {
expect(body.data.components.redis.responseTime).toBeGreaterThan(0);
}
}
});
});
describe('健康检查错误处理', () => {
it('健康检查应该处理组件异常', async () => {
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
);
const body = (await res.json()) as any;
// 即使有组件异常,也应该返回结构化的响应
expect(res.status).toBe(200);
expect(body.code).toBeTypeOf('number');
expect(body.message).toBeTypeOf('string');
expect(body.data).toBeTypeOf('object');
// 如果有组件异常整体状态可能是degraded或unhealthy
if (body.data.status === 'unhealthy' || body.data.status === 'degraded') {
// 应该有组件错误信息
const components = body.data.components;
let hasUnhealthyComponent = false;
Object.values(components).forEach((component: any) => {
if (component && component.status === 'unhealthy') {
hasUnhealthyComponent = true;
expect(component.error).toBeTypeOf('string');
}
});
if (!hasUnhealthyComponent && body.data.error) {
expect(body.data.error).toBeTypeOf('string');
}
}
});
});
describe('健康检查性能测试', () => {
it('健康检查应该快速响应', async () => {
const startTime = Date.now();
const res = await app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
);
const endTime = Date.now();
const responseTime = endTime - startTime;
expect(res.status).toBe(200);
expect(responseTime).toBeLessThan(2000); // 应该在2秒内完成
});
it('详细健康检查应该在合理时间内完成', async () => {
const startTime = Date.now();
const res = await app.fetch(
new Request('http://localhost/api/health/detailed', {
method: 'GET',
}),
);
const endTime = Date.now();
const responseTime = endTime - startTime;
expect(res.status).toBe(200);
expect(responseTime).toBeLessThan(3000); // 详细检查可能稍慢但应该在3秒内完成
});
it('并发健康检查应该正常处理', async () => {
const promises = [];
const concurrentRequests = 5;
for (let i = 0; i < concurrentRequests; i++) {
promises.push(
app.fetch(
new Request('http://localhost/api/health', {
method: 'GET',
}),
),
);
}
const responses = await Promise.all(promises);
responses.forEach(res => {
expect(res.status).toBe(200);
});
});
});
});

View File

@ -8,7 +8,7 @@
*/
import { describe, it, expect } from 'vitest';
import { pool } from './mysql';
import { pool } from '../utils/mysql';
// 基础连接测试

238
src/tests/redis.test.ts Normal file
View File

@ -0,0 +1,238 @@
/**
* @file Redis连接测试
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Redis连接
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { setTimeout } from 'node:timers';
import { Redis } from '@/utils/redis';
import { redisConfig } from '@/config/redis.config';
// 简单的延时函数
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
describe('Redis连接测试', () => {
let redis: Redis;
beforeAll(async () => {
redis = new Redis();
// 等待连接建立
await delay(1000);
});
afterAll(async () => {
if (redis) {
await redis.disconnectRedis();
}
});
beforeEach(async () => {
// 确保每个测试前Redis连接正常
if (!redis.redisClient.isOpen) {
await redis.connectRedis();
}
});
describe('Redis连接管理', () => {
it('应该成功连接到Redis服务器', async () => {
const isHealthy = await redis.checkRedisHealth();
expect(isHealthy).toBe(true);
});
it('应该正确返回Redis连接状态', () => {
const status = redis.getRedisStatus();
expect(status).toEqual({
isConnected: expect.any(Boolean),
config: {
host: redisConfig.host,
port: redisConfig.port,
database: redisConfig.database,
connectName: redisConfig.connectName,
},
});
});
it('应该能够执行ping命令', async () => {
const result = await redis.redisClient.ping();
expect(result).toBe('PONG');
});
});
describe('Redis基本操作', () => {
const testKey = 'test_key';
const testValue = 'test_value';
it('应该能够设置和获取字符串值', async () => {
// 设置值
await redis.redisClient.set(testKey, testValue);
// 获取值
const result = await redis.redisClient.get(testKey);
expect(result).toBe(testValue);
// 清理测试数据
await redis.redisClient.del(testKey);
});
it('应该能够设置带过期时间的值', async () => {
const expiryTime = 2; // 2秒过期
// 设置带过期时间的值
await redis.redisClient.setEx(testKey, expiryTime, testValue);
// 立即获取应该有值
const result1 = await redis.redisClient.get(testKey);
expect(result1).toBe(testValue);
// 等待过期
await delay(2100);
// 过期后应该为null
const result2 = await redis.redisClient.get(testKey);
expect(result2).toBeNull();
});
it('应该能够检查键是否存在', async () => {
// 设置测试键
await redis.redisClient.set(testKey, testValue);
// 检查存在
const exists1 = await redis.redisClient.exists(testKey);
expect(exists1).toBe(1);
// 删除键
await redis.redisClient.del(testKey);
// 检查不存在
const exists2 = await redis.redisClient.exists(testKey);
expect(exists2).toBe(0);
});
it('应该能够删除键', async () => {
// 设置测试键
await redis.redisClient.set(testKey, testValue);
// 删除键
const deleteCount = await redis.redisClient.del(testKey);
expect(deleteCount).toBe(1);
// 验证键已被删除
const result = await redis.redisClient.get(testKey);
expect(result).toBeNull();
});
});
describe('Redis Hash操作', () => {
const hashKey = 'test_hash';
const field1 = 'field1';
const value1 = 'value1';
const field2 = 'field2';
const value2 = 'value2';
it('应该能够设置和获取Hash字段', async () => {
// 设置Hash字段
await redis.redisClient.hSet(hashKey, field1, value1);
// 获取Hash字段
const result = await redis.redisClient.hGet(hashKey, field1);
expect(result).toBe(value1);
// 清理测试数据
await redis.redisClient.del(hashKey);
});
it('应该能够设置和获取多个Hash字段', async () => {
// 设置多个Hash字段
await redis.redisClient.hSet(hashKey, field1, value1);
await redis.redisClient.hSet(hashKey, field2, value2);
// 获取所有Hash字段
const result = await redis.redisClient.hGetAll(hashKey);
expect(result).toEqual({
[field1]: value1,
[field2]: value2,
});
// 清理测试数据
await redis.redisClient.del(hashKey);
});
});
describe('Redis列表操作', () => {
const listKey = 'test_list';
const value1 = 'item1';
const value2 = 'item2';
it('应该能够推入和弹出列表元素', async () => {
// 推入元素
await redis.redisClient.lPush(listKey, value1);
await redis.redisClient.lPush(listKey, value2);
// 获取列表长度
const length = await redis.redisClient.lLen(listKey);
expect(length).toBe(2);
// 弹出元素
const poppedValue = await redis.redisClient.lPop(listKey);
expect(poppedValue).toBe(value2);
// 清理测试数据
await redis.redisClient.del(listKey);
});
});
describe('Redis连接错误处理', () => {
it('健康检查在连接断开时应返回false', async () => {
// 暂时断开连接
await redis.disconnectRedis();
// 健康检查应该返回false
const isHealthy = await redis.checkRedisHealth();
expect(isHealthy).toBe(false);
// 重新连接
await redis.connectRedis();
});
it('应该能够重新连接Redis', async () => {
// 断开连接
await redis.disconnectRedis();
// 重新连接
await redis.connectRedis();
// 验证连接正常
const isHealthy = await redis.checkRedisHealth();
expect(isHealthy).toBe(true);
});
});
describe('Redis性能测试', () => {
it('应该能够快速执行大量set操作', async () => {
const startTime = Date.now();
const operations = [];
// 执行100次set操作
for (let i = 0; i < 1000; i++) {
operations.push(redis.redisClient.set(`perf_test_${i}`, `value_${i}`));
}
await Promise.all(operations);
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(1000); // 应该在1秒内完成
console.log(endTime - startTime);
// 清理测试数据
const deleteOperations = [];
for (let i = 0; i < 100; i++) {
deleteOperations.push(redis.redisClient.del(`perf_test_${i}`));
}
await Promise.all(deleteOperations);
});
});
});

291
src/tests/swagger.test.ts Normal file
View File

@ -0,0 +1,291 @@
/**
* @file Swagger文档功能测试
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Swagger API文档功能访
*/
import { describe, it, expect } from 'vitest';
import { setTimeout } from 'node:timers';
import { app } from '@/app';
describe('Swagger API文档测试', () => {
describe('GET /docs', () => {
it('应该可以访问Swagger文档页面', async () => {
const res = await app.fetch(
new Request('http://localhost/docs', {
method: 'GET',
}),
);
expect(res.status).toBe(200);
const contentType = res.headers.get('content-type');
expect(contentType).toContain('text/html');
});
it('Swagger文档应该包含基本配置信息', async () => {
const res = await app.fetch(
new Request('http://localhost/docs', {
method: 'GET',
}),
);
const html = await res.text();
// 检查基本配置
expect(html).toContain('Cursor Init API服务');
expect(html).toContain('swagger-ui');
});
});
describe('GET /docs/json', () => {
it('应该返回OpenAPI JSON文档', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
expect(res.status).toBe(200);
const contentType = res.headers.get('content-type');
expect(contentType).toContain('application/json');
const openApiDoc = await res.json();
// 验证OpenAPI文档结构
expect(openApiDoc).toHaveProperty('openapi');
expect(openApiDoc).toHaveProperty('info');
expect(openApiDoc).toHaveProperty('paths');
expect(openApiDoc).toHaveProperty('components');
// 验证基本信息
expect(openApiDoc.info.title).toBe('Cursor Init API服务');
expect(openApiDoc.info.version).toBe('1.0.0');
expect(openApiDoc.info.description).toContain('Cursor Init API服务');
});
it('OpenAPI文档应该包含安全配置', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证安全配置
expect(openApiDoc.components).toHaveProperty('securitySchemes');
expect(openApiDoc.components.securitySchemes).toHaveProperty('bearerAuth');
expect(openApiDoc.components.securitySchemes.bearerAuth.type).toBe('http');
expect(openApiDoc.components.securitySchemes.bearerAuth.scheme).toBe('bearer');
expect(openApiDoc.components.securitySchemes.bearerAuth.bearerFormat).toBe('JWT');
});
it('OpenAPI文档应该包含全局组件定义', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证全局组件
expect(openApiDoc.components).toHaveProperty('schemas');
expect(openApiDoc.components).toHaveProperty('responses');
// 验证响应组件
const responses = openApiDoc.components.responses;
expect(responses).toHaveProperty('Success');
expect(responses).toHaveProperty('BadRequest');
expect(responses).toHaveProperty('Unauthorized');
expect(responses).toHaveProperty('Forbidden');
expect(responses).toHaveProperty('NotFound');
expect(responses).toHaveProperty('ValidationError');
expect(responses).toHaveProperty('InternalError');
expect(responses).toHaveProperty('ServiceUnavailable');
// 验证Schema组件
const schemas = openApiDoc.components.schemas;
expect(schemas).toHaveProperty('BaseResponse');
expect(schemas).toHaveProperty('SuccessResponse');
expect(schemas).toHaveProperty('ErrorResponse');
expect(schemas).toHaveProperty('PaginationResponse');
});
it('OpenAPI文档应该包含健康检查接口', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证健康检查接口
expect(openApiDoc.paths).toHaveProperty('/api/health');
expect(openApiDoc.paths).toHaveProperty('/api/health/detailed');
const healthPath = openApiDoc.paths['/api/health'];
expect(healthPath).toHaveProperty('get');
expect(healthPath.get).toHaveProperty('tags');
expect(healthPath.get.tags).toContain('健康检查');
expect(healthPath.get).toHaveProperty('summary');
expect(healthPath.get).toHaveProperty('description');
});
it('OpenAPI文档应该包含认证接口', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证认证接口
expect(openApiDoc.paths).toHaveProperty('/api/auth/login');
const loginPath = openApiDoc.paths['/api/auth/login'];
expect(loginPath).toHaveProperty('post');
expect(loginPath.post).toHaveProperty('tags');
expect(loginPath.post).toHaveProperty('requestBody');
expect(loginPath.post).toHaveProperty('responses');
});
it('OpenAPI文档应该包含标签分类', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证标签
expect(openApiDoc).toHaveProperty('tags');
expect(Array.isArray(openApiDoc.tags)).toBe(true);
const tagNames = openApiDoc.tags.map((tag: any) => tag.name);
expect(tagNames).toContain('认证管理');
expect(tagNames).toContain('健康检查');
// 验证标签描述
const healthTag = openApiDoc.tags.find((tag: any) => tag.name === '健康检查');
expect(healthTag).toHaveProperty('description');
expect(healthTag.description).toContain('系统健康状态');
});
});
describe('Swagger文档内容验证', () => {
it('应该包含错误码说明', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证错误码描述在文档中
expect(openApiDoc.info.description).toContain('错误码说明');
expect(openApiDoc.info.description).toContain('| 错误码 | 说明 | 示例场景 |');
expect(openApiDoc.info.description).toContain('| 0 | 操作成功 |');
expect(openApiDoc.info.description).toContain('| 400 | 业务逻辑错误 |');
expect(openApiDoc.info.description).toContain('| 401 | 身份认证失败 |');
});
it('应该包含认证说明', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证认证说明
expect(openApiDoc.info.description).toContain('认证说明');
expect(openApiDoc.info.description).toContain('Authorization: Bearer');
expect(openApiDoc.components.securitySchemes.bearerAuth.description).toContain('JWT认证说明');
expect(openApiDoc.components.securitySchemes.bearerAuth.description).toContain('Token有效期为24小时');
});
it('应该包含响应格式说明', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证响应格式说明
expect(openApiDoc.info.description).toContain('响应格式');
expect(openApiDoc.info.description).toContain('"code": 0');
expect(openApiDoc.info.description).toContain('"message": "操作成功"');
expect(openApiDoc.info.description).toContain('"data"');
});
it('应该包含示例响应', async () => {
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const openApiDoc = await res.json();
// 验证示例响应
const successResponse = openApiDoc.components.responses.Success;
expect(successResponse.content['application/json']).toHaveProperty('examples');
const errorResponse = openApiDoc.components.responses.BadRequest;
expect(errorResponse.content['application/json']).toHaveProperty('examples');
const unauthorizedResponse = openApiDoc.components.responses.Unauthorized;
expect(unauthorizedResponse.content['application/json']).toHaveProperty('examples');
expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('tokenExpired');
expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('tokenInvalid');
expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('notLoggedIn');
});
});
describe('Swagger文档性能测试', () => {
it('文档页面应该快速加载', async () => {
const startTime = Date.now();
const res = await app.fetch(
new Request('http://localhost/docs', {
method: 'GET',
}),
);
const endTime = Date.now();
const responseTime = endTime - startTime;
expect(res.status).toBe(200);
expect(responseTime).toBeLessThan(1000); // 应该在1秒内完成
});
it('JSON文档应该快速响应', async () => {
const startTime = Date.now();
const res = await app.fetch(
new Request('http://localhost/docs/json', {
method: 'GET',
}),
);
const endTime = Date.now();
const responseTime = endTime - startTime;
expect(res.status).toBe(200);
expect(responseTime).toBeLessThan(500); // JSON文档应该更快
});
});
});

114
src/type/error.type.ts Normal file
View File

@ -0,0 +1,114 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description
*/
/**
*
*/
export interface ErrorResponse {
/** 错误码 */
code: number;
/** 错误消息 */
message: string;
/** 返回数据错误时通常为null */
data: null;
/** 请求追踪ID */
traceId?: string;
/** 错误详情(仅开发环境) */
details?: any;
/** 堆栈信息(仅开发环境) */
stack?: string;
}
/**
*
*/
export enum BusinessErrorType {
/** 参数校验错误 */
VALIDATION_ERROR = 'VALIDATION_ERROR',
/** 认证错误 */
AUTH_ERROR = 'AUTH_ERROR',
/** 权限错误 */
PERMISSION_ERROR = 'PERMISSION_ERROR',
/** 业务逻辑错误 */
BUSINESS_ERROR = 'BUSINESS_ERROR',
/** 资源不存在 */
NOT_FOUND_ERROR = 'NOT_FOUND_ERROR',
/** 系统错误 */
SYSTEM_ERROR = 'SYSTEM_ERROR',
/** 外部服务错误 */
EXTERNAL_ERROR = 'EXTERNAL_ERROR',
}
/**
*
*/
export class BusinessError extends Error {
/** 错误码 */
public readonly code: number;
/** 错误类型 */
public readonly type: BusinessErrorType;
/** 额外数据 */
public readonly data?: any;
/** HTTP状态码 */
public readonly statusCode: number;
constructor(type: BusinessErrorType, message: string, code: number, statusCode: number = 400, data?: any) {
super(message);
this.name = 'BusinessError';
this.type = type;
this.code = code;
this.statusCode = statusCode;
this.data = data;
// 保持正确的堆栈跟踪
if (Error.captureStackTrace) {
Error.captureStackTrace(this, BusinessError);
}
}
}
/**
*
*/
export class BusinessErrors {
/** 参数校验错误 */
static validation(message: string, data?: any): BusinessError {
return new BusinessError(BusinessErrorType.VALIDATION_ERROR, message, 400, 400, data);
}
/** 认证错误 */
static auth(message: string = '认证失败'): BusinessError {
return new BusinessError(BusinessErrorType.AUTH_ERROR, message, 401, 401);
}
/** 权限错误 */
static permission(message: string = '权限不足'): BusinessError {
return new BusinessError(BusinessErrorType.PERMISSION_ERROR, message, 403, 403);
}
/** 资源不存在 */
static notFound(message: string = '资源不存在'): BusinessError {
return new BusinessError(BusinessErrorType.NOT_FOUND_ERROR, message, 404, 404);
}
/** 业务逻辑错误 */
static business(message: string, code: number = 1000): BusinessError {
return new BusinessError(BusinessErrorType.BUSINESS_ERROR, message, code, 400);
}
/** 系统错误 */
static system(message: string = '系统错误'): BusinessError {
return new BusinessError(BusinessErrorType.SYSTEM_ERROR, message, 500, 500);
}
/** 外部服务错误 */
static external(message: string = '外部服务错误'): BusinessError {
return new BusinessError(BusinessErrorType.EXTERNAL_ERROR, message, 502, 502);
}
}

122
src/type/health.type.ts Normal file
View File

@ -0,0 +1,122 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description TypeScript类型定义
*/
/// <reference types="node" />
/**
*
*/
export type ComponentHealthStatus = 'healthy' | 'unhealthy' | 'degraded';
/**
*
*/
export type SystemHealthStatus = 'healthy' | 'unhealthy' | 'degraded';
/**
*
*/
export interface ComponentStatus {
/** 组件状态 */
status: ComponentHealthStatus;
/** 响应时间(毫秒) */
responseTime?: number;
/** 错误信息 */
error?: string;
/** 详细信息 */
details?: Record<string, any>;
}
/**
*
*/
export interface SystemInfo {
/** 操作系统平台 */
platform: string;
/** 系统架构 */
arch: string;
/** Node.js版本 */
nodeVersion: string;
/** 运行时 */
runtime: string;
/** 进程ID */
pid: number;
/** 当前工作目录 */
cwd: string;
}
/**
*
*/
export interface PerformanceMetrics {
/** CPU使用情况 */
cpuUsage: {
user: number;
system: number;
};
/** 内存使用情况 */
memoryUsage: {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
arrayBuffers: number;
};
/** 运行时间(秒) */
uptime: number;
}
/**
*
*/
export interface HealthStatus {
/** 响应码 */
code: number;
/** 响应消息 */
message: string;
/** 健康状态数据 */
data: {
/** 系统整体状态 */
status: SystemHealthStatus;
/** 时间戳 */
timestamp: string;
/** 系统运行时间(秒) */
uptime: number;
/** 响应时间(毫秒) */
responseTime: number;
/** 版本号 */
version: string;
/** 环境 */
environment: string;
/** 错误信息(仅在异常时) */
error?: string;
/** 各组件状态 */
components: {
/** MySQL数据库状态 */
mysql?: ComponentStatus;
/** Redis缓存状态 */
redis?: ComponentStatus;
/** 其他组件状态 */
[key: string]: ComponentStatus | undefined;
};
};
}
/**
*
*/
export interface DetailedHealthStatus extends HealthStatus {
/** 详细健康状态数据 */
data: HealthStatus['data'] & {
/** 系统信息 */
system?: SystemInfo;
/** 性能指标 */
performance?: PerformanceMetrics;
};
}

16
src/type/logger.type.ts Normal file
View File

@ -0,0 +1,16 @@
/**
*
* @interface LogConfig
*/
export interface LogConfigType {
/** 日志文件目录 */
directory: string;
/** 是否输出到控制台 */
console: boolean;
/** 单个日志文件最大大小 */
maxSize: string;
/** 最大保留文件数 */
maxFiles: string;
/** 日志等级 */
level: string;
}

34
src/utils/deviceInfo.ts Normal file
View File

@ -0,0 +1,34 @@
import { UAParser } from 'ua-parser-js';
/**
* UserAgent
* @param userAgent UserAgent
* @returns
*/
export const parseUserAgent = (userAgent: string | null) => {
if (!userAgent) return null;
const parser = new UAParser(userAgent);
const result = parser.getResult();
return {
browser: `${result.browser.name || 'Unknown'} ${result.browser.version || ''}`.trim(),
os: `${result.os.name || 'Unknown'} ${result.os.version || ''}`.trim(),
device: {
type: result.device.type || 'desktop', // mobile, tablet, desktop
vendor: result.device.vendor || 'Unknown',
model: result.device.model || 'Unknown',
},
engine: result.engine.name || 'Unknown',
isBot: /bot|crawler|spider|crawling/i.test(userAgent),
};
};
export const browserInfo = (userAgent: string | null) => {
if (!userAgent) return null;
const parser = new UAParser(userAgent);
const result = parser.getResult();
return `${result.browser.name || 'Unknown'} ${result.browser.version || ''}`.trim();
};

View File

@ -0,0 +1,12 @@
export const formatFileSize = (bytes: number): string => {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return ` ${size.toFixed(2)}${units[unitIndex]} `;
};

255
src/utils/logger.ts Normal file
View File

@ -0,0 +1,255 @@
/**
* @file Winston日志器工具类
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description winston的高性能日志记录器
*/
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import { loggerConfig } from '@/config/logger.config';
import chalk from 'chalk';
import { centerText } from '@/utils/text';
/**
*
*/
const colorMethods = {
error: (msg: string) => chalk.bgRed.white(msg),
warn: (msg: string) => chalk.bgYellow.black(msg),
info: (msg: string) => chalk.bgGreen(msg),
http: (msg: string) => chalk.bgCyan(msg),
verbose: (msg: string) => chalk.bgGray(msg),
debug: (msg: string) => chalk.bgMagenta(msg),
silly: (msg: string) => chalk.bgGray(msg),
};
const colorMethodsForStart = {
error: (msg: string) => chalk.red(msg),
warn: (msg: string) => chalk.yellow(msg),
info: (msg: string) => chalk.green(msg),
http: (msg: string) => chalk.cyan(msg),
verbose: (msg: string) => chalk.gray(msg),
debug: (msg: string) => chalk.magenta(msg),
silly: (msg: string) => chalk.gray(msg),
};
/**
*
* @param stack
* @returns
*/
const formatStack = (stack: string): string => {
return (
chalk.red('•••') +
'\n' +
stack
.split('\n')
.map((line, index) => {
if (index === 0) return line; // 第一行是错误消息,不处理
if (line.trim() === '') return line; // 空行不处理
// 为每行第一个字符添加红色背景
const firstChar = line.charAt(0);
const restOfLine = line.slice(1);
return chalk.bgRed(' ') + firstChar + restOfLine;
})
.join('\n')
);
};
/**
* JSON信息
* @param str JSON字符串
* @param level
* @returns JSON字符串
*/
const formatJSON = (str: string, level: string): string => {
if (typeof str !== 'string') {
console.log('str', str);
return JSON.stringify(str, null, 2);
}
if (!str?.includes('\n')) {
return str;
}
const color = colorMethodsForStart[level as keyof typeof colorMethods];
return (
'\n' +
color('|') +
str
.split('\n')
.map((line, index) => {
if (index === 0) return line; // 第一行是错误消息,不处理
if (line.trim() === '') return line; // 空行不处理
// 为每行第一个字符添加红色背景
const firstChar = line.charAt(0);
const restOfLine = line.slice(1);
return color('|') + firstChar + restOfLine;
})
.join('\n')
);
};
/**
* JSON信息
* @param str JSON字符串
* @param level
* @returns JSON字符串
*/
const formatHTTP = (obj: any): string => {
if (obj.type === 'request') {
return obj.color + `|< ${obj.method} ${obj.path} ${obj.ip} ${obj.browser}`;
} else if (obj.type === 'response') {
return (
obj.color +
`| > ${obj.method} ${obj.path} ${obj.statusCode} ${chalk.bold(obj.requestTime)} ${chalk.bgGreen(obj.responseSize)}`
);
}
};
/**
*
*/
const consoleTransport = new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp({ format: 'YY/MM/DD HH:mm:ss' }),
winston.format.printf(({ timestamp, message, level, stack }) => {
// 使用居中对齐格式化日志级别
const levelText = centerText(level.toUpperCase(), 7);
const levelFormatted = colorMethods[level as keyof typeof colorMethods](levelText);
if (level === 'error' && stack && typeof stack === 'string') {
const formattedStack = formatStack(stack);
return `[${chalk.gray(timestamp)}] ${levelFormatted} ${message}\n${formattedStack}`;
} else if (level === 'error') {
return `[${chalk.gray(timestamp)}] ${levelFormatted} 未定义的异常 ${message}`;
} else if (level === 'http') {
return `[${chalk.gray(timestamp)}] ${levelFormatted} ${formatHTTP(message)}`;
}
return `[${chalk.gray(timestamp)}] ${levelFormatted} ${formatJSON(message as string, level)}`;
}),
),
});
/**
*
*/
const appFileTransport = new DailyRotateFile({
filename: `${loggerConfig.directory}/app-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
maxSize: loggerConfig.maxSize,
maxFiles: loggerConfig.maxFiles,
format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.json()),
});
/**
*
*/
const errorFileTransport = new DailyRotateFile({
filename: `${loggerConfig.directory}/error-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
maxSize: loggerConfig.maxSize,
maxFiles: loggerConfig.maxFiles,
level: 'error',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }), // 确保堆栈信息被记录
winston.format.json(),
),
});
/**
* Winston日志器实例
*/
const logger = winston.createLogger({
/** 日志级别 */
level: loggerConfig.level,
/** 传输器配置 */
transports: [
// 应用主日志文件
appFileTransport,
// 错误专用日志文件
errorFileTransport,
// 控制台日志(如果启用)
...(loggerConfig.console ? [consoleTransport] : []),
],
});
/**
*
* @param message
* @returns
*/
const formatMessage = (message: string | object): string => {
if (typeof message === 'string') {
return message;
}
return JSON.stringify(message, null, 2);
};
/**
*
*/
export class Logger {
/**
*
* @param message
*/
static debug(message: string | object): void {
logger.debug(formatMessage(message));
}
/**
*
* @param message
*/
static info(message: string | object): void {
logger.info(formatMessage(message));
}
/**
*
* @param message
*/
static warn(message: string | object): void {
logger.warn(formatMessage(message));
}
/**
* - Error
* @param error Error
*/
static error(error: Error): void {
logger.error({
message: error.message,
stack: error.stack,
name: error.name,
cause: error.cause,
});
}
/**
* HTTP日志
* @param message
*/
static http(message: string | object): void {
logger.http(message);
}
/**
*
* @param message
*/
static verbose(message: string | object): void {
logger.verbose(formatMessage(message));
}
}
// 导出默认实例
export default Logger;

226
src/utils/randomChalk.ts Normal file
View File

@ -0,0 +1,226 @@
/**
* @file Chalk背景颜色工具
* @author hotok
* @date 2024-01-15
* @lastEditor hotok
* @lastEditTime 2024-01-15
* @description chalk背景颜色的工具函数
*/
import chalk from 'chalk';
/**
*
* @description chalk背景颜色方法
*/
const basicBackgroundColors = [
chalk.bgBlack,
chalk.bgRed,
chalk.bgGreen,
chalk.bgYellow,
chalk.bgBlue,
chalk.bgMagenta,
chalk.bgCyan,
chalk.bgWhite,
chalk.bgGray,
] as const;
/**
*
* @description chalk背景颜色方法
*/
const brightBackgroundColors = [
chalk.bgBlackBright,
chalk.bgRedBright,
chalk.bgGreenBright,
chalk.bgYellowBright,
chalk.bgBlueBright,
chalk.bgMagentaBright,
chalk.bgCyanBright,
chalk.bgWhiteBright,
] as const;
/**
*
* @description
*/
const allBackgroundColors = [...basicBackgroundColors, ...brightBackgroundColors] as const;
/**
*
* @description
*/
export type ColorType = 'basic' | 'bright' | 'all';
/**
*
* @param length
* @returns
* @example
* getRandomIndex(5) // 返回0-4之间的随机数
*/
function getRandomIndex(length: number): number {
return Math.floor(Math.random() * length);
}
/**
* chalk背景颜色函数
* @param type 'basic' | 'bright' | 'all'
* @returns chalk背景颜色函数
* @example
* const randomBg = getRandomBackgroundColor();
* console.log(randomBg('Hello World'));
*
* const randomBasicBg = getRandomBackgroundColor('basic');
* console.log(randomBasicBg('Basic Color'));
*
* const randomBrightBg = getRandomBackgroundColor('bright');
* console.log(randomBrightBg('Bright Color'));
*/
export function getRandomBackgroundColor(type: ColorType = 'all'): typeof chalk.bgRed {
let colorArray: readonly (typeof chalk.bgRed)[];
switch (type) {
case 'basic':
colorArray = basicBackgroundColors;
break;
case 'bright':
colorArray = brightBackgroundColors;
break;
case 'all':
default:
colorArray = allBackgroundColors;
break;
}
/** 获取随机索引 */
const randomIndex = getRandomIndex(colorArray.length);
/** 返回随机选择的背景颜色函数 */
return colorArray[randomIndex]!;
}
/**
*
* @param text
* @param type 'basic' | 'bright' | 'all'
* @returns
* @example
* console.log(randomBackgroundText('Hello World'));
* console.log(randomBackgroundText('Basic Color', 'basic'));
* console.log(randomBackgroundText('Bright Color', 'bright'));
*/
export function randomBackgroundText(text: string, type: ColorType = 'all'): string {
/** 获取随机背景颜色函数 */
const randomBgColor = getRandomBackgroundColor(type);
/** 返回着色后的文本 */
return randomBgColor(text);
}
/**
*
* @param text
* @param type 'basic' | 'bright' | 'all'
* @returns
* @example
* console.log(randomBackgroundWhiteText('Hello World'));
* console.log(randomBackgroundWhiteText('Important Message', 'bright'));
*/
export function randomBackgroundWhiteText(text: string, type: ColorType = 'all'): string {
/** 获取随机背景颜色函数 */
const randomBgColor = getRandomBackgroundColor(type);
/** 返回带白色文字和随机背景的文本 */
return randomBgColor.white(text);
}
/**
*
* @param text
* @param type 'basic' | 'bright' | 'all'
* @returns
* @example
* console.log(randomBackgroundBlackText('Hello World'));
* console.log(randomBackgroundBlackText('Important Message', 'basic'));
*/
export function randomBackgroundBlackText(text: string, type: ColorType = 'all'): string {
/** 获取随机背景颜色函数 */
const randomBgColor = getRandomBackgroundColor(type);
/** 返回带黑色文字和随机背景的文本 */
return randomBgColor.black(text);
}
/**
*
* @param text
* @param type 'basic' | 'bright' | 'all'
* @returns
* @example
* console.log(rainbowBackgroundText('Rainbow!'));
* console.log(rainbowBackgroundText('Colorful', 'bright'));
*/
export function rainbowBackgroundText(text: string, type: ColorType = 'all'): string {
return text
.split('')
.map((char) => {
/** 为每个字符生成随机背景颜色 */
const randomBgColor = getRandomBackgroundColor(type);
return randomBgColor.white(char);
})
.join('');
}
/**
*
* @description
*/
export const colorNames = {
basic: ['bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta', 'bgCyan', 'bgWhite', 'bgGray'],
bright: [
'bgBlackBright',
'bgRedBright',
'bgGreenBright',
'bgYellowBright',
'bgBlueBright',
'bgMagentaBright',
'bgCyanBright',
'bgWhiteBright',
],
} as const;
/**
*
* @param type 'basic' | 'bright' | 'all'
* @example
* showAllBackgroundColors(); // 展示所有颜色
* showAllBackgroundColors('basic'); // 只展示基本颜色
*/
export function showAllBackgroundColors(type: ColorType = 'all'): void {
console.log(chalk.bold.blue(`\n🎨 Chalk背景颜色展示 (${type}):\n`));
if (type === 'basic' || type === 'all') {
console.log(chalk.bold.yellow('基本背景颜色:'));
basicBackgroundColors.forEach((colorFunc, index) => {
const colorName = colorNames.basic[index];
console.log(` ${colorFunc.white(` ${colorName} `)} ${colorName}`);
});
console.log();
}
if (type === 'bright' || type === 'all') {
console.log(chalk.bold.yellow('明亮背景颜色:'));
brightBackgroundColors.forEach((colorFunc, index) => {
const colorName = colorNames.bright[index];
console.log(` ${colorFunc.black(` ${colorName} `)} ${colorName}`);
});
console.log();
}
}
export default {
getRandomBackgroundColor,
randomBackgroundText,
randomBackgroundWhiteText,
randomBackgroundBlackText,
rainbowBackgroundText,
showAllBackgroundColors,
colorNames,
};

127
src/utils/redis.ts Normal file
View File

@ -0,0 +1,127 @@
/**
* @file Redis数据库连接工具
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Redis连接实例
*/
import { createClient, type RedisClientType } from 'redis';
import { redisConfig, getRedisUrl } from '@/config/redis.config';
import { Logger } from '@/utils/logger';
/**
* Redis客户端实例
*/
class Redis {
public redisClient: RedisClientType;
/**
* Redis连接状态
*/
private isConnected = false;
constructor() {
this.redisClient = createClient({
name: redisConfig.connectName,
username: redisConfig.username,
password: redisConfig.password,
database: redisConfig.database,
url: getRedisUrl(),
});
// 错误处理
this.redisClient.on('error', (error) => {
Logger.error(error as Error);
this.isConnected = false;
});
this.redisClient.on('connect', () => {
Logger.info('Redis客户端连接建立');
this.isConnected = true;
});
this.redisClient.on('ready', () => {
Logger.info('Redis客户端准备就绪');
});
this.redisClient.on('end', () => {
Logger.info('Redis客户端连接结束');
this.isConnected = false;
});
this.redisClient.on('reconnecting', () => {
Logger.warn('Redis客户端正在重连');
});
// 初始化连接
this.connectRedis().catch((error) => {
Logger.error(error as Error);
});
}
/**
* Redis
* @returns Promise<void>
*/
async connectRedis() {
try {
if (!this.isConnected) {
await this.redisClient.connect();
this.isConnected = true;
}
} catch (error) {
Logger.error(error as Error);
throw error;
}
}
/**
* Redis连接
* @returns Promise<void>
*/
async disconnectRedis() {
try {
if (this.isConnected) {
await this.redisClient.close();
this.isConnected = false;
}
} catch (error) {
Logger.error(error as Error);
throw error;
}
}
/**
* Redis健康检查
* @returns Promise<boolean>
*/
async checkRedisHealth() {
try {
await this.redisClient.ping();
return true;
} catch (error) {
Logger.error(error as Error);
return false;
}
}
/**
* Redis连接状态
* @returns
*/
getRedisStatus() {
return {
isConnected: this.isConnected,
config: {
host: redisConfig.host,
port: redisConfig.port,
database: redisConfig.database,
connectName: redisConfig.connectName,
},
};
}
}
/**
* Redis类使
*/
export { Redis };

79
src/utils/text.ts Normal file
View File

@ -0,0 +1,79 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description
*/
/**
*
* @param text
* @param width
* @param fillChar
* @returns
*/
export const centerText = (text: string, width: number, fillChar: string = ' '): string => {
// 如果文本长度大于等于指定宽度,截断文本
if (text.length >= width) {
return text.slice(0, width);
}
// 计算左右需要填充的空格数
const totalPadding = width - text.length;
const leftPadding = Math.floor(totalPadding / 2);
const rightPadding = totalPadding - leftPadding;
return fillChar.repeat(leftPadding) + text + fillChar.repeat(rightPadding);
};
/**
*
* @param text
* @param width
* @param fillChar
* @returns
*/
export const leftPad = (text: string, width: number, fillChar: string = ' '): string => {
if (text.length >= width) {
return text.slice(0, width);
}
return text + fillChar.repeat(width - text.length);
};
/**
*
* @param text
* @param width
* @param fillChar
* @returns
*/
export const rightPad = (text: string, width: number, fillChar: string = ' '): string => {
if (text.length >= width) {
return text.slice(0, width);
}
return fillChar.repeat(width - text.length) + text;
};
/**
*
* @param text
* @param maxLength
* @param ellipsis ...
* @returns
*/
export const truncateText = (text: string, maxLength: number, ellipsis: string = '...'): string => {
if (text.length <= maxLength) {
return text;
}
return text.slice(0, maxLength - ellipsis.length) + ellipsis;
};
// // 测试示例
// console.log('=== 字符串格式化测试 ===');
// console.log(`"${centerText('居中居中居中居中居中居中居中', 10)}"`); // " 居中 "
// console.log(`"${centerText('很长的文本内容', 10)}"`); // "很长的文本内容"
// console.log(`"${leftPad('左对齐', 10)}"`); // "左对齐 "
// console.log(`"${rightPad('右对齐', 10)}"`); // " 右对齐"
// console.log(`"${truncateText('这是一个很长的文本', 8)}"`); // "这是一个很..."

View File

@ -0,0 +1,259 @@
/**
* @file Schema定义
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description Swagger文档和接口验证使用
*/
import { t } from 'elysia';
/**
*
* @description 便API文档查阅
*/
export const ERROR_CODES = {
/** 成功 */
SUCCESS: 0,
/** 通用业务错误 */
BUSINESS_ERROR: 400,
/** 认证失败 */
UNAUTHORIZED: 401,
/** 权限不足 */
FORBIDDEN: 403,
/** 资源未找到 */
NOT_FOUND: 404,
/** 参数验证失败 */
VALIDATION_ERROR: 422,
/** 服务器内部错误 */
INTERNAL_ERROR: 500,
/** 服务不可用 */
SERVICE_UNAVAILABLE: 503,
} as const;
/**
*
*/
export const ERROR_CODE_DESCRIPTIONS = {
[ERROR_CODES.SUCCESS]: '操作成功',
[ERROR_CODES.BUSINESS_ERROR]: '业务逻辑错误',
[ERROR_CODES.UNAUTHORIZED]: '身份认证失败,请重新登录',
[ERROR_CODES.FORBIDDEN]: '权限不足,无法访问该资源',
[ERROR_CODES.NOT_FOUND]: '请求的资源不存在',
[ERROR_CODES.VALIDATION_ERROR]: '请求参数验证失败',
[ERROR_CODES.INTERNAL_ERROR]: '服务器内部错误,请稍后重试',
[ERROR_CODES.SERVICE_UNAVAILABLE]: '服务暂时不可用,请稍后重试',
} as const;
/**
* Schema
*/
export const BaseResponseSchema = t.Object({
/** 响应码0表示成功其他表示错误 */
code: t.Number({
description: '响应码0表示成功其他表示错误',
examples: [0, 400, 401, 403, 404, 422, 500, 503],
}),
/** 响应消息 */
message: t.String({
description: '响应消息,描述操作结果',
examples: ['操作成功', '参数验证失败', '权限不足'],
}),
/** 响应数据 */
data: t.Any({
description: '响应数据成功时包含具体数据失败时通常为null',
}),
});
/**
* Schema
*/
export const SuccessResponseSchema = t.Object({
code: t.Literal(0, {
description: '成功响应码',
}),
message: t.String({
description: '成功消息',
examples: ['操作成功', '获取数据成功', '创建成功'],
}),
data: t.Any({
description: '成功时返回的数据',
}),
});
/**
* Schema
*/
export const ErrorResponseSchema = t.Object({
code: t.Number({
description: '错误响应码',
examples: [400, 401, 403, 404, 422, 500, 503],
}),
message: t.String({
description: '错误消息',
examples: ['参数验证失败', '认证失败', '权限不足', '资源不存在', '服务器内部错误'],
}),
data: t.Null({
description: '错误时数据字段为null',
}),
});
/**
* Schema
*/
export const PaginationResponseSchema = t.Object({
code: t.Literal(0),
message: t.String(),
data: t.Object({
/** 分页数据列表 */
list: t.Array(t.Any(), {
description: '数据列表',
}),
/** 分页信息 */
pagination: t.Object({
/** 当前页码 */
page: t.Number({
description: '当前页码从1开始',
minimum: 1,
examples: [1, 2, 3],
}),
/** 每页条数 */
pageSize: t.Number({
description: '每页条数',
minimum: 1,
maximum: 100,
examples: [10, 20, 50],
}),
/** 总条数 */
total: t.Number({
description: '总条数',
minimum: 0,
examples: [0, 100, 1500],
}),
/** 总页数 */
totalPages: t.Number({
description: '总页数',
minimum: 0,
examples: [0, 5, 75],
}),
/** 是否有下一页 */
hasNext: t.Boolean({
description: '是否有下一页',
}),
/** 是否有上一页 */
hasPrev: t.Boolean({
description: '是否有上一页',
}),
}),
}),
});
/**
* HTTP状态码响应模板
*/
export const CommonResponses = {
/** 200 成功 */
200: SuccessResponseSchema,
/** 400 业务错误 */
400: ErrorResponseSchema,
/** 401 认证失败 */
401: t.Object({
code: t.Literal(401),
message: t.String({
examples: ['身份认证失败,请重新登录', 'Token已过期', 'Token格式错误'],
}),
data: t.Null(),
}),
/** 403 权限不足 */
403: t.Object({
code: t.Literal(403),
message: t.String({
examples: ['权限不足,无法访问该资源', '用户角色权限不够'],
}),
data: t.Null(),
}),
/** 404 资源未找到 */
404: t.Object({
code: t.Literal(404),
message: t.String({
examples: ['请求的资源不存在', '用户不存在', '文件未找到'],
}),
data: t.Null(),
}),
/** 422 参数验证失败 */
422: t.Object({
code: t.Literal(422),
message: t.String({
examples: ['请求参数验证失败', '邮箱格式不正确', '密码长度不符合要求'],
}),
data: t.Null(),
}),
/** 500 服务器内部错误 */
500: t.Object({
code: t.Literal(500),
message: t.String({
examples: ['服务器内部错误,请稍后重试', '数据库连接失败', '系统异常'],
}),
data: t.Null(),
}),
/** 503 服务不可用 */
503: t.Object({
code: t.Literal(503),
message: t.String({
examples: ['服务暂时不可用,请稍后重试', '系统维护中', '依赖服务异常'],
}),
data: t.Null(),
}),
};
/**
* Schema
*/
export const HealthCheckResponseSchema = t.Object({
code: t.Number(),
message: t.String(),
data: t.Object({
status: t.Union([
t.Literal('healthy'),
t.Literal('unhealthy'),
t.Literal('degraded'),
], {
description: '系统健康状态healthy-健康unhealthy-不健康degraded-降级',
}),
timestamp: t.String({
description: 'ISO时间戳',
examples: ['2024-06-28T12:00:00.000Z'],
}),
uptime: t.Number({
description: '系统运行时间(秒)',
examples: [3600, 86400],
}),
responseTime: t.Number({
description: '响应时间(毫秒)',
examples: [15, 50, 100],
}),
version: t.String({
description: '系统版本',
examples: ['1.0.0', '1.2.3'],
}),
environment: t.String({
description: '运行环境',
examples: ['development', 'production', 'test'],
}),
components: t.Object({
mysql: t.Optional(t.Object({
status: t.String(),
responseTime: t.Optional(t.Number()),
error: t.Optional(t.String()),
details: t.Optional(t.Any()),
})),
redis: t.Optional(t.Object({
status: t.String(),
responseTime: t.Optional(t.Number()),
error: t.Optional(t.String()),
details: t.Optional(t.Any()),
})),
}),
}),
});

View File

@ -0,0 +1,181 @@
/**
* @file
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description
*/
import { t } from 'elysia';
/**
*
*/
const componentStatusSchema = t.Object({
/** 组件状态 */
status: t.Union([t.Literal('healthy'), t.Literal('unhealthy'), t.Literal('degraded')]),
/** 响应时间(毫秒) */
responseTime: t.Optional(t.Number()),
/** 错误信息 */
error: t.Optional(t.String()),
/** 详细信息 */
details: t.Optional(t.Record(t.String(), t.Any())),
});
/**
*
*/
const systemInfoSchema = t.Object({
/** 操作系统平台 */
platform: t.String(),
/** 系统架构 */
arch: t.String(),
/** Node.js版本 */
nodeVersion: t.String(),
/** 运行时 */
runtime: t.String(),
/** 进程ID */
pid: t.Number(),
/** 当前工作目录 */
cwd: t.String(),
});
/**
*
*/
const performanceMetricsSchema = t.Object({
/** CPU使用情况 */
cpuUsage: t.Object({
user: t.Number(),
system: t.Number(),
}),
/** 内存使用情况 */
memoryUsage: t.Object({
rss: t.Number(),
heapTotal: t.Number(),
heapUsed: t.Number(),
external: t.Number(),
arrayBuffers: t.Number(),
}),
/** 运行时间(秒) */
uptime: t.Number(),
});
/**
*
*/
const basicHealthDataSchema = t.Object({
/** 系统整体状态 */
status: t.Union([t.Literal('healthy'), t.Literal('unhealthy'), t.Literal('degraded')]),
/** 时间戳 */
timestamp: t.String(),
/** 系统运行时间(秒) */
uptime: t.Number(),
/** 响应时间(毫秒) */
responseTime: t.Number(),
/** 版本号 */
version: t.String(),
/** 环境 */
environment: t.String(),
/** 错误信息(仅在异常时) */
error: t.Optional(t.String()),
/** 各组件状态 */
components: t.Object({
/** MySQL数据库状态 */
mysql: t.Optional(componentStatusSchema),
/** Redis缓存状态 */
redis: t.Optional(componentStatusSchema),
}),
});
/**
*
*/
const detailedHealthDataSchema = t.Intersect([
basicHealthDataSchema,
t.Object({
/** 系统信息 */
system: t.Optional(systemInfoSchema),
/** 性能指标 */
performance: t.Optional(performanceMetricsSchema),
}),
]);
/**
*
*/
export const healthResponse = {
200: t.Object({
/** 响应码 */
code: t.Number(),
/** 响应消息 */
message: t.String(),
/** 健康状态数据 */
data: basicHealthDataSchema,
}),
500: t.Object({
/** 响应码 */
code: t.Number(),
/** 响应消息 */
message: t.String(),
/** 错误数据 */
data: t.Object({
/** 系统整体状态 */
status: t.Literal('unhealthy'),
/** 时间戳 */
timestamp: t.String(),
/** 系统运行时间(秒) */
uptime: t.Number(),
/** 响应时间(毫秒) */
responseTime: t.Number(),
/** 版本号 */
version: t.String(),
/** 环境 */
environment: t.String(),
/** 错误信息 */
error: t.String(),
/** 各组件状态 */
components: t.Object({}),
}),
}),
};
/**
*
*/
export const detailedHealthResponse = {
200: t.Object({
/** 响应码 */
code: t.Number(),
/** 响应消息 */
message: t.String(),
/** 详细健康状态数据 */
data: detailedHealthDataSchema,
}),
500: t.Object({
/** 响应码 */
code: t.Number(),
/** 响应消息 */
message: t.String(),
/** 错误数据 */
data: t.Object({
/** 系统整体状态 */
status: t.Literal('unhealthy'),
/** 时间戳 */
timestamp: t.String(),
/** 系统运行时间(秒) */
uptime: t.Number(),
/** 响应时间(毫秒) */
responseTime: t.Number(),
/** 版本号 */
version: t.String(),
/** 环境 */
environment: t.String(),
/** 错误信息 */
error: t.String(),
/** 各组件状态 */
components: t.Object({}),
}),
}),
};

View File

@ -1,105 +1,105 @@
# 架构优化 PRD
## 1. 引言/概述
本次架构优化旨在提升后端服务的可维护性、可观测性和健壮性。通过引入统一日志、全局错误处理和响应封装,规范接口行为,便于团队协作和后续扩展。新增 Redis 支持,为缓存、限流等功能打下基础。
## 2. 目标
- 实现统一、可扩展的日志记录方案,支持分环境美化/存储/清理
- 实现全局错误捕获与标准化响应,支持分环境详细度
- 实现接口响应结构统一封装
- 集成 Redis支持缓存、限流等能力
- 提升系统可观测性和异常可追溯性
## 3. 用户故事
- 作为开发者,我希望所有接口出错时能收到统一格式的错误响应,便于前端处理和排查。
- 作为运维,我希望能通过日志快速定位线上问题。
- 作为开发者,我希望所有接口响应结构一致,便于前后端协作。
- 作为开发者,我希望在开发环境下看到彩色美观的日志和详细错误堆栈。
- 作为运维,我希望生产环境日志能自动分文件存储并定期清理。
- 作为开发者,我希望能方便地使用 Redis 进行缓存、限流等操作。
## 4. 功能需求
1. **日志记录器**
- 支持 info、warn、error、debug 等日志等级
- 日志内容包含时间、等级、消息、上下文信息
- 日志默认输出到控制台,后续可扩展到文件/远程
- 关键操作、异常、接口请求需有日志
- 每条请求日志包含唯一请求 id、method、url、状态码、耗时、IP、时间戳
- dev 环境日志美化输出(带颜色)、控制台打印
- prod 环境日志按天/小时分文件存储,支持定时清理历史日志
- 日志格式美观dev 环境带颜色区分 info/warn/error
2. **全局错误处理器**
- 捕获所有未处理异常,返回统一 JSON 结构
- 支持自定义业务异常类型
- 错误日志自动记录
- 错误响应结构需包含 code、message、data 字段
- dev 环境异常响应包含详细堆栈prod 环境仅输出友好错误信息
3. **响应封装**
- 所有接口返回统一结构:`{ code, message, data }`
- 支持自定义响应码和 message
- 可扩展 traceId、耗时等字段
4. **请求日志中间件**
- 记录 method、url、耗时、状态码、IP、请求 id
- 日志链路追踪
5. **健康检查接口**
- 提供 `/health` 路由,返回服务健康状态
6. **配置中心优化**
- 所有配置集中到 config支持多环境
7. **Redis 支持**
- 集成 Redis配置集中管理
- Redis 连接池、健康检查
- 预留缓存、限流、会话等场景的基础能力
8. **接口文档自动化完善**
- Swagger UI 增加全局响应示例、错误码说明
## 5. 非目标(范围之外)
- 日志持久化到远程(本期仅本地文件)
- 速率限制、权限控制等安全功能
## 6. 设计考虑
- 日志、错误、响应封装均以中间件/插件方式实现,便于复用和扩展
- 代码与注释规范保持一致
- 目录结构建议:
- `src/middlewares/logger.ts`
- `src/middlewares/error-handler.ts`
- `src/utils/response.ts`
- `src/config/redis.config.ts`
- 日志库建议用 pino/winston/simple 自研
- 日志文件存储建议按天分目录,定时清理可用定时任务
- 请求 id 可用 nanoid/uuid 生成,并通过中间件注入上下文
- Redis 推荐用 bun:redis 原生或社区库
## 7. 技术考虑
- 日志库选型、日志文件清理策略
- 错误类型建议自定义 Error 类
- 响应封装为工具函数或 Elysia 插件
- Redis 连接池与健康检查实现
## 8. 成功指标
- 100% 接口响应结构统一
- 关键操作/异常均有日志
- 错误响应无堆栈泄漏,信息友好
- 日志分环境输出,生产环境日志可定期清理
- Redis 连接稳定,健康检查通过
## 9. 待解决问题
- 日志等级与格式细节
- 错误码与 message 规范
- 响应结构是否需 traceId、耗时等扩展字段
- Redis 具体使用场景细化
# 架构优化 PRD
## 1. 引言/概述
本次架构优化旨在提升后端服务的可维护性、可观测性和健壮性。通过引入统一日志、全局错误处理和响应封装,规范接口行为,便于团队协作和后续扩展。新增 Redis 支持,为缓存、限流等功能打下基础。
## 2. 目标
- 实现统一、可扩展的日志记录方案,支持分环境美化/存储/清理
- 实现全局错误捕获与标准化响应,支持分环境详细度
- 实现接口响应结构统一封装
- 集成 Redis支持缓存、限流等能力
- 提升系统可观测性和异常可追溯性
## 3. 用户故事
- 作为开发者,我希望所有接口出错时能收到统一格式的错误响应,便于前端处理和排查。
- 作为运维,我希望能通过日志快速定位线上问题。
- 作为开发者,我希望所有接口响应结构一致,便于前后端协作。
- 作为开发者,我希望在开发环境下看到彩色美观的日志和详细错误堆栈。
- 作为运维,我希望生产环境日志能自动分文件存储并定期清理。
- 作为开发者,我希望能方便地使用 Redis 进行缓存、限流等操作。
## 4. 功能需求
1. **日志记录器**
- 支持 info、warn、error、debug 等日志等级
- 日志内容包含时间、等级、消息、上下文信息
- 日志默认输出到控制台,后续可扩展到文件/远程
- 关键操作、异常、接口请求需有日志
- 每条请求日志包含唯一请求 id、method、url、状态码、耗时、IP、时间戳
- dev 环境日志美化输出(带颜色)、控制台打印
- prod 环境日志按天/小时分文件存储,支持定时清理历史日志
- 日志格式美观dev 环境带颜色区分 info/warn/error
2. **全局错误处理器**
- 捕获所有未处理异常,返回统一 JSON 结构
- 支持自定义业务异常类型
- 错误日志自动记录
- 错误响应结构需包含 code、message、data 字段
- dev 环境异常响应包含详细堆栈prod 环境仅输出友好错误信息
3. **响应封装**
- 所有接口返回统一结构:`{ code, message, data }`
- 支持自定义响应码和 message
- 可扩展 traceId、耗时等字段
4. **请求日志中间件**
- 记录 method、url、耗时、状态码、IP、请求 id
- 日志链路追踪
5. **健康检查接口**
- 提供 `/health` 路由,返回服务健康状态
6. **配置中心优化**
- 所有配置集中到 config支持多环境
7. **Redis 支持**
- 集成 Redis配置集中管理
- Redis 连接池、健康检查
- 预留缓存、限流、会话等场景的基础能力
8. **接口文档自动化完善**
- Swagger UI 增加全局响应示例、错误码说明
## 5. 非目标(范围之外)
- 日志持久化到远程(本期仅本地文件)
- 速率限制、权限控制等安全功能
## 6. 设计考虑
- 日志、错误、响应封装均以中间件/插件方式实现,便于复用和扩展
- 代码与注释规范保持一致
- 目录结构建议:
- `src/middlewares/logger.ts`
- `src/middlewares/error-handler.ts`
- `src/utils/response.ts`
- `src/config/redis.config.ts`
- 日志库建议用 pino/winston/simple 自研
- 日志文件存储建议按天分目录,定时清理可用定时任务
- 请求 id 可用 nanoid/uuid 生成,并通过中间件注入上下文
- Redis 推荐用 bun:redis 原生或社区库
## 7. 技术考虑
- 日志库选型、日志文件清理策略
- 错误类型建议自定义 Error 类
- 响应封装为工具函数或 Elysia 插件
- Redis 连接池与健康检查实现
## 8. 成功指标
- 100% 接口响应结构统一
- 关键操作/异常均有日志
- 错误响应无堆栈泄漏,信息友好
- 日志分环境输出,生产环境日志可定期清理
- Redis 连接稳定,健康检查通过
## 9. 待解决问题
- 日志等级与格式细节
- 错误码与 message 规范
- 响应结构是否需 traceId、耗时等扩展字段
- Redis 具体使用场景细化

View File

@ -0,0 +1,53 @@
## 相关文件 (Relevant Files)
- `src/config/logger.config.ts` - Winston日志器配置文件支持分环境、颜色配置。
- `src/utils/logger.ts` - 基于winston的高性能日志记录器支持分环境输出、按日期轮转、彩色美化。
- `src/middlewares/logger.ts` - 请求日志中间件,支持请求链路追踪、彩色输出、性能监控。
- `src/demo/logger-demo.ts` - Winston日志器演示脚本展示各种日志功能。
- `src/middlewares/error-handler.ts` - 全局错误处理中间件,支持分环境详细度、自动错误日志记录、请求追踪。
- `src/utils/response.ts` - 统一响应封装工具函数。
- `src/config/redis.config.ts` - Redis 配置与连接池,支持环境变量和默认值配置。
- `src/utils/redis.ts` - Redis连接工具提供连接池、健康检查、状态监控等功能。
- `src/middlewares/request-id.ts` - 请求id生成与注入中间件。
- `src/controllers/health.controller.ts` - 健康检查接口。
- `src/tests/logger.test.ts` - 日志中间件单元测试。
- `src/tests/error-handler.test.ts` - 错误处理中间件全面单元测试,覆盖各种错误类型、环境差异、日志记录。
- `src/tests/response.test.ts` - 响应封装工具测试。
- `src/tests/redis.test.ts` - Redis 连接与健康检查全面单元测试,覆盖连接管理、健康检查、基础功能。
- `src/type/error.type.ts` - 自定义业务异常类型定义包含BusinessError类和常用异常工厂。
- `docs/http-status-codes.md` - HTTP状态码完整指南包含所有常见状态码、错误处理最佳实践。
### 备注 (Notes)
- 单元测试建议与业务代码分离,统一放在 `src/tests/` 目录。
- 日志文件存放在 `logs/` 目录,按日期轮转,格式:`application-YYYY-MM-DD.log`、`error-YYYY-MM-DD.log`。
- 生产环境按日期分文件存储错误日志单独文件winston-daily-rotate-file自动处理文件轮转和清理。
- 开发环境彩色控制台输出,测试环境仅输出 ERROR 级别日志。
- 已完成winston日志系统支持请求追踪、彩色输出、分环境配置。
## 任务 (Tasks)
- [x] 1.0 设计与实现日志记录器
- [x] 1.1 选型并集成日志库winston + winston-daily-rotate-file
- [x] 1.2 实现分环境日志输出dev 彩色控制台prod 文件存储)
- [x] 1.3 日志内容包含请求id、method、url、状态码、耗时、IP
- [x] 1.4 日志文件按天分割,支持定时清理
- [x] 1.5 日志中间件单元测试
- [x] 2.0 设计与实现全局错误处理器
- [x] 2.1 支持自定义业务异常类型
- [x] 2.2 dev 环境输出详细堆栈prod 环境输出友好信息
- [x] 2.3 错误日志自动记录
- [x] 2.4 错误处理中间件单元测试
- [x] 3.0 设计与实现统一响应封装
- [x] 3.1 封装统一响应结构code/message/data/traceId/耗时)
- [x] 3.2 响应封装工具单元测试
- [x] 4.0 集成 Redis
- [x] 4.1 编写 Redis 配置与连接池
- [x] 4.2 实现 Redis 健康检查
- [x] 4.3 Redis 相关单元测试
- [x] 5.0 健康检查接口
- [x] 5.1 实现 /health 路由,返回服务与依赖健康状态
- [x] 6.0 配置中心优化
- [x] 6.1 所有配置集中到 config支持多环境
- [x] 7.0 Swagger 文档完善
- [x] 7.1 增加全局响应示例、错误码说明

View File

@ -1,44 +0,0 @@
## 相关文件 (Relevant Files)
- `src/middlewares/logger.ts` - 日志记录中间件,支持分环境、彩色/文件输出、请求id链路追踪。
- `src/middlewares/error-handler.ts` - 全局错误处理中间件,支持分环境详细度。
- `src/utils/response.ts` - 统一响应封装工具函数。
- `src/config/redis.config.ts` - Redis 配置与连接池。
- `src/middlewares/request-id.ts` - 请求id生成与注入中间件。
- `src/controllers/health.controller.ts` - 健康检查接口。
- `src/tests/logger.test.ts` - 日志中间件单元测试。
- `src/tests/error-handler.test.ts` - 错误处理中间件单元测试。
- `src/tests/response.test.ts` - 响应封装工具测试。
- `src/tests/redis.test.ts` - Redis 连接与健康检查测试。
### 备注 (Notes)
- 单元测试建议与业务代码分离,统一放在 `src/tests/` 目录。
- 日志文件建议存放在 `logs/` 目录,按天分文件。
## 任务 (Tasks)
- [ ] 1.0 设计与实现日志记录器
- [ ] 1.1 选型并集成日志库(如 pino/winston/自研)
- [ ] 1.2 实现分环境日志输出dev 彩色控制台prod 文件存储)
- [ ] 1.3 日志内容包含请求id、method、url、状态码、耗时、IP
- [ ] 1.4 日志文件按天分割,支持定时清理
- [ ] 1.5 日志中间件单元测试
- [ ] 2.0 设计与实现全局错误处理器
- [ ] 2.1 支持自定义业务异常类型
- [ ] 2.2 dev 环境输出详细堆栈prod 环境输出友好信息
- [ ] 2.3 错误日志自动记录
- [ ] 2.4 错误处理中间件单元测试
- [ ] 3.0 设计与实现统一响应封装
- [ ] 3.1 封装统一响应结构code/message/data/traceId/耗时)
- [ ] 3.2 响应封装工具单元测试
- [ ] 4.0 集成 Redis
- [ ] 4.1 编写 Redis 配置与连接池
- [ ] 4.2 实现 Redis 健康检查
- [ ] 4.3 Redis 相关单元测试
- [ ] 5.0 健康检查接口
- [ ] 5.1 实现 /health 路由,返回服务与依赖健康状态
- [ ] 6.0 配置中心优化
- [ ] 6.1 所有配置集中到 config支持多环境
- [ ] 7.0 Swagger 文档完善
- [ ] 7.1 增加全局响应示例、错误码说明