- 重构项目结构:controllers/services -> modules模块化组织 - 新增Drizzle ORM集成和数据库schema定义 - 添加完整的开发规范文档(.cursor/rules/) - 重新组织插件结构为子目录方式 - 新增用户模块和示例代码 - 更新类型定义并移除试验性代码 - 添加API文档和JWT使用示例 关联任务计划文档
1084 lines
30 KiB
Markdown
1084 lines
30 KiB
Markdown
# Elysia 接口编写规范(Elysia Interface Development Standards)
|
||
|
||
## 目标 (Goal)
|
||
|
||
本规范旨在提供一套完整的 Elysia 接口开发标准,结合官方文档最佳实践、社区经验和项目实际需求,确保代码质量、类型安全和开发效率。
|
||
|
||
## 核心原则 (Core Principles)
|
||
|
||
### 1. 一切皆组件 (Everything is a Component)
|
||
- 每个 Elysia 实例都是一个组件
|
||
- 组件可以被插入到其他实例中
|
||
- 强制将应用拆分为小块,便于添加或移除功能
|
||
|
||
### 2. 方法链式调用 (Method Chaining)
|
||
- **必须始终使用方法链式调用**
|
||
- 确保类型完整性和推断
|
||
- 每个方法返回新的类型引用
|
||
|
||
### 3. 类型安全优先 (Type Safety First)
|
||
- 使用 Elysia 内置类型系统
|
||
- 避免使用 `any` 类型
|
||
- 单一数据源原则
|
||
|
||
## 项目结构规范 (Project Structure Standards)
|
||
|
||
### 推荐目录结构
|
||
|
||
```
|
||
src/
|
||
├── controllers/ # 控制器(路由与业务入口)
|
||
│ ├── auth/
|
||
│ │ └── index.ts # 认证相关路由
|
||
│ └── user/
|
||
│ └── index.ts # 用户相关路由
|
||
├── services/ # 业务逻辑服务层
|
||
│ ├── auth/
|
||
│ │ └── auth.service.ts
|
||
│ └── user/
|
||
│ └── user.service.ts
|
||
├── validators/ # 参数校验(按路由结构组织)
|
||
│ ├── global.response.ts
|
||
│ ├── auth/
|
||
│ │ ├── auth.validator.ts
|
||
│ │ └── auth.response.ts
|
||
│ └── user/
|
||
│ ├── user.validator.ts
|
||
│ └── user.response.ts
|
||
├── models/ # 数据模型
|
||
├── plugins/ # Elysia 插件
|
||
├── utils/ # 工具函数
|
||
├── config/ # 配置文件
|
||
├── type/ # 类型定义文件
|
||
└── app.ts # 应用入口
|
||
```
|
||
|
||
## 接口设计规范 (Interface Design Standards)
|
||
|
||
### 1. 控制器规范 (Controller Standards)
|
||
|
||
**✅ 正确做法:使用 Elysia 实例作为控制器**
|
||
|
||
```typescript
|
||
/**
|
||
* @file 用户认证控制器
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @lastEditor 开发者姓名
|
||
* @lastEditTime 2024-01-01
|
||
* @description 用户认证相关接口,包含登录、注册、token 验证等功能
|
||
*/
|
||
|
||
import { Elysia } from 'elysia';
|
||
import { jwtPlugin } from '@/plugins/jwt.plugins';
|
||
import {
|
||
loginBodySchema,
|
||
registerBodySchema,
|
||
type LoginBody,
|
||
type RegisterBody
|
||
} from '@/validators/auth/auth.validator';
|
||
import {
|
||
loginResponse200Schema,
|
||
loginResponse400Schema,
|
||
registerResponse200Schema,
|
||
registerResponse400Schema
|
||
} from '@/validators/auth/auth.response';
|
||
import { loginService, registerService } from '@/services/auth/auth.service';
|
||
|
||
/**
|
||
* 认证控制器
|
||
* @description 处理用户认证相关的 HTTP 请求
|
||
*/
|
||
export const authController = new Elysia({ prefix: '/api/auth' })
|
||
.use(jwtPlugin)
|
||
.post('/login',
|
||
({ body, jwt, set }: { body: LoginBody; jwt: any; set: any }) =>
|
||
loginService(body, jwt, set),
|
||
{
|
||
body: loginBodySchema,
|
||
detail: {
|
||
tags: ['认证'],
|
||
summary: '用户登录',
|
||
description: '用户使用用户名和密码进行登录,成功后返回 JWT token',
|
||
},
|
||
response: {
|
||
200: loginResponse200Schema,
|
||
400: loginResponse400Schema,
|
||
},
|
||
}
|
||
)
|
||
.post('/register',
|
||
({ body, set }: { body: RegisterBody; set: any }) =>
|
||
registerService(body, set),
|
||
{
|
||
body: registerBodySchema,
|
||
detail: {
|
||
tags: ['认证'],
|
||
summary: '用户注册',
|
||
description: '新用户注册账户',
|
||
},
|
||
response: {
|
||
200: registerResponse200Schema,
|
||
400: registerResponse400Schema,
|
||
},
|
||
}
|
||
);
|
||
```
|
||
|
||
**❌ 错误做法:创建单独的控制器类**
|
||
|
||
```typescript
|
||
// ❌ 不要这样做
|
||
abstract class AuthController {
|
||
static login(context: Context) {
|
||
return AuthService.login(context.body);
|
||
}
|
||
}
|
||
|
||
new Elysia()
|
||
.post('/login', AuthController.login);
|
||
```
|
||
|
||
### 2. 服务层规范 (Service Layer Standards)
|
||
|
||
#### 非请求依赖服务 (Non-Request Dependent Service)
|
||
|
||
```typescript
|
||
/**
|
||
* @file 用户认证业务逻辑服务
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @lastEditor 开发者姓名
|
||
* @lastEditTime 2024-01-01
|
||
* @description 处理用户认证相关的业务逻辑,与 HTTP 请求解耦
|
||
*/
|
||
|
||
import { hash, verify } from 'bun';
|
||
import { sign } from 'jsonwebtoken';
|
||
import type { LoginBody, RegisterBody } from '@/validators/auth/auth.validator';
|
||
|
||
/**
|
||
* 认证服务类
|
||
* @description 处理用户认证业务逻辑
|
||
*/
|
||
export abstract class AuthService {
|
||
/**
|
||
* 用户登录业务逻辑
|
||
* @param body 登录请求体
|
||
* @param jwt JWT 插件实例
|
||
* @param set Elysia set 对象
|
||
* @returns 登录响应
|
||
* @modification 张三 2024-01-02 添加密码验证逻辑
|
||
*/
|
||
static async login(
|
||
body: LoginBody,
|
||
jwt: any,
|
||
set: any
|
||
) {
|
||
const { username, password } = body;
|
||
|
||
try {
|
||
// 查询用户
|
||
const user = await this.findUserByUsername(username);
|
||
if (!user) {
|
||
set.status = 400;
|
||
return {
|
||
code: 400,
|
||
message: '用户名或密码错误',
|
||
data: null,
|
||
};
|
||
}
|
||
|
||
// 验证密码
|
||
const isValidPassword = await this.verifyPassword(password, user.password);
|
||
if (!isValidPassword) {
|
||
set.status = 400;
|
||
return {
|
||
code: 400,
|
||
message: '用户名或密码错误',
|
||
data: null,
|
||
};
|
||
}
|
||
|
||
// 生成 token
|
||
const token = await jwt.sign({
|
||
userId: user.id,
|
||
username: user.username
|
||
});
|
||
|
||
return {
|
||
code: 0,
|
||
message: '登录成功',
|
||
data: {
|
||
token,
|
||
userInfo: {
|
||
id: user.id,
|
||
username: user.username,
|
||
email: user.email
|
||
}
|
||
},
|
||
};
|
||
} catch (error) {
|
||
set.status = 500;
|
||
return {
|
||
code: 500,
|
||
message: '服务器内部错误',
|
||
data: null,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户注册业务逻辑
|
||
* @param body 注册请求体
|
||
* @param set Elysia set 对象
|
||
* @returns 注册响应
|
||
*/
|
||
static async register(body: RegisterBody, set: any) {
|
||
const { username, email, password } = body;
|
||
|
||
try {
|
||
// 检查用户是否已存在
|
||
const existingUser = await this.findUserByUsername(username);
|
||
if (existingUser) {
|
||
set.status = 400;
|
||
return {
|
||
code: 400,
|
||
message: '用户名已存在',
|
||
data: null,
|
||
};
|
||
}
|
||
|
||
// 检查邮箱是否已存在
|
||
const existingEmail = await this.findUserByEmail(email);
|
||
if (existingEmail) {
|
||
set.status = 400;
|
||
return {
|
||
code: 400,
|
||
message: '邮箱已被注册',
|
||
data: null,
|
||
};
|
||
}
|
||
|
||
// 加密密码
|
||
const hashedPassword = await this.hashPassword(password);
|
||
|
||
// 创建用户
|
||
const newUser = await this.createUser({
|
||
username,
|
||
email,
|
||
password: hashedPassword
|
||
});
|
||
|
||
return {
|
||
code: 0,
|
||
message: '注册成功',
|
||
data: {
|
||
userId: newUser.id,
|
||
username: newUser.username
|
||
},
|
||
};
|
||
} catch (error) {
|
||
set.status = 500;
|
||
return {
|
||
code: 500,
|
||
message: '服务器内部错误',
|
||
data: null,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据用户名查找用户
|
||
* @param username 用户名
|
||
* @returns 用户信息或 null
|
||
*/
|
||
private static async findUserByUsername(username: string) {
|
||
// 实际项目中应该从数据库查询
|
||
// 这里仅作示例
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 根据邮箱查找用户
|
||
* @param email 邮箱
|
||
* @returns 用户信息或 null
|
||
*/
|
||
private static async findUserByEmail(email: string) {
|
||
// 实际项目中应该从数据库查询
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 验证密码
|
||
* @param plainPassword 明文密码
|
||
* @param hashedPassword 加密后的密码
|
||
* @returns 是否匹配
|
||
*/
|
||
private static async verifyPassword(
|
||
plainPassword: string,
|
||
hashedPassword: string
|
||
): Promise<boolean> {
|
||
return await verify(plainPassword, hashedPassword);
|
||
}
|
||
|
||
/**
|
||
* 密码加密
|
||
* @param password 明文密码
|
||
* @returns 加密后的密码
|
||
*/
|
||
private static async hashPassword(password: string): Promise<string> {
|
||
return await hash(password);
|
||
}
|
||
|
||
/**
|
||
* 创建用户
|
||
* @param userData 用户数据
|
||
* @returns 创建的用户信息
|
||
*/
|
||
private static async createUser(userData: {
|
||
username: string;
|
||
email: string;
|
||
password: string;
|
||
}) {
|
||
// 实际项目中应该保存到数据库
|
||
return {
|
||
id: Math.random().toString(36),
|
||
...userData
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 请求依赖服务 (Request Dependent Service)
|
||
|
||
```typescript
|
||
/**
|
||
* 请求依赖的认证服务
|
||
* @description 需要访问请求上下文的服务应该作为 Elysia 实例
|
||
*/
|
||
export const RequestAuthService = new Elysia({ name: 'Auth.Service' })
|
||
.derive({ as: 'global' }, ({ cookie: { session } }) => ({
|
||
Auth: {
|
||
user: session.value
|
||
}
|
||
}))
|
||
.macro(({ onBeforeHandle }) => ({
|
||
/**
|
||
* 检查用户是否已登录
|
||
* @param value 是否需要登录
|
||
*/
|
||
requireAuth(value: boolean) {
|
||
if (value) {
|
||
onBeforeHandle(({ Auth, status }) => {
|
||
if (!Auth?.user) {
|
||
return status(401, {
|
||
code: 401,
|
||
message: '请先登录',
|
||
data: null
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}));
|
||
```
|
||
|
||
### 3. 参数校验规范 (Validation Standards)
|
||
|
||
```typescript
|
||
/**
|
||
* @file 认证接口参数校验规则
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @lastEditor 开发者姓名
|
||
* @lastEditTime 2024-01-01
|
||
* @description 认证相关接口的参数校验规则,包含详细的验证规则和错误提示
|
||
*/
|
||
|
||
import { t } from 'elysia';
|
||
import type { Static } from 'elysia';
|
||
|
||
/**
|
||
* 登录请求参数校验规则
|
||
* @property {string} username - 用户名,2-16位字符
|
||
* @property {string} password - 密码,6-32位字符
|
||
*/
|
||
export const loginBodySchema = t.Object({
|
||
username: t.String({
|
||
minLength: 2,
|
||
maxLength: 16,
|
||
description: '用户名,2-16位字符',
|
||
examples: ['admin', 'user123']
|
||
}),
|
||
password: t.String({
|
||
minLength: 6,
|
||
maxLength: 32,
|
||
description: '密码,6-32位字符'
|
||
}),
|
||
}, {
|
||
description: '用户登录请求参数'
|
||
});
|
||
|
||
/**
|
||
* 注册请求参数校验规则
|
||
* @property {string} username - 用户名,2-16位字符
|
||
* @property {string} email - 邮箱地址
|
||
* @property {string} password - 密码,6-32位字符
|
||
* @property {string} confirmPassword - 确认密码
|
||
*/
|
||
export const registerBodySchema = t.Object({
|
||
username: t.String({
|
||
minLength: 2,
|
||
maxLength: 16,
|
||
description: '用户名,2-16位字符',
|
||
pattern: '^[a-zA-Z0-9_]+$' // 只允许字母、数字、下划线
|
||
}),
|
||
email: t.String({
|
||
format: 'email',
|
||
description: '邮箱地址',
|
||
examples: ['user@example.com']
|
||
}),
|
||
password: t.String({
|
||
minLength: 6,
|
||
maxLength: 32,
|
||
description: '密码,6-32位字符,至少包含字母和数字',
|
||
pattern: '^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]+$'
|
||
}),
|
||
confirmPassword: t.String({
|
||
description: '确认密码,必须与密码一致'
|
||
})
|
||
}, {
|
||
description: '用户注册请求参数'
|
||
});
|
||
|
||
/**
|
||
* 密码重置请求参数校验规则
|
||
*/
|
||
export const resetPasswordBodySchema = t.Object({
|
||
email: t.String({
|
||
format: 'email',
|
||
description: '注册时使用的邮箱地址'
|
||
}),
|
||
newPassword: t.String({
|
||
minLength: 6,
|
||
maxLength: 32,
|
||
description: '新密码,6-32位字符'
|
||
}),
|
||
verificationCode: t.String({
|
||
minLength: 6,
|
||
maxLength: 6,
|
||
description: '6位数字验证码',
|
||
pattern: '^\\d{6}$'
|
||
})
|
||
});
|
||
|
||
/**
|
||
* 查询参数校验规则
|
||
*/
|
||
export const userListQuerySchema = t.Object({
|
||
page: t.Optional(t.Number({
|
||
minimum: 1,
|
||
default: 1,
|
||
description: '页码,从1开始'
|
||
})),
|
||
pageSize: t.Optional(t.Number({
|
||
minimum: 1,
|
||
maximum: 100,
|
||
default: 10,
|
||
description: '每页数量,1-100'
|
||
})),
|
||
keyword: t.Optional(t.String({
|
||
maxLength: 50,
|
||
description: '搜索关键词'
|
||
}))
|
||
});
|
||
|
||
/**
|
||
* 路径参数校验规则
|
||
*/
|
||
export const userParamsSchema = t.Object({
|
||
id: t.String({
|
||
minLength: 1,
|
||
description: '用户ID'
|
||
})
|
||
});
|
||
|
||
// 类型导出
|
||
export type LoginBody = Static<typeof loginBodySchema>;
|
||
export type RegisterBody = Static<typeof registerBodySchema>;
|
||
export type ResetPasswordBody = Static<typeof resetPasswordBodySchema>;
|
||
export type UserListQuery = Static<typeof userListQuerySchema>;
|
||
export type UserParams = Static<typeof userParamsSchema>;
|
||
```
|
||
|
||
### 4. 响应格式规范 (Response Format Standards)
|
||
|
||
```typescript
|
||
/**
|
||
* @file 认证接口响应格式定义
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @lastEditor 开发者姓名
|
||
* @lastEditTime 2024-01-01
|
||
* @description 认证相关接口的响应格式定义,确保响应结构的一致性
|
||
*/
|
||
|
||
import { t } from 'elysia';
|
||
|
||
/**
|
||
* 登录成功响应格式
|
||
*/
|
||
export const loginResponse200Schema = t.Object({
|
||
code: t.Literal(0, {
|
||
description: '成功响应码'
|
||
}),
|
||
message: t.String({
|
||
description: '响应消息',
|
||
examples: ['登录成功']
|
||
}),
|
||
data: t.Object({
|
||
/** JWT token */
|
||
token: t.String({
|
||
description: 'JWT 访问令牌',
|
||
examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...']
|
||
}),
|
||
/** 用户信息 */
|
||
userInfo: t.Object({
|
||
id: t.String({
|
||
description: '用户ID'
|
||
}),
|
||
username: t.String({
|
||
description: '用户名'
|
||
}),
|
||
email: t.String({
|
||
description: '邮箱地址'
|
||
})
|
||
})
|
||
})
|
||
}, {
|
||
description: '登录成功响应'
|
||
});
|
||
|
||
/**
|
||
* 登录失败响应格式
|
||
*/
|
||
export const loginResponse400Schema = t.Object({
|
||
code: t.Literal(400, {
|
||
description: '客户端错误响应码'
|
||
}),
|
||
message: t.String({
|
||
description: '错误消息',
|
||
examples: ['用户名或密码错误', '参数验证失败']
|
||
}),
|
||
data: t.Null({
|
||
description: '错误时数据为null'
|
||
}),
|
||
}, {
|
||
description: '登录失败响应'
|
||
});
|
||
|
||
/**
|
||
* 注册成功响应格式
|
||
*/
|
||
export const registerResponse200Schema = t.Object({
|
||
code: t.Literal(0),
|
||
message: t.String({
|
||
examples: ['注册成功']
|
||
}),
|
||
data: t.Object({
|
||
userId: t.String({
|
||
description: '新创建的用户ID'
|
||
}),
|
||
username: t.String({
|
||
description: '用户名'
|
||
})
|
||
})
|
||
});
|
||
|
||
/**
|
||
* 注册失败响应格式
|
||
*/
|
||
export const registerResponse400Schema = t.Object({
|
||
code: t.Literal(400),
|
||
message: t.String({
|
||
examples: ['用户名已存在', '邮箱已被注册', '密码不符合要求']
|
||
}),
|
||
data: t.Null(),
|
||
});
|
||
|
||
/**
|
||
* 通用未授权响应格式
|
||
*/
|
||
export const unauthorizedResponse401Schema = t.Object({
|
||
code: t.Literal(401),
|
||
message: t.String({
|
||
examples: ['请先登录', 'Token已过期', 'Token无效']
|
||
}),
|
||
data: t.Null(),
|
||
});
|
||
|
||
/**
|
||
* 用户列表响应格式
|
||
*/
|
||
export const userListResponse200Schema = t.Object({
|
||
code: t.Literal(0),
|
||
message: t.String(),
|
||
data: t.Object({
|
||
list: t.Array(t.Object({
|
||
id: t.String(),
|
||
username: t.String(),
|
||
email: t.String(),
|
||
createdAt: t.String({
|
||
format: 'date-time',
|
||
description: '创建时间'
|
||
}),
|
||
updatedAt: t.String({
|
||
format: 'date-time',
|
||
description: '更新时间'
|
||
})
|
||
})),
|
||
pagination: t.Object({
|
||
page: t.Number({
|
||
description: '当前页码'
|
||
}),
|
||
pageSize: t.Number({
|
||
description: '每页数量'
|
||
}),
|
||
total: t.Number({
|
||
description: '总条数'
|
||
}),
|
||
totalPages: t.Number({
|
||
description: '总页数'
|
||
})
|
||
})
|
||
})
|
||
});
|
||
```
|
||
|
||
## 错误处理规范 (Error Handling Standards)
|
||
|
||
### 全局错误处理插件
|
||
|
||
```typescript
|
||
/**
|
||
* @file 全局错误处理插件
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @description 统一处理应用中的错误,提供标准化的错误响应格式
|
||
*/
|
||
|
||
import { Elysia } from 'elysia';
|
||
import { logger } from '@/utils/logger';
|
||
|
||
/**
|
||
* 错误响应接口
|
||
*/
|
||
interface ErrorResponse {
|
||
code: number;
|
||
message: string;
|
||
data: null;
|
||
}
|
||
|
||
/**
|
||
* 全局错误处理插件
|
||
*/
|
||
export const errorHandlerPlugin = new Elysia({ name: 'errorHandler' })
|
||
.onError(({ code, error, set }) => {
|
||
// 记录错误日志
|
||
logger.error('API Error:', {
|
||
code,
|
||
error: error.message,
|
||
stack: error.stack,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
const response: ErrorResponse = {
|
||
code: 500,
|
||
message: '服务器内部错误',
|
||
data: null
|
||
};
|
||
|
||
switch (code) {
|
||
case 'VALIDATION':
|
||
set.status = 400;
|
||
response.code = 400;
|
||
response.message = '请求参数验证失败:' + error.message;
|
||
break;
|
||
|
||
case 'NOT_FOUND':
|
||
set.status = 404;
|
||
response.code = 404;
|
||
response.message = '请求的资源不存在';
|
||
break;
|
||
|
||
case 'PARSE':
|
||
set.status = 400;
|
||
response.code = 400;
|
||
response.message = '请求数据格式错误';
|
||
break;
|
||
|
||
case 'UNAUTHORIZED':
|
||
set.status = 401;
|
||
response.code = 401;
|
||
response.message = '未授权访问';
|
||
break;
|
||
|
||
case 'FORBIDDEN':
|
||
set.status = 403;
|
||
response.code = 403;
|
||
response.message = '权限不足';
|
||
break;
|
||
|
||
default:
|
||
set.status = 500;
|
||
response.code = 500;
|
||
response.message = '服务器内部错误';
|
||
break;
|
||
}
|
||
|
||
return response;
|
||
});
|
||
|
||
/**
|
||
* 业务错误类
|
||
* @description 用于抛出业务逻辑错误
|
||
*/
|
||
export class BusinessError extends Error {
|
||
public code: number;
|
||
|
||
constructor(code: number, message: string) {
|
||
super(message);
|
||
this.code = code;
|
||
this.name = 'BusinessError';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 抛出业务错误的辅助函数
|
||
* @param code 错误码
|
||
* @param message 错误消息
|
||
*/
|
||
export function throwBusinessError(code: number, message: string): never {
|
||
throw new BusinessError(code, message);
|
||
}
|
||
```
|
||
|
||
## 中间件与插件规范 (Middleware & Plugin Standards)
|
||
|
||
### JWT 认证插件
|
||
|
||
```typescript
|
||
/**
|
||
* @file JWT 认证插件
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @description JWT 令牌处理插件,提供 token 生成和验证功能
|
||
*/
|
||
|
||
import { Elysia } from 'elysia';
|
||
import { jwt } from '@elysiajs/jwt';
|
||
import { jwtConfig } from '@/config/jwt.config';
|
||
|
||
/**
|
||
* JWT 认证插件
|
||
*/
|
||
export const jwtPlugin = new Elysia({ name: 'jwt' })
|
||
.use(jwt({
|
||
name: 'jwt',
|
||
secret: jwtConfig.secret,
|
||
exp: jwtConfig.expiresIn
|
||
}))
|
||
.derive(({ jwt, headers }) => ({
|
||
/**
|
||
* 获取当前用户信息
|
||
* @returns 用户信息或 null
|
||
*/
|
||
getCurrentUser: async () => {
|
||
try {
|
||
const authorization = headers.authorization;
|
||
if (!authorization?.startsWith('Bearer ')) {
|
||
return null;
|
||
}
|
||
|
||
const token = authorization.slice(7);
|
||
const payload = await jwt.verify(token);
|
||
return payload;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
}))
|
||
.macro(({ onBeforeHandle }) => ({
|
||
/**
|
||
* 权限验证宏
|
||
* @param options 验证选项
|
||
*/
|
||
auth(options: { required?: boolean } = {}) {
|
||
const { required = true } = options;
|
||
|
||
onBeforeHandle(async ({ getCurrentUser, status }) => {
|
||
const user = await getCurrentUser();
|
||
|
||
if (required && !user) {
|
||
return status(401, {
|
||
code: 401,
|
||
message: '请先登录',
|
||
data: null
|
||
});
|
||
}
|
||
|
||
return { user };
|
||
});
|
||
}
|
||
}));
|
||
```
|
||
|
||
### 请求日志插件
|
||
|
||
```typescript
|
||
/**
|
||
* @file 请求日志插件
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @description 记录 API 请求和响应的详细信息
|
||
*/
|
||
|
||
import { Elysia } from 'elysia';
|
||
import { logger } from '@/utils/logger';
|
||
|
||
/**
|
||
* 请求日志插件
|
||
*/
|
||
export const requestLoggerPlugin = new Elysia({ name: 'requestLogger' })
|
||
.onRequest(({ request, path }) => {
|
||
const startTime = Date.now();
|
||
|
||
logger.info('API Request', {
|
||
method: request.method,
|
||
url: request.url,
|
||
path,
|
||
userAgent: request.headers.get('user-agent'),
|
||
ip: request.headers.get('x-forwarded-for') || 'unknown',
|
||
timestamp: new Date().toISOString(),
|
||
startTime
|
||
});
|
||
|
||
// 将开始时间存储在请求上下文中
|
||
return { startTime };
|
||
})
|
||
.onAfterHandle(({ request, response, path, startTime }) => {
|
||
const duration = Date.now() - (startTime || Date.now());
|
||
|
||
logger.info('API Response', {
|
||
method: request.method,
|
||
path,
|
||
status: response.status,
|
||
duration: `${duration}ms`,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
});
|
||
```
|
||
|
||
## 最佳实践示例 (Best Practice Examples)
|
||
|
||
### 完整的 CRUD 接口示例
|
||
|
||
```typescript
|
||
/**
|
||
* @file 用户管理完整示例
|
||
* @author 开发者姓名
|
||
* @date 2024-01-01
|
||
* @description 展示完整的 CRUD 接口实现,包含分页、搜索、排序等功能
|
||
*/
|
||
|
||
import { Elysia, t } from 'elysia';
|
||
import { jwtPlugin } from '@/plugins/jwt.plugins';
|
||
import { errorHandlerPlugin } from '@/plugins/errorHandler.plugins';
|
||
import { UserService } from '@/services/user/user.service';
|
||
|
||
// 参数校验
|
||
const createUserSchema = t.Object({
|
||
username: t.String({ minLength: 2, maxLength: 16 }),
|
||
email: t.String({ format: 'email' }),
|
||
password: t.String({ minLength: 6, maxLength: 32 }),
|
||
role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')], { default: 'user' }))
|
||
});
|
||
|
||
const updateUserSchema = t.Object({
|
||
username: t.Optional(t.String({ minLength: 2, maxLength: 16 })),
|
||
email: t.Optional(t.String({ format: 'email' })),
|
||
role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')]))
|
||
});
|
||
|
||
const userParamsSchema = t.Object({
|
||
id: t.String({ minLength: 1 })
|
||
});
|
||
|
||
const userQuerySchema = t.Object({
|
||
page: t.Optional(t.Number({ minimum: 1, default: 1 })),
|
||
pageSize: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })),
|
||
keyword: t.Optional(t.String({ maxLength: 50 })),
|
||
role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')])),
|
||
sortBy: t.Optional(t.Union([t.Literal('createdAt'), t.Literal('username')], { default: 'createdAt' })),
|
||
sortOrder: t.Optional(t.Union([t.Literal('asc'), t.Literal('desc')], { default: 'desc' }))
|
||
});
|
||
|
||
// 响应格式
|
||
const userItemSchema = t.Object({
|
||
id: t.String(),
|
||
username: t.String(),
|
||
email: t.String(),
|
||
role: t.String(),
|
||
isActive: t.Boolean(),
|
||
createdAt: t.String({ format: 'date-time' }),
|
||
updatedAt: t.String({ format: 'date-time' })
|
||
});
|
||
|
||
const successResponse = (data: any) => t.Object({
|
||
code: t.Literal(0),
|
||
message: t.String(),
|
||
data
|
||
});
|
||
|
||
const errorResponse = (code: number) => t.Object({
|
||
code: t.Literal(code),
|
||
message: t.String(),
|
||
data: t.Null()
|
||
});
|
||
|
||
/**
|
||
* 用户管理控制器
|
||
*/
|
||
export const userController = new Elysia({ prefix: '/api/users' })
|
||
.use(jwtPlugin)
|
||
.use(errorHandlerPlugin)
|
||
|
||
// 获取用户列表(支持分页、搜索、排序)
|
||
.get('/',
|
||
async ({ query, getCurrentUser }) => {
|
||
const currentUser = await getCurrentUser();
|
||
return await UserService.getUserList(query, currentUser);
|
||
},
|
||
{
|
||
query: userQuerySchema,
|
||
detail: {
|
||
tags: ['用户管理'],
|
||
summary: '获取用户列表',
|
||
description: '获取用户列表,支持分页、搜索和排序功能'
|
||
},
|
||
response: {
|
||
200: successResponse(t.Object({
|
||
list: t.Array(userItemSchema),
|
||
pagination: t.Object({
|
||
page: t.Number(),
|
||
pageSize: t.Number(),
|
||
total: t.Number(),
|
||
totalPages: t.Number()
|
||
})
|
||
})),
|
||
401: errorResponse(401)
|
||
},
|
||
auth: { required: true }
|
||
}
|
||
)
|
||
|
||
// 获取单个用户
|
||
.get('/:id',
|
||
async ({ params, getCurrentUser }) => {
|
||
const currentUser = await getCurrentUser();
|
||
return await UserService.getUserById(params.id, currentUser);
|
||
},
|
||
{
|
||
params: userParamsSchema,
|
||
detail: {
|
||
tags: ['用户管理'],
|
||
summary: '获取用户详情',
|
||
description: '根据用户ID获取用户详细信息'
|
||
},
|
||
response: {
|
||
200: successResponse(userItemSchema),
|
||
404: errorResponse(404),
|
||
401: errorResponse(401)
|
||
},
|
||
auth: { required: true }
|
||
}
|
||
)
|
||
|
||
// 创建用户
|
||
.post('/',
|
||
async ({ body, getCurrentUser }) => {
|
||
const currentUser = await getCurrentUser();
|
||
return await UserService.createUser(body, currentUser);
|
||
},
|
||
{
|
||
body: createUserSchema,
|
||
detail: {
|
||
tags: ['用户管理'],
|
||
summary: '创建用户',
|
||
description: '创建新用户账户'
|
||
},
|
||
response: {
|
||
200: successResponse(userItemSchema),
|
||
400: errorResponse(400),
|
||
401: errorResponse(401),
|
||
403: errorResponse(403)
|
||
},
|
||
auth: { required: true }
|
||
}
|
||
)
|
||
|
||
// 更新用户
|
||
.put('/:id',
|
||
async ({ params, body, getCurrentUser }) => {
|
||
const currentUser = await getCurrentUser();
|
||
return await UserService.updateUser(params.id, body, currentUser);
|
||
},
|
||
{
|
||
params: userParamsSchema,
|
||
body: updateUserSchema,
|
||
detail: {
|
||
tags: ['用户管理'],
|
||
summary: '更新用户',
|
||
description: '更新用户信息'
|
||
},
|
||
response: {
|
||
200: successResponse(userItemSchema),
|
||
400: errorResponse(400),
|
||
404: errorResponse(404),
|
||
401: errorResponse(401),
|
||
403: errorResponse(403)
|
||
},
|
||
auth: { required: true }
|
||
}
|
||
)
|
||
|
||
// 删除用户
|
||
.delete('/:id',
|
||
async ({ params, getCurrentUser }) => {
|
||
const currentUser = await getCurrentUser();
|
||
return await UserService.deleteUser(params.id, currentUser);
|
||
},
|
||
{
|
||
params: userParamsSchema,
|
||
detail: {
|
||
tags: ['用户管理'],
|
||
summary: '删除用户',
|
||
description: '删除指定用户'
|
||
},
|
||
response: {
|
||
200: successResponse(t.Object({
|
||
deleted: t.Boolean()
|
||
})),
|
||
404: errorResponse(404),
|
||
401: errorResponse(401),
|
||
403: errorResponse(403)
|
||
},
|
||
auth: { required: true }
|
||
}
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
**请严格遵守以上规范,确保 Elysia 接口的一致性、安全性和可维护性。** |