cursor-init/.cursor/rules/elysia-interface-standards.md
expressgy a23d336ebd refactor: 重构项目架构并标准化开发规范
- 重构项目结构:controllers/services -> modules模块化组织

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

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

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

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

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

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

关联任务计划文档
2025-06-30 01:25:17 +08:00

1084 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 接口的一致性、安全性和可维护性。**