16 KiB
16 KiB
ElysiaAPI开发流程
0. 概览
这是一个基于 Bun + Elysia 的现代化后端API项目,采用TypeScript开发,集成了MySQL、Redis、JWT认证、Swagger文档等功能。
- 运行时:Bun
- 框架:Elysia
- 语言:TypeScript
- 数据库:MySQL + Drizzle ORM
- 缓存:Redis
- 认证:JWT
- 测试:Vitest
- 文档:Swagger
- 日志:Winston
- 代码规范:ESLint + Prettier
使用的技术和库
- 数据库:
"mysql2": "^3.14.1"
,"drizzle-orm": "^0.44.2"
- token验证:
"jsonwebtoken": "^9.0.2"
- 密码加密:
"bcrypt": "^6.0.0"
1. 相关目录结构和描述
1.0 整体目录结构
project/
├── 📋 配置文件(config/)
├── 📁 源代码 (src/)
├───└───config
├───├───eneities
├───├───modules
│ ├───├───auth
│ ├───├───captcha
│ ├───├───health
│ ├───├───test
│ ├───└───user
├───├───plugins
│ ├───├───drizzle
│ ├───├───email
│ ├───├───errorHandle
│ ├───├───jwt
│ ├───├───logger
│ ├───├───redis
│ ├───└───swagger
├───└───tests
│ └───demo
├───type
├───utils
├── 📁 文档 (docs/)
├── 📁 需求文档 (prd/)
├── 📁 任务管理 (tasks/)
├── 📁 AI对话记录 (aiChat/)
├── 📁 数据库迁移 (drizzle/)
└── 📁 静态资源 (public/)
| 文件 | 说明 |
|------|------|
| `package.json` | 项目依赖和脚本配置 |
| `tsconfig.json` | TypeScript编译配置 |
| `tsconfig.test.json` | 测试环境TypeScript配置 |
| `vitest.config.ts` | Vitest测试框架配置 |
| `eslint.config.js` | ESLint代码规范配置 |
| `bunfig.toml` | Bun运行时配置 |
| `drizzle.config.ts` | Drizzle ORM配置 |
| `README.md` | 项目说明文档 |
1.1 接口目录结构
src/modules/
├── index.ts # API总入口:所有的模块文件都由此导出给主程序
├── [moduleName]/ # 模块目录:每个模块的名称做目录名
│ ├── [moduleName].docs.md # 接口逻辑规则
│ ├── [moduleName].schema.ts # Schema定义
│ ├── [moduleName].response.ts # 响应格式
│ ├── [moduleName].service.ts # 业务逻辑
│ ├── [moduleName].controller.ts # 路由控制器
│ └── [moduleName].test.doc # 测试用例文档
1.2 接口目录功能
- 每个模块目录下的文件,都需要有对应的功能,不能只有docs.md,其他文件缺一不可
- [moduleName].docs.md:需要包含接口开发的业务逻辑,包括schema的注意点,response格式的注意点,接口的业务流程,分析相关的数据库,性能问题,安全问题等等需要考虑的地方和注意点
- [moduleName].schema.ts:定义接口的输入参数,包括参数的类型、参数的必填、参数的默认值、参数的描述、参数的示例
- [moduleName].response.ts:接口返回数据的格式定义文件
- [moduleName].service.ts:接口的具体业务逻辑方法
- [moduleName].controller.ts:接口路由定义文件,在这里组合接口的请求方式、参数类型、响应格式
1.3 插件目录结构
src/plugins/
├── index.ts # 插件总入口
├── drizzle/ # 数据库ORM插件
│ ├── drizzle.plugins.ts
│ ├── drizzle.service.ts
│ └── README.md
├── email/ # 邮件插件
│ ├── email.plugins.ts
│ ├── email.service.ts
│ └── README.md
├── errorHandle/ # 错误处理插件
│ └── errorHandler.plugins.ts
├── jwt/ # JWT认证插件
│ ├── jwt.plugins.ts
│ └── jwt.service.ts
├── logger/ # 日志插件
│ ├── logger.plugins.ts
│ └── logger.service.ts
├── redis/ # Redis插件
│ ├── redis.plugins.ts
│ └── redis.service.ts
└── swagger/ # API文档插件
└── swagger.plugins.ts
1.4 类型定义 (type/)
src/type/
├── config.type.ts # 配置相关类型
├── drizzle.type.ts # 数据库相关类型
├── error.type.ts # 错误相关类型
├── jwt.type.ts # JWT相关类型
├── logger.type.ts # 日志相关类型
├── redis.type.ts # Redis相关类型
└── email.type.ts # 邮件相关类型
1.5 工具函数 (utils/)
src/utils/
├── deviceInfo.ts # 设备信息工具
├── distributedLock.ts # 分布式锁工具
├── formatFileSize.ts # 文件大小格式化
├── formatRoute.ts # 路由格式化
├── jwt.helper.ts # JWT工具函数
├── mysql.ts # MySQL工具
├── pagination.ts # 分页请求参数工厂函数工具
├── randomChalk.ts # 随机颜色工具
├── redis.ts # Redis工具
├── responseFormate.ts # 响应统一格式化处理工具
├── snowflake.ts # 雪花ID生成器
└── text.ts # 文本处理工具
1.6 常量定义 (constants/)
src/constants/
├── swaggerTags.ts # Swagger标签定义:所有模块的tag应该集中在此定义
└── 其他常量
1.7 数据库实体
src/eneities/
├── index.ts # 实体总入口
├── customType.ts # 自定义类型
└── [table].ts # 数据表定义
1.8 文件命名约定
- 模块名使用 单数形式:
auth
、user
、product
、order
- 文件名格式:
[module].[type].ts
- 导出名格式:
[module][类型名]
2. Schema(请求参数类型)
主要指请求参数验证工具,不包括响应
2.1 Schema定义规范
- 一个模块的所有schema都在一个文件中
- 使用elysia自带的类型共据
t
- 参数一定要明确是必填还是选填,是否有默认值
- 对必要的数据进行转化,如用户名全小写并且去除两端空格
- 必须有描述和示例参数
- 导出类型
- 注意已经存在的工具函数,比如分页有现成的简化工具,且能够统一请求格式
2.2 特别注意点
必须遵循的命名模式:
- Request类型:
[动作][模块]Request
→RegisterRequest
- Schema名:
[动作][模块]Schema
→RegisterSchema
2.3 代码示例
/**
* @file 用户模块Schema定义
* @author AI Assistant
* @date 2024-12-19
* @lastEditor AI Assistant
* @lastEditTime 2025-01-07
* @description 定义用户模块的Schema,包括获取当前用户信息、用户列表查询等
*/
import { t, type Static } from 'elysia';
import { createQuerySchema } from '@/utils/pagination';
/**
* 用户列表查询参数Schema
* @description 用户列表查询的请求参数验证规则
*/
export const UserListQuerySchema = createQuerySchema(t.Object({
// 用户特有参数
keyword: t.Optional(t.String({
minLength: 1,
maxLength: 100,
description: '搜索关键词,支持用户名、邮箱模糊搜索',
examples: ['admin', 'test@example.com']
})),
status: t.Optional(t.Union([
t.Literal('active'),
t.Literal('inactive'),
t.Literal('pending')
], {
description: '用户状态筛选',
examples: ['active', 'inactive', 'pending']
})),
gender: t.Optional(t.Union([
t.Literal(0),
t.Literal(1),
t.Literal(2),
t.Literal('0'),
t.Literal('1'),
t.Literal('2'),
], {
description: '性别筛选:0-未知,1-男,2-女',
examples: [0, 1, 2]
})),
isRoot: t.Optional(t.Boolean({
description: '是否超级管理员筛选',
examples: [true, false]
}))
}));
/** 用户列表查询参数类型 */
export type UserListQueryRequest = Static<typeof UserListQuerySchema>;
3. Response(响应数据类型)
3.1 响应格式定义规范
- 使用统一的相应格式工具
responseWrapperSchema
- 错误相应只提供示例和描述
- 导出响应成功类型
3.2 特别注意点
必须遵循的命名模式:
- Response格式定义:
[动作][模块]Response
→RegisterResponse
- Response成功类型:
[动作][模块]SuccessType
→RegisterSuccessType
3.3 代码示例
/**
* @file 用户模块响应格式定义
* @author AI Assistant
* @date 2024-12-19
* @lastEditor AI Assistant
* @lastEditTime 2025-01-07
* @description 定义用户模块的响应格式,包括获取当前用户信息、用户列表查询等
*/
import { t, type Static } from 'elysia';
import { responseWrapperSchema } from '@/utils/responseFormate';
import { createPaginationResponseSchema } from '@/utils/pagination';
/**
* 用户列表项Schema
* @description 用户列表中单个用户的数据结构
*/
export const UserListItemSchema = t.Object({
/** 用户ID */
id: t.String({
description: '用户ID(bigint类型以字符串形式返回防止精度丢失)',
examples: ['1', '2', '3']
}),
/** 用户名 */
username: t.String({
description: '用户名',
examples: ['admin', 'testuser']
}),
/** 更多字段... */
/** 更新时间 */
updatedAt: t.String({
description: '更新时间',
examples: ['2024-12-19T10:30:00Z']
})
});
/**
* 获取用户列表接口响应组合
* @description 用于Controller中定义所有可能的响应格式
*/
export const GetUserListResponsesSchema = {
200: responseWrapperSchema(createPaginationResponseSchema(UserListItemSchema)),
401: responseWrapperSchema(t.Object({
error: t.String({
description: '认证失败',
examples: ['未提供有效的认证令牌', '令牌已过期']
})
})),
400: responseWrapperSchema(t.Object({
error: t.String({
description: '参数错误',
examples: ['分页参数无效', '搜索关键词格式错误']
})
})),
500: responseWrapperSchema(t.Object({
error: t.String({
description: '服务器错误',
examples: ['内部服务器错误']
})
}))
};
/** 获取用户列表成功响应数据类型 */
export type GetUserListSuccessType = Static<typeof GetUserListResponsesSchema[200]>;
4. Service(业务逻辑层)
4.1 Service层要求
- ✅ 所有方法必须有完整的类型注解
- ✅ 所有方法必须有详细的JSDoc注释
- ✅ 必须使用统一的响应格式工具
- ✅ 必须使用统一的错误码
- ✅ 必须有详细的日志记录,接口有请求响应记录,无需记录每次请求
- ✅ 需要有完整的错误处理,服务错误有拦截器处理可以不管,只判断逻辑上的错误
- ✅ 导出单例实例供controller使用
4.2 注意点
- 开发逻辑时,注意数据库实体,必须严格按照数据库字段进行开发,不能出错
- 更新时间数据库一般会自动天写,不用手动设置或更新
- 数据库id一般为bigint,注意类型和精度问题
- 在写入数据时,必要的增加分布式锁
- 逻辑要清晰,代码要简洁
4.3 代码示例
/**
* @file 用户模块Service层实现
* @author AI Assistant
* @date 2024-12-19
* @lastEditor AI Assistant
* @lastEditTime 2025-01-07
* @description 用户模块的业务逻辑实现,包括获取当前用户信息、用户列表查询等
*/
import { Logger } from '@/plugins/logger/logger.service';
import { db } from '@/plugins/drizzle/drizzle.service';
import { sysUsers } from '@/eneities';
import { eq, like, and, desc, asc, sql } from 'drizzle-orm';
import { successResponse, errorResponse, BusinessError } from '@/utils/responseFormate';
import { calculatePagination, normalizePaginationParams } from '@/utils/pagination';
import type { GetCurrentUserSuccessType, GetUserListSuccessType } from './user.response';
import type { UserListQueryRequest, UserListItem } from './user.schema';
/**
* 用户服务类
* @description 处理用户相关的业务逻辑
*/
export class UserService {
/**
* 获取当前用户信息
* @param userId 用户ID
* @returns Promise<GetCurrentUserSuccessType>
* @throws BusinessError 业务逻辑错误
* @type API =====================================================================
*/
public async getCurrentUser(userId: string): Promise<GetCurrentUserSuccessType> {
Logger.info(`获取用户信息:${userId}`);
// 查询用户信息
const user = await db()
.select({
id: sysUsers.id,
username: sysUsers.username,
email: sysUsers.email,
nickname: sysUsers.nickname,
avatar: sysUsers.avatar,
mobile: sysUsers.mobile,
status: sysUsers.status,
lastLoginAt: sysUsers.lastLoginAt,
createdAt: sysUsers.createdAt,
updatedAt: sysUsers.updatedAt
})
.from(sysUsers)
.where(eq(sysUsers.id, BigInt(userId)))
.limit(1);
if (!user || user.length === 0) {
Logger.warn(`用户不存在:${userId}`);
throw new BusinessError(
`用户不存在:${userId}`,
404
);
}
const userData = user[0]!;
Logger.info(`获取用户信息成功:${userId} - ${userData.username}`);
return successResponse({
id: userId, // 使用传入的字符串ID,避免精度丢失
username: userData.username,
email: userData.email,
nickname: userData.nickname,
avatar: userData.avatar,
phone: userData.mobile,
status: userData.status,
lastLoginAt: userData.lastLoginAt || null,
createdAt: userData.createdAt,
updatedAt: userData.updatedAt
}, '获取用户信息成功');
}
}
// 导出单例实例
export const userService = new UserService();
5. Controllers(接口名称接入)
5.1 Controllers层要求
- ✅ 路由方法必须有完整的JSDoc注释
- ✅ 必须定义完整的response schema
- ✅ 必须有适当的tags分类
- ✅ 必须有operationId用于API文档
- ✅ 错误处理由Service层统一处理
5.2 Controllers层注意点
- 使用service时要简洁,如
({ body }) => authService.register(body),
5.3 Controllers层代码示例
/**
* 认证控制器
* @description 处理用户认证相关的HTTP请求
*/
export const authController = new Elysia()
/**
* 用户注册接口
* @route POST /api/auth/register
* @description 用户注册,包含验证码验证、用户名邮箱唯一性检查等
*/
.post(
'/register',
({ body }) => authService.register(body),
{
body: RegisterSchema,
detail: {
summary: '用户注册',
description: '用户注册接口,需要提供用户名、邮箱、密码和验证码',
tags: [tags.auth],
operationId: 'registerUser',
},
response: RegisterResponsesSchema,
}
);
6. 错误处理
6.1 错误处理规范
- 统一
import { BusinessError } from '@/utils/responseFormate';
引入错误工具抛出错误 - 错误处理在errorHandle.plugins中统一处理,
6.2 主动抛出异常示例
// ✅ Service层错误处理
import { BusinessError } from '@/utils/responseFormate';
// 抛出业务错误
throw new BusinessError('消息说明...', 409);
7. 测试用例文档
- 分模块
- 分接口
- 测试名称
- 场景
- 方法