- 重构项目结构:controllers/services -> modules模块化组织 - 新增Drizzle ORM集成和数据库schema定义 - 添加完整的开发规范文档(.cursor/rules/) - 重新组织插件结构为子目录方式 - 新增用户模块和示例代码 - 更新类型定义并移除试验性代码 - 添加API文档和JWT使用示例 关联任务计划文档
30 KiB
30 KiB
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 实例作为控制器
/**
* @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,
},
}
);
❌ 错误做法:创建单独的控制器类
// ❌ 不要这样做
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)
/**
* @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)
/**
* 请求依赖的认证服务
* @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)
/**
* @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)
/**
* @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)
全局错误处理插件
/**
* @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 认证插件
/**
* @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 };
});
}
}));
请求日志插件
/**
* @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 接口示例
/**
* @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 接口的一致性、安全性和可维护性。