chore: 结构化调整,mysql、redis、数据库实体、cursorRules
This commit is contained in:
parent
a23d336ebd
commit
200c3b0b10
377
.cursor/rules/README.mdc
Normal file
377
.cursor/rules/README.mdc
Normal file
@ -0,0 +1,377 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 🤖 AI友好的Elysia开发规则体系
|
||||
|
||||
## 快速开始 🚀
|
||||
|
||||
### 1. 对于开发者
|
||||
|
||||
**首次使用:**
|
||||
1. 阅读 `ai-friendly-elysia-rules.md` 了解规范
|
||||
2. 查看 `ai-development-workflow.md` 学习如何与AI协作
|
||||
3. 在项目中导入支持文件
|
||||
|
||||
**日常开发:**
|
||||
```typescript
|
||||
// 使用标准错误码
|
||||
import { ERROR_CODES } from '@/constants/error-codes';
|
||||
|
||||
// 使用响应工具
|
||||
import { successResponse, errorResponse, BusinessError } from '@/utils/response.helper';
|
||||
```
|
||||
|
||||
### 2. 对于AI助手
|
||||
|
||||
**工作原则:**
|
||||
- 严格遵循 `ai-friendly-elysia-rules.md` 规范
|
||||
- 按照 `ai-development-workflow.md` 工作流程
|
||||
- 确保代码质量和一致性
|
||||
|
||||
**开发顺序:**
|
||||
1. Schema定义(最高优先级)
|
||||
2. Response格式
|
||||
3. Service层实现
|
||||
4. Controller层实现
|
||||
5. 测试用例
|
||||
|
||||
## 核心优势 💪
|
||||
|
||||
### 1. 一致性
|
||||
- 统一的文件结构
|
||||
- 标准的命名规范
|
||||
- 一致的错误处理
|
||||
|
||||
### 2. 类型安全
|
||||
- TypeBox Schema + TypeScript类型
|
||||
- 编译时类型检查
|
||||
- 运行时参数验证
|
||||
|
||||
### 3. 可维护性
|
||||
- 清晰的模块划分
|
||||
- 完整的文档注释
|
||||
- 系统化的测试
|
||||
|
||||
### 4. AI友好
|
||||
- 语义清晰的代码结构
|
||||
- 明确的依赖关系
|
||||
- 标准化的模式
|
||||
|
||||
## 使用示例 📝
|
||||
|
||||
### 创建新模块
|
||||
|
||||
当你需要创建新的业务模块时,告诉AI助手:
|
||||
|
||||
```
|
||||
功能:用户管理
|
||||
模块:user
|
||||
接口:注册、登录、获取个人信息、修改个人信息
|
||||
认证:登录后的接口需要JWT认证
|
||||
特殊要求:密码需要加密存储
|
||||
```
|
||||
|
||||
AI助手会自动创建:
|
||||
- `src/modules/user/user.schema.ts`
|
||||
- `src/modules/user/user.response.ts`
|
||||
- `src/modules/user/user.service.ts`
|
||||
- `src/modules/user/user.controller.ts`
|
||||
- `src/modules/user/user.test.ts`
|
||||
|
||||
### 修改现有功能
|
||||
|
||||
```
|
||||
修改用户查询接口,添加分页功能,每页最多50条记录
|
||||
```
|
||||
|
||||
AI助手会:
|
||||
1. 分析现有代码
|
||||
2. 更新Schema定义
|
||||
3. 修改Service逻辑
|
||||
4. 更新Controller
|
||||
5. 补充测试用例
|
||||
|
||||
## 代码质量保证 ✅
|
||||
|
||||
### 自动检查项
|
||||
- [ ] TypeScript类型正确性
|
||||
- [ ] Schema验证完整性
|
||||
- [ ] 错误处理覆盖
|
||||
- [ ] 响应格式一致性
|
||||
- [ ] 命名规范符合标准
|
||||
- [ ] 文档注释完整
|
||||
|
||||
### 人工Review项
|
||||
- [ ] 业务逻辑正确性
|
||||
- [ ] 安全性考虑
|
||||
- [ ] 性能优化
|
||||
- [ ] 用户体验
|
||||
|
||||
## 最佳实践 🌟
|
||||
|
||||
### 1. 需求描述
|
||||
```
|
||||
✅ 清晰:实现用户注册接口,包含邮箱验证和密码强度检查
|
||||
❌ 模糊:做一个用户功能
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
```typescript
|
||||
// ✅ 使用标准错误类
|
||||
throw new BusinessError('用户名已存在', ERROR_CODES.USER_ALREADY_EXISTS);
|
||||
|
||||
// ❌ 直接抛出Error
|
||||
throw new Error('用户名已存在');
|
||||
```
|
||||
|
||||
### 3. 响应格式
|
||||
```typescript
|
||||
// ✅ 使用工具函数
|
||||
return successResponse(userData, '查询成功');
|
||||
|
||||
// ❌ 手动构造
|
||||
return { code: 'SUCCESS', message: '查询成功', data: userData };
|
||||
```
|
||||
|
||||
## 性能考虑 ⚡
|
||||
|
||||
### 数据库优化
|
||||
- 使用索引优化查询
|
||||
- 避免N+1查询问题
|
||||
- 合理使用连接池
|
||||
|
||||
### 缓存策略
|
||||
- 热点数据Redis缓存
|
||||
- 适当的缓存过期时间
|
||||
- 缓存穿透防护
|
||||
|
||||
### 并发处理
|
||||
- 避免阻塞操作
|
||||
- 合理的超时设置
|
||||
- 资源清理
|
||||
|
||||
## 故障排除 🔧
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **类型错误**
|
||||
- 检查Schema定义
|
||||
- 确认类型导出
|
||||
- 验证导入路径
|
||||
|
||||
2. **运行时错误**
|
||||
- 查看错误日志
|
||||
- 检查参数验证
|
||||
- 确认业务逻辑
|
||||
|
||||
3. **性能问题**
|
||||
- 分析慢查询
|
||||
- 检查缓存命中率
|
||||
- 监控资源使用
|
||||
|
||||
### 调试技巧
|
||||
|
||||
```typescript
|
||||
// 添加调试日志
|
||||
Logger.debug(`处理用户请求: ${JSON.stringify(params)}`);
|
||||
|
||||
// 性能监控
|
||||
const startTime = Date.now();
|
||||
// ... 业务逻辑
|
||||
Logger.info(`操作耗时: ${Date.now() - startTime}ms`);
|
||||
```
|
||||
|
||||
## 版本历史 📚
|
||||
|
||||
- **v1.0.0** - 初始版本,包含核心规范
|
||||
- **v1.1.0** - 添加AI协作指南
|
||||
- **v1.2.0** - 完善错误处理和响应格式
|
||||
|
||||
记住:好的规范不是限制,而是让团队更高效协作的基础! 🎯 # 🤖 AI友好的Elysia开发规则体系
|
||||
|
||||
## 快速开始 🚀
|
||||
|
||||
### 1. 对于开发者
|
||||
|
||||
**首次使用:**
|
||||
1. 阅读 `ai-friendly-elysia-rules.md` 了解规范
|
||||
2. 查看 `ai-development-workflow.md` 学习如何与AI协作
|
||||
3. 在项目中导入支持文件
|
||||
|
||||
**日常开发:**
|
||||
```typescript
|
||||
// 使用标准错误码
|
||||
import { ERROR_CODES } from '@/constants/error-codes';
|
||||
|
||||
// 使用响应工具
|
||||
import { successResponse, errorResponse, BusinessError } from '@/utils/response.helper';
|
||||
```
|
||||
|
||||
### 2. 对于AI助手
|
||||
|
||||
**工作原则:**
|
||||
- 严格遵循 `ai-friendly-elysia-rules.md` 规范
|
||||
- 按照 `ai-development-workflow.md` 工作流程
|
||||
- 确保代码质量和一致性
|
||||
|
||||
**开发顺序:**
|
||||
1. Schema定义(最高优先级)
|
||||
2. Response格式
|
||||
3. Service层实现
|
||||
4. Controller层实现
|
||||
5. 测试用例
|
||||
|
||||
## 核心优势 💪
|
||||
|
||||
### 1. 一致性
|
||||
- 统一的文件结构
|
||||
- 标准的命名规范
|
||||
- 一致的错误处理
|
||||
|
||||
### 2. 类型安全
|
||||
- TypeBox Schema + TypeScript类型
|
||||
- 编译时类型检查
|
||||
- 运行时参数验证
|
||||
|
||||
### 3. 可维护性
|
||||
- 清晰的模块划分
|
||||
- 完整的文档注释
|
||||
- 系统化的测试
|
||||
|
||||
### 4. AI友好
|
||||
- 语义清晰的代码结构
|
||||
- 明确的依赖关系
|
||||
- 标准化的模式
|
||||
|
||||
## 使用示例 📝
|
||||
|
||||
### 创建新模块
|
||||
|
||||
当你需要创建新的业务模块时,告诉AI助手:
|
||||
|
||||
```
|
||||
功能:用户管理
|
||||
模块:user
|
||||
接口:注册、登录、获取个人信息、修改个人信息
|
||||
认证:登录后的接口需要JWT认证
|
||||
特殊要求:密码需要加密存储
|
||||
```
|
||||
|
||||
AI助手会自动创建:
|
||||
- `src/modules/user/user.schema.ts`
|
||||
- `src/modules/user/user.response.ts`
|
||||
- `src/modules/user/user.service.ts`
|
||||
- `src/modules/user/user.controller.ts`
|
||||
- `src/modules/user/user.test.ts`
|
||||
|
||||
### 修改现有功能
|
||||
|
||||
```
|
||||
修改用户查询接口,添加分页功能,每页最多50条记录
|
||||
```
|
||||
|
||||
AI助手会:
|
||||
1. 分析现有代码
|
||||
2. 更新Schema定义
|
||||
3. 修改Service逻辑
|
||||
4. 更新Controller
|
||||
5. 补充测试用例
|
||||
|
||||
## 代码质量保证 ✅
|
||||
|
||||
### 自动检查项
|
||||
- [ ] TypeScript类型正确性
|
||||
- [ ] Schema验证完整性
|
||||
- [ ] 错误处理覆盖
|
||||
- [ ] 响应格式一致性
|
||||
- [ ] 命名规范符合标准
|
||||
- [ ] 文档注释完整
|
||||
|
||||
### 人工Review项
|
||||
- [ ] 业务逻辑正确性
|
||||
- [ ] 安全性考虑
|
||||
- [ ] 性能优化
|
||||
- [ ] 用户体验
|
||||
|
||||
## 最佳实践 🌟
|
||||
|
||||
### 1. 需求描述
|
||||
```
|
||||
✅ 清晰:实现用户注册接口,包含邮箱验证和密码强度检查
|
||||
❌ 模糊:做一个用户功能
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
```typescript
|
||||
// ✅ 使用标准错误类
|
||||
throw new BusinessError('用户名已存在', ERROR_CODES.USER_ALREADY_EXISTS);
|
||||
|
||||
// ❌ 直接抛出Error
|
||||
throw new Error('用户名已存在');
|
||||
```
|
||||
|
||||
### 3. 响应格式
|
||||
```typescript
|
||||
// ✅ 使用工具函数
|
||||
return successResponse(userData, '查询成功');
|
||||
|
||||
// ❌ 手动构造
|
||||
return { code: 'SUCCESS', message: '查询成功', data: userData };
|
||||
```
|
||||
|
||||
## 性能考虑 ⚡
|
||||
|
||||
### 数据库优化
|
||||
- 使用索引优化查询
|
||||
- 避免N+1查询问题
|
||||
- 合理使用连接池
|
||||
|
||||
### 缓存策略
|
||||
- 热点数据Redis缓存
|
||||
- 适当的缓存过期时间
|
||||
- 缓存穿透防护
|
||||
|
||||
### 并发处理
|
||||
- 避免阻塞操作
|
||||
- 合理的超时设置
|
||||
- 资源清理
|
||||
|
||||
## 故障排除 🔧
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **类型错误**
|
||||
- 检查Schema定义
|
||||
- 确认类型导出
|
||||
- 验证导入路径
|
||||
|
||||
2. **运行时错误**
|
||||
- 查看错误日志
|
||||
- 检查参数验证
|
||||
- 确认业务逻辑
|
||||
|
||||
3. **性能问题**
|
||||
- 分析慢查询
|
||||
- 检查缓存命中率
|
||||
- 监控资源使用
|
||||
|
||||
### 调试技巧
|
||||
|
||||
```typescript
|
||||
// 添加调试日志
|
||||
Logger.debug(`处理用户请求: ${JSON.stringify(params)}`);
|
||||
|
||||
// 性能监控
|
||||
const startTime = Date.now();
|
||||
// ... 业务逻辑
|
||||
Logger.info(`操作耗时: ${Date.now() - startTime}ms`);
|
||||
```
|
||||
|
||||
## 版本历史 📚
|
||||
|
||||
- **v1.0.0** - 初始版本,包含核心规范
|
||||
- **v1.1.0** - 添加AI协作指南
|
||||
- **v1.2.0** - 完善错误处理和响应格式
|
||||
|
||||
记住:好的规范不是限制,而是让团队更高效协作的基础! 🎯
|
403
.cursor/rules/ai-development-workflow.mdc
Normal file
403
.cursor/rules/ai-development-workflow.mdc
Normal file
@ -0,0 +1,403 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# AI助手开发工作流程指南 🤖
|
||||
|
||||
## 快速开始
|
||||
|
||||
当你需要我帮你开发新功能时,请按照以下格式提供信息:
|
||||
|
||||
```
|
||||
功能:[功能名称]
|
||||
模块:[模块名,如 user、product]
|
||||
接口:[接口列表,如 创建用户、查询用户列表]
|
||||
认证:[是否需要JWT认证]
|
||||
特殊要求:[任何特殊要求]
|
||||
```
|
||||
|
||||
## 我的工作流程
|
||||
|
||||
### 1. 分析阶段(30秒内)
|
||||
- 理解功能需求
|
||||
- 分析现有代码结构
|
||||
- 确定模块依赖关系
|
||||
- 制定开发计划
|
||||
|
||||
### 2. 设计阶段(按优先级)
|
||||
1. **Schema设计**(最优先)
|
||||
- 定义请求参数Schema
|
||||
- 定义响应数据Schema
|
||||
- 导出TypeScript类型
|
||||
|
||||
2. **Response格式设计**
|
||||
- 成功响应格式
|
||||
- 错误响应格式
|
||||
- 组合响应定义
|
||||
|
||||
3. **Service层设计**
|
||||
- 业务逻辑接口
|
||||
- 数据访问逻辑
|
||||
- 错误处理逻辑
|
||||
|
||||
4. **Controller层设计**
|
||||
- 路由定义
|
||||
- 参数验证
|
||||
- 错误处理
|
||||
|
||||
5. **测试用例设计**
|
||||
- 正常流程测试
|
||||
- 异常流程测试
|
||||
- 边界条件测试
|
||||
|
||||
### 3. 实现阶段(批量操作)
|
||||
|
||||
我会同时创建/修改多个文件:
|
||||
|
||||
```typescript
|
||||
// 1. 同时读取相关文件了解现状
|
||||
// 2. 并行创建所有必需的文件
|
||||
// 3. 更新路由和导出文件
|
||||
// 4. 验证代码完整性
|
||||
```
|
||||
|
||||
### 4. 验证阶段
|
||||
|
||||
- 检查类型安全性
|
||||
- 确认错误处理完整性
|
||||
- 验证响应格式一致性
|
||||
- 检查测试覆盖率
|
||||
|
||||
## 交互模式
|
||||
|
||||
### 快速开发模式 ⚡
|
||||
适用于:标准CRUD操作、常见业务场景
|
||||
|
||||
**你只需要说:**
|
||||
```
|
||||
"帮我实现用户模块的CRUD接口"
|
||||
"添加产品管理功能"
|
||||
"实现订单状态查询"
|
||||
```
|
||||
|
||||
**AI会自动:**
|
||||
- 按照规范创建完整的5个文件
|
||||
- 集成到现有路由系统
|
||||
- 提供完整的类型安全
|
||||
- 包含基础测试用例
|
||||
|
||||
### 定制开发模式 🔧
|
||||
适用于:复杂业务逻辑、特殊需求
|
||||
|
||||
**你需要提供:**
|
||||
- 详细的业务规则
|
||||
- 特殊的验证要求
|
||||
- 复杂的数据关联
|
||||
- 性能要求
|
||||
|
||||
**AI会:**
|
||||
- 详细分析需求
|
||||
- 提供设计方案
|
||||
- 征求确认后实现
|
||||
- 优化性能和安全性
|
||||
|
||||
## 我擅长处理的场景
|
||||
|
||||
### ✅ 高效处理
|
||||
- 标准REST API开发
|
||||
- CRUD操作实现
|
||||
- 数据验证和类型安全
|
||||
- 错误处理和响应格式
|
||||
- JWT认证集成
|
||||
- 数据库操作(Drizzle ORM)
|
||||
- Redis缓存集成
|
||||
- 测试用例编写
|
||||
- API文档生成
|
||||
|
||||
### ⚡ 批量操作
|
||||
- 多文件同时创建/修改
|
||||
- 路由批量注册
|
||||
- 类型定义批量导出
|
||||
- 测试用例批量生成
|
||||
|
||||
### 🔍 代码分析
|
||||
- 现有代码结构理解
|
||||
- 依赖关系分析
|
||||
- 潜在问题识别
|
||||
- 优化建议提供
|
||||
|
||||
## 沟通最佳实践
|
||||
|
||||
### 清晰描述需求
|
||||
```
|
||||
❌ "做一个用户功能"
|
||||
✅ "实现用户注册、登录、个人信息查询和修改功能,需要JWT认证"
|
||||
|
||||
❌ "这个接口有问题"
|
||||
✅ "用户登录接口返回401错误,但是用户名密码正确"
|
||||
```
|
||||
|
||||
### 提供上下文信息
|
||||
```
|
||||
✅ "在现有的用户模块基础上添加头像上传功能"
|
||||
✅ "这个接口需要管理员权限才能访问"
|
||||
✅ "数据需要缓存到Redis,缓存时间1小时"
|
||||
```
|
||||
|
||||
### 明确期望结果
|
||||
```
|
||||
✅ "创建完整的用户CRUD接口,包含测试"
|
||||
✅ "只需要修改现有的查询接口,添加分页功能"
|
||||
✅ "优化这个接口的性能,响应时间控制在100ms内"
|
||||
```
|
||||
|
||||
## 错误处理和调试
|
||||
|
||||
### 当代码出现问题时
|
||||
|
||||
1. **我会主动分析**
|
||||
- 检查类型错误
|
||||
- 验证语法正确性
|
||||
- 确认导入导出关系
|
||||
|
||||
2. **提供修复方案**
|
||||
- 直接修复简单问题
|
||||
- 解释复杂问题的原因
|
||||
- 提供多种解决方案
|
||||
|
||||
3. **验证修复结果**
|
||||
- 确保修复后代码可运行
|
||||
- 检查是否引入新问题
|
||||
- 验证功能完整性
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
我会在适当时候提供:
|
||||
- 数据库查询优化
|
||||
- 缓存策略建议
|
||||
- 并发处理优化
|
||||
- 内存使用优化
|
||||
|
||||
## 质量保证
|
||||
|
||||
### 代码质量检查
|
||||
- [ ] 类型安全性
|
||||
- [ ] 错误处理完整性
|
||||
- [ ] 响应格式一致性
|
||||
- [ ] 命名规范符合标准
|
||||
- [ ] 注释文档完整
|
||||
|
||||
### 功能完整性检查
|
||||
- [ ] Schema定义完整
|
||||
- [ ] Response格式正确
|
||||
- [ ] Service逻辑完整
|
||||
- [ ] Controller路由正确
|
||||
- [ ] 测试用例覆盖
|
||||
|
||||
### 安全性检查
|
||||
- [ ] 参数验证到位
|
||||
- [ ] 认证授权正确
|
||||
- [ ] 敏感信息保护
|
||||
- [ ] SQL注入防护
|
||||
- [ ] XSS攻击防护
|
||||
|
||||
记住:我的目标是让你专注于业务逻辑,而我来确保代码的规范性、安全性和可维护性! 🎯 # AI助手开发工作流程指南 🤖
|
||||
|
||||
## 快速开始
|
||||
|
||||
当你需要我帮你开发新功能时,请按照以下格式提供信息:
|
||||
|
||||
```
|
||||
功能:[功能名称]
|
||||
模块:[模块名,如 user、product]
|
||||
接口:[接口列表,如 创建用户、查询用户列表]
|
||||
认证:[是否需要JWT认证]
|
||||
特殊要求:[任何特殊要求]
|
||||
```
|
||||
|
||||
## 我的工作流程
|
||||
|
||||
### 1. 分析阶段(30秒内)
|
||||
- 理解功能需求
|
||||
- 分析现有代码结构
|
||||
- 确定模块依赖关系
|
||||
- 制定开发计划
|
||||
|
||||
### 2. 设计阶段(按优先级)
|
||||
1. **Schema设计**(最优先)
|
||||
- 定义请求参数Schema
|
||||
- 定义响应数据Schema
|
||||
- 导出TypeScript类型
|
||||
|
||||
2. **Response格式设计**
|
||||
- 成功响应格式
|
||||
- 错误响应格式
|
||||
- 组合响应定义
|
||||
|
||||
3. **Service层设计**
|
||||
- 业务逻辑接口
|
||||
- 数据访问逻辑
|
||||
- 错误处理逻辑
|
||||
|
||||
4. **Controller层设计**
|
||||
- 路由定义
|
||||
- 参数验证
|
||||
- 错误处理
|
||||
|
||||
5. **测试用例设计**
|
||||
- 正常流程测试
|
||||
- 异常流程测试
|
||||
- 边界条件测试
|
||||
|
||||
### 3. 实现阶段(批量操作)
|
||||
|
||||
我会同时创建/修改多个文件:
|
||||
|
||||
```typescript
|
||||
// 1. 同时读取相关文件了解现状
|
||||
// 2. 并行创建所有必需的文件
|
||||
// 3. 更新路由和导出文件
|
||||
// 4. 验证代码完整性
|
||||
```
|
||||
|
||||
### 4. 验证阶段
|
||||
|
||||
- 检查类型安全性
|
||||
- 确认错误处理完整性
|
||||
- 验证响应格式一致性
|
||||
- 检查测试覆盖率
|
||||
|
||||
## 交互模式
|
||||
|
||||
### 快速开发模式 ⚡
|
||||
适用于:标准CRUD操作、常见业务场景
|
||||
|
||||
**你只需要说:**
|
||||
```
|
||||
"帮我实现用户模块的CRUD接口"
|
||||
"添加产品管理功能"
|
||||
"实现订单状态查询"
|
||||
```
|
||||
|
||||
**AI会自动:**
|
||||
- 按照规范创建完整的5个文件
|
||||
- 集成到现有路由系统
|
||||
- 提供完整的类型安全
|
||||
- 包含基础测试用例
|
||||
|
||||
### 定制开发模式 🔧
|
||||
适用于:复杂业务逻辑、特殊需求
|
||||
|
||||
**你需要提供:**
|
||||
- 详细的业务规则
|
||||
- 特殊的验证要求
|
||||
- 复杂的数据关联
|
||||
- 性能要求
|
||||
|
||||
**AI会:**
|
||||
- 详细分析需求
|
||||
- 提供设计方案
|
||||
- 征求确认后实现
|
||||
- 优化性能和安全性
|
||||
|
||||
## 我擅长处理的场景
|
||||
|
||||
### ✅ 高效处理
|
||||
- 标准REST API开发
|
||||
- CRUD操作实现
|
||||
- 数据验证和类型安全
|
||||
- 错误处理和响应格式
|
||||
- JWT认证集成
|
||||
- 数据库操作(Drizzle ORM)
|
||||
- Redis缓存集成
|
||||
- 测试用例编写
|
||||
- API文档生成
|
||||
|
||||
### ⚡ 批量操作
|
||||
- 多文件同时创建/修改
|
||||
- 路由批量注册
|
||||
- 类型定义批量导出
|
||||
- 测试用例批量生成
|
||||
|
||||
### 🔍 代码分析
|
||||
- 现有代码结构理解
|
||||
- 依赖关系分析
|
||||
- 潜在问题识别
|
||||
- 优化建议提供
|
||||
|
||||
## 沟通最佳实践
|
||||
|
||||
### 清晰描述需求
|
||||
```
|
||||
❌ "做一个用户功能"
|
||||
✅ "实现用户注册、登录、个人信息查询和修改功能,需要JWT认证"
|
||||
|
||||
❌ "这个接口有问题"
|
||||
✅ "用户登录接口返回401错误,但是用户名密码正确"
|
||||
```
|
||||
|
||||
### 提供上下文信息
|
||||
```
|
||||
✅ "在现有的用户模块基础上添加头像上传功能"
|
||||
✅ "这个接口需要管理员权限才能访问"
|
||||
✅ "数据需要缓存到Redis,缓存时间1小时"
|
||||
```
|
||||
|
||||
### 明确期望结果
|
||||
```
|
||||
✅ "创建完整的用户CRUD接口,包含测试"
|
||||
✅ "只需要修改现有的查询接口,添加分页功能"
|
||||
✅ "优化这个接口的性能,响应时间控制在100ms内"
|
||||
```
|
||||
|
||||
## 错误处理和调试
|
||||
|
||||
### 当代码出现问题时
|
||||
|
||||
1. **我会主动分析**
|
||||
- 检查类型错误
|
||||
- 验证语法正确性
|
||||
- 确认导入导出关系
|
||||
|
||||
2. **提供修复方案**
|
||||
- 直接修复简单问题
|
||||
- 解释复杂问题的原因
|
||||
- 提供多种解决方案
|
||||
|
||||
3. **验证修复结果**
|
||||
- 确保修复后代码可运行
|
||||
- 检查是否引入新问题
|
||||
- 验证功能完整性
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
我会在适当时候提供:
|
||||
- 数据库查询优化
|
||||
- 缓存策略建议
|
||||
- 并发处理优化
|
||||
- 内存使用优化
|
||||
|
||||
## 质量保证
|
||||
|
||||
### 代码质量检查
|
||||
- [ ] 类型安全性
|
||||
- [ ] 错误处理完整性
|
||||
- [ ] 响应格式一致性
|
||||
- [ ] 命名规范符合标准
|
||||
- [ ] 注释文档完整
|
||||
|
||||
### 功能完整性检查
|
||||
- [ ] Schema定义完整
|
||||
- [ ] Response格式正确
|
||||
- [ ] Service逻辑完整
|
||||
- [ ] Controller路由正确
|
||||
- [ ] 测试用例覆盖
|
||||
|
||||
### 安全性检查
|
||||
- [ ] 参数验证到位
|
||||
- [ ] 认证授权正确
|
||||
- [ ] 敏感信息保护
|
||||
- [ ] SQL注入防护
|
||||
- [ ] XSS攻击防护
|
||||
|
||||
记住:我的目标是让你专注于业务逻辑,而我来确保代码的规范性、安全性和可维护性! 🎯
|
@ -1,174 +0,0 @@
|
||||
# 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结构)
|
@ -1,112 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
## Cursor 代码注释规范(Code Comment Rules)
|
||||
|
||||
### 1. 文件头部注释
|
||||
每个源文件开头应包含如下信息:
|
||||
```javascript
|
||||
/**
|
||||
* @file 文件简要说明
|
||||
* @author 创建者姓名(如:张三 <zhangsan@example.com>)
|
||||
* @date 创建时间(如:2024-06-01)
|
||||
* @lastEditor 最后修改人
|
||||
* @lastEditTime最后修改时间
|
||||
* @description 文件详细描述(可选)
|
||||
*/
|
||||
```
|
||||
|
||||
### 2. 函数/方法注释(JSDoc)
|
||||
- 每个公开函数、类、接口都应有 JSDoc 注释
|
||||
- 增加修改记录,包含修改人、修改时间、修改描述
|
||||
|
||||
**推荐标签:**
|
||||
- `@param` 参数说明
|
||||
- `@returns` 返回值说明
|
||||
- `@throws` 可能抛出的异常
|
||||
- `@deprecated` 弃用说明
|
||||
- `@example` 使用示例
|
||||
- `@modification` 修改记录(格式:修改人 修改时间 修改描述)
|
||||
|
||||
**示例:**
|
||||
```typescript
|
||||
/**
|
||||
* 计算两个数的和
|
||||
* @param a 第一个加数
|
||||
* @param b 第二个加数
|
||||
* @returns 两数之和
|
||||
* @example
|
||||
* add(1, 2) // 3
|
||||
* @modification 李四 2024-06-05 优化了参数校验
|
||||
*/
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 注释类型与风格
|
||||
- 单行注释:`//`,用于简短说明
|
||||
- 多行注释:`/* ... */`,用于较长描述
|
||||
- 文档注释(JSDoc):`/** ... */`,用于结构化说明
|
||||
- 注释应简洁明了,避免废话和重复代码内容
|
||||
- 注释内容使用中文或英文均可,但需统一
|
||||
- 代码变更时同步更新相关注释,避免注释与代码不符
|
||||
- 不要注释掉无用代码,直接删除,必要时可通过版本管理找回
|
||||
|
||||
### 4. 特殊标记
|
||||
- `TODO:` 需要补充或优化的内容
|
||||
- `FIXME:` 需要修复的问题
|
||||
- `HACK:` 临时解决方案,需后续优化
|
||||
|
||||
**示例:**
|
||||
```javascript
|
||||
// TODO: 优化此处的性能
|
||||
// FIXME: 这里有边界条件未处理
|
||||
// HACK: 临时绕过接口校验
|
||||
```
|
||||
|
||||
### 5. 变量注释
|
||||
|
||||
- 每一个变量遵照JSDoc添加注释,携带描述、用途
|
||||
|
||||
参照
|
||||
```ts
|
||||
/**
|
||||
* MySQL数据库连接配置
|
||||
* @property {string} host - 数据库主机地址
|
||||
* @property {number} port - 数据库端口号
|
||||
* @property {string} user - 数据库用户名
|
||||
* @property {string} password - 数据库密码
|
||||
* @property {string} database - 数据库名称
|
||||
*/
|
||||
export const dbConfig = {
|
||||
/** 数据库主机地址 */
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
/** 数据库端口号 */
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
/** 数据库用户名 */
|
||||
user: process.env.DB_USER || 'root',
|
||||
/** 数据库密码 */
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
/** 数据库名称 */
|
||||
database: process.env.DB_NAME || 'test',
|
||||
};
|
||||
```
|
||||
|
||||
### 6. 规范补充建议
|
||||
- 注释应随代码同步更新,避免"注释失效"或误导他人。
|
||||
- 代码评审时,建议同时检查注释的准确性和完整性。
|
||||
- 复杂算法、业务逻辑、边界处理、特殊依赖等务必详细注释。
|
||||
- 简单、易懂的代码无需过度注释,避免注释冗余。
|
||||
- 团队应约定注释统一使用中文或英文,避免混杂,提升协作效率。
|
||||
- 推荐使用 ESLint、TSLint 等工具结合注释相关插件(如 eslint-plugin-jsdoc)进行注释规范自动校验。
|
||||
- 可使用 IDE 插件(如 VSCode 的 JSDoc Generator)自动生成注释模板,提升效率。
|
||||
- 建议在项目根目录下提供注释模板(如 `.comment-templates`),便于新成员快速上手。
|
||||
- 注释中严禁出现密码、密钥、用户隐私等敏感信息。
|
||||
- 重要模块、核心业务建议将注释内容同步到项目文档,便于知识传承和查阅。
|
||||
|
||||
---
|
||||
|
||||
**请所有开发者严格遵守以上注释规范,提升代码可读性与可维护性。**
|
@ -1,110 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Elysia 后台接口 API 设计与校验规范
|
||||
|
||||
## 1. 接口文档自动生成(Swagger/OpenAPI)
|
||||
- 每个接口必须通过 `@elysiajs/swagger` 插件自动生成 Swagger 文档,便于前后端协作与接口管理。
|
||||
- 路由、参数、返回类型、错误码等需在 Swagger 中完整描述。
|
||||
- 推荐在开发环境默认启用 Swagger UI。
|
||||
|
||||
## 2. 参数与返回类型校验
|
||||
- 所有接口参数(body、query、params、headers)必须定义类型并进行严格校验。
|
||||
- 推荐使用Elysia原生t类型校验库。
|
||||
- 校验规则需明确字段必填、类型、长度、格式、范围等。
|
||||
- 校验失败时,接口需返回标准 JSON 错误响应,便于用户理解。
|
||||
- 返回类型也需定义并校验,保证接口契约。
|
||||
|
||||
## 3. 错误响应规范
|
||||
- 所有参数校验失败、类型不匹配等错误,需统一返回如下结构:
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "error message",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
- 错误码、错误信息需文档化,便于前后端协作。
|
||||
- 常见的公共的错误和正常相应,统一放在validators中的global.response.ts
|
||||
|
||||
|
||||
**注意,其他的请求参数和响应验证,在同一批接口下,放在同一个validators文件中**
|
||||
|
||||
|
||||
## 4. 示例代码
|
||||
|
||||
### 4.1 安装依赖
|
||||
```sh
|
||||
bun add elysia @elysiajs/swagger valibot
|
||||
```
|
||||
|
||||
### 4.2 接口定义与参数校验
|
||||
|
||||
```ts
|
||||
.get(
|
||||
'/note/:index',
|
||||
({ note, params: { index } }) => {
|
||||
return note.data[index]
|
||||
},
|
||||
{
|
||||
params: t.Object({
|
||||
index: t.Number()
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 4.3 调用接口示例
|
||||
- 请求:
|
||||
```json
|
||||
POST /api/user
|
||||
{
|
||||
"name": "A"
|
||||
}
|
||||
```
|
||||
- 响应:
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "姓名长度不能少于2个字符",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
- 请求:
|
||||
```json
|
||||
POST /api/user
|
||||
{
|
||||
"name": "张三丰丰丰丰丰丰丰丰"
|
||||
}
|
||||
```
|
||||
- 响应:
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "姓名长度不能超过8个字符",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
- 请求:
|
||||
```json
|
||||
POST /api/user
|
||||
{
|
||||
"name": "张三"
|
||||
}
|
||||
```
|
||||
- 响应:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "创建成功",
|
||||
"data": { "name": "张三" }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**请所有开发者严格遵守以上规范,确保接口文档完整、参数校验严格、错误提示友好(中文),提升用户体验与协作效率。**
|
@ -1,144 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Elysia(Bun.js)后端开发规范
|
||||
|
||||
## 1. 项目结构
|
||||
|
||||
- 推荐目录结构如下,分层清晰,便于维护和扩展:
|
||||
```
|
||||
├── src/
|
||||
│ ├── controllers/ # 路由与业务入口
|
||||
│ ├── services/ # 业务逻辑
|
||||
│ ├── models/ # 数据模型与类型定义
|
||||
│ ├── plugins/ # Elysia 插件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── validators/ # 参数校验,注意!!!所有的参数校验必须挡在此目录中,目录结构遵照路由结构
|
||||
│ ├── config/ # 配置文件
|
||||
│ ├── type/ # 类型定义文件
|
||||
│ └── app.ts # 应用入口
|
||||
├── tests/ # 测试用例
|
||||
├── public/ # 静态资源
|
||||
├── .env # 环境变量
|
||||
├── bun.lockb # Bun 依赖锁
|
||||
├── package.json
|
||||
└── README.md
|
||||
└── .git/ # Git版本控制目录,跟踪项目历史和协作
|
||||
```
|
||||
|
||||
## 2. 代码风格
|
||||
|
||||
- 强制使用 ESLint + Prettier 统一代码风格,TypeScript 项目建议使用 `@typescript-eslint`。
|
||||
- 文件、变量、函数、类命名采用小驼峰(camelCase),类名用大驼峰(PascalCase)。
|
||||
- 严格类型检查,禁止使用 `any`,优先使用类型推断和类型声明。
|
||||
- 每个文件、类、方法、复杂逻辑必须有规范注释(参考注释规范 rules)。
|
||||
- 代码提交前必须通过 lint 检查和格式化。
|
||||
|
||||
## 3. 路由与接口设计
|
||||
|
||||
- 路由统一注册在 `controllers` 目录下,按业务模块拆分。
|
||||
- 遵循 RESTful API 设计原则,接口资源命名清晰、语义化。
|
||||
- 路由统一使用小写、短横线分隔(如 `/api/v1/user-info`)。
|
||||
- GET 用于获取资源,POST 用于创建,PUT/PATCH 用于更新,DELETE 用于删除。
|
||||
- 所有接口返回统一的数据结构,例如:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 错误码、错误信息需文档化,便于前后端协作。
|
||||
|
||||
## 4. 参数校验
|
||||
|
||||
- 所有接口参数必须进行校验。
|
||||
- 校验逻辑统一放在 `validators` 目录,便于复用和维护。
|
||||
- 校验失败时返回标准错误响应,禁止直接抛出异常。
|
||||
|
||||
## 5. 中间件与插件
|
||||
|
||||
- 公共逻辑(如鉴权、日志、限流、CORS、错误处理等)必须通过中间件实现,统一注册在 `middlewares` 或 `plugins` 目录。
|
||||
- 推荐使用 Elysia 官方和社区插件(如 `@elysiajs/cors`、`@elysiajs/jwt`、`@elysiajs/swagger` 等)。
|
||||
- 自定义插件需有详细注释和文档。
|
||||
|
||||
## 6. 安全规范
|
||||
|
||||
- 禁止 SQL 注入、XSS、CSRF 等常见安全漏洞,使用 ORM/参数化查询。
|
||||
- 重要接口需鉴权(如 JWT、Session),敏感操作需权限校验。
|
||||
- 密码等敏感信息必须加密存储,严禁明文。
|
||||
- 配置信息(如密钥、数据库连接)使用环境变量管理,严禁写死在代码中。
|
||||
- 日志中不得记录敏感信息。
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
- 所有异常必须统一捕获和处理,返回标准错误响应。
|
||||
- 推荐全局错误处理中间件,记录错误日志,便于排查问题。
|
||||
- 错误响应结构示例:
|
||||
```json
|
||||
{
|
||||
"code": 1001,
|
||||
"message": "参数校验失败",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 日志与监控
|
||||
|
||||
- 统一使用日志库(如 pino、winston),区分 info、warn、error 等级。
|
||||
- 关键操作、异常、接口请求需有日志记录,便于排查问题。
|
||||
- 推荐接入监控系统(如 Prometheus、ELK)。
|
||||
|
||||
## 9. 测试
|
||||
|
||||
- 必须编写单元测试和集成测试,覆盖核心业务逻辑。
|
||||
- 推荐使用 Vitest(Bun 原生支持)、Jest 等测试框架。
|
||||
- 新增功能需同步补充测试用例,CI 阶段自动跑测试。
|
||||
- 测试代码与业务代码分离,统一放在 `tests` 目录。
|
||||
|
||||
## 10. 依赖与版本管理
|
||||
|
||||
- 依赖包需定期升级,避免使用过时或有安全漏洞的库。
|
||||
- 使用 `bun.lockb` 锁定依赖版本。
|
||||
- 不得将未使用的依赖、临时文件提交到仓库。
|
||||
|
||||
## 11. 持续集成与部署
|
||||
|
||||
- 推荐使用 CI 工具(如 GitHub Actions、GitLab CI)自动化测试、构建、部署。
|
||||
- 生产环境部署需有回滚机制,避免单点故障。
|
||||
- 环境变量、密钥等敏感信息通过 CI/CD 平台安全注入。
|
||||
|
||||
## 12. 文档与协作
|
||||
|
||||
- 所有接口、核心模块需有详细文档,推荐使用 OpenAPI/Swagger 自动生成接口文档(可用 `@elysiajs/swagger`)。
|
||||
- 重要变更需在 README/CHANGELOG 中记录。
|
||||
- 团队成员需遵守代码评审流程,确保代码质量。
|
||||
- 项目根目录应有完整的 README,包含启动、开发、测试、部署等说明。
|
||||
|
||||
## 13. 其他最佳实践
|
||||
|
||||
- 代码与注释同步更新,避免注释失效。
|
||||
- 复杂算法、业务逻辑、边界处理、特殊依赖等务必详细注释。
|
||||
- 严禁在注释、日志、代码中出现密码、密钥、用户隐私等敏感信息。
|
||||
- 推荐在项目根目录下提供注释模板和接口模板,便于新成员快速上手。
|
||||
- 重要模块、核心业务建议将注释内容同步到项目文档,便于知识传承和查阅。
|
||||
|
||||
## 14. 模块引入
|
||||
|
||||
- 全部使用路径别名,如`@/app` `@/config/db.config`
|
||||
- 注意更新 tsconfig.json bunfig.toml 等配置中关于路径别名的配置
|
||||
|
||||
## 15. 配置文件
|
||||
|
||||
- 配置请全部存放在config中,不要再其他文件使用process.env
|
||||
- 除了密码,所有配置都需要有默认值
|
||||
|
||||
## 16. 类型文件
|
||||
|
||||
- 所有的类型定义全部放在公共位置,type文件夹下,**不允许在其他文件中定义**
|
||||
|
||||
---
|
||||
|
||||
**请所有开发者严格遵守以上规范,保障 Elysia 后端服务的健壮性、安全性与可维护性。**
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,680 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Bun Elysia框架业务开发规则
|
||||
|
||||
|
||||
## 0. 概览
|
||||
|
||||
这是一个基于 **Bun + Elysia** 的现代化后端API项目,采用TypeScript开发,集成了MySQL、Redis、JWT认证、Swagger文档等功能。
|
||||
|
||||
- **运行时**:Bun
|
||||
- **框架**:Elysia
|
||||
- **语言**:TypeScript
|
||||
- **数据库**:MySQL + Drizzle ORM
|
||||
- **缓存**:Redis
|
||||
- **认证**:JWT
|
||||
- **测试**:Vitest
|
||||
- **文档**:Swagger
|
||||
- **日志**:Winston
|
||||
- **代码规范**:ESLint + Prettier
|
||||
|
||||
- 📂 根目录结构
|
||||
|
||||
```
|
||||
project/
|
||||
├── 📋 配置文件(config/)
|
||||
├── 📁 源代码 (src/)
|
||||
├── 📁 文档 (docs/)
|
||||
├── 📁 需求文档 (prd/)
|
||||
├── 📁 任务管理 (tasks/)
|
||||
├── 📁 AI对话记录 (aiChat/)
|
||||
└── 📁 静态资源 (public/)
|
||||
```
|
||||
|
||||
- 🔧 配置文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `package.json` | 项目依赖和脚本配置 |
|
||||
| `tsconfig.json` | TypeScript编译配置 |
|
||||
| `tsconfig.test.json` | 测试环境TypeScript配置 |
|
||||
| `vitest.config.ts` | Vitest测试框架配置 |
|
||||
| `eslint.config.js` | ESLint代码规范配置 |
|
||||
| `bunfig.toml` | Bun运行时配置 |
|
||||
| `bun.lock` | Bun依赖锁定文件 |
|
||||
| `README.md` | 项目说明文档 |
|
||||
|
||||
- 应用入口
|
||||
```
|
||||
src/
|
||||
├── app.ts # Elysia应用主入口
|
||||
└── server.ts # 服务器启动文件
|
||||
```
|
||||
|
||||
- 配置管理 (config/)
|
||||
```
|
||||
src/config/
|
||||
├── index.ts # 配置总入口
|
||||
├── db.config.ts # 数据库配置
|
||||
├── redis.config.ts # Redis配置
|
||||
├── jwt.config.ts # JWT配置
|
||||
└── logger.config.ts # 日志配置
|
||||
```
|
||||
|
||||
- 数据实体 (eneities/)
|
||||
```
|
||||
src/eneities/
|
||||
├── index.ts # 实体总入口
|
||||
└── users.ts # 用户实体定义
|
||||
```
|
||||
|
||||
- 业务模块 (modules/)
|
||||
```
|
||||
src/modules/
|
||||
├── index.ts # 模块总入口
|
||||
├── tags.ts # Swagger标签定义
|
||||
├── example/ # 示例模块
|
||||
│ ├── example.schema.ts # Schema定义
|
||||
│ ├── example.response.ts # 响应格式
|
||||
│ ├── example.service.ts # 业务逻辑
|
||||
│ ├── example.controller.ts # 路由控制器
|
||||
│ └── example.test.ts # 测试用例
|
||||
├── health/ # 健康检查模块
|
||||
│ ├── health.controller.ts
|
||||
│ └── health.service.ts
|
||||
├── user/ # 用户模块
|
||||
│ └── user.controller.ts
|
||||
└── test/ # 测试模块
|
||||
└── test.controller.ts
|
||||
```
|
||||
|
||||
- 插件系统 (plugins/)
|
||||
```
|
||||
src/plugins/
|
||||
├── index.ts # 插件总入口
|
||||
├── drizzle/ # 数据库ORM插件
|
||||
│ ├── drizzle.config.ts
|
||||
│ ├── drizzle.plugins.ts
|
||||
│ ├── drizzle.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
|
||||
```
|
||||
|
||||
- 类型定义 (type/)
|
||||
```
|
||||
src/type/
|
||||
├── config.type.ts # 配置相关类型
|
||||
├── drizzle.type.ts # 数据库相关类型
|
||||
├── error.type.ts # 错误相关类型
|
||||
├── jwt.type.ts # JWT相关类型
|
||||
├── logger.type.ts # 日志相关类型
|
||||
└── redis.type.ts # Redis相关类型
|
||||
```
|
||||
|
||||
- 工具函数 (utils/)
|
||||
```
|
||||
src/utils/
|
||||
├── deviceInfo.ts # 设备信息工具
|
||||
├── formatFileSize.ts # 文件大小格式化
|
||||
├── formatRoute.ts # 路由格式化
|
||||
├── jwt.helper.ts # JWT工具函数
|
||||
├── mysql.ts # MySQL工具
|
||||
├── randomChalk.ts # 随机颜色工具
|
||||
├── redis.ts # Redis工具
|
||||
├── text.ts # 文本处理工具
|
||||
└── response.helper.ts # 响应格式工具 (新增)
|
||||
```
|
||||
|
||||
- 验证器 (validators/)
|
||||
```
|
||||
src/validators/
|
||||
└── global.response.ts # 全局响应格式验证
|
||||
```
|
||||
|
||||
- 测试文件 (tests/)
|
||||
```
|
||||
src/tests/
|
||||
├── app.test.ts # 应用测试
|
||||
├── health.test.ts # 健康检查测试
|
||||
├── mysql.test.ts # MySQL测试
|
||||
├── redis.test.ts # Redis测试
|
||||
├── swagger.test.ts # Swagger测试
|
||||
└── demo/
|
||||
└── testLogger.ts # 日志测试演示
|
||||
```
|
||||
|
||||
- 常量定义 (constants/)
|
||||
```
|
||||
src/constants/
|
||||
└── error-codes.ts # 统一错误码定义 (新增)
|
||||
```
|
||||
|
||||
|
||||
## 1. 文件组织规范
|
||||
|
||||
### 1.1 必须的文件结构
|
||||
每个业务模块必须包含以下5个文件,**按照固定顺序**:
|
||||
|
||||
```
|
||||
src/modules/[module]/
|
||||
├── [module].schema.ts # 1️⃣ 数据结构定义(优先级最高)
|
||||
├── [module].response.ts # 2️⃣ 响应格式定义
|
||||
├── [module].service.ts # 3️⃣ 业务逻辑实现
|
||||
├── [module].controller.ts # 4️⃣ 路由控制器
|
||||
└── [module].test.ts # 5️⃣ 测试用例
|
||||
```
|
||||
|
||||
### 1.2 文件命名约定
|
||||
- 模块名使用 **单数形式**:`user`、`product`、`order`(不是 users、products)
|
||||
- 文件名格式:`[模块名].[类型].ts`
|
||||
- 导出名格式:`[模块名][类型名]`
|
||||
|
||||
## 2. Schema & 类型系统(🔥 重点)
|
||||
|
||||
### 2.1 Schema定义规范
|
||||
|
||||
```typescript
|
||||
// ✅ 正确示例 - user.schema.ts
|
||||
import { t, type Static } from 'elysia';
|
||||
|
||||
// 1. 定义Schema(运行时验证)
|
||||
export const CreateUserSchema = t.Object({
|
||||
username: t.String({
|
||||
minLength: 2,
|
||||
maxLength: 50,
|
||||
description: '用户名,2-50字符',
|
||||
examples: ['admin', 'testuser']
|
||||
}),
|
||||
email: t.String({
|
||||
format: 'email',
|
||||
description: '用户邮箱',
|
||||
examples: ['user@example.com']
|
||||
}),
|
||||
password: t.String({
|
||||
minLength: 6,
|
||||
description: '密码,至少6位',
|
||||
examples: ['123456']
|
||||
}),
|
||||
});
|
||||
|
||||
// 2. 导出TypeScript类型(编译时类型检查)
|
||||
export type CreateUserRequest = Static<typeof CreateUserSchema>;
|
||||
|
||||
// 3. 数据模型Schema
|
||||
export const UserSchema = t.Object({
|
||||
id: t.Number({ description: '用户ID' }),
|
||||
username: t.String({ description: '用户名' }),
|
||||
email: t.String({ description: '邮箱' }),
|
||||
createdAt: t.String({ description: '创建时间' }),
|
||||
updatedAt: t.String({ description: '更新时间' }),
|
||||
});
|
||||
|
||||
export type User = Static<typeof UserSchema>;
|
||||
```
|
||||
|
||||
### 2.2 类型导出规范
|
||||
|
||||
**必须遵循的命名模式:**
|
||||
- Request类型:`[动作][模块]Request` → `CreateUserRequest`
|
||||
- Response类型:`[动作][模块]Response` → `GetUserResponse`
|
||||
- 数据模型:`[模块]` → `User`、`Product`
|
||||
- Schema名:`[动作][模块]Schema` → `CreateUserSchema`
|
||||
|
||||
## 3. Response定义规范
|
||||
|
||||
### 3.1 统一响应格式
|
||||
|
||||
```typescript
|
||||
// ✅ 正确示例 - user.response.ts
|
||||
import { t, type Static } from 'elysia';
|
||||
import { globalResponseWrapperSchema } from '@/validators/global.response';
|
||||
import { UserSchema } from './user.schema';
|
||||
|
||||
// 成功响应
|
||||
export const GetUserSuccessResponseSchema = globalResponseWrapperSchema(UserSchema);
|
||||
export type GetUserSuccessResponse = Static<typeof GetUserSuccessResponseSchema>;
|
||||
|
||||
// 错误响应
|
||||
export const UserNotFoundResponseSchema = t.Object({
|
||||
code: t.Literal('USER_NOT_FOUND'),
|
||||
message: t.String({ examples: ['用户不存在'] }),
|
||||
data: t.Null(),
|
||||
});
|
||||
export type UserNotFoundResponse = Static<typeof UserNotFoundResponseSchema>;
|
||||
|
||||
// 组合响应(供controller使用)
|
||||
export const GetUserResponses = {
|
||||
200: GetUserSuccessResponseSchema,
|
||||
404: UserNotFoundResponseSchema,
|
||||
401: t.Object({
|
||||
code: t.Literal('UNAUTHORIZED'),
|
||||
message: t.String(),
|
||||
data: t.Null(),
|
||||
}),
|
||||
};
|
||||
```
|
||||
|
||||
## 4. Service层规范
|
||||
|
||||
### 4.1 Service类定义
|
||||
|
||||
```typescript
|
||||
// ✅ 正确示例 - user.service.ts
|
||||
import type { CreateUserRequest, User } from './user.schema';
|
||||
import type { GetUserSuccessResponse, UserNotFoundResponse } from './user.response';
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import { ERROR_CODES } from '@/constants/error-codes';
|
||||
|
||||
export class UserService {
|
||||
/**
|
||||
* 创建用户
|
||||
* @param request 创建用户请求参数
|
||||
* @returns Promise<GetUserSuccessResponse>
|
||||
*/
|
||||
async createUser(request: CreateUserRequest): Promise<GetUserSuccessResponse> {
|
||||
try {
|
||||
Logger.info(`创建用户请求:${JSON.stringify(request)}`);
|
||||
|
||||
// 业务逻辑实现
|
||||
const user: User = {
|
||||
id: 1,
|
||||
username: request.username,
|
||||
email: request.email,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 进行数据库操作逻辑...
|
||||
|
||||
Logger.info(`用户创建成功:${user.id}`);
|
||||
|
||||
return {
|
||||
code: ERROR_CODES.SUCCESS,
|
||||
message: '用户创建成功',
|
||||
data: user,
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`创建用户失败:${error}`));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
* @param id 用户ID
|
||||
* @returns Promise<GetUserSuccessResponse | UserNotFoundResponse>
|
||||
*/
|
||||
async getUserById(id: number): Promise<GetUserSuccessResponse | UserNotFoundResponse> {
|
||||
// 实现逻辑...
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const userService = new UserService();
|
||||
```
|
||||
|
||||
### 4.2 Service层要求
|
||||
|
||||
- ✅ 所有方法必须有完整的类型注解
|
||||
- ✅ 所有方法必须有JSDoc注释
|
||||
- ✅ 必须有详细的日志记录
|
||||
- ✅ 必须有错误处理
|
||||
- ✅ 导出单例实例供controller使用
|
||||
|
||||
## 5. Controller层规范
|
||||
|
||||
### 5.1 Controller定义
|
||||
|
||||
```typescript
|
||||
// ✅ 正确示例 - user.controller.ts
|
||||
import { Elysia } from 'elysia';
|
||||
import { jwtAuthPlugin } from '@/plugins/jwt/jwt.plugins';
|
||||
import { CreateUserSchema } from './user.schema';
|
||||
import { GetUserResponses } from './user.response';
|
||||
import { userService } from './user.service';
|
||||
import { tags } from '@/modules/tags';
|
||||
|
||||
export const userController = new Elysia({ prefix: '/user' })
|
||||
/**
|
||||
* 创建用户
|
||||
* @route POST /api/user
|
||||
*/
|
||||
.post(
|
||||
'/',
|
||||
({ body }) => userService.createUser(body);,
|
||||
{
|
||||
body: CreateUserSchema,
|
||||
detail: {
|
||||
summary: '创建用户',
|
||||
description: '创建新用户账户',
|
||||
tags: [tags.user],
|
||||
},
|
||||
response: GetUserResponses,
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取用户信息(需要认证)
|
||||
* @route GET /api/user/:id
|
||||
*/
|
||||
.use(jwtAuthPlugin)
|
||||
.get(
|
||||
'/:id',
|
||||
async ({ params }) => userService.getUserById(Number(params.id));,
|
||||
{
|
||||
params: t.Object({
|
||||
id: t.Numeric({ description: '用户ID' })
|
||||
}),
|
||||
detail: {
|
||||
summary: '获取用户信息',
|
||||
description: '根据用户ID获取用户详细信息',
|
||||
tags: [tags.user],
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
response: GetUserResponses,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## 6. 错误处理规范
|
||||
|
||||
### 6.1 统一错误码
|
||||
|
||||
```typescript
|
||||
// src/constants/error-codes.ts
|
||||
export const ERROR_CODES = {
|
||||
SUCCESS: 'SUCCESS',
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||
FORBIDDEN: 'FORBIDDEN',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
BUSINESS_ERROR: 'BUSINESS_ERROR',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 6.2 错误处理模式
|
||||
|
||||
```typescript
|
||||
// Service层
|
||||
try {
|
||||
// 业务逻辑
|
||||
const result = await someOperation();
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`操作失败:${error}`));
|
||||
|
||||
if (error instanceof ValidationError) {
|
||||
throw new BusinessError('参数验证失败', ERROR_CODES.VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
throw new InternalError('内部服务错误', ERROR_CODES.INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
// Controller层
|
||||
try {
|
||||
const result = await service.method();
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof BusinessError) {
|
||||
set.status = 400;
|
||||
return errorResponse(error.code, error.message);
|
||||
}
|
||||
|
||||
set.status = 500;
|
||||
return errorResponse(ERROR_CODES.INTERNAL_ERROR, '服务器内部错误');
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 测试规范
|
||||
|
||||
### 7.1 测试文件结构
|
||||
|
||||
```typescript
|
||||
// ✅ 正确示例 - user.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { app } from '@/app';
|
||||
import type { CreateUserRequest } from './user.schema';
|
||||
|
||||
describe('User API', () => {
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 设置测试环境
|
||||
authToken = 'test-jwt-token';
|
||||
});
|
||||
|
||||
describe('POST /api/user', () => {
|
||||
it('应该成功创建用户', async () => {
|
||||
const payload: CreateUserRequest = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: '123456',
|
||||
};
|
||||
|
||||
const response = await app
|
||||
.handle(new Request('http://localhost/api/user', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const result = await response.json();
|
||||
expect(result.code).toBe('SUCCESS');
|
||||
expect(result.data.username).toBe(payload.username);
|
||||
});
|
||||
|
||||
it('应该验证必填字段', async () => {
|
||||
const payload = { username: 'test' }; // 缺少email和password
|
||||
|
||||
const response = await app
|
||||
.handle(new Request('http://localhost/api/user', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}));
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 8. AI助手协作规范
|
||||
|
||||
### 8.1 注释规范(关键❗️)
|
||||
|
||||
|
||||
|
||||
#### 1. 文件头部注释
|
||||
每个源文件开头应包含如下信息:
|
||||
```javascript
|
||||
/**
|
||||
* @file 文件简要说明
|
||||
* @author 创建者姓名(如:张三 <zhangsan@example.com>)
|
||||
* @date 创建时间(如:2024-06-01)
|
||||
* @lastEditor 最后修改人
|
||||
* @lastEditTime最后修改时间
|
||||
* @description 文件详细描述(可选)
|
||||
*/
|
||||
```
|
||||
|
||||
#### 2. 函数/方法/请求注释(JSDoc)
|
||||
- 每个公开函数、类、接口都应有 JSDoc 注释
|
||||
- 增加修改记录,包含修改人、修改时间、修改描述
|
||||
|
||||
**推荐标签:**
|
||||
- `@param` 参数说明
|
||||
- `@returns` 返回值说明
|
||||
- `@throws` 可能抛出的异常
|
||||
- `@deprecated` 弃用说明
|
||||
- `@example` 使用示例
|
||||
- `@modification` 修改记录(格式:修改人 修改时间 修改描述)
|
||||
|
||||
|
||||
|
||||
**示例:**
|
||||
```typescript
|
||||
/**
|
||||
* 计算两个数的和
|
||||
* @param a 第一个加数
|
||||
* @param b 第二个加数
|
||||
* @returns 两数之和
|
||||
* @example
|
||||
* add(1, 2) // 3
|
||||
* @modification 李四 2024-06-05 优化了参数校验
|
||||
*/
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 方法功能说明
|
||||
* @route HTTP方法 路径
|
||||
* @description 详细描述
|
||||
* @param 参数名 参数说明
|
||||
* @returns 返回值类型和说明
|
||||
* @throws 可能抛出的异常
|
||||
* @example 使用示例
|
||||
* @modification 作者 日期 修改说明
|
||||
*/
|
||||
```
|
||||
|
||||
#### 3. 注释类型与风格
|
||||
- 单行注释:`//`,用于简短说明
|
||||
- 多行注释:`/* ... */`,用于较长描述
|
||||
- 文档注释(JSDoc):`/** ... */`,用于结构化说明
|
||||
- 注释应简洁明了,避免废话和重复代码内容
|
||||
- 注释内容使用中文或英文均可,但需统一
|
||||
- 代码变更时同步更新相关注释,避免注释与代码不符
|
||||
- 不要注释掉无用代码,直接删除,必要时可通过版本管理找回
|
||||
|
||||
#### 4. 特殊标记
|
||||
- `TODO:` 需要补充或优化的内容
|
||||
- `FIXME:` 需要修复的问题
|
||||
- `HACK:` 临时解决方案,需后续优化
|
||||
|
||||
**示例:**
|
||||
```javascript
|
||||
// TODO: 优化此处的性能
|
||||
// FIXME: 这里有边界条件未处理
|
||||
// HACK: 临时绕过接口校验
|
||||
```
|
||||
|
||||
#### 5. 变量注释
|
||||
|
||||
- 每一个变量遵照JSDoc添加注释,携带描述、用途
|
||||
|
||||
参照
|
||||
```ts
|
||||
/**
|
||||
* MySQL数据库连接配置
|
||||
* @property {string} host - 数据库主机地址
|
||||
* @property {number} port - 数据库端口号
|
||||
* @property {string} user - 数据库用户名
|
||||
* @property {string} password - 数据库密码
|
||||
* @property {string} database - 数据库名称
|
||||
*/
|
||||
export const dbConfig = {
|
||||
/** 数据库主机地址 */
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
/** 数据库端口号 */
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
/** 数据库用户名 */
|
||||
user: process.env.DB_USER || 'root',
|
||||
/** 数据库密码 */
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
/** 数据库名称 */
|
||||
database: process.env.DB_NAME || 'test',
|
||||
};
|
||||
```
|
||||
|
||||
#### 6. 规范补充建议
|
||||
- 注释应随代码同步更新,避免"注释失效"或误导他人。
|
||||
- 代码评审时,建议同时检查注释的准确性和完整性。
|
||||
- 复杂算法、业务逻辑、边界处理、特殊依赖等务必详细注释。
|
||||
- 简单、易懂的代码无需过度注释,避免注释冗余。
|
||||
- 团队应约定注释统一使用中文或英文,避免混杂,提升协作效率。
|
||||
- 推荐使用 ESLint、TSLint 等工具结合注释相关插件(如 eslint-plugin-jsdoc)进行注释规范自动校验。
|
||||
- 可使用 IDE 插件(如 VSCode 的 JSDoc Generator)自动生成注释模板,提升效率。
|
||||
- 建议在项目根目录下提供注释模板(如 `.comment-templates`),便于新成员快速上手。
|
||||
- 注释中严禁出现密码、密钥、用户隐私等敏感信息。
|
||||
- 重要模块、核心业务建议将注释内容同步到项目文档,便于知识传承和查阅。
|
||||
|
||||
### 8.2 代码组织原则
|
||||
|
||||
1. **单一职责**:每个文件只负责一个清晰的功能
|
||||
2. **依赖注入**:通过构造函数或导入明确依赖关系
|
||||
3. **类型优先**:先定义Schema和类型,再实现逻辑
|
||||
4. **错误优先**:优先考虑错误处理和边界情况
|
||||
5. **测试驱动**:每个功能都有对应的测试用例
|
||||
|
||||
### 8.3 命名约定总结
|
||||
|
||||
| 类型 | 格式 | 示例 |
|
||||
|------|------|------|
|
||||
| 文件名 | `[module].[type].ts` | `user.controller.ts` |
|
||||
| Schema | `[Action][Module]Schema` | `CreateUserSchema` |
|
||||
| Type | `[Action][Module][Type]` | `CreateUserRequest` |
|
||||
| Service类 | `[Module]Service` | `UserService` |
|
||||
| Service实例 | `[module]Service` | `userService` |
|
||||
| Controller | `[module]Controller` | `userController` |
|
||||
|
||||
## 9. 快速检查清单
|
||||
|
||||
开发新功能时,按此顺序检查:
|
||||
|
||||
- [ ] 1. Schema定义完整(包含验证规则和示例)
|
||||
- [ ] 2. 类型导出正确(Request/Response类型)
|
||||
- [ ] 3. Response格式统一(成功/错误响应)
|
||||
- [ ] 4. Service类型注解完整
|
||||
- [ ] 5. Controller错误处理完整
|
||||
- [ ] 6. 测试用例覆盖主要场景
|
||||
- [ ] 7. JSDoc注释完整
|
||||
- [ ] 8. 日志记录到位
|
||||
|
||||
## 10. 最佳实践
|
||||
|
||||
### 10.1 性能优化
|
||||
- 使用连接池管理数据库连接
|
||||
- 实现合理的缓存策略
|
||||
- 避免N+1查询问题
|
||||
|
||||
### 10.2 安全考虑
|
||||
- 输入验证和清理
|
||||
- 适当的认证和授权
|
||||
- 敏感信息不记录日志
|
||||
|
||||
### 10.3 监控和日志
|
||||
- 关键操作必须有日志
|
||||
- 错误信息要有足够上下文
|
||||
- 性能敏感操作要有监控
|
||||
|
||||
这套规则确保了代码的一致性、可维护性和AI友好性,让我能够更高效地理解和协助你的开发工作。
|
||||
|
||||
## 11. 模块引入
|
||||
|
||||
- 全部使用路径别名,如`@/app` `@/config/db.config`
|
||||
- 注意更新 tsconfig.json bunfig.toml 等配置中关于路径别名的配置
|
||||
|
||||
---
|
||||
|
||||
**请所有开发者严格遵守以上规范,保障 Elysia 接口的一致性、后端服务的健壮性、安全性与可维护性。**
|
@ -1,108 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Elysia(Bun.js)接口开发测试规范
|
||||
|
||||
## 1. 测试类型与原则
|
||||
|
||||
- **单元测试**:聚焦于最小功能单元(如函数、service),保证核心逻辑正确。
|
||||
- **接口测试**:验证 HTTP API 的输入输出、边界条件、异常处理,确保接口契约。
|
||||
- **性能测试**:评估接口在高并发、大数据量下的响应速度与稳定性。
|
||||
- **基本原则**:
|
||||
- 测试应小而快、独立、可重复、可读。
|
||||
- 测试覆盖正常流程、边界条件、异常分支。
|
||||
- 测试代码与业务代码同等重要,需持续维护。
|
||||
|
||||
## 2. 目录结构建议
|
||||
|
||||
```
|
||||
├── src/
|
||||
├── tests/
|
||||
│ ├── unit/ # 单元测试
|
||||
│ ├── api/ # 接口测试
|
||||
│ └── performance/ # 性能测试
|
||||
```
|
||||
|
||||
- 测试文件与被测模块一一对应,命名规范如 `xxx.spec.ts` 或 `xxx.test.ts`。
|
||||
|
||||
## 3. 工具推荐
|
||||
|
||||
- **单元/接口测试**:推荐 [Vitest](mdc:https:/vitest.dev)(Bun 原生支持,兼容 Jest 语法)。
|
||||
- **接口请求模拟**:可用 [undici](mdc:https:/github.com/nodejs/undici)、[supertest](mdc:https:/github.com/ladjs/supertest) 等。
|
||||
- **Mock/Stub**:推荐 [sinon](mdc:https:/sinonjs.org)、[msw](mdc:https:/mswjs.io) 等。
|
||||
- **性能测试**:推荐 [autocannon](mdc:https:/github.com/mcollina/autocannon)、[wrk](mdc:https:/github.com/wg/wrk)。
|
||||
- **覆盖率统计**:集成 c8、nyc 或 Vitest 内置覆盖率。
|
||||
|
||||
## 4. 单元测试规范
|
||||
|
||||
- 每个 service、util、核心函数都应有单元测试。
|
||||
- 用 Arrange-Act-Assert(AAA)模式编写:准备数据、执行逻辑、断言结果。
|
||||
- 对外部依赖(如数据库、第三方服务)使用 mock,保证测试纯粹。
|
||||
- 用例命名清晰,表达测试目的。
|
||||
- 示例:
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { sum } from '../../src/utils/sum'
|
||||
|
||||
describe('sum', () => {
|
||||
it('should return 3 when 1 + 2', () => {
|
||||
expect(sum(1, 2)).toBe(3)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 5. 接口测试规范
|
||||
|
||||
- 覆盖所有 API 路由的正常、异常、边界场景。
|
||||
- 启动 Elysia 实例,使用 supertest/undici 发起 HTTP 请求,断言响应。
|
||||
- 可用 mock 数据库、mock token 等方式隔离外部依赖。
|
||||
- 推荐自动化生成接口文档与测试用例(如结合 Swagger/OpenAPI)。
|
||||
- 示例:
|
||||
```typescript
|
||||
import { app } from '../../src/app'
|
||||
import request from 'supertest'
|
||||
|
||||
describe('GET /api/v1/hello', () => {
|
||||
it('should return hello world', async () => {
|
||||
const res = await request(app.listen()).get('/api/v1/hello')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.message).toBe('hello world')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 6. 性能测试规范
|
||||
|
||||
- 关键接口需定期进行性能压测,评估 QPS、延迟、并发等指标。
|
||||
- 使用 autocannon/wrk 等工具模拟高并发请求,记录响应时间、错误率。
|
||||
- 性能基线与目标需文档化,便于回归对比。
|
||||
- 示例命令:
|
||||
```sh
|
||||
autocannon -c 100 -d 30 http://localhost:3000/api/v1/hello
|
||||
```
|
||||
- 性能测试脚本与结果建议归档在 `tests/performance/` 目录。
|
||||
|
||||
## 7. Mock 与数据隔离
|
||||
|
||||
- 测试用例中对数据库、缓存、外部 API 等依赖统一 mock/stub,避免真实调用。
|
||||
- 测试数据应自动生成或在每次测试前清理,保证测试独立。
|
||||
- 推荐使用内存数据库或 mock server 进行隔离。
|
||||
|
||||
## 8. 持续集成与覆盖率
|
||||
|
||||
- 测试必须集成到 CI 流程,提交/合并前自动运行。
|
||||
- 要求单元测试覆盖率不低于 80%,核心业务代码 100%。
|
||||
- 覆盖率报告需归档,便于追踪。
|
||||
|
||||
## 9. 其他建议
|
||||
|
||||
- 测试代码应有规范注释,便于理解和维护。
|
||||
- 重要 bug 修复需补充回归测试。
|
||||
- 推荐采用 TDD(测试驱动开发)提升设计质量。
|
||||
- 测试失败时应优先修复,保证主干分支始终通过所有测试。
|
||||
|
||||
---
|
||||
|
||||
**请所有开发者严格遵守以上测试规范,保障 Elysia 项目的高质量交付。**
|
@ -1,104 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Elysia + Vitest 测试用例编写规范
|
||||
|
||||
## 1. 文件结构与命名
|
||||
|
||||
- 每个被测模块应有对应的 `xxx.test.ts` 文件,放在同目录或 `tests/` 下。
|
||||
- 测试文件需有文件头注释,说明作者、日期、描述等。
|
||||
|
||||
## 2. 测试用例基本格式
|
||||
|
||||
- 使用 `describe` 分组,`it`(或 `test`)描述单个场景。
|
||||
- 每个 `it` 需有明确的行为描述。
|
||||
|
||||
## 3. 参数传递与类型
|
||||
|
||||
- 明确传递参数类型,推荐用 TypeBox schema 自动推导类型。
|
||||
- 测试用例中参数应覆盖正常、异常、边界值。
|
||||
|
||||
## 4. 响应结构与 code 校验
|
||||
|
||||
- 断言 HTTP 状态码(如 200、400、401)。
|
||||
- 断言响应体结构(如 code、message、data),类型可用 `as any` 或 schema 类型断言。
|
||||
|
||||
## 5. 边界与异常测试
|
||||
|
||||
- 必须覆盖参数边界(如最短/最长用户名、最短密码等)。
|
||||
- 必须覆盖异常分支(如缺少参数、token 错误、未授权等)。
|
||||
|
||||
## 6. 类型安全
|
||||
|
||||
- 推荐用 TypeBox 的 `Static<typeof schema>` 推导类型,或在测试用例中用 `as any` 简化断言。
|
||||
- 对于复杂响应,可定义类型辅助断言。
|
||||
|
||||
## 7. 断言范围
|
||||
|
||||
- 断言应覆盖:HTTP 状态码、响应 code、message、data 字段、关键数据类型。
|
||||
- 对于 token、id 等动态值,用 `typeof` 或正则断言。
|
||||
|
||||
## 8. 其他建议
|
||||
|
||||
- 用 `it.only`/`describe.only` 聚焦调试,提交前移除。
|
||||
- 用 `console.log` 输出实际响应,便于排查失败。
|
||||
- 每个接口的正常、异常、边界场景都应有测试覆盖。
|
||||
|
||||
## 9. 示例
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @file 用户登录接口测试
|
||||
* @author hotok
|
||||
* @date 2025-06-28
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-28
|
||||
* @description 覆盖登录接口的正常、异常、边界场景
|
||||
*/
|
||||
|
||||
describe('POST /api/login', () => {
|
||||
it('应登录成功并返回token', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: '123456' }),
|
||||
}),
|
||||
);
|
||||
const body = (await res.json()) as any;
|
||||
expect(res.status).toBe(200);
|
||||
expect(body.code).toBe(0);
|
||||
expect(typeof body.data.token).toBe('string');
|
||||
});
|
||||
|
||||
it('用户名过短应返回400', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'a', password: '123456' }),
|
||||
}),
|
||||
);
|
||||
const body = (await res.json()) as any;
|
||||
expect(res.status).toBe(400);
|
||||
expect(body.code).toBe(400);
|
||||
expect(body.message).toMatch(/用户名长度/);
|
||||
});
|
||||
|
||||
it('密码错误应返回400', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'wrong' }),
|
||||
}),
|
||||
);
|
||||
const body = (await res.json()) as any;
|
||||
expect(res.status).toBe(400);
|
||||
expect(body.code).toBe(400);
|
||||
expect(body.message).toBe('用户名或密码错误');
|
||||
});
|
||||
});
|
||||
```
|
500
database_export.sql
Normal file
500
database_export.sql
Normal file
@ -0,0 +1,500 @@
|
||||
-- =============================================
|
||||
-- 星撰个人综合平台 - 基础用户系统数据库结构导出
|
||||
-- 版本: M2 基础用户系统
|
||||
-- 生成时间: 2024-12-30
|
||||
-- 数据库: MySQL 8.0+
|
||||
-- =============================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- =============================================
|
||||
-- 1. 用户表 (sys_users)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_users`;
|
||||
CREATE TABLE `sys_users` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键,雪花ID',
|
||||
`username` VARCHAR(50) NOT NULL COMMENT '用户名,唯一',
|
||||
`email` VARCHAR(100) NOT NULL COMMENT '邮箱,唯一',
|
||||
`mobile` VARCHAR(20) NULL COMMENT '手机号',
|
||||
`password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希值',
|
||||
`avatar` VARCHAR(255) NULL COMMENT '头像URL',
|
||||
`nickname` VARCHAR(100) NULL COMMENT '昵称',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-正常,inactive-未激活,locked-锁定,disabled-禁用',
|
||||
`gender` TINYINT NULL DEFAULT 0 COMMENT '性别:0-未知,1-男,2-女',
|
||||
`birthday` DATE NULL COMMENT '生日',
|
||||
`bio` VARCHAR(500) NULL COMMENT '个人简介',
|
||||
`login_count` INT NOT NULL DEFAULT 0 COMMENT '登录次数',
|
||||
`last_login_at` DATETIME NULL COMMENT '最后登录时间',
|
||||
`last_login_ip` VARCHAR(45) NULL COMMENT '最后登录IP',
|
||||
`failed_attempts` INT NOT NULL DEFAULT 0 COMMENT '连续失败尝试次数',
|
||||
`locked_until` DATETIME NULL COMMENT '锁定截止时间',
|
||||
`is_root` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否超级管理员',
|
||||
`extra` JSON NULL COMMENT '扩展信息,JSON格式',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT NULL COMMENT '更新人ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间,软删除标记',
|
||||
`version` INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- 用户表索引
|
||||
CREATE UNIQUE INDEX `uk_username` ON `sys_users` (`username`, `deleted_at`);
|
||||
CREATE UNIQUE INDEX `uk_email` ON `sys_users` (`email`, `deleted_at`);
|
||||
CREATE INDEX `idx_mobile` ON `sys_users` (`mobile`);
|
||||
CREATE INDEX `idx_status` ON `sys_users` (`status`);
|
||||
CREATE INDEX `idx_created_at` ON `sys_users` (`created_at`);
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_users` (`deleted_at`);
|
||||
CREATE INDEX `idx_is_root` ON `sys_users` (`is_root`);
|
||||
CREATE INDEX `idx_last_login` ON `sys_users` (`last_login_at`);
|
||||
|
||||
-- =============================================
|
||||
-- 2. 角色表 (sys_roles)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_roles`;
|
||||
CREATE TABLE `sys_roles` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`code` VARCHAR(50) NOT NULL COMMENT '角色代码,唯一',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '角色名称',
|
||||
`description` TEXT NULL COMMENT '角色描述',
|
||||
`pid` BIGINT NULL DEFAULT 0 COMMENT '父角色ID,0表示顶级',
|
||||
`path` VARCHAR(500) NULL COMMENT '层级路径,如:/1/2/3/',
|
||||
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '同级排序号',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用',
|
||||
`is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统内置角色',
|
||||
`permissions_snapshot` JSON NULL COMMENT '权限快照,用于优化查询',
|
||||
`extra` JSON NULL COMMENT '扩展信息',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT NULL COMMENT '更新人ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
`version` INT NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
|
||||
|
||||
-- 角色表索引
|
||||
CREATE UNIQUE INDEX `uk_code` ON `sys_roles` (`code`, `deleted_at`);
|
||||
CREATE INDEX `idx_name` ON `sys_roles` (`name`);
|
||||
CREATE INDEX `idx_pid` ON `sys_roles` (`pid`);
|
||||
CREATE INDEX `idx_path` ON `sys_roles` (`path`);
|
||||
CREATE INDEX `idx_status` ON `sys_roles` (`status`);
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_roles` (`deleted_at`);
|
||||
CREATE INDEX `idx_is_system` ON `sys_roles` (`is_system`);
|
||||
CREATE INDEX `idx_sort` ON `sys_roles` (`pid`, `sort_order`);
|
||||
|
||||
-- =============================================
|
||||
-- 3. 权限表 (sys_permissions)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_permissions`;
|
||||
CREATE TABLE `sys_permissions` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`code` VARCHAR(100) NOT NULL COMMENT '权限代码,唯一',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '权限名称',
|
||||
`type` VARCHAR(20) NOT NULL COMMENT '权限类型:menu-菜单,button-按钮,api-接口,data-数据',
|
||||
`resource` VARCHAR(50) NULL COMMENT '资源标识,如:user,role,post',
|
||||
`action` VARCHAR(50) NULL COMMENT '操作标识,如:read,create,update,delete',
|
||||
`description` TEXT NULL COMMENT '权限描述',
|
||||
`pid` BIGINT NULL DEFAULT 0 COMMENT '父权限ID',
|
||||
`path` VARCHAR(500) NULL COMMENT '层级路径',
|
||||
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态',
|
||||
`meta` JSON NULL COMMENT '元数据,如:图标、路由等',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT NULL COMMENT '更新人ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';
|
||||
|
||||
-- 权限表索引
|
||||
CREATE UNIQUE INDEX `uk_code` ON `sys_permissions` (`code`, `deleted_at`);
|
||||
CREATE INDEX `idx_type` ON `sys_permissions` (`type`);
|
||||
CREATE INDEX `idx_resource_action` ON `sys_permissions` (`resource`, `action`);
|
||||
CREATE INDEX `idx_pid` ON `sys_permissions` (`pid`);
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_permissions` (`deleted_at`);
|
||||
CREATE INDEX `idx_status` ON `sys_permissions` (`status`);
|
||||
CREATE INDEX `idx_sort` ON `sys_permissions` (`pid`, `sort_order`);
|
||||
|
||||
-- =============================================
|
||||
-- 4. 组织架构表 (sys_organizations)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_organizations`;
|
||||
CREATE TABLE `sys_organizations` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`code` VARCHAR(100) NOT NULL COMMENT '组织代码,唯一',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '组织名称',
|
||||
`full_name` VARCHAR(200) NULL COMMENT '组织全称',
|
||||
`description` TEXT NULL COMMENT '组织描述',
|
||||
`pid` BIGINT NULL DEFAULT 0 COMMENT '父组织ID',
|
||||
`path` VARCHAR(500) NULL COMMENT '层级路径',
|
||||
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度',
|
||||
`type` VARCHAR(20) NULL COMMENT '组织类型:company,department,team',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`leader_id` BIGINT NULL COMMENT '负责人ID',
|
||||
`address` VARCHAR(200) NULL COMMENT '地址',
|
||||
`phone` VARCHAR(50) NULL COMMENT '联系电话',
|
||||
`extra` JSON NULL COMMENT '扩展信息',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT NULL COMMENT '更新人ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
`version` INT NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织架构表';
|
||||
|
||||
-- 组织架构表索引
|
||||
CREATE UNIQUE INDEX `uk_code` ON `sys_organizations` (`code`, `deleted_at`);
|
||||
CREATE INDEX `idx_name` ON `sys_organizations` (`name`);
|
||||
CREATE INDEX `idx_pid` ON `sys_organizations` (`pid`);
|
||||
CREATE INDEX `idx_path` ON `sys_organizations` (`path`);
|
||||
CREATE INDEX `idx_type` ON `sys_organizations` (`type`);
|
||||
CREATE INDEX `idx_leader_id` ON `sys_organizations` (`leader_id`);
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_organizations` (`deleted_at`);
|
||||
CREATE INDEX `idx_status` ON `sys_organizations` (`status`);
|
||||
CREATE INDEX `idx_sort` ON `sys_organizations` (`pid`, `sort_order`);
|
||||
|
||||
-- =============================================
|
||||
-- 5. 字典类型表 (sys_dict_types)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_dict_types`;
|
||||
CREATE TABLE `sys_dict_types` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`code` VARCHAR(50) NOT NULL COMMENT '字典类型代码,唯一',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '字典类型名称',
|
||||
`description` TEXT NULL COMMENT '描述',
|
||||
`pid` BIGINT NULL DEFAULT 0 COMMENT '父字典类型ID,支持字典分类',
|
||||
`path` VARCHAR(500) NULL COMMENT '层级路径',
|
||||
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用',
|
||||
`is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统内置',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT NULL COMMENT '更新人ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典类型表';
|
||||
|
||||
-- 字典类型表索引
|
||||
CREATE UNIQUE INDEX `uk_code` ON `sys_dict_types` (`code`, `deleted_at`);
|
||||
CREATE INDEX `idx_name` ON `sys_dict_types` (`name`);
|
||||
CREATE INDEX `idx_pid` ON `sys_dict_types` (`pid`);
|
||||
CREATE INDEX `idx_path` ON `sys_dict_types` (`path`);
|
||||
CREATE INDEX `idx_status` ON `sys_dict_types` (`status`);
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_dict_types` (`deleted_at`);
|
||||
CREATE INDEX `idx_is_system` ON `sys_dict_types` (`is_system`);
|
||||
CREATE INDEX `idx_sort` ON `sys_dict_types` (`pid`, `sort_order`);
|
||||
|
||||
-- =============================================
|
||||
-- 6. 字典项表 (sys_dict_items)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_dict_items`;
|
||||
CREATE TABLE `sys_dict_items` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`type_id` BIGINT NOT NULL COMMENT '字典类型ID',
|
||||
`item_key` VARCHAR(50) NOT NULL COMMENT '字典项键',
|
||||
`item_value` VARCHAR(200) NOT NULL COMMENT '字典项值',
|
||||
`label` VARCHAR(100) NOT NULL COMMENT '显示标签',
|
||||
`label_en` VARCHAR(200) NULL COMMENT '英文标签',
|
||||
`description` TEXT NULL COMMENT '描述',
|
||||
`pid` BIGINT NULL DEFAULT 0 COMMENT '父字典项ID,支持树形字典',
|
||||
`path` VARCHAR(500) NULL COMMENT '层级路径',
|
||||
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态',
|
||||
`css_class` VARCHAR(50) NULL COMMENT 'CSS样式类',
|
||||
`color` VARCHAR(50) NULL COMMENT '颜色值,如:#FF0000',
|
||||
`extra` JSON NULL COMMENT '扩展属性',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT NULL COMMENT '更新人ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典项表';
|
||||
|
||||
-- 字典项表索引
|
||||
CREATE UNIQUE INDEX `uk_type_key` ON `sys_dict_items` (`type_id`, `item_key`, `deleted_at`);
|
||||
CREATE INDEX `idx_type_id` ON `sys_dict_items` (`type_id`);
|
||||
CREATE INDEX `idx_pid` ON `sys_dict_items` (`pid`);
|
||||
CREATE INDEX `idx_status` ON `sys_dict_items` (`status`);
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_dict_items` (`deleted_at`);
|
||||
CREATE INDEX `idx_sort` ON `sys_dict_items` (`type_id`, `sort_order`);
|
||||
CREATE INDEX `idx_key` ON `sys_dict_items` (`item_key`);
|
||||
|
||||
-- =============================================
|
||||
-- 7. 标签表 (sys_tags)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_tags`;
|
||||
CREATE TABLE `sys_tags` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`name` VARCHAR(50) NOT NULL COMMENT '标签名称',
|
||||
`type` VARCHAR(50) NULL DEFAULT 'user' COMMENT '标签类型:user-用户标签,role-角色标签,content-内容标签',
|
||||
`color` VARCHAR(50) NULL COMMENT '标签颜色,如:#FF0000',
|
||||
`description` TEXT NULL COMMENT '描述',
|
||||
`usage_count` INT NOT NULL DEFAULT 0 COMMENT '使用次数统计',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签表';
|
||||
|
||||
-- 标签表索引
|
||||
CREATE UNIQUE INDEX `uk_name_type` ON `sys_tags` (`name`, `type`, `deleted_at`);
|
||||
CREATE INDEX `idx_type` ON `sys_tags` (`type`);
|
||||
CREATE INDEX `idx_usage_count` ON `sys_tags` (`usage_count` DESC);
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_tags` (`deleted_at`);
|
||||
CREATE INDEX `idx_name` ON `sys_tags` (`name`);
|
||||
|
||||
-- =============================================
|
||||
-- 8. 用户角色关联表 (sys_user_roles)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_user_roles`;
|
||||
CREATE TABLE `sys_user_roles` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`role_id` BIGINT NOT NULL COMMENT '角色ID',
|
||||
`expired_at` DATETIME NULL COMMENT '过期时间,NULL表示永久',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';
|
||||
|
||||
-- 用户角色关联表索引
|
||||
CREATE UNIQUE INDEX `uk_user_role` ON `sys_user_roles` (`user_id`, `role_id`);
|
||||
CREATE INDEX `idx_user_id` ON `sys_user_roles` (`user_id`);
|
||||
CREATE INDEX `idx_role_id` ON `sys_user_roles` (`role_id`);
|
||||
CREATE INDEX `idx_expired_at` ON `sys_user_roles` (`expired_at`);
|
||||
CREATE INDEX `idx_created_at` ON `sys_user_roles` (`created_at`);
|
||||
|
||||
-- =============================================
|
||||
-- 9. 角色权限关联表 (sys_role_permissions)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_role_permissions`;
|
||||
CREATE TABLE `sys_role_permissions` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`role_id` BIGINT NOT NULL COMMENT '角色ID',
|
||||
`permission_id` BIGINT NOT NULL COMMENT '权限ID',
|
||||
`is_half` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否半选状态(树形权限)',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色权限关联表';
|
||||
|
||||
-- 角色权限关联表索引
|
||||
CREATE UNIQUE INDEX `uk_role_permission` ON `sys_role_permissions` (`role_id`, `permission_id`);
|
||||
CREATE INDEX `idx_role_id` ON `sys_role_permissions` (`role_id`);
|
||||
CREATE INDEX `idx_permission_id` ON `sys_role_permissions` (`permission_id`);
|
||||
CREATE INDEX `idx_is_half` ON `sys_role_permissions` (`is_half`);
|
||||
|
||||
-- =============================================
|
||||
-- 10. 用户组织关联表 (sys_user_organizations)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_user_organizations`;
|
||||
CREATE TABLE `sys_user_organizations` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`organization_id` BIGINT NOT NULL COMMENT '组织ID',
|
||||
`is_primary` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否主组织',
|
||||
`position` VARCHAR(100) NULL COMMENT '职位',
|
||||
`joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户组织关联表';
|
||||
|
||||
-- 用户组织关联表索引
|
||||
CREATE UNIQUE INDEX `uk_user_org` ON `sys_user_organizations` (`user_id`, `organization_id`);
|
||||
CREATE INDEX `idx_user_id` ON `sys_user_organizations` (`user_id`);
|
||||
CREATE INDEX `idx_organization_id` ON `sys_user_organizations` (`organization_id`);
|
||||
CREATE INDEX `idx_is_primary` ON `sys_user_organizations` (`is_primary`);
|
||||
CREATE INDEX `idx_joined_at` ON `sys_user_organizations` (`joined_at`);
|
||||
|
||||
-- =============================================
|
||||
-- 11. 用户标签关联表 (sys_user_tags)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_user_tags`;
|
||||
CREATE TABLE `sys_user_tags` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`tag_id` BIGINT NOT NULL COMMENT '标签ID',
|
||||
`created_by` BIGINT NULL COMMENT '创建人ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户标签关联表';
|
||||
|
||||
-- 用户标签关联表索引
|
||||
CREATE UNIQUE INDEX `uk_user_tag` ON `sys_user_tags` (`user_id`, `tag_id`);
|
||||
CREATE INDEX `idx_user_id` ON `sys_user_tags` (`user_id`);
|
||||
CREATE INDEX `idx_tag_id` ON `sys_user_tags` (`tag_id`);
|
||||
CREATE INDEX `idx_created_at` ON `sys_user_tags` (`created_at`);
|
||||
|
||||
-- =============================================
|
||||
-- 12. 操作日志表 (sys_operation_logs)
|
||||
-- =============================================
|
||||
DROP TABLE IF EXISTS `sys_operation_logs`;
|
||||
CREATE TABLE `sys_operation_logs` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键',
|
||||
`user_id` BIGINT NULL COMMENT '操作用户ID',
|
||||
`username` VARCHAR(100) NULL COMMENT '操作用户名',
|
||||
`module` VARCHAR(50) NOT NULL COMMENT '操作模块',
|
||||
`action` VARCHAR(50) NOT NULL COMMENT '操作类型',
|
||||
`target` VARCHAR(200) NULL COMMENT '操作对象描述',
|
||||
`target_id` BIGINT NULL COMMENT '操作对象ID',
|
||||
`request_data` TEXT NULL COMMENT '请求数据',
|
||||
`response_data` TEXT NULL COMMENT '响应数据',
|
||||
`status` VARCHAR(20) NOT NULL COMMENT '操作状态:success-成功,fail-失败',
|
||||
`ip` VARCHAR(45) NULL COMMENT 'IP地址',
|
||||
`user_agent` VARCHAR(200) NULL COMMENT '用户代理',
|
||||
`duration` BIGINT NULL COMMENT '操作耗时(毫秒)',
|
||||
`error_msg` TEXT NULL COMMENT '错误信息',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
|
||||
|
||||
-- 操作日志表索引
|
||||
CREATE INDEX `idx_user_id` ON `sys_operation_logs` (`user_id`);
|
||||
CREATE INDEX `idx_module_action` ON `sys_operation_logs` (`module`, `action`);
|
||||
CREATE INDEX `idx_target` ON `sys_operation_logs` (`target_id`);
|
||||
CREATE INDEX `idx_status` ON `sys_operation_logs` (`status`);
|
||||
CREATE INDEX `idx_created_at` ON `sys_operation_logs` (`created_at`);
|
||||
CREATE INDEX `idx_ip` ON `sys_operation_logs` (`ip`);
|
||||
|
||||
-- =============================================
|
||||
-- 初始化数据
|
||||
-- =============================================
|
||||
|
||||
-- -- 1. 系统初始用户
|
||||
-- INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `is_root`, `status`)
|
||||
-- VALUES (1, 'root', 'root@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '超级管理员', TRUE, 'active');
|
||||
|
||||
-- INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `status`)
|
||||
-- VALUES (2, 'admin', 'admin@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '系统管理员', 'active');
|
||||
|
||||
-- -- 2. 系统初始角色
|
||||
-- INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`)
|
||||
-- VALUES (1, 'super_admin', '超级管理员', '拥有系统所有权限', TRUE, 'active');
|
||||
|
||||
-- INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`, `pid`, `path`, `level`)
|
||||
-- VALUES (2, 'admin', '系统管理员', '负责系统配置和用户管理', TRUE, 'active', 1, '/1/2/', 2);
|
||||
|
||||
-- INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`)
|
||||
-- VALUES (3, 'user', '普通用户', '普通注册用户默认角色', TRUE, 'active');
|
||||
|
||||
-- -- 3. 分配角色
|
||||
-- INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (1, 1);
|
||||
-- INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (2, 2);
|
||||
|
||||
-- -- 4. 字典分类
|
||||
-- INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`) VALUES
|
||||
-- (1, 'system', '系统字典', TRUE),
|
||||
-- (2, 'business', '业务字典', TRUE);
|
||||
|
||||
-- -- 5. 用户状态字典
|
||||
-- INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES
|
||||
-- (10, 'user_status', '用户状态', TRUE, 1, '/1/10/');
|
||||
|
||||
-- INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `color`, `sort_order`) VALUES
|
||||
-- (10, 'active', 'active', '正常', '#52c41a', 1),
|
||||
-- (10, 'inactive', 'inactive', '未激活', '#faad14', 2),
|
||||
-- (10, 'locked', 'locked', '锁定', '#ff4d4f', 3),
|
||||
-- (10, 'disabled', 'disabled', '禁用', '#d9d9d9', 4);
|
||||
|
||||
-- -- 6. 组织类型字典
|
||||
-- INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES
|
||||
-- (11, 'org_type', '组织类型', TRUE, 1, '/1/11/');
|
||||
|
||||
-- INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES
|
||||
-- (11, 'company', 'company', '公司', 1),
|
||||
-- (11, 'department', 'department', '部门', 2),
|
||||
-- (11, 'team', 'team', '团队', 3),
|
||||
-- (11, 'group', 'group', '小组', 4);
|
||||
|
||||
-- -- 7. 性别字典
|
||||
-- INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES
|
||||
-- (12, 'gender', '性别', TRUE, 1, '/1/12/');
|
||||
|
||||
-- INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES
|
||||
-- (12, '0', '0', '未知', 1),
|
||||
-- (12, '1', '1', '男', 2),
|
||||
-- (12, '2', '2', '女', 3);
|
||||
|
||||
-- -- 8. 权限类型字典
|
||||
-- INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES
|
||||
-- (13, 'permission_type', '权限类型', TRUE, 1, '/1/13/');
|
||||
|
||||
-- INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `color`, `sort_order`) VALUES
|
||||
-- (13, 'menu', 'menu', '菜单', '#1890ff', 1),
|
||||
-- (13, 'button', 'button', '按钮', '#52c41a', 2),
|
||||
-- (13, 'api', 'api', '接口', '#fa8c16', 3),
|
||||
-- (13, 'data', 'data', '数据', '#722ed1', 4);
|
||||
|
||||
-- -- 9. 标签类型字典
|
||||
-- INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES
|
||||
-- (14, 'tag_type', '标签类型', TRUE, 1, '/1/14/');
|
||||
|
||||
-- INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES
|
||||
-- (14, 'user', 'user', '用户标签', 1),
|
||||
-- (14, 'role', 'role', '角色标签', 2),
|
||||
-- (14, 'content', 'content', '内容标签', 3);
|
||||
|
||||
-- -- 10. 地区字典(示例)
|
||||
-- INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES
|
||||
-- (20, 'region', '地区', TRUE, 2, '/2/20/');
|
||||
|
||||
-- INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES
|
||||
-- (20, 'CN', 'CN', '中国', 1);
|
||||
|
||||
-- -- 11. 基础权限数据
|
||||
-- INSERT INTO `sys_permissions` (`id`, `code`, `name`, `type`, `resource`, `action`) VALUES
|
||||
-- (1, 'system:manage', '系统管理', 'menu', 'system', 'manage'),
|
||||
-- (2, 'user:read', '查看用户', 'api', 'user', 'read'),
|
||||
-- (3, 'user:create', '创建用户', 'api', 'user', 'create'),
|
||||
-- (4, 'user:update', '更新用户', 'api', 'user', 'update'),
|
||||
-- (5, 'user:delete', '删除用户', 'api', 'user', 'delete'),
|
||||
-- (6, 'role:read', '查看角色', 'api', 'role', 'read'),
|
||||
-- (7, 'role:create', '创建角色', 'api', 'role', 'create'),
|
||||
-- (8, 'role:update', '更新角色', 'api', 'role', 'update'),
|
||||
-- (9, 'role:delete', '删除角色', 'api', 'role', 'delete');
|
||||
|
||||
-- -- 12. 为超级管理员角色分配所有权限
|
||||
-- INSERT INTO `sys_role_permissions` (`role_id`, `permission_id`)
|
||||
-- SELECT 1, id FROM `sys_permissions`;
|
||||
|
||||
-- -- 13. 初始组织架构
|
||||
-- INSERT INTO `sys_organizations` (`id`, `code`, `name`, `type`, `status`) VALUES
|
||||
-- (1, 'ROOT', '星撰集团', 'company', 'active');
|
||||
|
||||
-- INSERT INTO `sys_organizations` (`id`, `code`, `name`, `type`, `pid`, `path`, `level`, `status`) VALUES
|
||||
-- (2, 'TECH', '技术部', 'department', 1, '/1/2/', 2, 'active'),
|
||||
-- (3, 'PRODUCT', '产品部', 'department', 1, '/1/3/', 2, 'active'),
|
||||
-- (4, 'OPERATE', '运营部', 'department', 1, '/1/4/', 2, 'active');
|
||||
|
||||
-- -- 14. 示例标签数据
|
||||
-- INSERT INTO `sys_tags` (`name`, `type`, `color`, `description`) VALUES
|
||||
-- ('VIP', 'user', '#ff4d4f', 'VIP用户'),
|
||||
-- ('活跃用户', 'user', '#52c41a', '经常登录的用户'),
|
||||
-- ('内容创作者', 'user', '#1890ff', '发布优质内容的用户'),
|
||||
-- ('新用户', 'user', '#faad14', '注册不满30天的用户'),
|
||||
-- ('核心角色', 'role', '#ff4d4f', '系统核心角色'),
|
||||
-- ('业务角色', 'role', '#1890ff', '业务相关角色');
|
||||
|
||||
-- SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- -- =============================================
|
||||
-- -- 导出完成
|
||||
-- -- 说明:
|
||||
-- -- 1. 默认密码哈希对应的明文密码是 "123456"
|
||||
-- -- 2. 所有ID使用雪花算法生成,这里为演示使用了简单数字
|
||||
-- -- 3. 建议在生产环境中修改默认密码
|
||||
-- -- 4. 可根据实际需求调整初始化数据
|
||||
-- -- =============================================
|
@ -90,7 +90,7 @@ describe('用户查询测试', () => {
|
||||
|
||||
```typescript
|
||||
// utils/validators.ts
|
||||
import type { GetUserByUsernameParams } from '../modules/sample/example.schema';
|
||||
import type { GetUserByUsernameParams } from '../modules/example/example.schema';
|
||||
|
||||
// 类型安全的验证函数
|
||||
export function validateUsername(params: GetUserByUsernameParams): boolean {
|
||||
|
@ -38,7 +38,7 @@ interface JwtPayloadType extends JwtUserType {
|
||||
```typescript
|
||||
// auth.controller.ts
|
||||
import { createJwtPayload } from '@/utils/jwt.helper';
|
||||
import type { UserInfoType } from '@/modules/sample/example.schema';
|
||||
import type { UserInfoType } from '@/modules/example/example.schema';
|
||||
|
||||
export const authController = new Elysia()
|
||||
.use(jwtPlugin)
|
||||
|
@ -27,7 +27,7 @@ export default {
|
||||
},
|
||||
|
||||
/** Schema文件路径 */
|
||||
schema: './src/plugins/drizzle/schema/*',
|
||||
schema: './src/entities/schema.js',
|
||||
|
||||
/** 迁移文件输出目录 */
|
||||
out: './drizzle',
|
||||
@ -37,4 +37,9 @@ export default {
|
||||
|
||||
/** 严格模式 */
|
||||
strict: true,
|
||||
|
||||
introspect: {
|
||||
// 启用驼峰命名
|
||||
casing: 'camel',
|
||||
},
|
||||
} as const;
|
293
drizzle/0000_nostalgic_eternity.sql
Normal file
293
drizzle/0000_nostalgic_eternity.sql
Normal file
@ -0,0 +1,293 @@
|
||||
-- Current sql file was generated after introspecting the database
|
||||
-- If you want to run this migration please uncomment this code before executing migrations
|
||||
/*
|
||||
CREATE TABLE `sys_dict_items` (
|
||||
`id` bigint NOT NULL,
|
||||
`type_id` bigint NOT NULL,
|
||||
`item_key` varchar(50) NOT NULL,
|
||||
`item_value` varchar(200) NOT NULL,
|
||||
`label` varchar(100) NOT NULL,
|
||||
`label_en` varchar(200),
|
||||
`description` text,
|
||||
`pid` bigint DEFAULT 0,
|
||||
`path` varchar(500),
|
||||
`level` int NOT NULL DEFAULT 1,
|
||||
`sort_order` int NOT NULL DEFAULT 0,
|
||||
`status` varchar(20) NOT NULL DEFAULT 'active',
|
||||
`css_class` varchar(50),
|
||||
`color` varchar(50),
|
||||
`extra` json,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_by` bigint,
|
||||
`updated_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`deleted_at` datetime,
|
||||
CONSTRAINT `sys_dict_items_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_type_key` UNIQUE(`type_id`,`item_key`,`deleted_at`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_dict_types` (
|
||||
`id` bigint NOT NULL,
|
||||
`code` varchar(50) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`description` text,
|
||||
`pid` bigint DEFAULT 0,
|
||||
`path` varchar(500),
|
||||
`level` int NOT NULL DEFAULT 1,
|
||||
`status` varchar(20) NOT NULL DEFAULT 'active',
|
||||
`is_system` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`sort_order` int NOT NULL DEFAULT 0,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_by` bigint,
|
||||
`updated_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`deleted_at` datetime,
|
||||
CONSTRAINT `sys_dict_types_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_code` UNIQUE(`code`,`deleted_at`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_operation_logs` (
|
||||
`id` bigint NOT NULL,
|
||||
`user_id` bigint,
|
||||
`username` varchar(100),
|
||||
`module` varchar(50) NOT NULL,
|
||||
`action` varchar(50) NOT NULL,
|
||||
`target` varchar(200),
|
||||
`target_id` bigint,
|
||||
`request_data` text,
|
||||
`response_data` text,
|
||||
`status` varchar(20) NOT NULL,
|
||||
`ip` varchar(45),
|
||||
`user_agent` varchar(200),
|
||||
`duration` bigint,
|
||||
`error_msg` text,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
CONSTRAINT `sys_operation_logs_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_organizations` (
|
||||
`id` bigint NOT NULL,
|
||||
`code` varchar(100) NOT NULL,
|
||||
`name` varchar(200) NOT NULL,
|
||||
`full_name` varchar(200),
|
||||
`description` text,
|
||||
`pid` bigint DEFAULT 0,
|
||||
`path` varchar(500),
|
||||
`level` int NOT NULL DEFAULT 1,
|
||||
`type` varchar(20),
|
||||
`status` varchar(20) NOT NULL DEFAULT 'active',
|
||||
`sort_order` int NOT NULL DEFAULT 0,
|
||||
`leader_id` bigint,
|
||||
`address` varchar(200),
|
||||
`phone` varchar(50),
|
||||
`extra` json,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_by` bigint,
|
||||
`updated_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`deleted_at` datetime,
|
||||
`version` int NOT NULL DEFAULT 1,
|
||||
CONSTRAINT `sys_organizations_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_code` UNIQUE(`code`,`deleted_at`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_permissions` (
|
||||
`id` bigint NOT NULL,
|
||||
`code` varchar(100) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`type` varchar(20) NOT NULL,
|
||||
`resource` varchar(50),
|
||||
`action` varchar(50),
|
||||
`description` text,
|
||||
`pid` bigint DEFAULT 0,
|
||||
`path` varchar(500),
|
||||
`level` int NOT NULL DEFAULT 1,
|
||||
`sort_order` int NOT NULL DEFAULT 0,
|
||||
`status` varchar(20) NOT NULL DEFAULT 'active',
|
||||
`meta` json,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_by` bigint,
|
||||
`updated_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`deleted_at` datetime,
|
||||
CONSTRAINT `sys_permissions_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_code` UNIQUE(`code`,`deleted_at`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_role_permissions` (
|
||||
`id` bigint NOT NULL,
|
||||
`role_id` bigint NOT NULL,
|
||||
`permission_id` bigint NOT NULL,
|
||||
`is_half` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
CONSTRAINT `sys_role_permissions_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_role_permission` UNIQUE(`role_id`,`permission_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_roles` (
|
||||
`id` bigint NOT NULL,
|
||||
`code` varchar(50) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`description` text,
|
||||
`pid` bigint DEFAULT 0,
|
||||
`path` varchar(500),
|
||||
`level` int NOT NULL DEFAULT 1,
|
||||
`sort_order` int NOT NULL DEFAULT 0,
|
||||
`status` varchar(20) NOT NULL DEFAULT 'active',
|
||||
`is_system` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`permissions_snapshot` json,
|
||||
`extra` json,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_by` bigint,
|
||||
`updated_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`deleted_at` datetime,
|
||||
`version` int NOT NULL DEFAULT 1,
|
||||
CONSTRAINT `sys_roles_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_code` UNIQUE(`code`,`deleted_at`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_tags` (
|
||||
`id` bigint NOT NULL,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`type` varchar(50) DEFAULT 'user',
|
||||
`color` varchar(50),
|
||||
`description` text,
|
||||
`usage_count` int NOT NULL DEFAULT 0,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`deleted_at` datetime,
|
||||
CONSTRAINT `sys_tags_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_name_type` UNIQUE(`name`,`type`,`deleted_at`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_user_organizations` (
|
||||
`id` bigint NOT NULL,
|
||||
`user_id` bigint NOT NULL,
|
||||
`organization_id` bigint NOT NULL,
|
||||
`is_primary` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`position` varchar(100),
|
||||
`joined_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
CONSTRAINT `sys_user_organizations_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_user_org` UNIQUE(`user_id`,`organization_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_user_roles` (
|
||||
`id` bigint NOT NULL,
|
||||
`user_id` bigint NOT NULL,
|
||||
`role_id` bigint NOT NULL,
|
||||
`expired_at` datetime,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
CONSTRAINT `sys_user_roles_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_user_role` UNIQUE(`user_id`,`role_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_user_tags` (
|
||||
`id` bigint NOT NULL,
|
||||
`user_id` bigint NOT NULL,
|
||||
`tag_id` bigint NOT NULL,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
CONSTRAINT `sys_user_tags_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_user_tag` UNIQUE(`user_id`,`tag_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sys_users` (
|
||||
`id` bigint NOT NULL,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`email` varchar(100) NOT NULL,
|
||||
`mobile` varchar(20),
|
||||
`password_hash` varchar(255) NOT NULL,
|
||||
`avatar` varchar(255),
|
||||
`nickname` varchar(100),
|
||||
`status` varchar(20) NOT NULL DEFAULT 'active',
|
||||
`gender` tinyint DEFAULT 0,
|
||||
`birthday` date,
|
||||
`bio` varchar(500),
|
||||
`login_count` int NOT NULL DEFAULT 0,
|
||||
`last_login_at` datetime,
|
||||
`last_login_ip` varchar(45),
|
||||
`failed_attempts` int NOT NULL DEFAULT 0,
|
||||
`locked_until` datetime,
|
||||
`is_root` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`extra` json,
|
||||
`created_by` bigint,
|
||||
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_by` bigint,
|
||||
`updated_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`deleted_at` datetime,
|
||||
`version` int NOT NULL DEFAULT 1,
|
||||
CONSTRAINT `sys_users_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uk_email` UNIQUE(`email`,`deleted_at`),
|
||||
CONSTRAINT `uk_username` UNIQUE(`username`,`deleted_at`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_dict_items` (`deleted_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_key` ON `sys_dict_items` (`item_key`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_pid` ON `sys_dict_items` (`pid`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_sort` ON `sys_dict_items` (`type_id`,`sort_order`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `sys_dict_items` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_type_id` ON `sys_dict_items` (`type_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_dict_types` (`deleted_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_is_system` ON `sys_dict_types` (`is_system`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_name` ON `sys_dict_types` (`name`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_path` ON `sys_dict_types` (`path`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_pid` ON `sys_dict_types` (`pid`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_sort` ON `sys_dict_types` (`pid`,`sort_order`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `sys_dict_types` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_created_at` ON `sys_operation_logs` (`created_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_ip` ON `sys_operation_logs` (`ip`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_module_action` ON `sys_operation_logs` (`module`,`action`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `sys_operation_logs` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_target` ON `sys_operation_logs` (`target_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_id` ON `sys_operation_logs` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_organizations` (`deleted_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_leader_id` ON `sys_organizations` (`leader_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_name` ON `sys_organizations` (`name`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_path` ON `sys_organizations` (`path`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_pid` ON `sys_organizations` (`pid`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_sort` ON `sys_organizations` (`pid`,`sort_order`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `sys_organizations` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_type` ON `sys_organizations` (`type`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_permissions` (`deleted_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_pid` ON `sys_permissions` (`pid`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_resource_action` ON `sys_permissions` (`resource`,`action`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_sort` ON `sys_permissions` (`pid`,`sort_order`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `sys_permissions` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_type` ON `sys_permissions` (`type`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_is_half` ON `sys_role_permissions` (`is_half`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_permission_id` ON `sys_role_permissions` (`permission_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_role_id` ON `sys_role_permissions` (`role_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_roles` (`deleted_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_is_system` ON `sys_roles` (`is_system`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_name` ON `sys_roles` (`name`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_path` ON `sys_roles` (`path`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_pid` ON `sys_roles` (`pid`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_sort` ON `sys_roles` (`pid`,`sort_order`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `sys_roles` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_tags` (`deleted_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_name` ON `sys_tags` (`name`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_type` ON `sys_tags` (`type`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_usage_count` ON `sys_tags` (`usage_count`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_is_primary` ON `sys_user_organizations` (`is_primary`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_joined_at` ON `sys_user_organizations` (`joined_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_organization_id` ON `sys_user_organizations` (`organization_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_id` ON `sys_user_organizations` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_created_at` ON `sys_user_roles` (`created_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_expired_at` ON `sys_user_roles` (`expired_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_role_id` ON `sys_user_roles` (`role_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_id` ON `sys_user_roles` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_created_at` ON `sys_user_tags` (`created_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_tag_id` ON `sys_user_tags` (`tag_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_id` ON `sys_user_tags` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_created_at` ON `sys_users` (`created_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_deleted_at` ON `sys_users` (`deleted_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_is_root` ON `sys_users` (`is_root`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_last_login` ON `sys_users` (`last_login_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_mobile` ON `sys_users` (`mobile`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `sys_users` (`status`);
|
||||
*/
|
2100
drizzle/meta/0000_snapshot.json
Normal file
2100
drizzle/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "mysql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1751620262784,
|
||||
"tag": "0000_nostalgic_eternity",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
3
drizzle/relations.ts
Normal file
3
drizzle/relations.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { relations } from "drizzle-orm/relations";
|
||||
import { } from "./schema";
|
||||
|
316
drizzle/schema.ts
Normal file
316
drizzle/schema.ts
Normal file
@ -0,0 +1,316 @@
|
||||
import { mysqlTable, mysqlSchema, AnyMySqlColumn, index, primaryKey, unique, bigint, varchar, text, int, json, datetime, tinyint, date } from "drizzle-orm/mysql-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
|
||||
export const sysDictItems = mysqlTable("sys_dict_items", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
typeId: bigint("type_id", { mode: "number" }).notNull(),
|
||||
itemKey: varchar("item_key", { length: 50 }).notNull(),
|
||||
itemValue: varchar("item_value", { length: 200 }).notNull(),
|
||||
label: varchar({ length: 100 }).notNull(),
|
||||
labelEn: varchar("label_en", { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
cssClass: varchar("css_class", { length: 50 }),
|
||||
color: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_key").on(table.itemKey),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.typeId, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type_id").on(table.typeId),
|
||||
primaryKey({ columns: [table.id], name: "sys_dict_items_id"}),
|
||||
unique("uk_type_key").on(table.typeId, table.itemKey, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysDictTypes = mysqlTable("sys_dict_types", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_system").on(table.isSystem),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_dict_types_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysOperationLogs = mysqlTable("sys_operation_logs", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }),
|
||||
username: varchar({ length: 100 }),
|
||||
module: varchar({ length: 50 }).notNull(),
|
||||
action: varchar({ length: 50 }).notNull(),
|
||||
target: varchar({ length: 200 }),
|
||||
targetId: bigint("target_id", { mode: "number" }),
|
||||
requestData: text("request_data"),
|
||||
responseData: text("response_data"),
|
||||
status: varchar({ length: 20 }).notNull(),
|
||||
ip: varchar({ length: 45 }),
|
||||
userAgent: varchar("user_agent", { length: 200 }),
|
||||
duration: bigint({ mode: "number" }),
|
||||
errorMsg: text("error_msg"),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_ip").on(table.ip),
|
||||
index("idx_module_action").on(table.module, table.action),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_target").on(table.targetId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}),
|
||||
]);
|
||||
|
||||
export const sysOrganizations = mysqlTable("sys_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 200 }).notNull(),
|
||||
fullName: varchar("full_name", { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
type: varchar({ length: 20 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
leaderId: bigint("leader_id", { mode: "number" }),
|
||||
address: varchar({ length: 200 }),
|
||||
phone: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_leader_id").on(table.leaderId),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type").on(table.type),
|
||||
primaryKey({ columns: [table.id], name: "sys_organizations_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysPermissions = mysqlTable("sys_permissions", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
type: varchar({ length: 20 }).notNull(),
|
||||
resource: varchar({ length: 50 }),
|
||||
action: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
meta: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_resource_action").on(table.resource, table.action),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type").on(table.type),
|
||||
primaryKey({ columns: [table.id], name: "sys_permissions_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysRolePermissions = mysqlTable("sys_role_permissions", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
roleId: bigint("role_id", { mode: "number" }).notNull(),
|
||||
permissionId: bigint("permission_id", { mode: "number" }).notNull(),
|
||||
isHalf: tinyint("is_half").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_is_half").on(table.isHalf),
|
||||
index("idx_permission_id").on(table.permissionId),
|
||||
index("idx_role_id").on(table.roleId),
|
||||
primaryKey({ columns: [table.id], name: "sys_role_permissions_id"}),
|
||||
unique("uk_role_permission").on(table.roleId, table.permissionId),
|
||||
]);
|
||||
|
||||
export const sysRoles = mysqlTable("sys_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
permissionsSnapshot: json("permissions_snapshot"),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_system").on(table.isSystem),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_roles_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysTags = mysqlTable("sys_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
name: varchar({ length: 50 }).notNull(),
|
||||
type: varchar({ length: 50 }).default('user'),
|
||||
color: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
usageCount: int("usage_count").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_type").on(table.type),
|
||||
index("idx_usage_count").on(table.usageCount),
|
||||
primaryKey({ columns: [table.id], name: "sys_tags_id"}),
|
||||
unique("uk_name_type").on(table.name, table.type, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysUserOrganizations = mysqlTable("sys_user_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
organizationId: bigint("organization_id", { mode: "number" }).notNull(),
|
||||
isPrimary: tinyint("is_primary").default(0).notNull(),
|
||||
position: varchar({ length: 100 }),
|
||||
joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_is_primary").on(table.isPrimary),
|
||||
index("idx_joined_at").on(table.joinedAt),
|
||||
index("idx_organization_id").on(table.organizationId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}),
|
||||
unique("uk_user_org").on(table.userId, table.organizationId),
|
||||
]);
|
||||
|
||||
export const sysUserRoles = mysqlTable("sys_user_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
roleId: bigint("role_id", { mode: "number" }).notNull(),
|
||||
expiredAt: datetime("expired_at", { mode: 'string'}),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_expired_at").on(table.expiredAt),
|
||||
index("idx_role_id").on(table.roleId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_roles_id"}),
|
||||
unique("uk_user_role").on(table.userId, table.roleId),
|
||||
]);
|
||||
|
||||
export const sysUserTags = mysqlTable("sys_user_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
tagId: bigint("tag_id", { mode: "number" }).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_tag_id").on(table.tagId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_tags_id"}),
|
||||
unique("uk_user_tag").on(table.userId, table.tagId),
|
||||
]);
|
||||
|
||||
export const sysUsers = mysqlTable("sys_users", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
username: varchar({ length: 50 }).notNull(),
|
||||
email: varchar({ length: 100 }).notNull(),
|
||||
mobile: varchar({ length: 20 }),
|
||||
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
||||
avatar: varchar({ length: 255 }),
|
||||
nickname: varchar({ length: 100 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
gender: tinyint().default(0),
|
||||
// you can use { mode: 'date' }, if you want to have Date as type for this column
|
||||
birthday: date({ mode: 'string' }),
|
||||
bio: varchar({ length: 500 }),
|
||||
loginCount: int("login_count").default(0).notNull(),
|
||||
lastLoginAt: datetime("last_login_at", { mode: 'string'}),
|
||||
lastLoginIp: varchar("last_login_ip", { length: 45 }),
|
||||
failedAttempts: int("failed_attempts").default(0).notNull(),
|
||||
lockedUntil: datetime("locked_until", { mode: 'string'}),
|
||||
isRoot: tinyint("is_root").default(0).notNull(),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_root").on(table.isRoot),
|
||||
index("idx_last_login").on(table.lastLoginAt),
|
||||
index("idx_mobile").on(table.mobile),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_users_id"}),
|
||||
unique("uk_email").on(table.email, table.deletedAt),
|
||||
unique("uk_username").on(table.username, table.deletedAt),
|
||||
]);
|
@ -45,15 +45,14 @@
|
||||
"dev": "bun --watch --env-file=.env --hot src/server.ts",
|
||||
"start": "bun --env-file=.env.prod src/server.ts",
|
||||
"test": "bun test",
|
||||
"vitest": "bun --env-file=.env x vitest run",
|
||||
"test:watch": "bun test --watch",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"check": "bun run lint && bun run format:check",
|
||||
"fix": "bun run lint:fix && bun run format",
|
||||
"demo:logger": "bun src/demo/logger-demo.ts",
|
||||
"demo:logger:prod": "NODE_ENV=production bun src/demo/logger-demo.ts"
|
||||
"makeSQL": "drizzle-kit generate",
|
||||
"makeEntity": "drizzle-kit introspect",
|
||||
"syncDB": "drizzle-kit migrate",
|
||||
"sqlV": "drizzle-kit studio"
|
||||
}
|
||||
}
|
||||
|
111
src/constants/error-codes.ts
Normal file
111
src/constants/error-codes.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @file 统一错误码定义
|
||||
* @author AI助手
|
||||
* @date 2025-06-29
|
||||
* @description 定义整个应用的统一错误码,提供类型安全的错误处理
|
||||
*/
|
||||
|
||||
/**
|
||||
* 应用错误码枚举
|
||||
* @description 统一管理所有错误码,确保错误处理的一致性
|
||||
*/
|
||||
export const ERROR_CODES = {
|
||||
// 成功
|
||||
SUCCESS: 'SUCCESS',
|
||||
|
||||
// 客户端错误 4xx
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR', // 参数验证失败
|
||||
UNAUTHORIZED: 'UNAUTHORIZED', // 未授权
|
||||
FORBIDDEN: 'FORBIDDEN', // 禁止访问
|
||||
NOT_FOUND: 'NOT_FOUND', // 资源不存在
|
||||
METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED', // 方法不允许
|
||||
CONFLICT: 'CONFLICT', // 资源冲突
|
||||
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED', // 请求频率超限
|
||||
|
||||
// 业务错误 4xx
|
||||
BUSINESS_ERROR: 'BUSINESS_ERROR', // 通用业务错误
|
||||
USER_NOT_FOUND: 'USER_NOT_FOUND', // 用户不存在
|
||||
USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS', // 用户已存在
|
||||
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS', // 凭据无效
|
||||
TOKEN_EXPIRED: 'TOKEN_EXPIRED', // Token过期
|
||||
TOKEN_INVALID: 'TOKEN_INVALID', // Token无效
|
||||
INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS', // 权限不足
|
||||
|
||||
// 服务器错误 5xx
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR', // 内部服务器错误
|
||||
DATABASE_ERROR: 'DATABASE_ERROR', // 数据库错误
|
||||
REDIS_ERROR: 'REDIS_ERROR', // Redis错误
|
||||
EXTERNAL_API_ERROR: 'EXTERNAL_API_ERROR', // 外部API错误
|
||||
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', // 服务不可用
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 错误码类型
|
||||
*/
|
||||
export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
|
||||
|
||||
/**
|
||||
* 错误码到HTTP状态码的映射
|
||||
*/
|
||||
export const ERROR_CODE_TO_HTTP_STATUS: Record<ErrorCode, number> = {
|
||||
// 成功
|
||||
[ERROR_CODES.SUCCESS]: 200,
|
||||
|
||||
// 客户端错误 4xx
|
||||
[ERROR_CODES.VALIDATION_ERROR]: 400,
|
||||
[ERROR_CODES.UNAUTHORIZED]: 401,
|
||||
[ERROR_CODES.FORBIDDEN]: 403,
|
||||
[ERROR_CODES.NOT_FOUND]: 404,
|
||||
[ERROR_CODES.METHOD_NOT_ALLOWED]: 405,
|
||||
[ERROR_CODES.CONFLICT]: 409,
|
||||
[ERROR_CODES.RATE_LIMIT_EXCEEDED]: 429,
|
||||
|
||||
// 业务错误 4xx
|
||||
[ERROR_CODES.BUSINESS_ERROR]: 400,
|
||||
[ERROR_CODES.USER_NOT_FOUND]: 404,
|
||||
[ERROR_CODES.USER_ALREADY_EXISTS]: 409,
|
||||
[ERROR_CODES.INVALID_CREDENTIALS]: 401,
|
||||
[ERROR_CODES.TOKEN_EXPIRED]: 401,
|
||||
[ERROR_CODES.TOKEN_INVALID]: 401,
|
||||
[ERROR_CODES.INSUFFICIENT_PERMISSIONS]: 403,
|
||||
|
||||
// 服务器错误 5xx
|
||||
[ERROR_CODES.INTERNAL_ERROR]: 500,
|
||||
[ERROR_CODES.DATABASE_ERROR]: 500,
|
||||
[ERROR_CODES.REDIS_ERROR]: 500,
|
||||
[ERROR_CODES.EXTERNAL_API_ERROR]: 502,
|
||||
[ERROR_CODES.SERVICE_UNAVAILABLE]: 503,
|
||||
};
|
||||
|
||||
/**
|
||||
* 错误码描述映射
|
||||
*/
|
||||
export const ERROR_CODE_MESSAGES: Record<ErrorCode, string> = {
|
||||
// 成功
|
||||
[ERROR_CODES.SUCCESS]: '操作成功',
|
||||
|
||||
// 客户端错误
|
||||
[ERROR_CODES.VALIDATION_ERROR]: '请求参数验证失败',
|
||||
[ERROR_CODES.UNAUTHORIZED]: '未授权访问',
|
||||
[ERROR_CODES.FORBIDDEN]: '禁止访问',
|
||||
[ERROR_CODES.NOT_FOUND]: '请求的资源不存在',
|
||||
[ERROR_CODES.METHOD_NOT_ALLOWED]: '请求方法不被允许',
|
||||
[ERROR_CODES.CONFLICT]: '请求与当前资源状态冲突',
|
||||
[ERROR_CODES.RATE_LIMIT_EXCEEDED]: '请求频率超过限制',
|
||||
|
||||
// 业务错误
|
||||
[ERROR_CODES.BUSINESS_ERROR]: '业务处理失败',
|
||||
[ERROR_CODES.USER_NOT_FOUND]: '用户不存在',
|
||||
[ERROR_CODES.USER_ALREADY_EXISTS]: '用户已存在',
|
||||
[ERROR_CODES.INVALID_CREDENTIALS]: '用户名或密码错误',
|
||||
[ERROR_CODES.TOKEN_EXPIRED]: '访问令牌已过期',
|
||||
[ERROR_CODES.TOKEN_INVALID]: '访问令牌无效',
|
||||
[ERROR_CODES.INSUFFICIENT_PERMISSIONS]: '权限不足',
|
||||
|
||||
// 服务器错误
|
||||
[ERROR_CODES.INTERNAL_ERROR]: '服务器内部错误',
|
||||
[ERROR_CODES.DATABASE_ERROR]: '数据库操作失败',
|
||||
[ERROR_CODES.REDIS_ERROR]: 'Redis操作失败',
|
||||
[ERROR_CODES.EXTERNAL_API_ERROR]: '外部服务调用失败',
|
||||
[ERROR_CODES.SERVICE_UNAVAILABLE]: '服务暂时不可用',
|
||||
};
|
27
src/eneities/customType.ts
Normal file
27
src/eneities/customType.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// | ------------------------------------------------------------
|
||||
// | @版本: version 0.1
|
||||
// | @创建人: 【Nie-x7129】
|
||||
// | @E-mail: x71291@outlook.com
|
||||
// | @所在项目: pac-auth
|
||||
// | @文件描述: customType.ts -
|
||||
// | @创建时间: 2024-06-04 16:27
|
||||
// | @更新时间: 2024-06-04 16:27
|
||||
// | @修改记录:
|
||||
// | -*-*-*- (时间--修改人--修改说明) -*-*-*-
|
||||
// | =
|
||||
// | ------------------------------------------------------------
|
||||
// 定义自定义类型
|
||||
import { customType } from 'drizzle-orm/mysql-core';
|
||||
|
||||
// 写入读取是将bigint转化为string
|
||||
export const bigintString = customType({
|
||||
dataType() {
|
||||
return 'bigint';
|
||||
},
|
||||
fromDriver(value) { // 数据库 -> JS
|
||||
return value?.toString(); // 处理 null 值
|
||||
},
|
||||
toDriver(value) { // JS -> 数据库
|
||||
return BigInt(value as string); // 确保写入时为数字类型
|
||||
}
|
||||
});
|
317
src/eneities/index.ts
Normal file
317
src/eneities/index.ts
Normal file
@ -0,0 +1,317 @@
|
||||
import { mysqlTable, mysqlSchema, AnyMySqlColumn, index, primaryKey, unique, varchar, text, int, json, datetime, tinyint, date } from "drizzle-orm/mysql-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
import { bigintString as bigint } from "./customType"
|
||||
|
||||
export const sysDictItems = mysqlTable("sys_dict_items", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
typeId: bigint("type_id", { mode: "number" }).notNull(),
|
||||
itemKey: varchar("item_key", { length: 50 }).notNull(),
|
||||
itemValue: varchar("item_value", { length: 200 }).notNull(),
|
||||
label: varchar({ length: 100 }).notNull(),
|
||||
labelEn: varchar("label_en", { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
cssClass: varchar("css_class", { length: 50 }),
|
||||
color: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_key").on(table.itemKey),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.typeId, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type_id").on(table.typeId),
|
||||
primaryKey({ columns: [table.id], name: "sys_dict_items_id"}),
|
||||
unique("uk_type_key").on(table.typeId, table.itemKey, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysDictTypes = mysqlTable("sys_dict_types", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_system").on(table.isSystem),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_dict_types_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysOperationLogs = mysqlTable("sys_operation_logs", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }),
|
||||
username: varchar({ length: 100 }),
|
||||
module: varchar({ length: 50 }).notNull(),
|
||||
action: varchar({ length: 50 }).notNull(),
|
||||
target: varchar({ length: 200 }),
|
||||
targetId: bigint("target_id", { mode: "number" }),
|
||||
requestData: text("request_data"),
|
||||
responseData: text("response_data"),
|
||||
status: varchar({ length: 20 }).notNull(),
|
||||
ip: varchar({ length: 45 }),
|
||||
userAgent: varchar("user_agent", { length: 200 }),
|
||||
duration: bigint({ mode: "number" }),
|
||||
errorMsg: text("error_msg"),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_ip").on(table.ip),
|
||||
index("idx_module_action").on(table.module, table.action),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_target").on(table.targetId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}),
|
||||
]);
|
||||
|
||||
export const sysOrganizations = mysqlTable("sys_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 200 }).notNull(),
|
||||
fullName: varchar("full_name", { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
type: varchar({ length: 20 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
leaderId: bigint("leader_id", { mode: "number" }),
|
||||
address: varchar({ length: 200 }),
|
||||
phone: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_leader_id").on(table.leaderId),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type").on(table.type),
|
||||
primaryKey({ columns: [table.id], name: "sys_organizations_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysPermissions = mysqlTable("sys_permissions", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
type: varchar({ length: 20 }).notNull(),
|
||||
resource: varchar({ length: 50 }),
|
||||
action: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
meta: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_resource_action").on(table.resource, table.action),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type").on(table.type),
|
||||
primaryKey({ columns: [table.id], name: "sys_permissions_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysRolePermissions = mysqlTable("sys_role_permissions", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
roleId: bigint("role_id", { mode: "number" }).notNull(),
|
||||
permissionId: bigint("permission_id", { mode: "number" }).notNull(),
|
||||
isHalf: tinyint("is_half").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_is_half").on(table.isHalf),
|
||||
index("idx_permission_id").on(table.permissionId),
|
||||
index("idx_role_id").on(table.roleId),
|
||||
primaryKey({ columns: [table.id], name: "sys_role_permissions_id"}),
|
||||
unique("uk_role_permission").on(table.roleId, table.permissionId),
|
||||
]);
|
||||
|
||||
export const sysRoles = mysqlTable("sys_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
permissionsSnapshot: json("permissions_snapshot"),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_system").on(table.isSystem),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_roles_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysTags = mysqlTable("sys_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
name: varchar({ length: 50 }).notNull(),
|
||||
type: varchar({ length: 50 }).default('user'),
|
||||
color: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
usageCount: int("usage_count").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_type").on(table.type),
|
||||
index("idx_usage_count").on(table.usageCount),
|
||||
primaryKey({ columns: [table.id], name: "sys_tags_id"}),
|
||||
unique("uk_name_type").on(table.name, table.type, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysUserOrganizations = mysqlTable("sys_user_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
organizationId: bigint("organization_id", { mode: "number" }).notNull(),
|
||||
isPrimary: tinyint("is_primary").default(0).notNull(),
|
||||
position: varchar({ length: 100 }),
|
||||
joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_is_primary").on(table.isPrimary),
|
||||
index("idx_joined_at").on(table.joinedAt),
|
||||
index("idx_organization_id").on(table.organizationId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}),
|
||||
unique("uk_user_org").on(table.userId, table.organizationId),
|
||||
]);
|
||||
|
||||
export const sysUserRoles = mysqlTable("sys_user_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
roleId: bigint("role_id", { mode: "number" }).notNull(),
|
||||
expiredAt: datetime("expired_at", { mode: 'string'}),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_expired_at").on(table.expiredAt),
|
||||
index("idx_role_id").on(table.roleId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_roles_id"}),
|
||||
unique("uk_user_role").on(table.userId, table.roleId),
|
||||
]);
|
||||
|
||||
export const sysUserTags = mysqlTable("sys_user_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
tagId: bigint("tag_id", { mode: "number" }).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_tag_id").on(table.tagId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_tags_id"}),
|
||||
unique("uk_user_tag").on(table.userId, table.tagId),
|
||||
]);
|
||||
|
||||
export const sysUsers = mysqlTable("sys_users", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
username: varchar({ length: 50 }).notNull(),
|
||||
email: varchar({ length: 100 }).notNull(),
|
||||
mobile: varchar({ length: 20 }),
|
||||
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
||||
avatar: varchar({ length: 255 }),
|
||||
nickname: varchar({ length: 100 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
gender: tinyint().default(0),
|
||||
// you can use { mode: 'date' }, if you want to have Date as type for this column
|
||||
birthday: date({ mode: 'string' }),
|
||||
bio: varchar({ length: 500 }),
|
||||
loginCount: int("login_count").default(0).notNull(),
|
||||
lastLoginAt: datetime("last_login_at", { mode: 'string'}),
|
||||
lastLoginIp: varchar("last_login_ip", { length: 45 }),
|
||||
failedAttempts: int("failed_attempts").default(0).notNull(),
|
||||
lockedUntil: datetime("locked_until", { mode: 'string'}),
|
||||
isRoot: tinyint("is_root").default(0).notNull(),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_root").on(table.isRoot),
|
||||
index("idx_last_login").on(table.lastLoginAt),
|
||||
index("idx_mobile").on(table.mobile),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_users_id"}),
|
||||
unique("uk_email").on(table.email, table.deletedAt),
|
||||
unique("uk_username").on(table.username, table.deletedAt),
|
||||
]);
|
@ -19,12 +19,12 @@ import { exampleService } from './example.service';
|
||||
* @description 提供样例接口的路由定义和请求处理逻辑
|
||||
* @modification hotok 2025-06-29 实现根据用户名查询用户接口
|
||||
*/
|
||||
export const sampleController = new Elysia()
|
||||
export const exampleController = new Elysia()
|
||||
// 使用JWT认证插件
|
||||
.use(jwtAuthPlugin)
|
||||
/**
|
||||
* 根据用户名查询用户信息
|
||||
* @route GET /api/sample/user/:username
|
||||
* @route GET /api/example/user/:username
|
||||
* @description 通过用户名查询用户的详细信息,需要JWT认证
|
||||
* @param username 用户名,路径参数,长度2-50字符
|
||||
* @returns 用户信息对象或错误响应
|
||||
@ -44,7 +44,7 @@ export const sampleController = new Elysia()
|
||||
summary: '根据用户名查询用户信息',
|
||||
description:
|
||||
'通过用户名查询用户的详细信息,需要JWT身份认证。返回用户的基本信息,不包含敏感数据如密码。',
|
||||
tags: [tags.user, tags.sample],
|
||||
tags: [tags.user, tags.example],
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
// 响应格式定义
|
54
src/modules/example/example.response.ts
Normal file
54
src/modules/example/example.response.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @file 样例接口响应Schema定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 样例接口的响应结构定义,基于全局响应格式扩展
|
||||
*
|
||||
* ⚠️ 响应格式管理规范:
|
||||
* 1. 当前文件只定义成功响应(200)的具体数据结构
|
||||
* 2. 错误响应(400, 401, 422, 500等)统一使用 @/validators/global.response.ts 中的 CommonResponses
|
||||
* 3. 这样可以保证错误响应格式的一致性,便于前端统一处理
|
||||
* 4. 如需自定义错误响应,请在 CommonResponses 中添加,而不是在具体业务文件中定义
|
||||
*/
|
||||
|
||||
import { t } from 'elysia';
|
||||
import { UserInfoSchema } from './example.schema';
|
||||
import { CommonResponses } from '@/validators/global.response';
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户成功响应
|
||||
* @description 定义成功查询用户时返回的数据结构
|
||||
*/
|
||||
export const GetUserByUsernameSuccessResponse = t.Object({
|
||||
code: t.Literal(0, {
|
||||
description: '成功响应码',
|
||||
}),
|
||||
message: t.String({
|
||||
description: '成功消息',
|
||||
examples: ['查询用户成功'],
|
||||
}),
|
||||
data: UserInfoSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户接口的所有可能响应
|
||||
* @description 组合成功响应和公共错误响应,确保格式一致性
|
||||
*/
|
||||
export const GetUserByUsernameResponses = {
|
||||
/** 200 查询成功 - 使用自定义成功响应 */
|
||||
200: GetUserByUsernameSuccessResponse,
|
||||
|
||||
/** 400 业务错误 - 使用全局公共错误响应 */
|
||||
400: CommonResponses[400],
|
||||
|
||||
/** 401 认证失败 - 使用全局公共错误响应 */
|
||||
401: CommonResponses[401],
|
||||
|
||||
/** 422 参数验证失败 - 使用全局公共错误响应 */
|
||||
422: CommonResponses[422],
|
||||
|
||||
/** 500 服务器内部错误 - 使用全局公共错误响应 */
|
||||
500: CommonResponses[500],
|
||||
};
|
@ -22,7 +22,7 @@
|
||||
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { db } from '@/plugins/drizzle/drizzle.service';
|
||||
import { users } from '@/plugins/drizzle/schema/users';
|
||||
import { users } from '@/eneities/users';
|
||||
import { ERROR_CODES } from '@/validators/global.response';
|
||||
import { type GetUserByUsernameType } from './example.schema';
|
||||
import type { JwtUserType } from '@/type/jwt.type';
|
||||
@ -36,7 +36,7 @@ export class ExampleService {
|
||||
const { username } = params;
|
||||
user;
|
||||
// 使用Drizzle ORM查询用户信息
|
||||
const userList = await db
|
||||
const userList = await db()
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
@ -60,7 +60,7 @@ export class ExampleService {
|
||||
};
|
||||
}
|
||||
|
||||
const userInfo = userList[0];
|
||||
const userInfo = userList[0]!;
|
||||
|
||||
// 返回成功响应
|
||||
return {
|
@ -9,13 +9,13 @@
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { Elysia } from 'elysia';
|
||||
import { sampleController } from './example.controller';
|
||||
import { exampleController } from './example.controller';
|
||||
import { jwtPlugin } from '@/plugins/jwt/jwt.plugins';
|
||||
|
||||
// 创建测试应用实例
|
||||
const app = new Elysia()
|
||||
.use(jwtPlugin)
|
||||
.use(sampleController);
|
||||
.use(exampleController);
|
||||
|
||||
// 测试用的JWT Token(需要根据实际情况生成)
|
||||
let testToken = '';
|
||||
@ -35,10 +35,10 @@ describe('样例接口测试', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sample/user/:username', () => {
|
||||
describe('GET /api/example/user/:username', () => {
|
||||
it('应该成功查询存在的用户', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
new Request('http://localhost/example/user/admin', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
@ -60,7 +60,7 @@ describe('样例接口测试', () => {
|
||||
|
||||
it('用户名过短应返回422', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/a', {
|
||||
new Request('http://localhost/example/user/a', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
@ -78,7 +78,7 @@ describe('样例接口测试', () => {
|
||||
|
||||
it('用户名过长应返回422', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/' + 'a'.repeat(51), {
|
||||
new Request('http://localhost/example/user/' + 'a'.repeat(51), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
@ -96,7 +96,7 @@ describe('样例接口测试', () => {
|
||||
|
||||
it('用户不存在应返回400', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/nonexistentuser12345', {
|
||||
new Request('http://localhost/example/user/nonexistentuser12345', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${testToken}`,
|
||||
@ -115,7 +115,7 @@ describe('样例接口测试', () => {
|
||||
|
||||
it('缺少Authorization头应返回401', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
new Request('http://localhost/example/user/admin', {
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
@ -130,7 +130,7 @@ describe('样例接口测试', () => {
|
||||
|
||||
it('无效Token应返回401', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
new Request('http://localhost/example/user/admin', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer invalid_token_here',
|
||||
@ -148,7 +148,7 @@ describe('样例接口测试', () => {
|
||||
|
||||
it('错误的Authorization格式应返回401', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/user/admin', {
|
||||
new Request('http://localhost/example/user/admin', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'InvalidFormat token',
|
||||
@ -165,10 +165,10 @@ describe('样例接口测试', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sample/health', () => {
|
||||
describe('GET /api/example/health', () => {
|
||||
it('应该返回模块健康状态', async () => {
|
||||
const res = await app.fetch(
|
||||
new Request('http://localhost/sample/health', {
|
||||
new Request('http://localhost/example/health', {
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
@ -180,7 +180,7 @@ describe('样例接口测试', () => {
|
||||
expect(body.code).toBe(0);
|
||||
expect(body.message).toBe('样例模块运行正常');
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.module).toBe('sample');
|
||||
expect(body.data.module).toBe('example');
|
||||
expect(body.data.status).toBe('healthy');
|
||||
expect(typeof body.data.timestamp).toBe('string');
|
||||
});
|
@ -11,7 +11,7 @@ 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';
|
||||
import { exampleController } from './example/example.controller';
|
||||
|
||||
/**
|
||||
* 主路由控制器 - API 路由总入口
|
||||
@ -32,4 +32,4 @@ export const controllers = new Elysia({
|
||||
// 健康检查接口
|
||||
.group('/health', (app) => app.use(healthController))
|
||||
// 样例接口
|
||||
.group('/sample', (app) => app.use(sampleController));
|
||||
.group('/example', (app) => app.use(exampleController));
|
||||
|
@ -1,82 +0,0 @@
|
||||
/**
|
||||
* @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(),
|
||||
}),
|
||||
};
|
@ -21,7 +21,7 @@ export const tags = {
|
||||
/** 测试接口 */
|
||||
test: 'Test',
|
||||
/** 样例接口 */
|
||||
sample: 'Sample',
|
||||
example: 'example',
|
||||
/** 文件上传接口 */
|
||||
upload: 'Upload',
|
||||
/** 系统管理接口 */
|
||||
|
@ -8,17 +8,17 @@
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import * as schema from './schema';
|
||||
import { createDrizzleDB } from './drizzle.service';
|
||||
import * as schema from '../../eneities';
|
||||
import { drizzleService } from './drizzle.service';
|
||||
|
||||
/**
|
||||
* Drizzle ORM 插件
|
||||
* 提供类型安全的数据库操作接口
|
||||
*/
|
||||
export const drizzlePlugin = new Elysia({ name: 'drizzle' }).onStart(async () => {
|
||||
await createDrizzleDB();
|
||||
await drizzleService.initialize();
|
||||
});
|
||||
|
||||
/** 导出数据库类型,供其他模块使用 */
|
||||
export type DB = typeof schema;
|
||||
export type DrizzleDB = Awaited<ReturnType<typeof createDrizzleDB>>;
|
||||
export type DrizzleDB = Awaited<ReturnType<typeof drizzleService.initialize>>;
|
||||
|
@ -1,61 +1,461 @@
|
||||
/**
|
||||
* @file Drizzle ORM服务类
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 专业的Drizzle ORM数据库连接服务类,支持连接池管理、状态跟踪和优雅关闭
|
||||
*/
|
||||
|
||||
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';
|
||||
import * as schema from '../../eneities';
|
||||
import type { DrizzleDB, ConnectionStatus, DatabaseConnectionInfo } from '@/type/drizzle.type';
|
||||
|
||||
export let db: DrizzleDB;
|
||||
/**
|
||||
* 创建MySQL连接池
|
||||
* Drizzle数据库服务类
|
||||
* 使用单例模式管理数据库连接
|
||||
*/
|
||||
const createConnection = async () => {
|
||||
try {
|
||||
/** MySQL连接池配置 */
|
||||
const connection = mysql.createPool({
|
||||
export class DrizzleService {
|
||||
/** 单例实例 */
|
||||
private static instance: DrizzleService | null = null;
|
||||
|
||||
/** 数据库实例 */
|
||||
private _db: DrizzleDB | null = null;
|
||||
|
||||
/** 连接池实例 */
|
||||
private _connectionPool: mysql.Pool | null = null;
|
||||
|
||||
/** 连接状态信息 */
|
||||
private _connectionInfo: DatabaseConnectionInfo;
|
||||
|
||||
/** 初始化标志 */
|
||||
private _isInitialized = false;
|
||||
|
||||
/** 连接池配置 */
|
||||
private readonly _poolConfig = {
|
||||
/** 最大连接数 */
|
||||
connectionLimit: Number(process.env.DB_CONNECTION_LIMIT) || 10,
|
||||
/** 队列限制 */
|
||||
queueLimit: Number(process.env.DB_QUEUE_LIMIT) || 0,
|
||||
/** 连接超时 */
|
||||
acquireTimeout: Number(process.env.DB_ACQUIRE_TIMEOUT) || 60000,
|
||||
/** 空闲超时 */
|
||||
timeout: Number(process.env.DB_TIMEOUT) || 60000,
|
||||
/** 重连配置 */
|
||||
reconnect: true,
|
||||
/** 最大重连次数 */
|
||||
maxReconnects: 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止外部实例化
|
||||
*/
|
||||
private constructor() {
|
||||
this._connectionInfo = {
|
||||
status: 'disconnected',
|
||||
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连接池创建失败');
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(): DrizzleService {
|
||||
if (!DrizzleService.instance) {
|
||||
DrizzleService.instance = new DrizzleService();
|
||||
}
|
||||
return DrizzleService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库实例
|
||||
*/
|
||||
public get db(): DrizzleDB {
|
||||
if (!this._db) {
|
||||
throw new Error('数据库未初始化,请先调用 initialize() 方法');
|
||||
}
|
||||
return this._db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态信息
|
||||
*/
|
||||
public get connectionInfo(): DatabaseConnectionInfo {
|
||||
return { ...this._connectionInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
*/
|
||||
public get isInitialized(): boolean {
|
||||
return this._isInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数据库配置
|
||||
*/
|
||||
private validateConfig(): void {
|
||||
const requiredFields = ['host', 'port', 'user', 'password', 'database'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!dbConfig[field as keyof typeof dbConfig]) {
|
||||
throw new Error(`数据库配置缺少必需字段: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dbConfig.port < 1 || dbConfig.port > 65535) {
|
||||
throw new Error(`数据库端口号无效: ${dbConfig.port}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新连接状态
|
||||
*/
|
||||
private updateConnectionStatus(status: ConnectionStatus, error?: string): void {
|
||||
this._connectionInfo.status = status;
|
||||
this._connectionInfo.error = error;
|
||||
|
||||
if (status === 'connected') {
|
||||
this._connectionInfo.connectedAt = new Date();
|
||||
this._connectionInfo.error = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建MySQL连接池
|
||||
*/
|
||||
private async createConnection(): Promise<mysql.Pool> {
|
||||
try {
|
||||
this.validateConfig();
|
||||
|
||||
this.updateConnectionStatus('connecting');
|
||||
|
||||
/** MySQL连接池配置 */
|
||||
const connection = mysql.createPool({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database,
|
||||
...this._poolConfig,
|
||||
});
|
||||
|
||||
// 测试连接
|
||||
const testConnection = await connection.getConnection();
|
||||
await testConnection.ping();
|
||||
testConnection.release();
|
||||
|
||||
this.updateConnectionStatus('connected');
|
||||
Logger.info({
|
||||
message: 'MySQL连接池创建成功',
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
database: dbConfig.database,
|
||||
connectionLimit: this._poolConfig.connectionLimit,
|
||||
});
|
||||
|
||||
return connection;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.updateConnectionStatus('error', errorMessage);
|
||||
Logger.error(new Error(`MySQL连接池创建失败: ${errorMessage}`));
|
||||
throw new Error(`MySQL连接池创建失败: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库连接
|
||||
*/
|
||||
public async initialize(): Promise<DrizzleDB> {
|
||||
// 防止重复初始化
|
||||
if (this._isInitialized && this._db) {
|
||||
Logger.info('Drizzle ORM 已初始化,返回现有实例');
|
||||
return this._db;
|
||||
}
|
||||
|
||||
try {
|
||||
this._connectionPool = await this.createConnection();
|
||||
|
||||
/** Drizzle数据库实例 */
|
||||
this._db = drizzle(this._connectionPool, {
|
||||
schema,
|
||||
mode: 'default',
|
||||
logger: process.env.NODE_ENV === 'development' ? {
|
||||
logQuery: (query, params) => {
|
||||
Logger.debug({
|
||||
message: 'SQL查询执行',
|
||||
query: query.replace(/\s+/g, ' ').trim(),
|
||||
params: params,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
} : false,
|
||||
});
|
||||
|
||||
this._isInitialized = true;
|
||||
Logger.info({
|
||||
message: 'Drizzle ORM 初始化成功',
|
||||
schema: Object.keys(schema).length > 0 ? Object.keys(schema) : ['无schema'],
|
||||
loggerEnabled: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
return this._db;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.updateConnectionStatus('error', errorMessage);
|
||||
Logger.error(new Error(`Drizzle ORM 初始化失败: ${errorMessage}`));
|
||||
throw new Error(`Drizzle ORM 初始化失败: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库连接状态
|
||||
*/
|
||||
public async checkConnection(): Promise<boolean> {
|
||||
try {
|
||||
if (!this._connectionPool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const connection = await this._connectionPool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error(error instanceof Error ? error : new Error('数据库连接检查失败'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优雅关闭数据库连接
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
try {
|
||||
if (this._connectionPool) {
|
||||
await this._connectionPool.end();
|
||||
this._connectionPool = null;
|
||||
this._db = null;
|
||||
this.updateConnectionStatus('disconnected');
|
||||
this._isInitialized = false;
|
||||
Logger.info('数据库连接已关闭');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
Logger.error(new Error(`关闭数据库连接时出错: ${errorMessage}`));
|
||||
throw new Error(`关闭数据库连接失败: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新连接数据库
|
||||
*/
|
||||
public async reconnect(): Promise<DrizzleDB> {
|
||||
Logger.info('正在重新连接数据库...');
|
||||
|
||||
// 先关闭现有连接
|
||||
await this.close();
|
||||
|
||||
// 重新初始化连接
|
||||
return await this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接池统计信息
|
||||
*/
|
||||
public getPoolStats(): {
|
||||
connectionLimit: number;
|
||||
acquireTimeout: number;
|
||||
timeout: number;
|
||||
queueLimit: number;
|
||||
} | null {
|
||||
if (!this._connectionPool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
connectionLimit: this._poolConfig.connectionLimit,
|
||||
acquireTimeout: this._poolConfig.acquireTimeout,
|
||||
timeout: this._poolConfig.timeout,
|
||||
queueLimit: this._poolConfig.queueLimit,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行健康检查
|
||||
*/
|
||||
public async healthCheck(): Promise<{
|
||||
status: 'healthy' | 'unhealthy';
|
||||
connectionInfo: DatabaseConnectionInfo;
|
||||
isConnected: boolean;
|
||||
poolStats?: ReturnType<DrizzleService['getPoolStats']>;
|
||||
}> {
|
||||
const isConnected = await this.checkConnection();
|
||||
|
||||
return {
|
||||
status: isConnected ? 'healthy' : 'unhealthy',
|
||||
connectionInfo: this.connectionInfo,
|
||||
isConnected,
|
||||
poolStats: this.getPoolStats(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Drizzle数据库实例
|
||||
* ==============================================
|
||||
* 主要导出 - 推荐使用的API
|
||||
* ==============================================
|
||||
*/
|
||||
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)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Drizzle服务单例实例
|
||||
*
|
||||
* @description 获取DrizzleService的单例实例,推荐的使用方式
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { drizzleService } from '@/plugins/drizzle/drizzle.service';
|
||||
*
|
||||
* // 初始化数据库
|
||||
* await drizzleService.initialize();
|
||||
*
|
||||
* // 获取数据库实例
|
||||
* const database = drizzleService.db;
|
||||
*
|
||||
* // 检查连接状态
|
||||
* const isConnected = await drizzleService.checkConnection();
|
||||
* ```
|
||||
*/
|
||||
export const drizzleService = DrizzleService.getInstance();
|
||||
|
||||
Logger.info('Drizzle ORM 初始化成功');
|
||||
return db;
|
||||
} catch (error) {
|
||||
Logger.error(error as Error);
|
||||
throw new Error('Drizzle ORM 初始化失败');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* ==============================================
|
||||
* 向后兼容导出 - 保持原有函数式API
|
||||
* ==============================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建并初始化Drizzle数据库连接
|
||||
*
|
||||
* @description 向后兼容的初始化方法,内部调用drizzleService.initialize()
|
||||
* @returns {Promise<DrizzleDB>} 返回初始化后的Drizzle数据库实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createDrizzleDB } from '@/plugins/drizzle/drizzle.service';
|
||||
*
|
||||
* const database = await createDrizzleDB();
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 drizzleService.initialize() 替代
|
||||
*/
|
||||
export const createDrizzleDB = () => drizzleService.initialize();
|
||||
|
||||
/**
|
||||
* 获取数据库连接状态信息
|
||||
*
|
||||
* @description 向后兼容的状态获取方法,内部调用drizzleService.connectionInfo
|
||||
* @returns {DatabaseConnectionInfo} 返回数据库连接状态信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getConnectionInfo } from '@/plugins/drizzle/drizzle.service';
|
||||
*
|
||||
* const info = getConnectionInfo();
|
||||
* console.log(`数据库状态: ${info.status}`);
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 drizzleService.connectionInfo 替代
|
||||
*/
|
||||
export const getConnectionInfo = () => drizzleService.connectionInfo;
|
||||
|
||||
/**
|
||||
* 检查数据库连接状态
|
||||
*
|
||||
* @description 向后兼容的连接检查方法,内部调用drizzleService.checkConnection()
|
||||
* @returns {Promise<boolean>} 返回连接是否正常
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { checkConnection } from '@/plugins/drizzle/drizzle.service';
|
||||
*
|
||||
* const isConnected = await checkConnection();
|
||||
* if (!isConnected) {
|
||||
* console.log('数据库连接异常');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 drizzleService.checkConnection() 替代
|
||||
*/
|
||||
export const checkConnection = () => drizzleService.checkConnection();
|
||||
|
||||
/**
|
||||
* 优雅关闭数据库连接
|
||||
*
|
||||
* @description 向后兼容的连接关闭方法,内部调用drizzleService.close()
|
||||
* @returns {Promise<void>} 返回关闭操作的Promise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { closeDrizzleDB } from '@/plugins/drizzle/drizzle.service';
|
||||
*
|
||||
* // 应用关闭时清理资源
|
||||
* process.on('SIGTERM', async () => {
|
||||
* await closeDrizzleDB();
|
||||
* process.exit(0);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 drizzleService.close() 替代
|
||||
*/
|
||||
export const closeDrizzleDB = () => drizzleService.close();
|
||||
|
||||
/**
|
||||
* 重新连接数据库
|
||||
*
|
||||
* @description 向后兼容的重连方法,内部调用drizzleService.reconnect()
|
||||
* @returns {Promise<DrizzleDB>} 返回重新连接后的数据库实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { reconnectDB } from '@/plugins/drizzle/drizzle.service';
|
||||
*
|
||||
* try {
|
||||
* const database = await reconnectDB();
|
||||
* console.log('数据库重连成功');
|
||||
* } catch (error) {
|
||||
* console.error('数据库重连失败:', error);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 drizzleService.reconnect() 替代
|
||||
*/
|
||||
export const reconnectDB = () => drizzleService.reconnect();
|
||||
|
||||
/**
|
||||
* 获取数据库实例
|
||||
*
|
||||
* @description 向后兼容的数据库实例获取方法,内部调用drizzleService.db
|
||||
* @returns {DrizzleDB} 返回Drizzle数据库实例
|
||||
* @throws {Error} 如果数据库未初始化则抛出错误
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { db } from '@/plugins/drizzle/drizzle.service';
|
||||
*
|
||||
* // 确保先初始化
|
||||
* await createDrizzleDB();
|
||||
*
|
||||
* // 获取数据库实例
|
||||
* const database = db();
|
||||
* const users = await database.select().from(usersTable);
|
||||
* ```
|
||||
*
|
||||
* @deprecateds 推荐使用 drizzleService.db 替代
|
||||
*/
|
||||
export const db = () => drizzleService.db;
|
@ -1,15 +0,0 @@
|
||||
/**
|
||||
* @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';
|
@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @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;
|
@ -12,6 +12,7 @@ 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';
|
||||
import { redisPlugin } from '@/plugins/redis/redis.plugins';
|
||||
|
||||
export const plugins = (app: Elysia) =>
|
||||
app
|
||||
@ -21,5 +22,7 @@ export const plugins = (app: Elysia) =>
|
||||
.use(errorHandlerPlugin)
|
||||
// 数据库插件
|
||||
.use(drizzlePlugin)
|
||||
// Redis插件
|
||||
.use(redisPlugin)
|
||||
// API 文档插件
|
||||
.use(swaggerPlugin);
|
||||
|
@ -14,7 +14,7 @@ import type {
|
||||
JwtPayloadType,
|
||||
JwtSignOptionsType,
|
||||
} from '@/type/jwt.type';
|
||||
import type { UserInfoType } from '@/modules/sample/example.schema';
|
||||
import type { UserInfoType } from '@/modules/example/example.schema';
|
||||
|
||||
/**
|
||||
* JWT服务类
|
||||
|
20
src/plugins/redis/redis.plugins.ts
Normal file
20
src/plugins/redis/redis.plugins.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @file Drizzle ORM 数据库插件
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 集成Drizzle ORM到Elysia,提供类型安全的数据库操作
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { redisService } from './redis.service';
|
||||
|
||||
/**
|
||||
* Drizzle ORM 插件
|
||||
* 提供类型安全的数据库操作接口
|
||||
*/
|
||||
export const redisPlugin = new Elysia({ name: 'redis' }).onStart(async () => {
|
||||
await redisService.initialize();
|
||||
});
|
||||
|
399
src/plugins/redis/redis.service.ts
Normal file
399
src/plugins/redis/redis.service.ts
Normal file
@ -0,0 +1,399 @@
|
||||
/**
|
||||
* @file Redis服务类
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 精简的Redis连接服务类,支持连接管理、状态跟踪和健康检查
|
||||
*/
|
||||
|
||||
import { createClient } from 'redis';
|
||||
import { redisConfig, getRedisUrl } from '@/config/redis.config';
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import type {
|
||||
RedisConnectionStatus,
|
||||
RedisConnectionInfo,
|
||||
RedisHealthCheckResult
|
||||
} from '@/type/redis.type';
|
||||
|
||||
/**
|
||||
* Redis服务类
|
||||
* 使用单例模式管理Redis连接
|
||||
*/
|
||||
export class RedisService {
|
||||
/** 单例实例 */
|
||||
private static instance: RedisService | null = null;
|
||||
|
||||
/** Redis客户端实例 */
|
||||
private _client: any = null;
|
||||
|
||||
/** 连接状态信息 */
|
||||
private _connectionInfo: RedisConnectionInfo;
|
||||
|
||||
/** 初始化标志 */
|
||||
private _isInitialized = false;
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止外部实例化
|
||||
*/
|
||||
private constructor() {
|
||||
this._connectionInfo = {
|
||||
status: 'disconnected',
|
||||
host: redisConfig.host,
|
||||
port: redisConfig.port,
|
||||
database: redisConfig.database,
|
||||
connectName: redisConfig.connectName,
|
||||
isConnected: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(): RedisService {
|
||||
if (!RedisService.instance) {
|
||||
RedisService.instance = new RedisService();
|
||||
}
|
||||
return RedisService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis客户端实例
|
||||
*/
|
||||
public get client(): any {
|
||||
if (!this._client) {
|
||||
throw new Error('Redis未初始化,请先调用 initialize() 方法');
|
||||
}
|
||||
return this._client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态信息
|
||||
*/
|
||||
public get connectionInfo(): RedisConnectionInfo {
|
||||
return { ...this._connectionInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
*/
|
||||
public get isInitialized(): boolean {
|
||||
return this._isInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Redis配置
|
||||
*/
|
||||
private validateConfig(): void {
|
||||
if (!redisConfig.host || !redisConfig.port) {
|
||||
throw new Error('Redis配置无效:缺少host或port');
|
||||
}
|
||||
|
||||
if (redisConfig.port < 1 || redisConfig.port > 65535) {
|
||||
throw new Error(`Redis端口号无效: ${redisConfig.port}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新连接状态
|
||||
*/
|
||||
private updateConnectionStatus(status: RedisConnectionStatus, error?: string): void {
|
||||
this._connectionInfo.status = status;
|
||||
this._connectionInfo.error = error;
|
||||
this._connectionInfo.isConnected = status === 'connected';
|
||||
|
||||
if (status === 'connected') {
|
||||
this._connectionInfo.connectedAt = new Date();
|
||||
this._connectionInfo.error = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Redis连接
|
||||
*/
|
||||
public async initialize(): Promise<any> {
|
||||
// 防止重复初始化
|
||||
if (this._isInitialized && this._client) {
|
||||
Logger.info('Redis 已初始化,返回现有实例');
|
||||
return this._client;
|
||||
}
|
||||
|
||||
try {
|
||||
this.validateConfig();
|
||||
this.updateConnectionStatus('connecting');
|
||||
|
||||
// 创建Redis客户端
|
||||
this._client = createClient({
|
||||
name: redisConfig.connectName,
|
||||
username: redisConfig.username,
|
||||
password: redisConfig.password,
|
||||
database: redisConfig.database,
|
||||
url: getRedisUrl(),
|
||||
});
|
||||
|
||||
// 连接Redis
|
||||
await this._client.connect();
|
||||
|
||||
// 测试连接
|
||||
await this._client.ping();
|
||||
|
||||
this._isInitialized = true;
|
||||
this.updateConnectionStatus('connected');
|
||||
|
||||
Logger.info({
|
||||
message: 'Redis 初始化成功',
|
||||
host: redisConfig.host,
|
||||
port: redisConfig.port,
|
||||
database: redisConfig.database,
|
||||
connectName: redisConfig.connectName,
|
||||
});
|
||||
|
||||
return this._client;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.updateConnectionStatus('error', errorMessage);
|
||||
Logger.error(new Error(`Redis 初始化失败: ${errorMessage}`));
|
||||
throw new Error(`Redis 初始化失败: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*/
|
||||
public async checkConnection(): Promise<boolean> {
|
||||
try {
|
||||
if (!this._client || !this._connectionInfo.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this._client.ping();
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error(error instanceof Error ? error : new Error('Redis连接检查失败'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行健康检查
|
||||
*/
|
||||
public async healthCheck(): Promise<RedisHealthCheckResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
if (!this._client) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
responseTime: 0,
|
||||
connectionInfo: this.connectionInfo,
|
||||
error: 'Redis客户端未初始化',
|
||||
};
|
||||
}
|
||||
|
||||
// 执行ping测试
|
||||
await this._client.ping();
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: 'healthy',
|
||||
responseTime,
|
||||
connectionInfo: this.connectionInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
responseTime,
|
||||
connectionInfo: this.connectionInfo,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优雅关闭连接
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
try {
|
||||
if (this._client && this._connectionInfo.isConnected) {
|
||||
await this._client.quit();
|
||||
this._client = null;
|
||||
this.updateConnectionStatus('disconnected');
|
||||
this._isInitialized = false;
|
||||
Logger.info('Redis连接已关闭');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
Logger.error(new Error(`关闭Redis连接时出错: ${errorMessage}`));
|
||||
throw new Error(`关闭Redis连接失败: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新连接
|
||||
*/
|
||||
public async reconnect(): Promise<any> {
|
||||
Logger.info('正在重新连接Redis...');
|
||||
|
||||
// 先关闭现有连接
|
||||
await this.close();
|
||||
|
||||
// 重新初始化连接
|
||||
return await this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ==============================================
|
||||
* 主要导出 - 推荐使用的API
|
||||
* ==============================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Redis服务单例实例
|
||||
*
|
||||
* @description 获取RedisService的单例实例,推荐的使用方式
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { redisService } from '@/plugins/redis/redis.service';
|
||||
*
|
||||
* // 初始化Redis
|
||||
* await redisService.initialize();
|
||||
*
|
||||
* // 获取客户端实例
|
||||
* const client = redisService.client;
|
||||
*
|
||||
* // 检查连接状态
|
||||
* const isConnected = await redisService.checkConnection();
|
||||
* ```
|
||||
*/
|
||||
export const redisService = RedisService.getInstance();
|
||||
|
||||
/**
|
||||
* ==============================================
|
||||
* 向后兼容导出 - 保持原有函数式API
|
||||
* ==============================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建并初始化Redis连接
|
||||
*
|
||||
* @description 向后兼容的初始化方法,内部调用redisService.initialize()
|
||||
* @returns {Promise<any>} 返回初始化后的Redis客户端实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createRedisClient } from '@/plugins/redis/redis.service';
|
||||
*
|
||||
* const client = await createRedisClient();
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 redisService.initialize() 替代
|
||||
*/
|
||||
export const createRedisClient = () => redisService.initialize();
|
||||
|
||||
/**
|
||||
* 获取Redis连接状态信息
|
||||
*
|
||||
* @description 向后兼容的状态获取方法,内部调用redisService.connectionInfo
|
||||
* @returns {RedisConnectionInfo} 返回Redis连接状态信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getRedisConnectionInfo } from '@/plugins/redis/redis.service';
|
||||
*
|
||||
* const info = getRedisConnectionInfo();
|
||||
* console.log(`Redis状态: ${info.status}`);
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 redisService.connectionInfo 替代
|
||||
*/
|
||||
export const getRedisConnectionInfo = () => redisService.connectionInfo;
|
||||
|
||||
/**
|
||||
* 检查Redis连接状态
|
||||
*
|
||||
* @description 向后兼容的连接检查方法,内部调用redisService.checkConnection()
|
||||
* @returns {Promise<boolean>} 返回连接是否正常
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { checkRedisConnection } from '@/plugins/redis/redis.service';
|
||||
*
|
||||
* const isConnected = await checkRedisConnection();
|
||||
* if (!isConnected) {
|
||||
* console.log('Redis连接异常');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 redisService.checkConnection() 替代
|
||||
*/
|
||||
export const checkRedisConnection = () => redisService.checkConnection();
|
||||
|
||||
/**
|
||||
* 优雅关闭Redis连接
|
||||
*
|
||||
* @description 向后兼容的连接关闭方法,内部调用redisService.close()
|
||||
* @returns {Promise<void>} 返回关闭操作的Promise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { closeRedisConnection } from '@/plugins/redis/redis.service';
|
||||
*
|
||||
* // 应用关闭时清理资源
|
||||
* process.on('SIGTERM', async () => {
|
||||
* await closeRedisConnection();
|
||||
* process.exit(0);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 redisService.close() 替代
|
||||
*/
|
||||
export const closeRedisConnection = () => redisService.close();
|
||||
|
||||
/**
|
||||
* 重新连接Redis
|
||||
*
|
||||
* @description 向后兼容的重连方法,内部调用redisService.reconnect()
|
||||
* @returns {Promise<any>} 返回重新连接后的客户端实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { reconnectRedis } from '@/plugins/redis/redis.service';
|
||||
*
|
||||
* try {
|
||||
* const client = await reconnectRedis();
|
||||
* console.log('Redis重连成功');
|
||||
* } catch (error) {
|
||||
* console.error('Redis重连失败:', error);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 redisService.reconnect() 替代
|
||||
*/
|
||||
export const reconnectRedis = () => redisService.reconnect();
|
||||
|
||||
/**
|
||||
* 获取Redis客户端实例
|
||||
*
|
||||
* @description 向后兼容的客户端获取方法,内部调用redisService.client
|
||||
* @returns {any} 返回Redis客户端实例
|
||||
* @throws {Error} 如果Redis未初始化则抛出错误
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { redis } from '@/plugins/redis/redis.service';
|
||||
*
|
||||
* // 确保先初始化
|
||||
* await createRedisClient();
|
||||
*
|
||||
* // 获取客户端实例
|
||||
* const client = redis();
|
||||
* const result = await client.get('key');
|
||||
* ```
|
||||
*
|
||||
* @deprecated 推荐使用 redisService.client 替代
|
||||
*/
|
||||
export const redis = () => redisService.client;
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
||||
import type * as schema from '../src/plugins/drizzle/schema';
|
||||
import type * as schema from '@/eneities';
|
||||
|
||||
/**
|
||||
* Drizzle数据库实例类型
|
64
src/type/redis.type.ts
Normal file
64
src/type/redis.type.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @file Redis类型定义
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-06-29
|
||||
* @description 定义Redis相关的类型,包括连接状态、配置信息等
|
||||
*/
|
||||
|
||||
import type { RedisClientType } from 'redis';
|
||||
|
||||
/**
|
||||
* Redis客户端类型
|
||||
*/
|
||||
export type RedisClient = RedisClientType;
|
||||
|
||||
/**
|
||||
* Redis连接状态
|
||||
*/
|
||||
export type RedisConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected';
|
||||
|
||||
/**
|
||||
* Redis连接信息
|
||||
*/
|
||||
export interface RedisConnectionInfo {
|
||||
/** 连接状态 */
|
||||
status: RedisConnectionStatus;
|
||||
/** 连接主机 */
|
||||
host: string;
|
||||
/** 连接端口 */
|
||||
port: number;
|
||||
/** 数据库索引 */
|
||||
database: number;
|
||||
/** 连接名称 */
|
||||
connectName: string;
|
||||
/** 连接时间 */
|
||||
connectedAt?: Date;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 是否已连接 */
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展Elysia Context,添加Redis实例
|
||||
*/
|
||||
export interface RedisContext {
|
||||
/** Redis客户端实例 */
|
||||
redis: RedisClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis健康检查结果
|
||||
*/
|
||||
export interface RedisHealthCheckResult {
|
||||
/** 状态 */
|
||||
status: 'healthy' | 'unhealthy';
|
||||
/** 响应时间 */
|
||||
responseTime: number;
|
||||
/** 连接信息 */
|
||||
connectionInfo: RedisConnectionInfo;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type';
|
||||
import type { UserInfoType } from '@/modules/sample/example.schema';
|
||||
import type { UserInfoType } from '@/modules/example/example.schema';
|
||||
|
||||
/**
|
||||
* 从完整用户信息创建JWT用户信息
|
||||
|
170
src/utils/response.helper.ts
Normal file
170
src/utils/response.helper.ts
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @file 响应格式工具函数
|
||||
* @author AI助手
|
||||
* @date 2025-06-29
|
||||
* @description 提供统一的响应格式构造函数,确保API响应格式的一致性
|
||||
*/
|
||||
|
||||
import type { ErrorCode } from '@/constants/error-codes';
|
||||
import { ERROR_CODES, ERROR_CODE_MESSAGES } from '@/constants/error-codes';
|
||||
|
||||
/**
|
||||
* 标准API响应格式
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
/** 业务状态码 */
|
||||
code: ErrorCode;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 响应数据 */
|
||||
data: T;
|
||||
/** 时间戳 */
|
||||
timestamp?: string;
|
||||
/** 请求ID(可选,用于追踪) */
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应构造函数
|
||||
* @param data 响应数据
|
||||
* @param message 自定义消息,默认使用标准成功消息
|
||||
* @returns 标准成功响应格式
|
||||
*/
|
||||
export function successResponse<T>(
|
||||
data: T,
|
||||
message: string = ERROR_CODE_MESSAGES[ERROR_CODES.SUCCESS]
|
||||
): ApiResponse<T> {
|
||||
return {
|
||||
code: ERROR_CODES.SUCCESS,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应构造函数
|
||||
* @param code 错误码
|
||||
* @param message 错误消息,如果不提供则使用默认消息
|
||||
* @param data 错误详情数据,默认为null
|
||||
* @returns 标准错误响应格式
|
||||
*/
|
||||
export function errorResponse<T = null>(
|
||||
code: ErrorCode,
|
||||
message?: string,
|
||||
data: T = null as T
|
||||
): ApiResponse<T> {
|
||||
return {
|
||||
code,
|
||||
message: message || ERROR_CODE_MESSAGES[code] || '未知错误',
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页数据响应格式
|
||||
*/
|
||||
export interface PaginatedData<T> {
|
||||
/** 数据列表 */
|
||||
items: T[];
|
||||
/** 总记录数 */
|
||||
total: number;
|
||||
/** 当前页码 */
|
||||
page: number;
|
||||
/** 每页记录数 */
|
||||
pageSize: number;
|
||||
/** 总页数 */
|
||||
totalPages: number;
|
||||
/** 是否有下一页 */
|
||||
hasNext: boolean;
|
||||
/** 是否有上一页 */
|
||||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应构造函数
|
||||
* @param items 数据列表
|
||||
* @param total 总记录数
|
||||
* @param page 当前页码
|
||||
* @param pageSize 每页记录数
|
||||
* @param message 自定义消息
|
||||
* @returns 标准分页响应格式
|
||||
*/
|
||||
export function paginatedResponse<T>(
|
||||
items: T[],
|
||||
total: number,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
message: string = '查询成功'
|
||||
): ApiResponse<PaginatedData<T>> {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
return successResponse<PaginatedData<T>>({
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
}, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务错误类
|
||||
* @description 用于抛出具有特定错误码的业务异常
|
||||
*/
|
||||
export class BusinessError extends Error {
|
||||
public readonly code: ErrorCode;
|
||||
|
||||
constructor(message: string, code: ErrorCode = ERROR_CODES.BUSINESS_ERROR) {
|
||||
super(message);
|
||||
this.name = 'BusinessError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证错误类
|
||||
* @description 用于参数验证失败的场景
|
||||
*/
|
||||
export class ValidationError extends BusinessError {
|
||||
constructor(message: string) {
|
||||
super(message, ERROR_CODES.VALIDATION_ERROR);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证错误类
|
||||
* @description 用于认证相关的错误
|
||||
*/
|
||||
export class AuthenticationError extends BusinessError {
|
||||
constructor(message: string, code: ErrorCode = ERROR_CODES.UNAUTHORIZED) {
|
||||
super(message, code);
|
||||
this.name = 'AuthenticationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限错误类
|
||||
* @description 用于权限不足的场景
|
||||
*/
|
||||
export class ForbiddenError extends BusinessError {
|
||||
constructor(message: string = '权限不足') {
|
||||
super(message, ERROR_CODES.FORBIDDEN);
|
||||
this.name = 'ForbiddenError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在错误类
|
||||
* @description 用于资源不存在的场景
|
||||
*/
|
||||
export class NotFoundError extends BusinessError {
|
||||
constructor(message: string = '资源不存在') {
|
||||
super(message, ERROR_CODES.NOT_FOUND);
|
||||
this.name = 'NotFoundError';
|
||||
}
|
||||
}
|
@ -44,7 +44,7 @@
|
||||
"include": [
|
||||
"src",
|
||||
"types/**/*.d.ts"
|
||||
],
|
||||
, "drizzle.config.ts" ],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
|
Loading…
Reference in New Issue
Block a user