refactor: 重构项目架构并标准化开发规范

- 重构项目结构:controllers/services -> modules模块化组织

- 新增Drizzle ORM集成和数据库schema定义

- 添加完整的开发规范文档(.cursor/rules/)

- 重新组织插件结构为子目录方式

- 新增用户模块和示例代码

- 更新类型定义并移除试验性代码

- 添加API文档和JWT使用示例

关联任务计划文档
This commit is contained in:
expressgy 2025-06-30 01:25:17 +08:00
parent 2518986557
commit a23d336ebd
52 changed files with 3480 additions and 946 deletions

View 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结构

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
---
description:
globs:
alwaysApply: true
---

View 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
View 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. **权限分离**: 使用角色字段实现细粒度权限控制

View File

@ -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",

View File

@ -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 服务正在关闭...');
});

View File

@ -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,
},
});

View File

@ -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,
},
});

View File

@ -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,
});

View File

@ -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
View 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));

View 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,
},
);

View 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(),
}),
};

View 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>;

View 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();

View 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
View 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];

View File

@ -0,0 +1,5 @@
import { Elysia } from 'elysia';
export const testController = new Elysia({ name: 'testController' })
.get('/', () => ({ message: '验证性接口' }))
.get('/hello', () => ({ message: 'hello' }));

View File

@ -0,0 +1,3 @@
import { Elysia } from 'elysia';
export const userController = new Elysia({ name: 'userController' }).get('/', () => ({ message: '用户系统' }));

View 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)

View 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;

View 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>>;

View 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 初始化失败');
}
};

View 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';

View 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;

View File

@ -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
View 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);

View File

@ -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 }) => {

View 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 Tokennull
* @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();

View File

@ -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;

View 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),
});
});

View File

@ -1,255 +1,254 @@
/**
* @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;
/**
* @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';
import { formatFileSize } from '@/utils/formatFileSize';
/**
*
*/
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 {
static debug(message: string | object): void {
logger.debug(formatMessage(message));
}
static info(message: string | object): void {
logger.info(formatMessage(message));
}
static warn(message: string | object): void {
logger.warn(formatMessage(message));
}
static error(error: Error): void {
logger.error({
message: error.message,
stack: error.stack,
name: error.name,
cause: error.cause,
});
}
static http(message: string | object): void {
logger.http(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;

View File

@ -10,4 +10,3 @@
import { app } from './app';
app.listen(3000);
console.log('🚀 服务已启动http://localhost:3000');

View File

@ -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 },
};
};

View File

@ -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 ?? '' },
};
};

View File

@ -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
View 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;
}

View File

@ -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
View 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
View 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载荷对象iatexp等自动生成字段
* @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 ? '启用' : '禁用'}`;
}

View File

@ -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客户端实例

View File

@ -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({}),
}),
}),
};

View File

@ -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(),
});

View File

@ -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>;

View File

@ -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
View File

@ -0,0 +1,3 @@
1. 协助ai完成接口
2. 协助ai完成测试用例
3. 优化mdc关于drizzle和redis的使用

View File

@ -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"
]
},
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
"include": [
"src",
"types/**/*.d.ts"
],
"exclude": [
"node_modules",
"dist"
]
}

75
types/config.type.ts Normal file
View 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
View 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
View 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;
}