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:
parent
621963f82c
commit
2ee70e5d42
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
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
470
docs/chalk-guide.md
Normal 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
622
docs/http-status-codes.md
Normal 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
75
docs/life.md
Normal 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
294
docs/picocolors-guide.md
Normal 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
78
docs/winston.md
Normal 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'; // 记录所有级别
|
||||
```
|
||||
|
||||
这样你就可以根据不同环境灵活控制日志的详细程度了!
|
@ -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 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
28
package.json
28
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
32
src/app.ts
32
src/app.ts
@ -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
6
src/config/index.ts
Normal 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';
|
18
src/config/logger.config.ts
Normal file
18
src/config/logger.config.ts
Normal 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',
|
||||
};
|
45
src/config/redis.config.ts
Normal file
45
src/config/redis.config.ts
Normal 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}`;
|
||||
};
|
34
src/controllers/health.controller.ts
Normal file
34
src/controllers/health.controller.ts
Normal 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,
|
||||
});
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
84
src/plugins/errorHandler.plugins.ts
Normal file
84
src/plugins/errorHandler.plugins.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
@ -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,
|
||||
};
|
||||
}
|
||||
});
|
40
src/plugins/jwt.plugins.ts
Normal file
40
src/plugins/jwt.plugins.ts
Normal 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, '未授权');
|
||||
}
|
||||
});
|
@ -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,
|
||||
});
|
95
src/plugins/logger.plugins.ts
Normal file
95
src/plugins/logger.plugins.ts
Normal 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;
|
460
src/plugins/swagger.plugins.ts
Normal file
460
src/plugins/swagger.plugins.ts
Normal 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: '系统健康状态监控接口',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
@ -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: [] }],
|
||||
},
|
||||
});
|
300
src/services/health.service.ts
Normal file
300
src/services/health.service.ts
Normal 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();
|
@ -17,6 +17,7 @@ export const protectedService = (user: any) => {
|
||||
* @type {any} user - JWT解码后的用户信息
|
||||
* @description 由jwtAuthPlugin中间件注入
|
||||
*/
|
||||
console.log('user', user);
|
||||
return {
|
||||
code: 0,
|
||||
message: '受保护资源访问成功',
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { app } from './app';
|
||||
import { app } from '../app';
|
||||
|
||||
let token = '';
|
||||
|
51
src/tests/demo/testLogger.ts
Normal file
51
src/tests/demo/testLogger.ts
Normal 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
305
src/tests/health.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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
238
src/tests/redis.test.ts
Normal 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
291
src/tests/swagger.test.ts
Normal 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
114
src/type/error.type.ts
Normal 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
122
src/type/health.type.ts
Normal 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
16
src/type/logger.type.ts
Normal 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
34
src/utils/deviceInfo.ts
Normal 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();
|
||||
};
|
12
src/utils/formatFileSize.ts
Normal file
12
src/utils/formatFileSize.ts
Normal 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
255
src/utils/logger.ts
Normal 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
226
src/utils/randomChalk.ts
Normal 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
127
src/utils/redis.ts
Normal 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
79
src/utils/text.ts
Normal 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)}"`); // "这是一个很..."
|
259
src/validators/global.response.ts
Normal file
259
src/validators/global.response.ts
Normal 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()),
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
});
|
181
src/validators/health.response.ts
Normal file
181
src/validators/health.response.ts
Normal 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({}),
|
||||
}),
|
||||
}),
|
||||
};
|
@ -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 具体使用场景细化
|
53
tasks/archive/20250628-tasks-prd-架构优化.md
Normal file
53
tasks/archive/20250628-tasks-prd-架构优化.md
Normal 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 增加全局响应示例、错误码说明
|
@ -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 增加全局响应示例、错误码说明
|
Loading…
Reference in New Issue
Block a user