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
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
.env
|
.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 项目
|
// ESLint 9.x Flat Config for Elysia + TypeScript 项目
|
||||||
// 详细注释,含风格规范(四空格缩进、分号、单引号等)
|
// 详细注释,含风格规范(四空格缩进、分号、单引号等)
|
||||||
|
|
||||||
@ -29,6 +29,10 @@ export default [
|
|||||||
console: 'readonly',
|
console: 'readonly',
|
||||||
process: 'readonly',
|
process: 'readonly',
|
||||||
Bun: 'readonly',
|
Bun: 'readonly',
|
||||||
|
Response: 'readonly',
|
||||||
|
Blob: 'readonly',
|
||||||
|
File: 'readonly',
|
||||||
|
TextEncoder: 'readonly',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
@ -45,22 +49,29 @@ export default [
|
|||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
// 允许ts-ignore等注释
|
// 允许ts-ignore等注释
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
'@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'],
|
semi: ['error', 'always'],
|
||||||
// 强制单引号
|
// 强制单引号(与Prettier一致)
|
||||||
quotes: ['error', 'single'],
|
quotes: ['error', 'single'],
|
||||||
// 末尾逗号(多行对象/数组)
|
// 末尾逗号(与Prettier一致)
|
||||||
'comma-dangle': ['error', 'always-multiline'],
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
// 对象key统一加引号
|
// 对象key统一加引号(与Prettier一致)
|
||||||
'quote-props': ['error', 'as-needed'],
|
'quote-props': ['error', 'as-needed'],
|
||||||
|
|
||||||
|
// ===== 其他代码质量规则 =====
|
||||||
// 关键字前后空格
|
// 关键字前后空格
|
||||||
'keyword-spacing': ['error', { before: true, after: true }],
|
'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": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.0.25",
|
"@types/bun": "^1.0.25",
|
||||||
|
"@types/redis": "^4.0.11",
|
||||||
|
"@types/winston": "^2.4.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||||
"@typescript-eslint/parser": "^8.35.0",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
@ -26,20 +28,30 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/jwt": "^1.3.1",
|
"@elysiajs/jwt": "^1.3.1",
|
||||||
"@elysiajs/swagger": "^1.3.0",
|
"@elysiajs/swagger": "^1.3.0",
|
||||||
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"pino": "^9.7.0",
|
"nanoid": "^5.1.5",
|
||||||
"pino-pretty": "^13.0.0",
|
"picocolors": "^1.1.1",
|
||||||
"pino-roll": "^3.1.0",
|
"redis": "^5.5.6",
|
||||||
"undici": "^7.11.0"
|
"ua-parser-js": "^2.0.4",
|
||||||
|
"undici": "^7.11.0",
|
||||||
|
"winston": "^3.17.0",
|
||||||
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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",
|
"start": "bun --env-file=.env.prod src/server.ts",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"vitest": "bun --env-file=.env x vitest run",
|
"vitest": "bun --env-file=.env x vitest run",
|
||||||
"test:watch": "bun test --watch",
|
"test:watch": "bun test --watch",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --ext .ts --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier --write ."
|
"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
|
* @date 2025-06-28
|
||||||
* @lastEditor hotok
|
* @lastEditor hotok
|
||||||
* @lastEditTime 2025-06-28
|
* @lastEditTime 2025-06-28
|
||||||
* @description Elysia API服务应用入口文件
|
* @description Elysia API服务应用入口文件,集成Winston日志系统
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
import { swaggerPlugin } from '@/plugins/swagger';
|
import { swaggerPlugin } from '@/plugins/swagger.plugins';
|
||||||
import { authController } from '@/controllers/try/auth.controller';
|
import { authController } from '@/controllers/try/auth.controller';
|
||||||
import { protectedController } from '@/controllers/try/protected.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应用实例
|
* Elysia应用实例
|
||||||
* @type {Elysia}
|
* @type {Elysia}
|
||||||
*/
|
*/
|
||||||
export const app = new Elysia()
|
export const app = new Elysia()
|
||||||
|
.state('config', config)
|
||||||
|
.use(loggerPlugin)
|
||||||
|
.use(errorHandlerPlugin)
|
||||||
.use(swaggerPlugin)
|
.use(swaggerPlugin)
|
||||||
.use(authController)
|
.use(authController)
|
||||||
.use(protectedController)
|
.use(protectedController)
|
||||||
|
.use(healthController)
|
||||||
.state('counter', 0) // 定义名为 counter 的初始值
|
.state('counter', 0) // 定义名为 counter 的初始值
|
||||||
|
|
||||||
// 批量定义 不会覆盖前面单独定义的
|
// 批量定义 不会覆盖前面单独定义的
|
||||||
@ -27,14 +43,12 @@ export const app = new Elysia()
|
|||||||
version: '1.0',
|
version: '1.0',
|
||||||
server: 'Bun',
|
server: 'Bun',
|
||||||
})
|
})
|
||||||
|
.state('Error', (message: string) => {
|
||||||
|
console.log('message', message);
|
||||||
|
return new AuthenticationError(message);
|
||||||
|
})
|
||||||
.state('db', '一个方法')
|
.state('db', '一个方法')
|
||||||
.decorate('closeDB', () => console.log('关闭方法')); // 添加关闭方法
|
.decorate('closeDB', () => console.log('关闭方法')); // 添加关闭方法
|
||||||
|
|
||||||
// 健康检查接口
|
|
||||||
app.get('/api/health', () => ({
|
|
||||||
code: 0,
|
|
||||||
message: '服务运行正常',
|
|
||||||
data: null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// app.closeDB() 可以以在路由中调用
|
// 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 { Elysia } from 'elysia';
|
||||||
import { jwtPlugin } from '@/plugins/jwt';
|
import { jwtPlugin } from '@/plugins/jwt.plugins';
|
||||||
import { loginBodySchema, type LoginBody } from '@/validators/try/auth.validator';
|
import { loginBodySchema, type LoginBody } from '@/validators/try/auth.validator';
|
||||||
import { loginResponse200Schema, loginResponse400Schema } from '@/validators/try/auth.response';
|
import { loginResponse200Schema, loginResponse400Schema } from '@/validators/try/auth.response';
|
||||||
import { loginService } from '@/services/try/auth.service';
|
import { loginService } from '@/services/try/auth.service';
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Elysia } from 'elysia';
|
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 { protectedResponse200Schema, protectedResponse401Schema } from '@/validators/try/protected.response';
|
||||||
import { protectedService } from '@/services/try/protected.service';
|
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解码后的用户信息
|
* @type {any} user - JWT解码后的用户信息
|
||||||
* @description 由jwtAuthPlugin中间件注入
|
* @description 由jwtAuthPlugin中间件注入
|
||||||
*/
|
*/
|
||||||
|
console.log('user', user);
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
message: '受保护资源访问成功',
|
message: '受保护资源访问成功',
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { app } from './app';
|
import { app } from '../app';
|
||||||
|
|
||||||
let token = '';
|
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 { 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({}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
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