refactor: 重构项目架构并标准化开发规范
- 重构项目结构:controllers/services -> modules模块化组织 - 新增Drizzle ORM集成和数据库schema定义 - 添加完整的开发规范文档(.cursor/rules/) - 重新组织插件结构为子目录方式 - 新增用户模块和示例代码 - 更新类型定义并移除试验性代码 - 添加API文档和JWT使用示例 关联任务计划文档
This commit is contained in:
parent
2518986557
commit
a23d336ebd
174
.cursor/rules/api-development-standard.md
Normal file
174
.cursor/rules/api-development-standard.md
Normal file
@ -0,0 +1,174 @@
|
||||
# API 开发规范
|
||||
|
||||
## 文件结构
|
||||
|
||||
每个API模块必须包含以下文件:
|
||||
|
||||
- `*.schema.ts` - 请求参数和数据结构定义 + TypeScript类型导出
|
||||
- `*.response.ts` - 响应格式定义 + 响应类型导出
|
||||
- `*.service.ts` - 业务逻辑实现(使用类型注解)
|
||||
- `*.controller.ts` - 路由和控制器(使用Schema验证)
|
||||
- `*.test.ts` - 测试用例(类型安全的测试数据)
|
||||
|
||||
## 开发流程
|
||||
|
||||
1. **Schema 定义** - 使用 TypeBox 定义请求参数和数据结构,导出TypeScript类型
|
||||
2. **Response 定义** - 基于全局响应格式定义各种场景的响应,导出响应类型
|
||||
3. **Service 实现** - 编写业务逻辑,使用类型注解确保类型安全
|
||||
4. **Controller 实现** - 集成JWT认证、Schema验证、错误处理
|
||||
5. **测试编写** - 使用类型安全的测试数据,覆盖正常、异常、边界场景
|
||||
|
||||
## 必须遵循
|
||||
|
||||
### 1. 认证与授权
|
||||
|
||||
```typescript
|
||||
// 需要认证的接口必须使用 jwtAuthPlugin
|
||||
export const controller = new Elysia().use(jwtAuthPlugin).get('/protected-route', handler, options);
|
||||
```
|
||||
|
||||
### 2. 参数验证与类型使用
|
||||
|
||||
```typescript
|
||||
// example.schema.ts - 定义Schema和导出类型
|
||||
import { t, type Static } from 'elysia';
|
||||
|
||||
export const GetUserByUsernameSchema = t.Object({
|
||||
username: t.String({ minLength: 2, maxLength: 50 }),
|
||||
});
|
||||
|
||||
// 导出TypeScript类型
|
||||
export type GetUserByUsernameParams = Static<typeof GetUserByUsernameSchema>;
|
||||
|
||||
// example.service.ts - Service中使用类型
|
||||
import type { GetUserByUsernameParams } from './example.schema';
|
||||
|
||||
export class ExampleService {
|
||||
async getUserByUsername(params: GetUserByUsernameParams): Promise<UserInfo> {
|
||||
const { username } = params; // 类型安全
|
||||
// 业务逻辑...
|
||||
}
|
||||
}
|
||||
|
||||
// example.controller.ts - Controller中使用Schema验证
|
||||
export const controller = new Elysia().use(jwtAuthPlugin).get('/user/:username', handler, {
|
||||
params: GetUserByUsernameSchema, // 运行时验证
|
||||
});
|
||||
```
|
||||
|
||||
**要求:**
|
||||
- ✅ 每个Schema必须导出对应的TypeScript类型
|
||||
- ✅ Service方法必须使用类型注解
|
||||
- ❌ 禁止行内定义任何参数Schema
|
||||
|
||||
### 3. 统一响应格式
|
||||
|
||||
```typescript
|
||||
// 成功响应
|
||||
return {
|
||||
code: ERROR_CODES.SUCCESS,
|
||||
message: '操作成功',
|
||||
data: result,
|
||||
};
|
||||
|
||||
// 错误响应
|
||||
return {
|
||||
code: ERROR_CODES.BUSINESS_ERROR,
|
||||
message: '具体错误信息',
|
||||
data: null,
|
||||
};
|
||||
```
|
||||
|
||||
- 响应内容的类型需要在.response.ts中定义
|
||||
|
||||
### 4. 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await service.method();
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`操作失败: ${error}`));
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
if (errorMessage.includes('特定错误')) {
|
||||
set.status = 400;
|
||||
return errorResponse(ERROR_CODES.BUSINESS_ERROR, '业务错误消息');
|
||||
}
|
||||
|
||||
set.status = 500;
|
||||
return errorResponse(ERROR_CODES.INTERNAL_ERROR, '服务器内部错误');
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 文档配置
|
||||
|
||||
```typescript
|
||||
{
|
||||
detail: {
|
||||
summary: '接口简要描述',
|
||||
description: '接口详细描述',
|
||||
tags: [tags.moduleName],
|
||||
security: [{ bearerAuth: [] }], // 需要认证时添加
|
||||
},
|
||||
response: PredefinedResponses,
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 日志记录
|
||||
|
||||
```typescript
|
||||
// 接口调用日志
|
||||
Logger.info(`接口被调用,参数: ${param}, 用户: ${JSON.stringify(user)}`);
|
||||
|
||||
// 成功日志
|
||||
Logger.info(`操作成功,结果: ${result.id}`);
|
||||
|
||||
// 错误日志
|
||||
Logger.error(new Error(`操作失败,错误: ${error}`));
|
||||
```
|
||||
|
||||
### 7. 必要的注释
|
||||
|
||||
1. 接口名称注释
|
||||
|
||||
```typescript
|
||||
export const controller = new Elysia()
|
||||
.use(jwtAuthPlugin)
|
||||
/**
|
||||
* 根据用户名查询用户信息
|
||||
* @route GET /api/sample/user/:username
|
||||
* @description 通过用户名查询用户的详细信息,需要JWT认证
|
||||
* @param username 用户名,路径参数,长度2-50字符
|
||||
* @returns 用户信息对象或错误响应
|
||||
* @modification hotok 2025-06-29 初始实现
|
||||
*/
|
||||
.get('/protected-route', handler, options);
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- ❌ 直接在 Controller 中写业务逻辑
|
||||
- ❌ 不进行参数验证
|
||||
- ❌ 返回非标准格式的响应
|
||||
- ❌ 暴露敏感信息(如密码哈希)
|
||||
- ❌ 缺少错误处理和日志记录
|
||||
- ❌ 不编写测试用例
|
||||
|
||||
## 命名规范
|
||||
|
||||
- 文件名:`module.type.ts`(如:`user.controller.ts`)
|
||||
- Schema:`GetUserByIdSchema`、`CreateUserSchema`
|
||||
- Response:`GetUserSuccessResponse`、`UserErrorResponse`
|
||||
- Service 类:`UserService`、导出实例:`userService`
|
||||
- Controller:`userController`
|
||||
|
||||
## 测试要求
|
||||
|
||||
每个接口必须包含:
|
||||
|
||||
- ✅ 正常流程测试
|
||||
- ✅ 参数验证边界测试(最短、最长、无效格式)
|
||||
- ✅ 业务逻辑异常测试(不存在、权限不足等)
|
||||
- ✅ 认证相关测试(无Token、无效Token、过期Token)
|
||||
- ✅ 响应格式验证(状态码、code、message、data结构)
|
1084
.cursor/rules/elysia-interface-standards.md
Normal file
1084
.cursor/rules/elysia-interface-standards.md
Normal file
File diff suppressed because it is too large
Load Diff
5
.cursor/rules/elysia-rules.mdc
Normal file
5
.cursor/rules/elysia-rules.mdc
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
157
docs/api-type-usage-examples.md
Normal file
157
docs/api-type-usage-examples.md
Normal file
@ -0,0 +1,157 @@
|
||||
# API Schema 类型使用指南
|
||||
|
||||
## 1. Schema 转 TypeScript 类型
|
||||
|
||||
在 `.schema.ts` 文件中定义并导出类型:
|
||||
|
||||
```typescript
|
||||
// example.schema.ts
|
||||
import { t, type Static } from 'elysia';
|
||||
|
||||
// Schema 定义
|
||||
export const GetUserByUsernameSchema = t.Object({
|
||||
username: t.String({
|
||||
minLength: 2,
|
||||
maxLength: 50,
|
||||
description: '用户名',
|
||||
}),
|
||||
});
|
||||
|
||||
// 从 Schema 推断类型
|
||||
export type GetUserByUsernameParams = Static<typeof GetUserByUsernameSchema>;
|
||||
```
|
||||
|
||||
## 2. 在 Service 中使用类型
|
||||
|
||||
```typescript
|
||||
// example.service.ts
|
||||
import type { GetUserByUsernameParams, UserInfo } from './example.schema';
|
||||
|
||||
export class ExampleService {
|
||||
// 使用类型注解参数
|
||||
async getUserByUsername(params: GetUserByUsernameParams): Promise<UserInfo> {
|
||||
const { username } = params; // TypeScript 会自动推断类型
|
||||
|
||||
// 业务逻辑...
|
||||
return userResult;
|
||||
}
|
||||
|
||||
// 或者直接使用解构参数
|
||||
async getUserByUsername2({ username }: GetUserByUsernameParams): Promise<UserInfo> {
|
||||
// 业务逻辑...
|
||||
return userResult;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 在 Controller 中使用类型
|
||||
|
||||
```typescript
|
||||
// example.controller.ts
|
||||
import type { GetUserByUsernameParams, UserInfo } from './example.schema';
|
||||
import { GetUserByUsernameSchema } from './example.schema';
|
||||
|
||||
export const controller = new Elysia()
|
||||
.get(
|
||||
'/user/:username',
|
||||
async ({ params }) => {
|
||||
// params 自动推断为 GetUserByUsernameParams 类型
|
||||
const userInfo: UserInfo = await service.getUserByUsername(params);
|
||||
return successResponse(userInfo);
|
||||
},
|
||||
{
|
||||
// 使用 Schema 进行运行时验证
|
||||
params: GetUserByUsernameSchema,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## 4. 在测试中使用类型
|
||||
|
||||
```typescript
|
||||
// example.test.ts
|
||||
import type { GetUserByUsernameParams, UserInfo } from './example.schema';
|
||||
|
||||
describe('用户查询测试', () => {
|
||||
it('应该正确处理参数类型', () => {
|
||||
// 类型安全的测试数据
|
||||
const validParams: GetUserByUsernameParams = {
|
||||
username: 'testuser'
|
||||
};
|
||||
|
||||
const invalidParams = {
|
||||
username: 'a' // TypeScript 会提示这可能不符合验证规则
|
||||
};
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 5. 工具函数中使用类型
|
||||
|
||||
```typescript
|
||||
// utils/validators.ts
|
||||
import type { GetUserByUsernameParams } from '../modules/sample/example.schema';
|
||||
|
||||
// 类型安全的验证函数
|
||||
export function validateUsername(params: GetUserByUsernameParams): boolean {
|
||||
return params.username.length >= 2 && params.username.length <= 50;
|
||||
}
|
||||
|
||||
// 类型安全的格式化函数
|
||||
export function formatUserQuery(params: GetUserByUsernameParams): string {
|
||||
return `查询用户: ${params.username}`;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 响应类型使用示例
|
||||
|
||||
```typescript
|
||||
// example.response.ts
|
||||
import { t, type Static } from 'elysia';
|
||||
import { UserInfoSchema } from './example.schema';
|
||||
|
||||
export const GetUserSuccessResponse = t.Object({
|
||||
code: t.Literal(0),
|
||||
message: t.String(),
|
||||
data: UserInfoSchema,
|
||||
});
|
||||
|
||||
// 导出响应类型
|
||||
export type GetUserSuccessResponseType = Static<typeof GetUserSuccessResponse>;
|
||||
```
|
||||
|
||||
## 7. 完整的类型流转示例
|
||||
|
||||
```typescript
|
||||
// 完整的类型安全流程
|
||||
import type {
|
||||
GetUserByUsernameParams,
|
||||
UserInfo
|
||||
} from './example.schema';
|
||||
|
||||
// Service 层
|
||||
class UserService {
|
||||
async getUser(params: GetUserByUsernameParams): Promise<UserInfo> {
|
||||
// params.username 有完整的类型提示
|
||||
// 返回值必须符合 UserInfo 类型
|
||||
}
|
||||
}
|
||||
|
||||
// Controller 层
|
||||
const controller = new Elysia()
|
||||
.get('/user/:username', async ({ params }) => {
|
||||
// params 自动推断类型
|
||||
const user = await userService.getUser(params);
|
||||
// user 自动推断为 UserInfo 类型
|
||||
return { code: 0, message: '成功', data: user };
|
||||
}, {
|
||||
params: GetUserByUsernameSchema, // 运行时验证
|
||||
});
|
||||
```
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
1. **总是导出类型**:每个 Schema 都应该导出对应的 TypeScript 类型
|
||||
2. **类型注解**:在 Service 方法中明确使用类型注解
|
||||
3. **一致命名**:类型名应该与 Schema 名保持一致的命名规范
|
||||
4. **分离关注点**:Schema 用于运行时验证,Type 用于编译时类型检查
|
214
docs/jwt-usage-examples.md
Normal file
214
docs/jwt-usage-examples.md
Normal file
@ -0,0 +1,214 @@
|
||||
# JWT 用户类型使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
我们定义了完整的JWT类型系统,提供类型安全的JWT操作。
|
||||
|
||||
## 类型定义
|
||||
|
||||
### 1. JwtUserType - JWT中的用户信息
|
||||
```typescript
|
||||
interface JwtUserType {
|
||||
userId: number;
|
||||
username: string;
|
||||
email: string;
|
||||
nickname?: string;
|
||||
status: number;
|
||||
role?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. JwtPayloadType - 完整的JWT载荷
|
||||
```typescript
|
||||
interface JwtPayloadType extends JwtUserType {
|
||||
iat: number; // 发行时间
|
||||
exp: number; // 过期时间
|
||||
sub?: string; // 主题
|
||||
iss?: string; // 发行者
|
||||
aud?: string; // 受众
|
||||
jti?: string; // JWT ID
|
||||
nbf?: number; // 生效时间
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 在认证Controller中生成JWT Token
|
||||
|
||||
```typescript
|
||||
// auth.controller.ts
|
||||
import { createJwtPayload } from '@/utils/jwt.helper';
|
||||
import type { UserInfoType } from '@/modules/sample/example.schema';
|
||||
|
||||
export const authController = new Elysia()
|
||||
.use(jwtPlugin)
|
||||
.post('/login', async ({ body, jwt }) => {
|
||||
// 用户登录验证逻辑...
|
||||
const userInfo: UserInfoType = await getUserFromDatabase(body.username);
|
||||
|
||||
// 创建JWT载荷
|
||||
const payload = createJwtPayload(userInfo, {
|
||||
role: 'user', // 可选的角色信息
|
||||
issuer: 'my-api',
|
||||
audience: 'web-app',
|
||||
});
|
||||
|
||||
// 生成Token
|
||||
const token = await jwt.sign(payload);
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
token,
|
||||
user: payload, // 返回用户信息(不含敏感数据)
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 在需要认证的Controller中使用用户信息
|
||||
|
||||
```typescript
|
||||
// user.controller.ts
|
||||
import { formatUserForLog, isValidJwtUser } from '@/utils/jwt.helper';
|
||||
import type { JwtUserType } from '@/type/jwt.type';
|
||||
|
||||
export const userController = new Elysia()
|
||||
.use(jwtAuthPlugin)
|
||||
.get('/profile', async ({ user, payload }) => {
|
||||
// user 自动推断为 JwtUserType 类型
|
||||
// payload 自动推断为 JwtPayloadType 类型
|
||||
|
||||
// 验证用户有效性
|
||||
if (!isValidJwtUser(payload)) {
|
||||
Logger.warn(`无效用户尝试访问: ${formatUserForLog(user)}`);
|
||||
return { code: 401, message: '用户状态异常', data: null };
|
||||
}
|
||||
|
||||
// 使用类型安全的用户信息
|
||||
Logger.info(`用户查看个人资料: ${formatUserForLog(user)}`);
|
||||
|
||||
// 获取完整的用户信息(从数据库)
|
||||
const fullUserInfo = await getUserById(user.userId);
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data: fullUserInfo,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 在Service中使用JWT用户类型
|
||||
|
||||
```typescript
|
||||
// user.service.ts
|
||||
import type { JwtUserType } from '@/type/jwt.type';
|
||||
|
||||
export class UserService {
|
||||
// 使用JWT用户类型作为参数
|
||||
async updateUserProfile(currentUser: JwtUserType, updateData: any) {
|
||||
// 检查权限
|
||||
if (currentUser.status !== 1) {
|
||||
throw new Error('用户状态异常,无法操作');
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
const updatedUser = await db.update(users)
|
||||
.set(updateData)
|
||||
.where(eq(users.id, currentUser.userId));
|
||||
|
||||
Logger.info(`用户资料更新: ${currentUser.username} (ID: ${currentUser.userId})`);
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
// 根据JWT用户信息获取权限
|
||||
async getUserPermissions(jwtUser: JwtUserType): Promise<string[]> {
|
||||
const permissions = await db.select()
|
||||
.from(userPermissions)
|
||||
.where(eq(userPermissions.userId, jwtUser.userId));
|
||||
|
||||
return permissions.map(p => p.permission);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Token状态检查
|
||||
|
||||
```typescript
|
||||
// middleware/token-check.ts
|
||||
import {
|
||||
isTokenExpiringSoon,
|
||||
getTokenRemainingTime,
|
||||
formatRemainingTime
|
||||
} from '@/utils/jwt.helper';
|
||||
|
||||
export const tokenStatusMiddleware = (app: Elysia) =>
|
||||
app.derive(({ payload, user }) => {
|
||||
if (!payload) return {};
|
||||
|
||||
// 检查Token是否即将过期
|
||||
const expiringSoon = isTokenExpiringSoon(payload, 30); // 30分钟阈值
|
||||
const remainingTime = getTokenRemainingTime(payload);
|
||||
|
||||
if (expiringSoon) {
|
||||
Logger.warn(`用户Token即将过期: ${user.username}, 剩余时间: ${formatRemainingTime(remainingTime)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
tokenInfo: {
|
||||
expiringSoon,
|
||||
remainingTime,
|
||||
formattedTime: formatRemainingTime(remainingTime),
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 角色权限检查
|
||||
|
||||
```typescript
|
||||
// middleware/role-check.ts
|
||||
import type { JwtUserType } from '@/type/jwt.type';
|
||||
|
||||
export function requireRole(requiredRole: string) {
|
||||
return (app: Elysia) =>
|
||||
app.onBeforeHandle(({ user, set }) => {
|
||||
const jwtUser = user as JwtUserType;
|
||||
|
||||
if (!jwtUser.role || jwtUser.role !== requiredRole) {
|
||||
Logger.warn(`权限不足: 用户${jwtUser.username}尝试访问需要${requiredRole}角色的资源`);
|
||||
set.status = 403;
|
||||
return {
|
||||
code: 403,
|
||||
message: '权限不足',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
export const adminController = new Elysia()
|
||||
.use(jwtAuthPlugin)
|
||||
.use(requireRole('admin'))
|
||||
.get('/admin-only', () => {
|
||||
return { message: '只有管理员能看到这个内容' };
|
||||
});
|
||||
```
|
||||
|
||||
## 🎯 类型安全的好处
|
||||
|
||||
1. **编译时检查**: TypeScript 会在编译时检查所有JWT相关操作
|
||||
2. **智能提示**: IDE 提供完整的属性提示和自动补全
|
||||
3. **重构安全**: 修改用户类型时,所有相关代码都会得到类型检查
|
||||
4. **文档作用**: 类型定义本身就是最好的文档
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
1. **总是使用类型注解**: 在Service方法中明确使用 `JwtUserType` 类型
|
||||
2. **验证用户状态**: 使用 `isValidJwtUser()` 检查用户有效性
|
||||
3. **记录操作日志**: 使用 `formatUserForLog()` 格式化用户信息
|
||||
4. **检查Token状态**: 在关键操作前检查Token是否即将过期
|
||||
5. **权限分离**: 使用角色字段实现细粒度权限控制
|
@ -19,6 +19,7 @@
|
||||
"@types/winston": "^2.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||
"@typescript-eslint/parser": "^8.35.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
@ -30,6 +31,7 @@
|
||||
"@elysiajs/swagger": "^1.3.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"chalk": "^5.4.1",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"mysql2": "^3.14.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"picocolors": "^1.1.1",
|
||||
|
69
src/app.ts
69
src/app.ts
@ -3,18 +3,25 @@
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description Elysia API服务应用入口文件,集成Winston日志系统
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description Elysia API服务应用入口文件,集成Winston日志系统和统一路由管理
|
||||
*/
|
||||
|
||||
// Elysia
|
||||
import { Elysia } from 'elysia';
|
||||
import { swaggerPlugin } from '@/plugins/swagger.plugins';
|
||||
import { authController } from '@/controllers/try/auth.controller';
|
||||
import { protectedController } from '@/controllers/try/protected.controller';
|
||||
import { healthController } from '@/controllers/health.controller';
|
||||
import * as config from '@/config/logger.config';
|
||||
import loggerPlugin from '@/plugins/logger.plugins';
|
||||
import { errorHandlerPlugin } from '@/plugins/errorHandler.plugins';
|
||||
// 版本信息
|
||||
import * as packageJson from '@package.json';
|
||||
// 路由总入口
|
||||
import { controllers } from '@/modules/index'; // 使用路由总入口
|
||||
// 插件总入口
|
||||
import { plugins } from '@/plugins/index';
|
||||
// 格式化路由
|
||||
import { formatRoute } from '@/utils/formatRoute';
|
||||
|
||||
/**
|
||||
* 自定义认证错误类
|
||||
* @description 用于处理认证相关的错误
|
||||
*/
|
||||
class AuthenticationError extends Error {
|
||||
constructor(message: string, code = 500) {
|
||||
super(message);
|
||||
@ -24,31 +31,31 @@ class AuthenticationError extends Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elysia应用实例
|
||||
* @type {Elysia}
|
||||
* @description 主应用实例,集成所有插件和路由
|
||||
*/
|
||||
export const app = new Elysia()
|
||||
.state('config', config)
|
||||
.use(loggerPlugin)
|
||||
.use(errorHandlerPlugin)
|
||||
.use(swaggerPlugin)
|
||||
.use(authController)
|
||||
.use(protectedController)
|
||||
.use(healthController)
|
||||
.state('counter', 0) // 定义名为 counter 的初始值
|
||||
|
||||
// 批量定义 不会覆盖前面单独定义的
|
||||
export const app = new Elysia({ name: 'main-app' })
|
||||
// 环境变量
|
||||
.decorate('env', process.env.NODE_ENV || process.env.BUN_ENV || 'development')
|
||||
// 版本信息
|
||||
.state({
|
||||
version: '1.0',
|
||||
server: 'Bun',
|
||||
version: packageJson.version,
|
||||
})
|
||||
.state('Error', (message: string) => {
|
||||
console.log('message', message);
|
||||
return new AuthenticationError(message);
|
||||
// 使用插件
|
||||
.use(plugins)
|
||||
|
||||
// 使用统一的路由入口 所有业务的入口文件
|
||||
.use(controllers)
|
||||
|
||||
.onStart(() => {
|
||||
console.log('🚀 Elysia 服务启动成功');
|
||||
console.log(formatRoute(app.routes));
|
||||
console.log('📝 API 文档地址: http://localhost:3000/docs');
|
||||
console.log('🏥 健康检查: http://localhost:3000/api/health');
|
||||
console.log('📡 API 版本信息: http://localhost:3000/api/version');
|
||||
})
|
||||
.state('db', '一个方法')
|
||||
.decorate('closeDB', () => console.log('关闭方法')); // 添加关闭方法
|
||||
|
||||
|
||||
// app.closeDB() 可以以在路由中调用
|
||||
.onStop(() => {
|
||||
console.log('👋 Elysia 服务正在关闭...');
|
||||
});
|
||||
|
@ -1,34 +0,0 @@
|
||||
/**
|
||||
* @file 用户认证控制器(试运行)
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 提供基础登录接口,集成JWT签发与参数校验,业务逻辑分离到service
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { jwtPlugin } from '@/plugins/jwt.plugins';
|
||||
import { loginBodySchema, type LoginBody } from '@/validators/try/auth.validator';
|
||||
import { loginResponse200Schema, loginResponse400Schema } from '@/validators/try/auth.response';
|
||||
import { loginService } from '@/services/try/auth.service';
|
||||
|
||||
/**
|
||||
* 登录接口
|
||||
* @param app Elysia实例
|
||||
* @returns Elysia实例(带登录路由)
|
||||
*/
|
||||
export const authController = new Elysia()
|
||||
.use(jwtPlugin)
|
||||
.post('/api/login', ({ body, jwt, set }: { body: LoginBody; jwt: any; set: any }) => loginService(body, jwt, set), {
|
||||
body: loginBodySchema,
|
||||
detail: {
|
||||
tags: ['认证'],
|
||||
summary: '用户登录',
|
||||
description: '基础登录接口,用户名/密码校验通过后返回JWT',
|
||||
},
|
||||
response: {
|
||||
200: loginResponse200Schema,
|
||||
400: loginResponse400Schema,
|
||||
},
|
||||
});
|
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @file 受保护接口控制器(试运行)
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 提供需要JWT认证的受保护接口,业务逻辑分离到service
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { jwtAuthPlugin } from '@/plugins/jwt.plugins';
|
||||
import { protectedResponse200Schema, protectedResponse401Schema } from '@/validators/try/protected.response';
|
||||
import { protectedService } from '@/services/try/protected.service';
|
||||
|
||||
/**
|
||||
* 受保护接口
|
||||
* @param app Elysia实例
|
||||
* @returns Elysia实例(带受保护路由)
|
||||
*/
|
||||
export const protectedController = new Elysia()
|
||||
.use(jwtAuthPlugin)
|
||||
.get('/api/protected', ({ user }: any) => protectedService(user), {
|
||||
detail: {
|
||||
summary: '受保护接口',
|
||||
tags: ['认证'],
|
||||
description: '需要JWT认证的受保护接口,返回当前用户信息',
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
response: {
|
||||
200: protectedResponse200Schema,
|
||||
401: protectedResponse401Schema,
|
||||
},
|
||||
});
|
@ -8,27 +8,24 @@
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { healthService } from '@/services/health.service';
|
||||
import { healthResponse } from '@/validators/health.response';
|
||||
import { healthService } from '@/modules/health/health.service';
|
||||
|
||||
/**
|
||||
* 健康检查控制器
|
||||
* 提供系统健康状态检查接口
|
||||
*/
|
||||
export const healthController = new Elysia({ prefix: '/api' })
|
||||
.get('/health', async (ctx) => await healthService.getHealthStatus(ctx), {
|
||||
export const healthController = new Elysia()
|
||||
.get('/', async (ctx) => await healthService.getHealthStatus(ctx), {
|
||||
detail: {
|
||||
tags: ['健康检查'],
|
||||
summary: '获取系统健康状态',
|
||||
description: '检查系统及各依赖服务的健康状态,包括数据库、Redis等',
|
||||
},
|
||||
response: healthResponse,
|
||||
})
|
||||
.get('/health/detailed', async (ctx) => await healthService.getDetailedHealthStatus(ctx), {
|
||||
.get('/detailed', async (ctx) => await healthService.getDetailedHealthStatus(ctx), {
|
||||
detail: {
|
||||
tags: ['健康检查'],
|
||||
summary: '获取详细健康状态',
|
||||
description: '获取系统详细健康状态,包括性能指标、资源使用情况等',
|
||||
},
|
||||
response: healthResponse,
|
||||
});
|
@ -10,7 +10,7 @@
|
||||
import type { Context } from 'elysia';
|
||||
import { Redis } from '@/utils/redis';
|
||||
import { pool } from '@/utils/mysql';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
|
||||
// 临时内联类型定义
|
||||
interface ComponentStatus {
|
35
src/modules/index.ts
Normal file
35
src/modules/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @file API 路由总入口
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 所有 API 路由的统一入口,使用 group 进行模块化管理
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { healthController } from './health/health.controller';
|
||||
import { userController } from './user/user.controller';
|
||||
import { testController } from './test/test.controller';
|
||||
import { sampleController } from './sample/example.controller';
|
||||
|
||||
/**
|
||||
* 主路由控制器 - API 路由总入口
|
||||
* @description 统一管理所有 API 路由,便于维护和扩展
|
||||
*/
|
||||
export const controllers = new Elysia({
|
||||
prefix: '/api',
|
||||
name: 'controller',
|
||||
})
|
||||
// 版本信息
|
||||
.get('/version', () => ({
|
||||
version: '1.0.0',
|
||||
}))
|
||||
// 用户系统接口
|
||||
.group('/user', (app) => app.use(userController))
|
||||
// 验证性接口
|
||||
.group('/test', (app) => app.use(testController))
|
||||
// 健康检查接口
|
||||
.group('/health', (app) => app.use(healthController))
|
||||
// 样例接口
|
||||
.group('/sample', (app) => app.use(sampleController));
|
53
src/modules/sample/example.controller.ts
Normal file
53
src/modules/sample/example.controller.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file 样例控制器
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 样例接口控制器,演示完整的接口开发流程
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { jwtAuthPlugin } from '@/plugins/jwt/jwt.plugins';
|
||||
import { GetUserByUsernameSchema } from './example.schema';
|
||||
import { GetUserByUsernameResponses } from './example.response';
|
||||
import { tags } from '@/modules/tags';
|
||||
import { exampleService } from './example.service';
|
||||
|
||||
/**
|
||||
* 样例控制器
|
||||
* @description 提供样例接口的路由定义和请求处理逻辑
|
||||
* @modification hotok 2025-06-29 实现根据用户名查询用户接口
|
||||
*/
|
||||
export const sampleController = new Elysia()
|
||||
// 使用JWT认证插件
|
||||
.use(jwtAuthPlugin)
|
||||
/**
|
||||
* 根据用户名查询用户信息
|
||||
* @route GET /api/sample/user/:username
|
||||
* @description 通过用户名查询用户的详细信息,需要JWT认证
|
||||
* @param username 用户名,路径参数,长度2-50字符
|
||||
* @returns 用户信息对象或错误响应
|
||||
* @modification hotok 2025-06-29 初始实现
|
||||
*/
|
||||
.get(
|
||||
'/user/:username',
|
||||
({ params, user }) => {
|
||||
return exampleService.findUserByUsername({ params, user });
|
||||
},
|
||||
{
|
||||
// 路径参数验证
|
||||
params: GetUserByUsernameSchema,
|
||||
|
||||
// API文档配置
|
||||
detail: {
|
||||
summary: '根据用户名查询用户信息',
|
||||
description:
|
||||
'通过用户名查询用户的详细信息,需要JWT身份认证。返回用户的基本信息,不包含敏感数据如密码。',
|
||||
tags: [tags.user, tags.sample],
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
// 响应格式定义
|
||||
response: GetUserByUsernameResponses,
|
||||
},
|
||||
);
|
82
src/modules/sample/example.response.ts
Normal file
82
src/modules/sample/example.response.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @file 样例接口响应Schema定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 样例接口的响应结构定义,基于全局响应格式扩展
|
||||
*/
|
||||
|
||||
import { t } from 'elysia';
|
||||
import { UserInfoSchema } from './example.schema';
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户成功响应
|
||||
*/
|
||||
export const GetUserByUsernameSuccessResponse = t.Object({
|
||||
code: t.Literal(0, {
|
||||
description: '成功响应码',
|
||||
}),
|
||||
message: t.String({
|
||||
description: '成功消息',
|
||||
examples: ['查询用户成功'],
|
||||
}),
|
||||
data: UserInfoSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户失败响应
|
||||
*/
|
||||
export const GetUserByUsernameErrorResponse = t.Object({
|
||||
code: t.Number({
|
||||
description: '错误响应码',
|
||||
examples: [400, 401, 404],
|
||||
}),
|
||||
message: t.String({
|
||||
description: '错误消息',
|
||||
examples: ['用户不存在', '参数验证失败', '身份认证失败'],
|
||||
}),
|
||||
data: t.Null({
|
||||
description: '错误时数据字段为null',
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户接口的所有可能响应
|
||||
*/
|
||||
export const GetUserByUsernameResponses = {
|
||||
/** 200 查询成功 */
|
||||
200: GetUserByUsernameSuccessResponse,
|
||||
/** 400 业务错误 */
|
||||
400: t.Object({
|
||||
code: t.Literal(400),
|
||||
message: t.String({
|
||||
examples: ['用户不存在', '用户状态异常'],
|
||||
}),
|
||||
data: t.Null(),
|
||||
}),
|
||||
/** 401 认证失败 */
|
||||
401: t.Object({
|
||||
code: t.Literal(401),
|
||||
message: t.String({
|
||||
examples: ['身份认证失败,请重新登录', 'Token已过期'],
|
||||
}),
|
||||
data: t.Null(),
|
||||
}),
|
||||
/** 422 参数验证失败 */
|
||||
422: t.Object({
|
||||
code: t.Literal(422),
|
||||
message: t.String({
|
||||
examples: ['用户名长度必须在2-50字符之间', '用户名不能为空'],
|
||||
}),
|
||||
data: t.Null(),
|
||||
}),
|
||||
/** 500 服务器内部错误 */
|
||||
500: t.Object({
|
||||
code: t.Literal(500),
|
||||
message: t.String({
|
||||
examples: ['服务器内部错误,请稍后重试', '数据库查询失败'],
|
||||
}),
|
||||
data: t.Null(),
|
||||
}),
|
||||
};
|
79
src/modules/sample/example.schema.ts
Normal file
79
src/modules/sample/example.schema.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @file 样例接口Schema定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 样例接口的请求参数和响应数据的TypeBox schema定义
|
||||
*/
|
||||
|
||||
import { t, type Static } from 'elysia';
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户的请求参数Schema
|
||||
*/
|
||||
export const GetUserByUsernameSchema = t.Object({
|
||||
/** 用户名,必填,长度2-50字符 */
|
||||
username: t.String({
|
||||
minLength: 2,
|
||||
maxLength: 50,
|
||||
description: '用户名,用于查询用户信息',
|
||||
examples: ['admin', 'testuser', 'zhangsan'],
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 用户信息返回数据Schema
|
||||
*/
|
||||
export const UserInfoSchema = t.Object({
|
||||
/** 用户ID */
|
||||
id: t.Number({
|
||||
description: '用户唯一标识ID',
|
||||
examples: [1, 2, 100],
|
||||
}),
|
||||
/** 用户名 */
|
||||
username: t.String({
|
||||
description: '用户名',
|
||||
examples: ['admin', 'testuser'],
|
||||
}),
|
||||
/** 邮箱 */
|
||||
email: t.String({
|
||||
description: '用户邮箱',
|
||||
examples: ['admin@example.com', 'user@test.com'],
|
||||
}),
|
||||
/** 用户昵称 */
|
||||
nickname: t.Optional(t.String({
|
||||
description: '用户昵称',
|
||||
examples: ['管理员', '测试用户'],
|
||||
})),
|
||||
/** 用户头像URL */
|
||||
avatar: t.Optional(t.String({
|
||||
description: '用户头像URL',
|
||||
examples: ['https://example.com/avatar.jpg'],
|
||||
})),
|
||||
/** 用户状态:0-禁用,1-启用 */
|
||||
status: t.Number({
|
||||
description: '用户状态,0-禁用,1-启用',
|
||||
examples: [0, 1],
|
||||
}),
|
||||
/** 创建时间 */
|
||||
createdAt: t.String({
|
||||
description: '用户创建时间',
|
||||
examples: ['2024-06-29T10:30:00.000Z'],
|
||||
}),
|
||||
/** 更新时间 */
|
||||
updatedAt: t.String({
|
||||
description: '用户最后更新时间',
|
||||
examples: ['2024-06-29T10:30:00.000Z'],
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户的请求参数类型
|
||||
*/
|
||||
export type GetUserByUsernameType = Static<typeof GetUserByUsernameSchema>;
|
||||
|
||||
/**
|
||||
* 用户信息数据类型
|
||||
*/
|
||||
export type UserInfoType = Static<typeof UserInfoSchema>;
|
86
src/modules/sample/example.service.ts
Normal file
86
src/modules/sample/example.service.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @file 样例业务逻辑服务
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 样例接口的业务逻辑实现
|
||||
*
|
||||
* 设计思路:
|
||||
* 1. 接收用户名参数,对参数进行基础校验
|
||||
* 2. 使用Drizzle ORM查询数据库中的用户信息
|
||||
* 3. 处理查询结果:用户存在则返回用户信息,不存在则抛出业务异常
|
||||
* 4. 对敏感信息进行过滤,不返回密码哈希等敏感字段
|
||||
* 5. 统一异常处理,确保返回标准的错误响应格式
|
||||
* 6. 记录操作日志,便于系统监控和问题排查
|
||||
*
|
||||
* 安全考虑:
|
||||
* - 严格过滤敏感信息,不向客户端返回密码哈希
|
||||
* - 对查询参数进行SQL注入防护(Drizzle ORM自带防护)
|
||||
* - 记录查询日志,便于安全审计
|
||||
*/
|
||||
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { db } from '@/plugins/drizzle/drizzle.service';
|
||||
import { users } from '@/plugins/drizzle/schema/users';
|
||||
import { ERROR_CODES } from '@/validators/global.response';
|
||||
import { type GetUserByUsernameType } from './example.schema';
|
||||
import type { JwtUserType } from '@/type/jwt.type';
|
||||
|
||||
/**
|
||||
* 样例服务类
|
||||
* @description 提供用户相关的业务逻辑处理
|
||||
*/
|
||||
export class ExampleService {
|
||||
async findUserByUsername({ params, user }: { params: GetUserByUsernameType; user: JwtUserType }) {
|
||||
const { username } = params;
|
||||
user;
|
||||
// 使用Drizzle ORM查询用户信息
|
||||
const userList = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
nickname: users.nickname,
|
||||
avatar: users.avatar,
|
||||
status: users.status,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.username, username))
|
||||
.limit(1);
|
||||
|
||||
// 检查查询结果
|
||||
if (!userList || userList.length === 0) {
|
||||
return {
|
||||
code: 400 as const,
|
||||
message: '用户不存在',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const userInfo = userList[0];
|
||||
|
||||
// 返回成功响应
|
||||
return {
|
||||
code: ERROR_CODES.SUCCESS,
|
||||
message: '查询用户成功',
|
||||
data: {
|
||||
id: userInfo.id,
|
||||
username: userInfo.username,
|
||||
email: userInfo.email,
|
||||
nickname: userInfo.nickname || undefined,
|
||||
avatar: userInfo.avatar || undefined,
|
||||
status: userInfo.status,
|
||||
createdAt: userInfo.createdAt.toISOString(),
|
||||
updatedAt: userInfo.updatedAt.toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出样例服务实例
|
||||
*/
|
||||
export const exampleService = new ExampleService();
|
188
src/modules/sample/example.test.ts
Normal file
188
src/modules/sample/example.test.ts
Normal file
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* @file 样例接口测试用例
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 样例接口的完整测试用例,覆盖正常、异常、边界场景
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { Elysia } from 'elysia';
|
||||
import { sampleController } from './example.controller';
|
||||
import { jwtPlugin } from '@/plugins/jwt/jwt.plugins';
|
||||
|
||||
// 创建测试应用实例
|
||||
const app = new Elysia()
|
||||
.use(jwtPlugin)
|
||||
.use(sampleController);
|
||||
|
||||
// 测试用的JWT Token(需要根据实际情况生成)
|
||||
let testToken = '';
|
||||
|
||||
describe('样例接口测试', () => {
|
||||
beforeAll(async () => {
|
||||
// 在实际测试中,这里应该通过登录接口获取有效token
|
||||
// 这里为了演示,假设我们有一个有效的token
|
||||
// 创建临时的JWT实例来生成测试token
|
||||
const tempApp = new Elysia().use(jwtPlugin);
|
||||
const context = { jwt: tempApp.derive().jwt };
|
||||
|
||||
testToken = await context.jwt.sign({
|
||||
userId: 1,
|
||||
username: 'admin',
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sample/user/:username', () => {
|
||||
it('应该成功查询存在的用户', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('成功查询响应:', body);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(body.code).toBe(0);
|
||||
expect(body.message).toBe('查询用户成功');
|
||||
expect(body.data).toBeDefined();
|
||||
expect(typeof body.data.id).toBe('number');
|
||||
expect(typeof body.data.username).toBe('string');
|
||||
expect(typeof body.data.email).toBe('string');
|
||||
});
|
||||
|
||||
it('用户名过短应返回422', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/a', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('用户名过短响应:', body);
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(body.code).toBe(422);
|
||||
expect(body.message).toMatch(/用户名/);
|
||||
});
|
||||
|
||||
it('用户名过长应返回422', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/' + 'a'.repeat(51), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('用户名过长响应:', body);
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(body.code).toBe(422);
|
||||
expect(body.message).toMatch(/用户名/);
|
||||
});
|
||||
|
||||
it('用户不存在应返回400', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/nonexistentuser12345', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('用户不存在响应:', body);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(body.code).toBe(400);
|
||||
expect(body.message).toBe('用户不存在');
|
||||
expect(body.data).toBeNull();
|
||||
});
|
||||
|
||||
it('缺少Authorization头应返回401', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('缺少Authorization响应:', body);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(body.code).toBe(401);
|
||||
expect(body.message).toMatch(/Token|认证|授权/);
|
||||
});
|
||||
|
||||
it('无效Token应返回401', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer invalid_token_here',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('无效Token响应:', body);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(body.code).toBe(401);
|
||||
expect(body.message).toMatch(/Token|认证|授权/);
|
||||
});
|
||||
|
||||
it('错误的Authorization格式应返回401', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'InvalidFormat token',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('错误Authorization格式响应:', body);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(body.code).toBe(401);
|
||||
expect(body.message).toMatch(/Token|认证|授权/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sample/health', () => {
|
||||
it('应该返回模块健康状态', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/health', {
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
|
||||
const body = (await res.json()) as any;
|
||||
console.log('健康检查响应:', body);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(body.code).toBe(0);
|
||||
expect(body.message).toBe('样例模块运行正常');
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.module).toBe('sample');
|
||||
expect(body.data.status).toBe('healthy');
|
||||
expect(typeof body.data.timestamp).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
36
src/modules/tags.ts
Normal file
36
src/modules/tags.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file API文档标签定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 统一管理Swagger文档中的标签,便于接口分类和文档组织
|
||||
*/
|
||||
|
||||
/**
|
||||
* API文档标签枚举
|
||||
* @description 用于Swagger文档的接口分类
|
||||
*/
|
||||
export const tags = {
|
||||
/** 用户相关接口 */
|
||||
user: 'User',
|
||||
/** 认证相关接口 */
|
||||
auth: 'Auth',
|
||||
/** 健康检查接口 */
|
||||
health: 'Health',
|
||||
/** 测试接口 */
|
||||
test: 'Test',
|
||||
/** 样例接口 */
|
||||
sample: 'Sample',
|
||||
/** 文件上传接口 */
|
||||
upload: 'Upload',
|
||||
/** 系统管理接口 */
|
||||
system: 'System',
|
||||
/** 权限管理接口 */
|
||||
permission: 'Permission',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 标签类型定义
|
||||
*/
|
||||
export type ApiTag = typeof tags[keyof typeof tags];
|
5
src/modules/test/test.controller.ts
Normal file
5
src/modules/test/test.controller.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Elysia } from 'elysia';
|
||||
|
||||
export const testController = new Elysia({ name: 'testController' })
|
||||
.get('/', () => ({ message: '验证性接口' }))
|
||||
.get('/hello', () => ({ message: 'hello' }));
|
3
src/modules/user/user.controller.ts
Normal file
3
src/modules/user/user.controller.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Elysia } from 'elysia';
|
||||
|
||||
export const userController = new Elysia({ name: 'userController' }).get('/', () => ({ message: '用户系统' }));
|
121
src/plugins/drizzle/README.md
Normal file
121
src/plugins/drizzle/README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Drizzle ORM 插件
|
||||
|
||||
这是一个集成到 Elysia 框架的 Drizzle ORM 插件,提供类型安全的数据库操作。
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装 Drizzle ORM 核心包
|
||||
bun add drizzle-orm
|
||||
|
||||
# 安装 Drizzle Kit (开发工具)
|
||||
bun add drizzle-kit -D
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/plugins/drizzle/
|
||||
├── drizzle.plugins.ts # 主插件文件
|
||||
├── drizzle.config.ts # Drizzle Kit 配置
|
||||
├── schema/ # 数据库表结构定义
|
||||
│ ├── index.ts # Schema 总入口
|
||||
│ └── users.ts # 用户表示例
|
||||
└── README.md # 使用说明
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 在路由中使用数据库
|
||||
|
||||
```typescript
|
||||
import { Elysia } from 'elysia';
|
||||
import { users } from '@/plugins/drizzle/schema';
|
||||
|
||||
const app = new Elysia()
|
||||
.get('/users', async ({ db }) => {
|
||||
// 查询所有用户
|
||||
const allUsers = await db.select().from(users);
|
||||
return allUsers;
|
||||
})
|
||||
.get('/users/:id', async ({ db, params }) => {
|
||||
// 根据ID查询用户
|
||||
const user = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, parseInt(params.id)));
|
||||
return user[0];
|
||||
})
|
||||
.post('/users', async ({ db, body }) => {
|
||||
// 创建新用户
|
||||
const newUser = await db.insert(users).values(body);
|
||||
return newUser;
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 定义新的表结构
|
||||
|
||||
在 `schema/` 目录下创建新的表文件:
|
||||
|
||||
```typescript
|
||||
// schema/posts.ts
|
||||
import { mysqlTable, int, varchar, text, timestamp } from 'drizzle-orm/mysql-core';
|
||||
import { users } from './users';
|
||||
|
||||
export const posts = mysqlTable('posts', {
|
||||
id: int('id').primaryKey().autoincrement(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
content: text('content'),
|
||||
authorId: int('author_id').references(() => users.id),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
|
||||
});
|
||||
|
||||
export type Post = typeof posts.$inferSelect;
|
||||
export type InsertPost = typeof posts.$inferInsert;
|
||||
```
|
||||
|
||||
然后在 `schema/index.ts` 中导出:
|
||||
|
||||
```typescript
|
||||
export * from './posts';
|
||||
```
|
||||
|
||||
### 3. 生成和运行数据库迁移
|
||||
|
||||
```bash
|
||||
# 生成迁移文件
|
||||
bun drizzle-kit generate
|
||||
|
||||
# 推送迁移到数据库
|
||||
bun drizzle-kit push
|
||||
|
||||
# 查看数据库状态
|
||||
bun drizzle-kit studio
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
- **数据库连接**: 自动从 `@/config` 读取数据库配置
|
||||
- **连接池**: 默认最大连接数为 10
|
||||
- **日志**: 启用 SQL 查询日志
|
||||
- **迁移**: 迁移文件输出到 `./drizzle` 目录
|
||||
|
||||
## 类型支持
|
||||
|
||||
插件提供完整的 TypeScript 类型支持:
|
||||
|
||||
- `DrizzleDB`: 数据库实例类型
|
||||
- `User`, `InsertUser`: 用户表相关类型
|
||||
- `DrizzleContext`: Elysia 上下文扩展类型
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保数据库配置正确且数据库服务已启动
|
||||
2. 生产环境建议使用环境变量管理数据库凭据
|
||||
3. 定期备份数据库,特别是在运行迁移之前
|
||||
4. 使用 Drizzle Studio 可视化管理数据库
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [Drizzle ORM 官方文档](https://orm.drizzle.team/)
|
||||
- [Drizzle Kit 文档](https://orm.drizzle.team/kit-docs/overview)
|
40
src/plugins/drizzle/drizzle.config.ts
Normal file
40
src/plugins/drizzle/drizzle.config.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @file Drizzle配置文件
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description Drizzle Kit配置,用于数据库迁移和代码生成
|
||||
*/
|
||||
|
||||
import { dbConfig } from '@/config';
|
||||
|
||||
/**
|
||||
* Drizzle Kit配置对象
|
||||
* 使用前需要安装: bun add drizzle-kit -D
|
||||
*/
|
||||
export default {
|
||||
/** 数据库类型 */
|
||||
dialect: 'mysql',
|
||||
|
||||
/** 数据库连接配置 */
|
||||
dbCredentials: {
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database,
|
||||
},
|
||||
|
||||
/** Schema文件路径 */
|
||||
schema: './src/plugins/drizzle/schema/*',
|
||||
|
||||
/** 迁移文件输出目录 */
|
||||
out: './drizzle',
|
||||
|
||||
/** 详细日志 */
|
||||
verbose: true,
|
||||
|
||||
/** 严格模式 */
|
||||
strict: true,
|
||||
} as const;
|
24
src/plugins/drizzle/drizzle.plugins.ts
Normal file
24
src/plugins/drizzle/drizzle.plugins.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @file Drizzle ORM 数据库插件
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 集成Drizzle ORM到Elysia,提供类型安全的数据库操作
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import * as schema from './schema';
|
||||
import { createDrizzleDB } from './drizzle.service';
|
||||
|
||||
/**
|
||||
* Drizzle ORM 插件
|
||||
* 提供类型安全的数据库操作接口
|
||||
*/
|
||||
export const drizzlePlugin = new Elysia({ name: 'drizzle' }).onStart(async () => {
|
||||
await createDrizzleDB();
|
||||
});
|
||||
|
||||
/** 导出数据库类型,供其他模块使用 */
|
||||
export type DB = typeof schema;
|
||||
export type DrizzleDB = Awaited<ReturnType<typeof createDrizzleDB>>;
|
61
src/plugins/drizzle/drizzle.service.ts
Normal file
61
src/plugins/drizzle/drizzle.service.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { dbConfig } from '@/config';
|
||||
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import * as schema from './schema';
|
||||
import type { DrizzleDB } from '@/types/drizzle.type';
|
||||
|
||||
export let db: DrizzleDB;
|
||||
/**
|
||||
* 创建MySQL连接池
|
||||
*/
|
||||
const createConnection = async () => {
|
||||
try {
|
||||
/** MySQL连接池配置 */
|
||||
const connection = mysql.createPool({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database,
|
||||
/** 连接池配置 */
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
|
||||
Logger.info('MySQL连接池创建成功');
|
||||
return connection;
|
||||
} catch (error) {
|
||||
Logger.error(error as Error);
|
||||
throw new Error('MySQL连接池创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建Drizzle数据库实例
|
||||
*/
|
||||
export const createDrizzleDB = async () => {
|
||||
try {
|
||||
const connection = await createConnection();
|
||||
|
||||
/** Drizzle数据库实例 */
|
||||
db = drizzle(connection, {
|
||||
schema,
|
||||
mode: 'default',
|
||||
logger: {
|
||||
logQuery: (query, params) => {
|
||||
Logger.debug(
|
||||
`SQL: ${query} - Params: ${JSON.stringify(params)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Logger.info('Drizzle ORM 初始化成功');
|
||||
return db;
|
||||
} catch (error) {
|
||||
Logger.error(error as Error);
|
||||
throw new Error('Drizzle ORM 初始化失败');
|
||||
}
|
||||
};
|
15
src/plugins/drizzle/schema/index.ts
Normal file
15
src/plugins/drizzle/schema/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @file 数据库Schema总入口
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 导出所有数据库表的schema定义
|
||||
*/
|
||||
|
||||
// 导出用户表schema
|
||||
export * from './users';
|
||||
|
||||
// 其他表schema示例
|
||||
// export * from './posts';
|
||||
// export * from './comments';
|
48
src/plugins/drizzle/schema/users.ts
Normal file
48
src/plugins/drizzle/schema/users.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @file 用户表Schema定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 用户表的Drizzle ORM schema定义
|
||||
*/
|
||||
|
||||
import { mysqlTable, int, varchar, timestamp, text, tinyint } from 'drizzle-orm/mysql-core';
|
||||
|
||||
/**
|
||||
* 用户表
|
||||
*/
|
||||
export const users = mysqlTable('users', {
|
||||
/** 用户ID,主键,自增 */
|
||||
id: int('id').primaryKey().autoincrement(),
|
||||
|
||||
/** 用户名,唯一索引 */
|
||||
username: varchar('username', { length: 50 }).notNull().unique(),
|
||||
|
||||
/** 邮箱,唯一索引 */
|
||||
email: varchar('email', { length: 100 }).notNull().unique(),
|
||||
|
||||
/** 密码哈希 */
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||
|
||||
/** 用户昵称 */
|
||||
nickname: varchar('nickname', { length: 50 }),
|
||||
|
||||
/** 用户头像URL */
|
||||
avatar: text('avatar'),
|
||||
|
||||
/** 用户状态:0-禁用,1-启用 */
|
||||
status: tinyint('status').default(1).notNull(),
|
||||
|
||||
/** 创建时间 */
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
|
||||
/** 更新时间 */
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
|
||||
});
|
||||
|
||||
/** 用户表类型 */
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
/** 插入用户类型 */
|
||||
export type InsertUser = typeof users.$inferInsert;
|
@ -8,19 +8,17 @@
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { ENV } from '@/config';
|
||||
const isDevelopment = ENV === 'development';
|
||||
|
||||
/**
|
||||
* 全局错误处理插件
|
||||
*/
|
||||
export const errorHandlerPlugin = (app: Elysia) =>
|
||||
app.onError(({ error, set, code, log }) => {
|
||||
app.onError(({ error, set, code, log, env }) => {
|
||||
switch (code) {
|
||||
case 'VALIDATION': {
|
||||
set.status = 400;
|
||||
let errors = null as any;
|
||||
if (isDevelopment) {
|
||||
if (env === 'development') {
|
||||
errors = error.all.map((err) => ({
|
||||
field: err.path?.slice(1) || 'root',
|
||||
message: err.message,
|
||||
@ -66,7 +64,7 @@ export const errorHandlerPlugin = (app: Elysia) =>
|
||||
set.status = error.code;
|
||||
return {
|
||||
code: error.code,
|
||||
message: error.response.message || '服务器内部错误',
|
||||
message: error.response.message || error.response || '服务器内部错误',
|
||||
errors: error,
|
||||
};
|
||||
}
|
25
src/plugins/index.ts
Normal file
25
src/plugins/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file Plugins 插件总入口
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 所有插件的统一入口,使用 group 进行模块化管理
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { loggerPlugin } from '@/plugins/logger/logger.plugins';
|
||||
import { errorHandlerPlugin } from '@/plugins/errorHandle/errorHandler.plugins';
|
||||
import { swaggerPlugin } from '@/plugins/swagger/swagger.plugins';
|
||||
import { drizzlePlugin } from '@/plugins/drizzle/drizzle.plugins';
|
||||
|
||||
export const plugins = (app: Elysia) =>
|
||||
app
|
||||
// 日志插件
|
||||
.use(loggerPlugin)
|
||||
// 错误处理插件
|
||||
.use(errorHandlerPlugin)
|
||||
// 数据库插件
|
||||
.use(drizzlePlugin)
|
||||
// API 文档插件
|
||||
.use(swaggerPlugin);
|
@ -3,12 +3,13 @@
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 封装Elysia JWT插件,统一配置密钥
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 封装Elysia JWT插件,统一配置密钥,提供类型安全的JWT认证
|
||||
*/
|
||||
import { Elysia } from 'elysia';
|
||||
import { jwt } from '@elysiajs/jwt';
|
||||
import { jwtConfig } from '@/config/jwt.config';
|
||||
import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type';
|
||||
|
||||
export const jwtPlugin = jwt({
|
||||
name: 'jwt',
|
||||
@ -26,11 +27,22 @@ export const jwtAuthPlugin = (app: Elysia) =>
|
||||
}
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
try {
|
||||
const user = await jwt.verify(token);
|
||||
if (!user) return status(401, 'Token无效');
|
||||
return { user };
|
||||
const payload = await jwt.verify(token) as JwtPayloadType | false;
|
||||
if (!payload) return status(401, 'Token无效');
|
||||
|
||||
// 提取用户信息
|
||||
const user: JwtUserType = {
|
||||
userId: payload.userId,
|
||||
username: payload.username,
|
||||
email: payload.email,
|
||||
nickname: payload.nickname,
|
||||
status: payload.status,
|
||||
role: payload.role,
|
||||
};
|
||||
|
||||
return { user } as const;
|
||||
} catch {
|
||||
return {};
|
||||
return status(401, 'Token无效');
|
||||
}
|
||||
})
|
||||
.onBeforeHandle(({ user, status }) => {
|
177
src/plugins/jwt/jwt.service.ts
Normal file
177
src/plugins/jwt/jwt.service.ts
Normal file
@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @file JWT服务类
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 提供类型安全的JWT生成、验证和管理功能
|
||||
*/
|
||||
|
||||
import { jwtConfig } from '@/config/jwt.config';
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import type {
|
||||
JwtUserType,
|
||||
JwtPayloadType,
|
||||
JwtSignOptionsType,
|
||||
} from '@/type/jwt.type';
|
||||
import type { UserInfoType } from '@/modules/sample/example.schema';
|
||||
|
||||
/**
|
||||
* JWT服务类
|
||||
* @description 提供JWT Token的生成、验证、刷新等功能
|
||||
*/
|
||||
export class JwtService {
|
||||
/**
|
||||
* 生成JWT Token
|
||||
* @param userInfo 完整的用户信息
|
||||
* @param options 可选的JWT配置
|
||||
* @returns Promise<string> JWT Token字符串
|
||||
* @modification hotok 2025-06-29 初始实现JWT生成功能
|
||||
*/
|
||||
async generateToken(userInfo: UserInfoType, options?: Partial<JwtSignOptionsType>): Promise<string> {
|
||||
try {
|
||||
// 从完整用户信息提取JWT载荷所需的字段
|
||||
const jwtUser: JwtUserType = {
|
||||
userId: userInfo.id,
|
||||
username: userInfo.username,
|
||||
email: userInfo.email,
|
||||
nickname: userInfo.nickname,
|
||||
status: userInfo.status,
|
||||
role: options?.user?.role, // 如果有传入角色信息
|
||||
};
|
||||
|
||||
// 构建JWT载荷
|
||||
const payload: Omit<JwtPayloadType, 'iat' | 'exp'> = {
|
||||
...jwtUser,
|
||||
sub: userInfo.id.toString(),
|
||||
iss: options?.issuer || 'elysia-api',
|
||||
aud: options?.audience || 'web-client',
|
||||
};
|
||||
|
||||
// 注意:实际的token生成需要使用Elysia的jwt实例
|
||||
// 这里提供接口定义,具体实现需要在controller中使用jwt.sign
|
||||
const token = 'generated-token-placeholder';
|
||||
|
||||
Logger.info(`JWT Token生成成功,用户ID: ${userInfo.id}, 用户名: ${userInfo.username}`);
|
||||
return token;
|
||||
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`JWT Token生成失败: ${error}`));
|
||||
throw new Error('Token生成失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT Token
|
||||
* @param token JWT Token字符串
|
||||
* @returns Promise<JwtPayloadType | null> 验证成功返回载荷,失败返回null
|
||||
* @modification hotok 2025-06-29 初始实现JWT验证功能
|
||||
*/
|
||||
async verifyToken(token: string): Promise<JwtPayloadType | null> {
|
||||
try {
|
||||
// 注意:实际的token验证需要使用Elysia的jwt实例
|
||||
// 这里提供接口定义,具体实现需要在controller中使用jwt.verify
|
||||
const payload = null as any as JwtPayloadType;
|
||||
|
||||
if (!payload || !payload.userId) {
|
||||
Logger.warn(`JWT Token验证失败:载荷无效`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查Token是否过期
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp && payload.exp < now) {
|
||||
Logger.warn(`JWT Token已过期,用户ID: ${payload.userId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.info(`JWT Token验证成功,用户ID: ${payload.userId}`);
|
||||
return payload;
|
||||
|
||||
} catch (error) {
|
||||
Logger.warn(`JWT Token验证失败: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新JWT Token
|
||||
* @param oldToken 旧的JWT Token
|
||||
* @param userInfo 最新的用户信息
|
||||
* @returns Promise<string | null> 新的JWT Token,失败返回null
|
||||
* @modification hotok 2025-06-29 初始实现JWT刷新功能
|
||||
*/
|
||||
async refreshToken(oldToken: string, userInfo: UserInfoType): Promise<string | null> {
|
||||
try {
|
||||
// 验证旧Token
|
||||
const oldPayload = await this.verifyToken(oldToken);
|
||||
if (!oldPayload) {
|
||||
Logger.warn('Token刷新失败:旧Token无效');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成新Token
|
||||
const newToken = await this.generateToken(userInfo);
|
||||
|
||||
Logger.info(`JWT Token刷新成功,用户ID: ${userInfo.id}`);
|
||||
return newToken;
|
||||
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`JWT Token刷新失败: ${error}`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取JWT载荷中的用户信息
|
||||
* @param payload JWT载荷
|
||||
* @returns JwtUserType 用户信息
|
||||
* @modification hotok 2025-06-29 添加用户信息提取功能
|
||||
*/
|
||||
extractUserFromPayload(payload: JwtPayloadType): JwtUserType {
|
||||
return {
|
||||
userId: payload.userId,
|
||||
username: payload.username,
|
||||
email: payload.email,
|
||||
nickname: payload.nickname,
|
||||
status: payload.status,
|
||||
role: payload.role,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户Token是否即将过期
|
||||
* @param payload JWT载荷
|
||||
* @param thresholdMinutes 阈值分钟数(默认30分钟)
|
||||
* @returns boolean 是否即将过期
|
||||
* @modification hotok 2025-06-29 添加Token过期检查功能
|
||||
*/
|
||||
isTokenExpiringSoon(payload: JwtPayloadType, thresholdMinutes: number = 30): boolean {
|
||||
if (!payload.exp) return false;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const threshold = thresholdMinutes * 60; // 转换为秒
|
||||
|
||||
return (payload.exp - now) <= threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token剩余有效时间
|
||||
* @param payload JWT载荷
|
||||
* @returns number 剩余秒数,-1表示已过期
|
||||
* @modification hotok 2025-06-29 添加Token时间计算功能
|
||||
*/
|
||||
getTokenRemainingTime(payload: JwtPayloadType): number {
|
||||
if (!payload.exp) return -1;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const remaining = payload.exp - now;
|
||||
|
||||
return remaining > 0 ? remaining : -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出JWT服务实例
|
||||
*/
|
||||
export const jwtService = new JwtService();
|
@ -1,95 +0,0 @@
|
||||
/**
|
||||
* @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;
|
61
src/plugins/logger/logger.plugins.ts
Normal file
61
src/plugins/logger/logger.plugins.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @file HTTP请求日志插件
|
||||
* @author hotok
|
||||
* @date 2024-01-15
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2024-01-15
|
||||
* @description 记录HTTP请求和响应的详细信息,包括请求时间、响应时间、客户端信息等
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { browserInfo } from '@/utils/deviceInfo';
|
||||
import { getRandomBackgroundColor } from '@/utils/randomChalk';
|
||||
import Logger, { getResponseSize } from '@/plugins/logger/logger.service';
|
||||
|
||||
/**
|
||||
* HTTP请求日志插件
|
||||
* @description 为Elysia应用添加请求和响应日志功能
|
||||
* @param app Elysia应用实例
|
||||
* @returns 配置了日志功能的Elysia应用
|
||||
* @example
|
||||
* const app = new Elysia().use(loggerPlugin)
|
||||
*/
|
||||
export const loggerPlugin = (app: Elysia) =>
|
||||
app
|
||||
/** 注册请求开始时间到应用状态,用于计算响应时间 */
|
||||
.state('requestStart', null as [number, number] | null)
|
||||
.state('color', null as string | null)
|
||||
/** 请求拦截器 - 记录请求信息 */
|
||||
.onRequest(({ store, request, server, path,}) => {
|
||||
/** 记录请求开始时间 */
|
||||
store.requestStart = process.hrtime();
|
||||
/** 获取客户端IP信息 */
|
||||
const clientIP = server?.requestIP(request);
|
||||
store.color = getRandomBackgroundColor()(' ');
|
||||
/** 记录请求日志 */
|
||||
Logger.http({
|
||||
type: 'request',
|
||||
method: request.method,
|
||||
path,
|
||||
color: store.color,
|
||||
ip: `${clientIP?.family}: ${clientIP?.address}:${clientIP?.port}`,
|
||||
browser: browserInfo(request.headers.get('user-agent')),
|
||||
});
|
||||
})
|
||||
/** 响应拦截器 - 记录响应信息 */
|
||||
.onAfterResponse(({ store, request, set, response, path }) => {
|
||||
/** 计算请求处理时间 */
|
||||
const [seconds, nanoseconds] = process.hrtime(store.requestStart);
|
||||
const duration = seconds * 1000 + nanoseconds / 1000000;
|
||||
|
||||
/** 记录响应日志 */
|
||||
Logger.http({
|
||||
type: 'response',
|
||||
method: request.method,
|
||||
path,
|
||||
color: store.color,
|
||||
statusCode: set.status || 200,
|
||||
requestTime: `${duration.toFixed(2)}ms`,
|
||||
responseSize: getResponseSize(response),
|
||||
});
|
||||
});
|
@ -12,6 +12,7 @@ import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { loggerConfig } from '@/config/logger.config';
|
||||
import chalk from 'chalk';
|
||||
import { centerText } from '@/utils/text';
|
||||
import { formatFileSize } from '@/utils/formatFileSize';
|
||||
|
||||
/**
|
||||
* 日志等级颜色格式化方法
|
||||
@ -194,37 +195,18 @@ const formatMessage = (message: string | object): string => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 简单日志器类
|
||||
* 日志记录器类
|
||||
*/
|
||||
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,
|
||||
@ -233,23 +215,40 @@ export class Logger {
|
||||
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 function getResponseSize(response: unknown) {
|
||||
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 Logger;
|
@ -10,4 +10,3 @@
|
||||
import { app } from './app';
|
||||
|
||||
app.listen(3000);
|
||||
console.log('🚀 服务已启动:http://localhost:3000');
|
||||
|
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @file 用户认证业务逻辑
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 登录业务逻辑,供controller调用
|
||||
*/
|
||||
|
||||
/**
|
||||
* 登录业务逻辑
|
||||
* @param body 登录请求体
|
||||
* @param jwt JWT插件实例
|
||||
* @param set Elysia set对象
|
||||
* @returns 登录响应对象
|
||||
*/
|
||||
export const loginService = async (body: { username: string; password: string }, jwt: any, set: any) => {
|
||||
const { username, password } = body;
|
||||
if (username !== 'admin' || password !== '123456') {
|
||||
set.status = 400;
|
||||
return {
|
||||
code: 400,
|
||||
message: '用户名或密码错误',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
const token = await jwt.sign({ username });
|
||||
return {
|
||||
code: 0,
|
||||
message: '登录成功',
|
||||
data: { token },
|
||||
};
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* @file 受保护接口业务逻辑
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 受保护接口业务逻辑,供controller调用
|
||||
*/
|
||||
|
||||
/**
|
||||
* 受保护接口业务逻辑
|
||||
* @param store Elysia store对象
|
||||
* @returns 受保护接口响应对象
|
||||
*/
|
||||
export const protectedService = (user: any) => {
|
||||
/**
|
||||
* @type {any} user - JWT解码后的用户信息
|
||||
* @description 由jwtAuthPlugin中间件注入
|
||||
*/
|
||||
console.log('user', user);
|
||||
return {
|
||||
code: 0,
|
||||
message: '受保护资源访问成功',
|
||||
data: { username: user?.username ?? '' },
|
||||
};
|
||||
};
|
@ -1,122 +0,0 @@
|
||||
/**
|
||||
* @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;
|
||||
};
|
||||
}
|
74
src/type/jwt.type.ts
Normal file
74
src/type/jwt.type.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @file JWT类型定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description JWT Token载荷和用户信息的TypeScript类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* JWT Token中的用户信息类型
|
||||
* @description 存储在JWT Token中的用户基本信息,不包含敏感数据
|
||||
*/
|
||||
export interface JwtUserType {
|
||||
/** 用户ID */
|
||||
userId: number;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 用户邮箱 */
|
||||
email: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 用户状态:0-禁用,1-启用 */
|
||||
status: number;
|
||||
/** 用户角色(可选,用于权限控制) */
|
||||
role?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整的JWT载荷类型
|
||||
* @description JWT Token的完整载荷,包含用户信息和JWT标准字段
|
||||
*/
|
||||
export interface JwtPayloadType extends JwtUserType {
|
||||
/** Token发行时间(秒级时间戳) */
|
||||
iat: number;
|
||||
/** Token过期时间(秒级时间戳) */
|
||||
exp: number;
|
||||
/** Token主题,通常是用户ID */
|
||||
sub?: string;
|
||||
/** Token发行者 */
|
||||
iss?: string;
|
||||
/** Token受众 */
|
||||
aud?: string;
|
||||
/** JWT ID */
|
||||
jti?: string;
|
||||
/** Token生效时间(秒级时间戳) */
|
||||
nbf?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT认证上下文类型
|
||||
* @description 在认证中间件中使用的用户上下文类型
|
||||
*/
|
||||
export interface JwtContextType {
|
||||
/** 当前认证用户信息 */
|
||||
user: JwtUserType;
|
||||
/** 原始JWT载荷 */
|
||||
payload?: JwtPayloadType;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT生成参数类型
|
||||
* @description 生成JWT Token时的参数类型
|
||||
*/
|
||||
export interface JwtSignOptionsType {
|
||||
/** 用户信息 */
|
||||
user: JwtUserType;
|
||||
/** 自定义过期时间(可选) */
|
||||
expiresIn?: string;
|
||||
/** 自定义发行者(可选) */
|
||||
issuer?: string;
|
||||
/** 自定义受众(可选) */
|
||||
audience?: string;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 日志配置接口
|
||||
* @interface LogConfig
|
||||
*/
|
||||
export interface LogConfigType {
|
||||
/** 日志文件目录 */
|
||||
directory: string;
|
||||
/** 是否输出到控制台 */
|
||||
console: boolean;
|
||||
/** 单个日志文件最大大小 */
|
||||
maxSize: string;
|
||||
/** 最大保留文件数 */
|
||||
maxFiles: string;
|
||||
/** 日志等级 */
|
||||
level: string;
|
||||
}
|
5
src/utils/formatRoute.ts
Normal file
5
src/utils/formatRoute.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
export const formatRoute = (router: any) => {
|
||||
return router.map((route: any) => chalk.green.bold(route.method.padEnd(6, ' ')) + ' ' + route.path).join('\n');
|
||||
};
|
138
src/utils/jwt.helper.ts
Normal file
138
src/utils/jwt.helper.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @file JWT辅助工具
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 提供JWT相关的辅助函数,用于Controller中的JWT操作
|
||||
*/
|
||||
|
||||
import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type';
|
||||
import type { UserInfoType } from '@/modules/sample/example.schema';
|
||||
|
||||
/**
|
||||
* 从完整用户信息创建JWT用户信息
|
||||
* @param userInfo 完整的用户信息
|
||||
* @param role 可选的用户角色
|
||||
* @returns JwtUserType JWT中的用户信息
|
||||
* @modification hotok 2025-06-29 创建JWT用户信息转换函数
|
||||
*/
|
||||
export function createJwtUser(userInfo: UserInfoType, role?: string): JwtUserType {
|
||||
return {
|
||||
userId: userInfo.id,
|
||||
username: userInfo.username,
|
||||
email: userInfo.email,
|
||||
nickname: userInfo.nickname,
|
||||
status: userInfo.status,
|
||||
role: role,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建JWT载荷
|
||||
* @param userInfo 完整的用户信息
|
||||
* @param options 可选配置
|
||||
* @returns JWT载荷对象(不包含iat、exp等自动生成字段)
|
||||
* @modification hotok 2025-06-29 创建JWT载荷生成函数
|
||||
*/
|
||||
export function createJwtPayload(
|
||||
userInfo: UserInfoType,
|
||||
options?: {
|
||||
role?: string;
|
||||
issuer?: string;
|
||||
audience?: string;
|
||||
subject?: string;
|
||||
},
|
||||
): Omit<JwtPayloadType, 'iat' | 'exp'> {
|
||||
const jwtUser = createJwtUser(userInfo, options?.role);
|
||||
|
||||
return {
|
||||
...jwtUser,
|
||||
sub: options?.subject || userInfo.id.toString(),
|
||||
iss: options?.issuer || 'elysia-api',
|
||||
aud: options?.audience || 'web-client',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT载荷中的用户是否有效
|
||||
* @param payload JWT载荷
|
||||
* @returns boolean 用户是否有效
|
||||
* @modification hotok 2025-06-29 添加用户有效性验证
|
||||
*/
|
||||
export function isValidJwtUser(payload: JwtPayloadType): boolean {
|
||||
// 检查必需字段
|
||||
if (!payload.userId || !payload.username || !payload.email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查用户状态(1为启用)
|
||||
if (payload.status !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查JWT是否即将过期
|
||||
* @param payload JWT载荷
|
||||
* @param thresholdMinutes 阈值分钟数(默认30分钟)
|
||||
* @returns boolean 是否即将过期
|
||||
* @modification hotok 2025-06-29 添加过期检查功能
|
||||
*/
|
||||
export function isTokenExpiringSoon(payload: JwtPayloadType, thresholdMinutes: number = 30): boolean {
|
||||
if (!payload.exp) return false;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const threshold = thresholdMinutes * 60;
|
||||
|
||||
return (payload.exp - now) <= threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token剩余有效时间
|
||||
* @param payload JWT载荷
|
||||
* @returns number 剩余秒数,-1表示已过期或无过期时间
|
||||
* @modification hotok 2025-06-29 添加时间计算功能
|
||||
*/
|
||||
export function getTokenRemainingTime(payload: JwtPayloadType): number {
|
||||
if (!payload.exp) return -1;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const remaining = payload.exp - now;
|
||||
|
||||
return remaining > 0 ? remaining : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化Token剩余时间为可读字符串
|
||||
* @param seconds 剩余秒数
|
||||
* @returns string 格式化的时间字符串
|
||||
* @modification hotok 2025-06-29 添加时间格式化功能
|
||||
*/
|
||||
export function formatRemainingTime(seconds: number): string {
|
||||
if (seconds <= 0) return '已过期';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟${remainingSeconds}秒`;
|
||||
} else {
|
||||
return `${remainingSeconds}秒`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT用户信息的简化版本(用于日志记录)
|
||||
* @param user JWT用户信息
|
||||
* @returns string 简化的用户信息字符串
|
||||
* @modification hotok 2025-06-29 添加用户信息格式化功能
|
||||
*/
|
||||
export function formatUserForLog(user: JwtUserType): string {
|
||||
return `用户ID:${user.userId} 用户名:${user.username} 状态:${user.status === 1 ? '启用' : '禁用'}`;
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { createClient, type RedisClientType } from 'redis';
|
||||
import { redisConfig, getRedisUrl } from '@/config/redis.config';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
|
||||
/**
|
||||
* Redis客户端实例
|
||||
|
@ -1,181 +0,0 @@
|
||||
/**
|
||||
* @file 健康检查响应验证器
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 健康检查接口响应数据结构验证
|
||||
*/
|
||||
|
||||
import { t } from 'elysia';
|
||||
|
||||
/**
|
||||
* 组件状态验证器
|
||||
*/
|
||||
const componentStatusSchema = t.Object({
|
||||
/** 组件状态 */
|
||||
status: t.Union([t.Literal('healthy'), t.Literal('unhealthy'), t.Literal('degraded')]),
|
||||
/** 响应时间(毫秒) */
|
||||
responseTime: t.Optional(t.Number()),
|
||||
/** 错误信息 */
|
||||
error: t.Optional(t.String()),
|
||||
/** 详细信息 */
|
||||
details: t.Optional(t.Record(t.String(), t.Any())),
|
||||
});
|
||||
|
||||
/**
|
||||
* 系统信息验证器
|
||||
*/
|
||||
const systemInfoSchema = t.Object({
|
||||
/** 操作系统平台 */
|
||||
platform: t.String(),
|
||||
/** 系统架构 */
|
||||
arch: t.String(),
|
||||
/** Node.js版本 */
|
||||
nodeVersion: t.String(),
|
||||
/** 运行时 */
|
||||
runtime: t.String(),
|
||||
/** 进程ID */
|
||||
pid: t.Number(),
|
||||
/** 当前工作目录 */
|
||||
cwd: t.String(),
|
||||
});
|
||||
|
||||
/**
|
||||
* 性能指标验证器
|
||||
*/
|
||||
const performanceMetricsSchema = t.Object({
|
||||
/** CPU使用情况 */
|
||||
cpuUsage: t.Object({
|
||||
user: t.Number(),
|
||||
system: t.Number(),
|
||||
}),
|
||||
/** 内存使用情况 */
|
||||
memoryUsage: t.Object({
|
||||
rss: t.Number(),
|
||||
heapTotal: t.Number(),
|
||||
heapUsed: t.Number(),
|
||||
external: t.Number(),
|
||||
arrayBuffers: t.Number(),
|
||||
}),
|
||||
/** 运行时间(秒) */
|
||||
uptime: t.Number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* 基本健康状态响应验证器
|
||||
*/
|
||||
const basicHealthDataSchema = t.Object({
|
||||
/** 系统整体状态 */
|
||||
status: t.Union([t.Literal('healthy'), t.Literal('unhealthy'), t.Literal('degraded')]),
|
||||
/** 时间戳 */
|
||||
timestamp: t.String(),
|
||||
/** 系统运行时间(秒) */
|
||||
uptime: t.Number(),
|
||||
/** 响应时间(毫秒) */
|
||||
responseTime: t.Number(),
|
||||
/** 版本号 */
|
||||
version: t.String(),
|
||||
/** 环境 */
|
||||
environment: t.String(),
|
||||
/** 错误信息(仅在异常时) */
|
||||
error: t.Optional(t.String()),
|
||||
/** 各组件状态 */
|
||||
components: t.Object({
|
||||
/** MySQL数据库状态 */
|
||||
mysql: t.Optional(componentStatusSchema),
|
||||
/** Redis缓存状态 */
|
||||
redis: t.Optional(componentStatusSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 详细健康状态数据验证器
|
||||
*/
|
||||
const detailedHealthDataSchema = t.Intersect([
|
||||
basicHealthDataSchema,
|
||||
t.Object({
|
||||
/** 系统信息 */
|
||||
system: t.Optional(systemInfoSchema),
|
||||
/** 性能指标 */
|
||||
performance: t.Optional(performanceMetricsSchema),
|
||||
}),
|
||||
]);
|
||||
|
||||
/**
|
||||
* 健康检查响应验证器
|
||||
*/
|
||||
export const healthResponse = {
|
||||
200: t.Object({
|
||||
/** 响应码 */
|
||||
code: t.Number(),
|
||||
/** 响应消息 */
|
||||
message: t.String(),
|
||||
/** 健康状态数据 */
|
||||
data: basicHealthDataSchema,
|
||||
}),
|
||||
500: t.Object({
|
||||
/** 响应码 */
|
||||
code: t.Number(),
|
||||
/** 响应消息 */
|
||||
message: t.String(),
|
||||
/** 错误数据 */
|
||||
data: t.Object({
|
||||
/** 系统整体状态 */
|
||||
status: t.Literal('unhealthy'),
|
||||
/** 时间戳 */
|
||||
timestamp: t.String(),
|
||||
/** 系统运行时间(秒) */
|
||||
uptime: t.Number(),
|
||||
/** 响应时间(毫秒) */
|
||||
responseTime: t.Number(),
|
||||
/** 版本号 */
|
||||
version: t.String(),
|
||||
/** 环境 */
|
||||
environment: t.String(),
|
||||
/** 错误信息 */
|
||||
error: t.String(),
|
||||
/** 各组件状态 */
|
||||
components: t.Object({}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* 详细健康检查响应验证器
|
||||
*/
|
||||
export const detailedHealthResponse = {
|
||||
200: t.Object({
|
||||
/** 响应码 */
|
||||
code: t.Number(),
|
||||
/** 响应消息 */
|
||||
message: t.String(),
|
||||
/** 详细健康状态数据 */
|
||||
data: detailedHealthDataSchema,
|
||||
}),
|
||||
500: t.Object({
|
||||
/** 响应码 */
|
||||
code: t.Number(),
|
||||
/** 响应消息 */
|
||||
message: t.String(),
|
||||
/** 错误数据 */
|
||||
data: t.Object({
|
||||
/** 系统整体状态 */
|
||||
status: t.Literal('unhealthy'),
|
||||
/** 时间戳 */
|
||||
timestamp: t.String(),
|
||||
/** 系统运行时间(秒) */
|
||||
uptime: t.Number(),
|
||||
/** 响应时间(毫秒) */
|
||||
responseTime: t.Number(),
|
||||
/** 版本号 */
|
||||
version: t.String(),
|
||||
/** 环境 */
|
||||
environment: t.String(),
|
||||
/** 错误信息 */
|
||||
error: t.String(),
|
||||
/** 各组件状态 */
|
||||
components: t.Object({}),
|
||||
}),
|
||||
}),
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* @file 登录接口响应schema
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 登录接口响应结构定义
|
||||
*/
|
||||
|
||||
import { t } from 'elysia';
|
||||
|
||||
/** 登录成功响应schema */
|
||||
export const loginResponse200Schema = t.Object({
|
||||
code: t.Literal(0),
|
||||
message: t.String(),
|
||||
data: t.Object({ token: t.String() }),
|
||||
});
|
||||
|
||||
/** 登录失败响应schema */
|
||||
export const loginResponse400Schema = t.Object({
|
||||
code: t.Literal(400),
|
||||
message: t.String(),
|
||||
data: t.Null(),
|
||||
});
|
@ -1,23 +0,0 @@
|
||||
/**
|
||||
* @file 用户认证参数校验规则
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 登录接口参数校验规则,含中文错误提示
|
||||
*/
|
||||
|
||||
import { t } from 'elysia';
|
||||
import type { Static } from 'elysia';
|
||||
|
||||
/**
|
||||
* 登录请求参数校验规则
|
||||
* @property {string} username - 用户名,最少3位
|
||||
* @property {string} password - 密码,最少6位
|
||||
*/
|
||||
export const loginBodySchema = t.Object({
|
||||
username: t.String({ minLength: 2, maxLength: 16 }),
|
||||
password: t.String({ minLength: 6, maxLength: 32 }),
|
||||
});
|
||||
|
||||
export type LoginBody = Static<typeof loginBodySchema>;
|
@ -1,42 +0,0 @@
|
||||
/**
|
||||
* @file 受保护接口响应校验规则
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 受保护接口的响应类型定义,含成功和未授权两种情况
|
||||
*/
|
||||
|
||||
import { t } from 'elysia';
|
||||
import type { Static } from 'elysia';
|
||||
|
||||
/**
|
||||
* 受保护接口 200 响应 schema
|
||||
*/
|
||||
export const protectedResponse200Schema = t.Object({
|
||||
code: t.Literal(0),
|
||||
message: t.String(),
|
||||
data: t.Object({
|
||||
username: t.String(),
|
||||
// 可根据实际业务扩展字段
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 受保护接口 401 响应 schema
|
||||
*/
|
||||
export const protectedResponse401Schema = t.Object({
|
||||
code: t.Literal(401),
|
||||
message: t.String(),
|
||||
data: t.Null(),
|
||||
});
|
||||
|
||||
/**
|
||||
* 受保护接口 200 响应类型
|
||||
*/
|
||||
export type ProtectedResponse200 = Static<typeof protectedResponse200Schema>;
|
||||
|
||||
/**
|
||||
* 受保护接口 401 响应类型
|
||||
*/
|
||||
export type ProtectedResponse401 = Static<typeof protectedResponse401Schema>;
|
3
tasks/20250629-计划.md
Normal file
3
tasks/20250629-计划.md
Normal file
@ -0,0 +1,3 @@
|
||||
1. 协助ai完成接口
|
||||
2. 协助ai完成测试用例
|
||||
3. 优化mdc关于drizzle和redis的使用
|
@ -1,31 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "Node",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
// Paths
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
@ -33,9 +31,22 @@
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
"@/*": [
|
||||
"src/*"
|
||||
],
|
||||
"@package.json": [
|
||||
"package.json"
|
||||
]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"types/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
75
types/config.type.ts
Normal file
75
types/config.type.ts
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 数据库配置类型
|
||||
*/
|
||||
export interface DbConfig {
|
||||
/** 数据库主机地址 */
|
||||
host: string;
|
||||
/** 数据库端口号 */
|
||||
port: number;
|
||||
/** 数据库用户名 */
|
||||
user: string;
|
||||
/** 数据库密码 */
|
||||
password: string;
|
||||
/** 数据库名称 */
|
||||
database: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT配置类型
|
||||
*/
|
||||
export interface JwtConfig {
|
||||
/** JWT签名密钥 */
|
||||
secret: string;
|
||||
/** Token有效期 */
|
||||
exp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis配置类型
|
||||
*/
|
||||
export interface RedisConfig {
|
||||
/** Redis连接名称 */
|
||||
connectName: string;
|
||||
/** Redis服务器主机地址 */
|
||||
host: string;
|
||||
/** Redis服务器端口号 */
|
||||
port: number;
|
||||
/** Redis用户名 */
|
||||
username: string;
|
||||
/** Redis密码 */
|
||||
password: string;
|
||||
/** Redis数据库索引 */
|
||||
database: number;
|
||||
}
|
||||
/**
|
||||
* 日志配置接口
|
||||
* @interface LogConfig
|
||||
*/
|
||||
export interface LogConfigType {
|
||||
/** 日志文件目录 */
|
||||
directory: string;
|
||||
/** 是否输出到控制台 */
|
||||
console: boolean;
|
||||
/** 单个日志文件最大大小 */
|
||||
maxSize: string;
|
||||
/** 最大保留文件数 */
|
||||
maxFiles: string;
|
||||
/** 日志等级 */
|
||||
level: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局配置类型,包含所有配置项
|
||||
*/
|
||||
export interface GlobalConfig {
|
||||
/** 数据库配置 */
|
||||
db: DbConfig;
|
||||
/** JWT配置 */
|
||||
jwt: JwtConfig;
|
||||
/** Redis配置 */
|
||||
redis: RedisConfig;
|
||||
/** 日志配置 */
|
||||
logger: LogConfigType;
|
||||
/** 当前环境 */
|
||||
env: string;
|
||||
}
|
52
types/drizzle.type.ts
Normal file
52
types/drizzle.type.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @file Drizzle ORM类型定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 定义Drizzle ORM相关的类型,包括数据库实例和表类型
|
||||
*/
|
||||
|
||||
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
||||
import type * as schema from '../src/plugins/drizzle/schema';
|
||||
|
||||
/**
|
||||
* Drizzle数据库实例类型
|
||||
*/
|
||||
export type DrizzleDB = MySql2Database<typeof schema>;
|
||||
|
||||
/**
|
||||
* 数据库表Schema类型
|
||||
*/
|
||||
export type DatabaseSchema = typeof schema;
|
||||
|
||||
/**
|
||||
* 扩展Elysia Context,添加数据库实例
|
||||
*/
|
||||
export interface DrizzleContext {
|
||||
/** Drizzle数据库实例 */
|
||||
db: DrizzleDB;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库连接状态
|
||||
*/
|
||||
export type ConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected';
|
||||
|
||||
/**
|
||||
* 数据库连接信息
|
||||
*/
|
||||
export interface DatabaseConnectionInfo {
|
||||
/** 连接状态 */
|
||||
status: ConnectionStatus;
|
||||
/** 连接主机 */
|
||||
host: string;
|
||||
/** 连接端口 */
|
||||
port: number;
|
||||
/** 数据库名称 */
|
||||
database: string;
|
||||
/** 连接时间 */
|
||||
connectedAt?: Date;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
}
|
18
types/logger.type.ts
Normal file
18
types/logger.type.ts
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
/**
|
||||
* Logger类的类型定义
|
||||
*/
|
||||
export interface LoggerInstance {
|
||||
/** 调试级别日志 */
|
||||
debug(message: string | object): void;
|
||||
/** 信息级别日志 */
|
||||
info(message: string | object): void;
|
||||
/** 警告级别日志 */
|
||||
warn(message: string | object): void;
|
||||
/** 错误级别日志 */
|
||||
error(error: Error): void;
|
||||
/** HTTP级别日志 */
|
||||
http(message: string | object): void;
|
||||
/** 详细级别日志 */
|
||||
verbose(message: string | object): void;
|
||||
}
|
Loading…
Reference in New Issue
Block a user