From 200c3b0b1089f67b06717b95ffbd86c2541c7212 Mon Sep 17 00:00:00 2001 From: "HeXiaoLong:Suanier" Date: Fri, 4 Jul 2025 17:19:25 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=BB=93=E6=9E=84=E5=8C=96=E8=B0=83?= =?UTF-8?q?=E6=95=B4=EF=BC=8Cmysql=E3=80=81redis=E3=80=81=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=AE=9E=E4=BD=93=E3=80=81cursorRules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/README.mdc | 377 +++ .cursor/rules/ai-development-workflow.mdc | 403 ++++ .cursor/rules/api-development-standard.md | 174 -- .cursor/rules/cursor-comment-rules.mdc | 112 - .cursor/rules/elysia-api-rules.mdc | 110 - .cursor/rules/elysia-backend-rules.mdc | 144 -- .cursor/rules/elysia-interface-standards.md | 1084 --------- .cursor/rules/elysia-rules.mdc | 679 +++++- .cursor/rules/elysia-test-rules.mdc | 108 - .cursor/rules/test-rules.mdc | 104 - database_export.sql | 500 ++++ docs/api-type-usage-examples.md | 2 +- .../git-commit-rules.md | 108 +- docs/jwt-usage-examples.md | 2 +- .../drizzle.config.ts => drizzle.config.ts | 85 +- drizzle/0000_nostalgic_eternity.sql | 293 +++ drizzle/meta/0000_snapshot.json | 2100 +++++++++++++++++ drizzle/meta/_journal.json | 13 + drizzle/relations.ts | 3 + drizzle/schema.ts | 316 +++ package.json | 9 +- src/constants/error-codes.ts | 111 + src/eneities/customType.ts | 27 + src/eneities/index.ts | 317 +++ .../{sample => example}/example.controller.ts | 106 +- src/modules/example/example.response.ts | 54 + .../{sample => example}/example.schema.ts | 156 +- .../{sample => example}/example.service.ts | 172 +- .../{sample => example}/example.test.ts | 374 +-- src/modules/index.ts | 4 +- src/modules/sample/example.response.ts | 82 - src/modules/tags.ts | 2 +- src/plugins/drizzle/drizzle.plugins.ts | 8 +- src/plugins/drizzle/drizzle.service.ts | 490 +++- src/plugins/drizzle/schema/index.ts | 15 - src/plugins/drizzle/schema/users.ts | 48 - src/plugins/index.ts | 3 + src/plugins/jwt/jwt.service.ts | 2 +- src/plugins/redis/redis.plugins.ts | 20 + src/plugins/redis/redis.service.ts | 399 ++++ {types => src/type}/config.type.ts | 150 +- {types => src/type}/drizzle.type.ts | 102 +- {types => src/type}/logger.type.ts | 34 +- src/type/redis.type.ts | 64 + src/utils/jwt.helper.ts | 2 +- src/utils/response.helper.ts | 170 ++ tasks/{ => archive}/20250629-计划.md | 4 +- tsconfig.json | 2 +- 48 files changed, 6956 insertions(+), 2688 deletions(-) create mode 100644 .cursor/rules/README.mdc create mode 100644 .cursor/rules/ai-development-workflow.mdc delete mode 100644 .cursor/rules/api-development-standard.md delete mode 100644 .cursor/rules/cursor-comment-rules.mdc delete mode 100644 .cursor/rules/elysia-api-rules.mdc delete mode 100644 .cursor/rules/elysia-backend-rules.mdc delete mode 100644 .cursor/rules/elysia-interface-standards.md delete mode 100644 .cursor/rules/elysia-test-rules.mdc delete mode 100644 .cursor/rules/test-rules.mdc create mode 100644 database_export.sql rename .cursor/rules/git-commit-rules.mdc => docs/git-commit-rules.md (96%) rename src/plugins/drizzle/drizzle.config.ts => drizzle.config.ts (85%) create mode 100644 drizzle/0000_nostalgic_eternity.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 drizzle/relations.ts create mode 100644 drizzle/schema.ts create mode 100644 src/constants/error-codes.ts create mode 100644 src/eneities/customType.ts create mode 100644 src/eneities/index.ts rename src/modules/{sample => example}/example.controller.ts (90%) create mode 100644 src/modules/example/example.response.ts rename src/modules/{sample => example}/example.schema.ts (93%) rename src/modules/{sample => example}/example.service.ts (92%) rename src/modules/{sample => example}/example.test.ts (85%) delete mode 100644 src/modules/sample/example.response.ts delete mode 100644 src/plugins/drizzle/schema/index.ts delete mode 100644 src/plugins/drizzle/schema/users.ts create mode 100644 src/plugins/redis/redis.plugins.ts create mode 100644 src/plugins/redis/redis.service.ts rename {types => src/type}/config.type.ts (95%) rename {types => src/type}/drizzle.type.ts (90%) rename {types => src/type}/logger.type.ts (96%) create mode 100644 src/type/redis.type.ts create mode 100644 src/utils/response.helper.ts rename tasks/{ => archive}/20250629-计划.md (55%) diff --git a/.cursor/rules/README.mdc b/.cursor/rules/README.mdc new file mode 100644 index 0000000..7feda56 --- /dev/null +++ b/.cursor/rules/README.mdc @@ -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** - 完善错误处理和响应格式 + +记住:好的规范不是限制,而是让团队更高效协作的基础! 🎯 \ No newline at end of file diff --git a/.cursor/rules/ai-development-workflow.mdc b/.cursor/rules/ai-development-workflow.mdc new file mode 100644 index 0000000..a3b0b7a --- /dev/null +++ b/.cursor/rules/ai-development-workflow.mdc @@ -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攻击防护 + +记住:我的目标是让你专注于业务逻辑,而我来确保代码的规范性、安全性和可维护性! 🎯 \ No newline at end of file diff --git a/.cursor/rules/api-development-standard.md b/.cursor/rules/api-development-standard.md deleted file mode 100644 index 9eadad0..0000000 --- a/.cursor/rules/api-development-standard.md +++ /dev/null @@ -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; - -// example.service.ts - Service中使用类型 -import type { GetUserByUsernameParams } from './example.schema'; - -export class ExampleService { - async getUserByUsername(params: GetUserByUsernameParams): Promise { - 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结构) diff --git a/.cursor/rules/cursor-comment-rules.mdc b/.cursor/rules/cursor-comment-rules.mdc deleted file mode 100644 index 4d4d976..0000000 --- a/.cursor/rules/cursor-comment-rules.mdc +++ /dev/null @@ -1,112 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -## Cursor 代码注释规范(Code Comment Rules) - -### 1. 文件头部注释 -每个源文件开头应包含如下信息: -```javascript -/** - * @file 文件简要说明 - * @author 创建者姓名(如:张三 ) - * @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`),便于新成员快速上手。 -- 注释中严禁出现密码、密钥、用户隐私等敏感信息。 -- 重要模块、核心业务建议将注释内容同步到项目文档,便于知识传承和查阅。 - ---- - -**请所有开发者严格遵守以上注释规范,提升代码可读性与可维护性。** \ No newline at end of file diff --git a/.cursor/rules/elysia-api-rules.mdc b/.cursor/rules/elysia-api-rules.mdc deleted file mode 100644 index 548ae80..0000000 --- a/.cursor/rules/elysia-api-rules.mdc +++ /dev/null @@ -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": "张三" } - } - ``` - ---- - -**请所有开发者严格遵守以上规范,确保接口文档完整、参数校验严格、错误提示友好(中文),提升用户体验与协作效率。** \ No newline at end of file diff --git a/.cursor/rules/elysia-backend-rules.mdc b/.cursor/rules/elysia-backend-rules.mdc deleted file mode 100644 index 0ee7ad6..0000000 --- a/.cursor/rules/elysia-backend-rules.mdc +++ /dev/null @@ -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 后端服务的健壮性、安全性与可维护性。** \ No newline at end of file diff --git a/.cursor/rules/elysia-interface-standards.md b/.cursor/rules/elysia-interface-standards.md deleted file mode 100644 index 59ec71d..0000000 --- a/.cursor/rules/elysia-interface-standards.md +++ /dev/null @@ -1,1084 +0,0 @@ -# Elysia 接口编写规范(Elysia Interface Development Standards) - -## 目标 (Goal) - -本规范旨在提供一套完整的 Elysia 接口开发标准,结合官方文档最佳实践、社区经验和项目实际需求,确保代码质量、类型安全和开发效率。 - -## 核心原则 (Core Principles) - -### 1. 一切皆组件 (Everything is a Component) -- 每个 Elysia 实例都是一个组件 -- 组件可以被插入到其他实例中 -- 强制将应用拆分为小块,便于添加或移除功能 - -### 2. 方法链式调用 (Method Chaining) -- **必须始终使用方法链式调用** -- 确保类型完整性和推断 -- 每个方法返回新的类型引用 - -### 3. 类型安全优先 (Type Safety First) -- 使用 Elysia 内置类型系统 -- 避免使用 `any` 类型 -- 单一数据源原则 - -## 项目结构规范 (Project Structure Standards) - -### 推荐目录结构 - -``` -src/ -├── controllers/ # 控制器(路由与业务入口) -│ ├── auth/ -│ │ └── index.ts # 认证相关路由 -│ └── user/ -│ └── index.ts # 用户相关路由 -├── services/ # 业务逻辑服务层 -│ ├── auth/ -│ │ └── auth.service.ts -│ └── user/ -│ └── user.service.ts -├── validators/ # 参数校验(按路由结构组织) -│ ├── global.response.ts -│ ├── auth/ -│ │ ├── auth.validator.ts -│ │ └── auth.response.ts -│ └── user/ -│ ├── user.validator.ts -│ └── user.response.ts -├── models/ # 数据模型 -├── plugins/ # Elysia 插件 -├── utils/ # 工具函数 -├── config/ # 配置文件 -├── type/ # 类型定义文件 -└── app.ts # 应用入口 -``` - -## 接口设计规范 (Interface Design Standards) - -### 1. 控制器规范 (Controller Standards) - -**✅ 正确做法:使用 Elysia 实例作为控制器** - -```typescript -/** - * @file 用户认证控制器 - * @author 开发者姓名 - * @date 2024-01-01 - * @lastEditor 开发者姓名 - * @lastEditTime 2024-01-01 - * @description 用户认证相关接口,包含登录、注册、token 验证等功能 - */ - -import { Elysia } from 'elysia'; -import { jwtPlugin } from '@/plugins/jwt.plugins'; -import { - loginBodySchema, - registerBodySchema, - type LoginBody, - type RegisterBody -} from '@/validators/auth/auth.validator'; -import { - loginResponse200Schema, - loginResponse400Schema, - registerResponse200Schema, - registerResponse400Schema -} from '@/validators/auth/auth.response'; -import { loginService, registerService } from '@/services/auth/auth.service'; - -/** - * 认证控制器 - * @description 处理用户认证相关的 HTTP 请求 - */ -export const authController = new Elysia({ prefix: '/api/auth' }) - .use(jwtPlugin) - .post('/login', - ({ body, jwt, set }: { body: LoginBody; jwt: any; set: any }) => - loginService(body, jwt, set), - { - body: loginBodySchema, - detail: { - tags: ['认证'], - summary: '用户登录', - description: '用户使用用户名和密码进行登录,成功后返回 JWT token', - }, - response: { - 200: loginResponse200Schema, - 400: loginResponse400Schema, - }, - } - ) - .post('/register', - ({ body, set }: { body: RegisterBody; set: any }) => - registerService(body, set), - { - body: registerBodySchema, - detail: { - tags: ['认证'], - summary: '用户注册', - description: '新用户注册账户', - }, - response: { - 200: registerResponse200Schema, - 400: registerResponse400Schema, - }, - } - ); -``` - -**❌ 错误做法:创建单独的控制器类** - -```typescript -// ❌ 不要这样做 -abstract class AuthController { - static login(context: Context) { - return AuthService.login(context.body); - } -} - -new Elysia() - .post('/login', AuthController.login); -``` - -### 2. 服务层规范 (Service Layer Standards) - -#### 非请求依赖服务 (Non-Request Dependent Service) - -```typescript -/** - * @file 用户认证业务逻辑服务 - * @author 开发者姓名 - * @date 2024-01-01 - * @lastEditor 开发者姓名 - * @lastEditTime 2024-01-01 - * @description 处理用户认证相关的业务逻辑,与 HTTP 请求解耦 - */ - -import { hash, verify } from 'bun'; -import { sign } from 'jsonwebtoken'; -import type { LoginBody, RegisterBody } from '@/validators/auth/auth.validator'; - -/** - * 认证服务类 - * @description 处理用户认证业务逻辑 - */ -export abstract class AuthService { - /** - * 用户登录业务逻辑 - * @param body 登录请求体 - * @param jwt JWT 插件实例 - * @param set Elysia set 对象 - * @returns 登录响应 - * @modification 张三 2024-01-02 添加密码验证逻辑 - */ - static async login( - body: LoginBody, - jwt: any, - set: any - ) { - const { username, password } = body; - - try { - // 查询用户 - const user = await this.findUserByUsername(username); - if (!user) { - set.status = 400; - return { - code: 400, - message: '用户名或密码错误', - data: null, - }; - } - - // 验证密码 - const isValidPassword = await this.verifyPassword(password, user.password); - if (!isValidPassword) { - set.status = 400; - return { - code: 400, - message: '用户名或密码错误', - data: null, - }; - } - - // 生成 token - const token = await jwt.sign({ - userId: user.id, - username: user.username - }); - - return { - code: 0, - message: '登录成功', - data: { - token, - userInfo: { - id: user.id, - username: user.username, - email: user.email - } - }, - }; - } catch (error) { - set.status = 500; - return { - code: 500, - message: '服务器内部错误', - data: null, - }; - } - } - - /** - * 用户注册业务逻辑 - * @param body 注册请求体 - * @param set Elysia set 对象 - * @returns 注册响应 - */ - static async register(body: RegisterBody, set: any) { - const { username, email, password } = body; - - try { - // 检查用户是否已存在 - const existingUser = await this.findUserByUsername(username); - if (existingUser) { - set.status = 400; - return { - code: 400, - message: '用户名已存在', - data: null, - }; - } - - // 检查邮箱是否已存在 - const existingEmail = await this.findUserByEmail(email); - if (existingEmail) { - set.status = 400; - return { - code: 400, - message: '邮箱已被注册', - data: null, - }; - } - - // 加密密码 - const hashedPassword = await this.hashPassword(password); - - // 创建用户 - const newUser = await this.createUser({ - username, - email, - password: hashedPassword - }); - - return { - code: 0, - message: '注册成功', - data: { - userId: newUser.id, - username: newUser.username - }, - }; - } catch (error) { - set.status = 500; - return { - code: 500, - message: '服务器内部错误', - data: null, - }; - } - } - - /** - * 根据用户名查找用户 - * @param username 用户名 - * @returns 用户信息或 null - */ - private static async findUserByUsername(username: string) { - // 实际项目中应该从数据库查询 - // 这里仅作示例 - return null; - } - - /** - * 根据邮箱查找用户 - * @param email 邮箱 - * @returns 用户信息或 null - */ - private static async findUserByEmail(email: string) { - // 实际项目中应该从数据库查询 - return null; - } - - /** - * 验证密码 - * @param plainPassword 明文密码 - * @param hashedPassword 加密后的密码 - * @returns 是否匹配 - */ - private static async verifyPassword( - plainPassword: string, - hashedPassword: string - ): Promise { - return await verify(plainPassword, hashedPassword); - } - - /** - * 密码加密 - * @param password 明文密码 - * @returns 加密后的密码 - */ - private static async hashPassword(password: string): Promise { - return await hash(password); - } - - /** - * 创建用户 - * @param userData 用户数据 - * @returns 创建的用户信息 - */ - private static async createUser(userData: { - username: string; - email: string; - password: string; - }) { - // 实际项目中应该保存到数据库 - return { - id: Math.random().toString(36), - ...userData - }; - } -} -``` - -#### 请求依赖服务 (Request Dependent Service) - -```typescript -/** - * 请求依赖的认证服务 - * @description 需要访问请求上下文的服务应该作为 Elysia 实例 - */ -export const RequestAuthService = new Elysia({ name: 'Auth.Service' }) - .derive({ as: 'global' }, ({ cookie: { session } }) => ({ - Auth: { - user: session.value - } - })) - .macro(({ onBeforeHandle }) => ({ - /** - * 检查用户是否已登录 - * @param value 是否需要登录 - */ - requireAuth(value: boolean) { - if (value) { - onBeforeHandle(({ Auth, status }) => { - if (!Auth?.user) { - return status(401, { - code: 401, - message: '请先登录', - data: null - }); - } - }); - } - } - })); -``` - -### 3. 参数校验规范 (Validation Standards) - -```typescript -/** - * @file 认证接口参数校验规则 - * @author 开发者姓名 - * @date 2024-01-01 - * @lastEditor 开发者姓名 - * @lastEditTime 2024-01-01 - * @description 认证相关接口的参数校验规则,包含详细的验证规则和错误提示 - */ - -import { t } from 'elysia'; -import type { Static } from 'elysia'; - -/** - * 登录请求参数校验规则 - * @property {string} username - 用户名,2-16位字符 - * @property {string} password - 密码,6-32位字符 - */ -export const loginBodySchema = t.Object({ - username: t.String({ - minLength: 2, - maxLength: 16, - description: '用户名,2-16位字符', - examples: ['admin', 'user123'] - }), - password: t.String({ - minLength: 6, - maxLength: 32, - description: '密码,6-32位字符' - }), -}, { - description: '用户登录请求参数' -}); - -/** - * 注册请求参数校验规则 - * @property {string} username - 用户名,2-16位字符 - * @property {string} email - 邮箱地址 - * @property {string} password - 密码,6-32位字符 - * @property {string} confirmPassword - 确认密码 - */ -export const registerBodySchema = t.Object({ - username: t.String({ - minLength: 2, - maxLength: 16, - description: '用户名,2-16位字符', - pattern: '^[a-zA-Z0-9_]+$' // 只允许字母、数字、下划线 - }), - email: t.String({ - format: 'email', - description: '邮箱地址', - examples: ['user@example.com'] - }), - password: t.String({ - minLength: 6, - maxLength: 32, - description: '密码,6-32位字符,至少包含字母和数字', - pattern: '^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]+$' - }), - confirmPassword: t.String({ - description: '确认密码,必须与密码一致' - }) -}, { - description: '用户注册请求参数' -}); - -/** - * 密码重置请求参数校验规则 - */ -export const resetPasswordBodySchema = t.Object({ - email: t.String({ - format: 'email', - description: '注册时使用的邮箱地址' - }), - newPassword: t.String({ - minLength: 6, - maxLength: 32, - description: '新密码,6-32位字符' - }), - verificationCode: t.String({ - minLength: 6, - maxLength: 6, - description: '6位数字验证码', - pattern: '^\\d{6}$' - }) -}); - -/** - * 查询参数校验规则 - */ -export const userListQuerySchema = t.Object({ - page: t.Optional(t.Number({ - minimum: 1, - default: 1, - description: '页码,从1开始' - })), - pageSize: t.Optional(t.Number({ - minimum: 1, - maximum: 100, - default: 10, - description: '每页数量,1-100' - })), - keyword: t.Optional(t.String({ - maxLength: 50, - description: '搜索关键词' - })) -}); - -/** - * 路径参数校验规则 - */ -export const userParamsSchema = t.Object({ - id: t.String({ - minLength: 1, - description: '用户ID' - }) -}); - -// 类型导出 -export type LoginBody = Static; -export type RegisterBody = Static; -export type ResetPasswordBody = Static; -export type UserListQuery = Static; -export type UserParams = Static; -``` - -### 4. 响应格式规范 (Response Format Standards) - -```typescript -/** - * @file 认证接口响应格式定义 - * @author 开发者姓名 - * @date 2024-01-01 - * @lastEditor 开发者姓名 - * @lastEditTime 2024-01-01 - * @description 认证相关接口的响应格式定义,确保响应结构的一致性 - */ - -import { t } from 'elysia'; - -/** - * 登录成功响应格式 - */ -export const loginResponse200Schema = t.Object({ - code: t.Literal(0, { - description: '成功响应码' - }), - message: t.String({ - description: '响应消息', - examples: ['登录成功'] - }), - data: t.Object({ - /** JWT token */ - token: t.String({ - description: 'JWT 访问令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] - }), - /** 用户信息 */ - userInfo: t.Object({ - id: t.String({ - description: '用户ID' - }), - username: t.String({ - description: '用户名' - }), - email: t.String({ - description: '邮箱地址' - }) - }) - }) -}, { - description: '登录成功响应' -}); - -/** - * 登录失败响应格式 - */ -export const loginResponse400Schema = t.Object({ - code: t.Literal(400, { - description: '客户端错误响应码' - }), - message: t.String({ - description: '错误消息', - examples: ['用户名或密码错误', '参数验证失败'] - }), - data: t.Null({ - description: '错误时数据为null' - }), -}, { - description: '登录失败响应' -}); - -/** - * 注册成功响应格式 - */ -export const registerResponse200Schema = t.Object({ - code: t.Literal(0), - message: t.String({ - examples: ['注册成功'] - }), - data: t.Object({ - userId: t.String({ - description: '新创建的用户ID' - }), - username: t.String({ - description: '用户名' - }) - }) -}); - -/** - * 注册失败响应格式 - */ -export const registerResponse400Schema = t.Object({ - code: t.Literal(400), - message: t.String({ - examples: ['用户名已存在', '邮箱已被注册', '密码不符合要求'] - }), - data: t.Null(), -}); - -/** - * 通用未授权响应格式 - */ -export const unauthorizedResponse401Schema = t.Object({ - code: t.Literal(401), - message: t.String({ - examples: ['请先登录', 'Token已过期', 'Token无效'] - }), - data: t.Null(), -}); - -/** - * 用户列表响应格式 - */ -export const userListResponse200Schema = t.Object({ - code: t.Literal(0), - message: t.String(), - data: t.Object({ - list: t.Array(t.Object({ - id: t.String(), - username: t.String(), - email: t.String(), - createdAt: t.String({ - format: 'date-time', - description: '创建时间' - }), - updatedAt: t.String({ - format: 'date-time', - description: '更新时间' - }) - })), - pagination: t.Object({ - page: t.Number({ - description: '当前页码' - }), - pageSize: t.Number({ - description: '每页数量' - }), - total: t.Number({ - description: '总条数' - }), - totalPages: t.Number({ - description: '总页数' - }) - }) - }) -}); -``` - -## 错误处理规范 (Error Handling Standards) - -### 全局错误处理插件 - -```typescript -/** - * @file 全局错误处理插件 - * @author 开发者姓名 - * @date 2024-01-01 - * @description 统一处理应用中的错误,提供标准化的错误响应格式 - */ - -import { Elysia } from 'elysia'; -import { logger } from '@/utils/logger'; - -/** - * 错误响应接口 - */ -interface ErrorResponse { - code: number; - message: string; - data: null; -} - -/** - * 全局错误处理插件 - */ -export const errorHandlerPlugin = new Elysia({ name: 'errorHandler' }) - .onError(({ code, error, set }) => { - // 记录错误日志 - logger.error('API Error:', { - code, - error: error.message, - stack: error.stack, - timestamp: new Date().toISOString() - }); - - const response: ErrorResponse = { - code: 500, - message: '服务器内部错误', - data: null - }; - - switch (code) { - case 'VALIDATION': - set.status = 400; - response.code = 400; - response.message = '请求参数验证失败:' + error.message; - break; - - case 'NOT_FOUND': - set.status = 404; - response.code = 404; - response.message = '请求的资源不存在'; - break; - - case 'PARSE': - set.status = 400; - response.code = 400; - response.message = '请求数据格式错误'; - break; - - case 'UNAUTHORIZED': - set.status = 401; - response.code = 401; - response.message = '未授权访问'; - break; - - case 'FORBIDDEN': - set.status = 403; - response.code = 403; - response.message = '权限不足'; - break; - - default: - set.status = 500; - response.code = 500; - response.message = '服务器内部错误'; - break; - } - - return response; - }); - -/** - * 业务错误类 - * @description 用于抛出业务逻辑错误 - */ -export class BusinessError extends Error { - public code: number; - - constructor(code: number, message: string) { - super(message); - this.code = code; - this.name = 'BusinessError'; - } -} - -/** - * 抛出业务错误的辅助函数 - * @param code 错误码 - * @param message 错误消息 - */ -export function throwBusinessError(code: number, message: string): never { - throw new BusinessError(code, message); -} -``` - -## 中间件与插件规范 (Middleware & Plugin Standards) - -### JWT 认证插件 - -```typescript -/** - * @file JWT 认证插件 - * @author 开发者姓名 - * @date 2024-01-01 - * @description JWT 令牌处理插件,提供 token 生成和验证功能 - */ - -import { Elysia } from 'elysia'; -import { jwt } from '@elysiajs/jwt'; -import { jwtConfig } from '@/config/jwt.config'; - -/** - * JWT 认证插件 - */ -export const jwtPlugin = new Elysia({ name: 'jwt' }) - .use(jwt({ - name: 'jwt', - secret: jwtConfig.secret, - exp: jwtConfig.expiresIn - })) - .derive(({ jwt, headers }) => ({ - /** - * 获取当前用户信息 - * @returns 用户信息或 null - */ - getCurrentUser: async () => { - try { - const authorization = headers.authorization; - if (!authorization?.startsWith('Bearer ')) { - return null; - } - - const token = authorization.slice(7); - const payload = await jwt.verify(token); - return payload; - } catch { - return null; - } - } - })) - .macro(({ onBeforeHandle }) => ({ - /** - * 权限验证宏 - * @param options 验证选项 - */ - auth(options: { required?: boolean } = {}) { - const { required = true } = options; - - onBeforeHandle(async ({ getCurrentUser, status }) => { - const user = await getCurrentUser(); - - if (required && !user) { - return status(401, { - code: 401, - message: '请先登录', - data: null - }); - } - - return { user }; - }); - } - })); -``` - -### 请求日志插件 - -```typescript -/** - * @file 请求日志插件 - * @author 开发者姓名 - * @date 2024-01-01 - * @description 记录 API 请求和响应的详细信息 - */ - -import { Elysia } from 'elysia'; -import { logger } from '@/utils/logger'; - -/** - * 请求日志插件 - */ -export const requestLoggerPlugin = new Elysia({ name: 'requestLogger' }) - .onRequest(({ request, path }) => { - const startTime = Date.now(); - - logger.info('API Request', { - method: request.method, - url: request.url, - path, - userAgent: request.headers.get('user-agent'), - ip: request.headers.get('x-forwarded-for') || 'unknown', - timestamp: new Date().toISOString(), - startTime - }); - - // 将开始时间存储在请求上下文中 - return { startTime }; - }) - .onAfterHandle(({ request, response, path, startTime }) => { - const duration = Date.now() - (startTime || Date.now()); - - logger.info('API Response', { - method: request.method, - path, - status: response.status, - duration: `${duration}ms`, - timestamp: new Date().toISOString() - }); - }); -``` - -## 最佳实践示例 (Best Practice Examples) - -### 完整的 CRUD 接口示例 - -```typescript -/** - * @file 用户管理完整示例 - * @author 开发者姓名 - * @date 2024-01-01 - * @description 展示完整的 CRUD 接口实现,包含分页、搜索、排序等功能 - */ - -import { Elysia, t } from 'elysia'; -import { jwtPlugin } from '@/plugins/jwt.plugins'; -import { errorHandlerPlugin } from '@/plugins/errorHandler.plugins'; -import { UserService } from '@/services/user/user.service'; - -// 参数校验 -const createUserSchema = t.Object({ - username: t.String({ minLength: 2, maxLength: 16 }), - email: t.String({ format: 'email' }), - password: t.String({ minLength: 6, maxLength: 32 }), - role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')], { default: 'user' })) -}); - -const updateUserSchema = t.Object({ - username: t.Optional(t.String({ minLength: 2, maxLength: 16 })), - email: t.Optional(t.String({ format: 'email' })), - role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')])) -}); - -const userParamsSchema = t.Object({ - id: t.String({ minLength: 1 }) -}); - -const userQuerySchema = t.Object({ - page: t.Optional(t.Number({ minimum: 1, default: 1 })), - pageSize: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })), - keyword: t.Optional(t.String({ maxLength: 50 })), - role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')])), - sortBy: t.Optional(t.Union([t.Literal('createdAt'), t.Literal('username')], { default: 'createdAt' })), - sortOrder: t.Optional(t.Union([t.Literal('asc'), t.Literal('desc')], { default: 'desc' })) -}); - -// 响应格式 -const userItemSchema = t.Object({ - id: t.String(), - username: t.String(), - email: t.String(), - role: t.String(), - isActive: t.Boolean(), - createdAt: t.String({ format: 'date-time' }), - updatedAt: t.String({ format: 'date-time' }) -}); - -const successResponse = (data: any) => t.Object({ - code: t.Literal(0), - message: t.String(), - data -}); - -const errorResponse = (code: number) => t.Object({ - code: t.Literal(code), - message: t.String(), - data: t.Null() -}); - -/** - * 用户管理控制器 - */ -export const userController = new Elysia({ prefix: '/api/users' }) - .use(jwtPlugin) - .use(errorHandlerPlugin) - - // 获取用户列表(支持分页、搜索、排序) - .get('/', - async ({ query, getCurrentUser }) => { - const currentUser = await getCurrentUser(); - return await UserService.getUserList(query, currentUser); - }, - { - query: userQuerySchema, - detail: { - tags: ['用户管理'], - summary: '获取用户列表', - description: '获取用户列表,支持分页、搜索和排序功能' - }, - response: { - 200: successResponse(t.Object({ - list: t.Array(userItemSchema), - pagination: t.Object({ - page: t.Number(), - pageSize: t.Number(), - total: t.Number(), - totalPages: t.Number() - }) - })), - 401: errorResponse(401) - }, - auth: { required: true } - } - ) - - // 获取单个用户 - .get('/:id', - async ({ params, getCurrentUser }) => { - const currentUser = await getCurrentUser(); - return await UserService.getUserById(params.id, currentUser); - }, - { - params: userParamsSchema, - detail: { - tags: ['用户管理'], - summary: '获取用户详情', - description: '根据用户ID获取用户详细信息' - }, - response: { - 200: successResponse(userItemSchema), - 404: errorResponse(404), - 401: errorResponse(401) - }, - auth: { required: true } - } - ) - - // 创建用户 - .post('/', - async ({ body, getCurrentUser }) => { - const currentUser = await getCurrentUser(); - return await UserService.createUser(body, currentUser); - }, - { - body: createUserSchema, - detail: { - tags: ['用户管理'], - summary: '创建用户', - description: '创建新用户账户' - }, - response: { - 200: successResponse(userItemSchema), - 400: errorResponse(400), - 401: errorResponse(401), - 403: errorResponse(403) - }, - auth: { required: true } - } - ) - - // 更新用户 - .put('/:id', - async ({ params, body, getCurrentUser }) => { - const currentUser = await getCurrentUser(); - return await UserService.updateUser(params.id, body, currentUser); - }, - { - params: userParamsSchema, - body: updateUserSchema, - detail: { - tags: ['用户管理'], - summary: '更新用户', - description: '更新用户信息' - }, - response: { - 200: successResponse(userItemSchema), - 400: errorResponse(400), - 404: errorResponse(404), - 401: errorResponse(401), - 403: errorResponse(403) - }, - auth: { required: true } - } - ) - - // 删除用户 - .delete('/:id', - async ({ params, getCurrentUser }) => { - const currentUser = await getCurrentUser(); - return await UserService.deleteUser(params.id, currentUser); - }, - { - params: userParamsSchema, - detail: { - tags: ['用户管理'], - summary: '删除用户', - description: '删除指定用户' - }, - response: { - 200: successResponse(t.Object({ - deleted: t.Boolean() - })), - 404: errorResponse(404), - 401: errorResponse(401), - 403: errorResponse(403) - }, - auth: { required: true } - } - ); -``` - ---- - -**请严格遵守以上规范,确保 Elysia 接口的一致性、安全性和可维护性。** \ No newline at end of file diff --git a/.cursor/rules/elysia-rules.mdc b/.cursor/rules/elysia-rules.mdc index b6ecb6a..e472281 100644 --- a/.cursor/rules/elysia-rules.mdc +++ b/.cursor/rules/elysia-rules.mdc @@ -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; + +// 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; +``` + +### 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; + +// 错误响应 +export const UserNotFoundResponseSchema = t.Object({ + code: t.Literal('USER_NOT_FOUND'), + message: t.String({ examples: ['用户不存在'] }), + data: t.Null(), +}); +export type UserNotFoundResponse = Static; + +// 组合响应(供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 + */ + async createUser(request: CreateUserRequest): Promise { + 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 + */ + async getUserById(id: number): Promise { + // 实现逻辑... + } +} + +// 导出单例实例 +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 创建者姓名(如:张三 ) + * @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 接口的一致性、后端服务的健壮性、安全性与可维护性。** \ No newline at end of file diff --git a/.cursor/rules/elysia-test-rules.mdc b/.cursor/rules/elysia-test-rules.mdc deleted file mode 100644 index f0b1655..0000000 --- a/.cursor/rules/elysia-test-rules.mdc +++ /dev/null @@ -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 项目的高质量交付。** \ No newline at end of file diff --git a/.cursor/rules/test-rules.mdc b/.cursor/rules/test-rules.mdc deleted file mode 100644 index ad492f3..0000000 --- a/.cursor/rules/test-rules.mdc +++ /dev/null @@ -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` 推导类型,或在测试用例中用 `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('用户名或密码错误'); - }); -}); -``` diff --git a/database_export.sql b/database_export.sql new file mode 100644 index 0000000..8820e0a --- /dev/null +++ b/database_export.sql @@ -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. 可根据实际需求调整初始化数据 +-- -- ============================================= \ No newline at end of file diff --git a/docs/api-type-usage-examples.md b/docs/api-type-usage-examples.md index c895302..c16e8da 100644 --- a/docs/api-type-usage-examples.md +++ b/docs/api-type-usage-examples.md @@ -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 { diff --git a/.cursor/rules/git-commit-rules.mdc b/docs/git-commit-rules.md similarity index 96% rename from .cursor/rules/git-commit-rules.mdc rename to docs/git-commit-rules.md index f01abf3..e379c0b 100644 --- a/.cursor/rules/git-commit-rules.mdc +++ b/docs/git-commit-rules.md @@ -1,55 +1,55 @@ ---- -description: -globs: -alwaysApply: true ---- -## Git 提交规范(Commit Message Rules) - -1. **提交格式** - ``` - (): - ``` - - `type`:提交类型(必填) - - `scope`:影响范围(可选) - - `subject`:简要描述(必填) - -2. **type 类型** - - feat:新功能 - - fix:修复 bug - - docs:文档变更 - - style:代码格式(不影响功能,例如空格、分号等) - - refactor:代码重构(既不是新增功能,也不是修复 bug) - - perf:性能优化 - - test:增加或修改测试 - - chore:构建过程或辅助工具的变动 - - revert:回滚某个提交 - -3. **scope 范围** - - 用于说明 commit 影响的模块或文件(如 user、api、core 等),可省略。 - -4. **subject 描述** - - 简明扼要,建议不超过 50 字符 - - 以动词开头,首字母小写 - - 结尾不加句号 - -5. **正文(body)** - - 可选,详细描述本次提交的内容、动机、对比信息等 - - 建议每行不超过 72 字符 - -6. **Footer** - - 可选,用于关联 issue 或进行破坏性变更说明 - - 例如:`BREAKING CHANGE: xxx` 或 `Closes #123` - -### 示例 - -``` -feat(user): 新增用户登录功能 - -fix(api): 修复获取数据时的空指针异常 - -docs(readme): 更新安装说明 - -refactor(core): 优化数据处理逻辑 - -chore: 升级依赖包 +--- +description: +globs: +alwaysApply: true +--- +## Git 提交规范(Commit Message Rules) + +1. **提交格式** + ``` + (): + ``` + - `type`:提交类型(必填) + - `scope`:影响范围(可选) + - `subject`:简要描述(必填) + +2. **type 类型** + - feat:新功能 + - fix:修复 bug + - docs:文档变更 + - style:代码格式(不影响功能,例如空格、分号等) + - refactor:代码重构(既不是新增功能,也不是修复 bug) + - perf:性能优化 + - test:增加或修改测试 + - chore:构建过程或辅助工具的变动 + - revert:回滚某个提交 + +3. **scope 范围** + - 用于说明 commit 影响的模块或文件(如 user、api、core 等),可省略。 + +4. **subject 描述** + - 简明扼要,建议不超过 50 字符 + - 以动词开头,首字母小写 + - 结尾不加句号 + +5. **正文(body)** + - 可选,详细描述本次提交的内容、动机、对比信息等 + - 建议每行不超过 72 字符 + +6. **Footer** + - 可选,用于关联 issue 或进行破坏性变更说明 + - 例如:`BREAKING CHANGE: xxx` 或 `Closes #123` + +### 示例 + +``` +feat(user): 新增用户登录功能 + +fix(api): 修复获取数据时的空指针异常 + +docs(readme): 更新安装说明 + +refactor(core): 优化数据处理逻辑 + +chore: 升级依赖包 ``` \ No newline at end of file diff --git a/docs/jwt-usage-examples.md b/docs/jwt-usage-examples.md index 477b04a..b6a8db4 100644 --- a/docs/jwt-usage-examples.md +++ b/docs/jwt-usage-examples.md @@ -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) diff --git a/src/plugins/drizzle/drizzle.config.ts b/drizzle.config.ts similarity index 85% rename from src/plugins/drizzle/drizzle.config.ts rename to drizzle.config.ts index 0e290e5..d8484f1 100644 --- a/src/plugins/drizzle/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,40 +1,45 @@ -/** - * @file Drizzle配置文件 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description Drizzle Kit配置,用于数据库迁移和代码生成 - */ - -import { dbConfig } from '@/config'; - -/** - * Drizzle Kit配置对象 - * 使用前需要安装: bun add drizzle-kit -D - */ -export default { - /** 数据库类型 */ - dialect: 'mysql', - - /** 数据库连接配置 */ - dbCredentials: { - host: dbConfig.host, - port: dbConfig.port, - user: dbConfig.user, - password: dbConfig.password, - database: dbConfig.database, - }, - - /** Schema文件路径 */ - schema: './src/plugins/drizzle/schema/*', - - /** 迁移文件输出目录 */ - out: './drizzle', - - /** 详细日志 */ - verbose: true, - - /** 严格模式 */ - strict: true, -} as const; \ No newline at end of file +/** + * @file Drizzle配置文件 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description Drizzle Kit配置,用于数据库迁移和代码生成 + */ + +import { dbConfig } from '@/config'; + +/** + * Drizzle Kit配置对象 + * 使用前需要安装: bun add drizzle-kit -D + */ +export default { + /** 数据库类型 */ + dialect: 'mysql', + + /** 数据库连接配置 */ + dbCredentials: { + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.user, + password: dbConfig.password, + database: dbConfig.database, + }, + + /** Schema文件路径 */ + schema: './src/entities/schema.js', + + /** 迁移文件输出目录 */ + out: './drizzle', + + /** 详细日志 */ + verbose: true, + + /** 严格模式 */ + strict: true, + + introspect: { + // 启用驼峰命名 + casing: 'camel', + }, +} as const; \ No newline at end of file diff --git a/drizzle/0000_nostalgic_eternity.sql b/drizzle/0000_nostalgic_eternity.sql new file mode 100644 index 0000000..c0408d2 --- /dev/null +++ b/drizzle/0000_nostalgic_eternity.sql @@ -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`); +*/ \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..6396652 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,2100 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", + "version": "5", + "dialect": "mysql", + "tables": { + "sys_dict_items": { + "name": "sys_dict_items", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "autoincrement": false, + "name": "type_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "autoincrement": false, + "name": "item_key", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "item_value": { + "autoincrement": false, + "name": "item_value", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "label": { + "autoincrement": false, + "name": "label", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "label_en": { + "autoincrement": false, + "name": "label_en", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "css_class": { + "autoincrement": false, + "name": "css_class", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "color": { + "autoincrement": false, + "name": "color", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_dict_items_id": { + "name": "sys_dict_items_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "idx_key": { + "name": "idx_key", + "columns": [ + "item_key" + ], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": [ + "pid" + ], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": [ + "type_id", + "sort_order" + ], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_type_id": { + "name": "idx_type_id", + "columns": [ + "type_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_type_key": { + "name": "uk_type_key", + "columns": [ + "type_id", + "item_key", + "deleted_at" + ] + } + }, + "checkConstraint": {} + }, + "sys_dict_types": { + "name": "sys_dict_types", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "default": 0, + "autoincrement": false, + "name": "is_system", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_dict_types_id": { + "name": "sys_dict_types_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "idx_is_system": { + "name": "idx_is_system", + "columns": [ + "is_system" + ], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": [ + "name" + ], + "isUnique": false + }, + "idx_path": { + "name": "idx_path", + "columns": [ + "path" + ], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": [ + "pid" + ], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": [ + "pid", + "sort_order" + ], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": [ + "code", + "deleted_at" + ] + } + }, + "checkConstraint": {} + }, + "sys_operation_logs": { + "name": "sys_operation_logs", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "username": { + "autoincrement": false, + "name": "username", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "module": { + "autoincrement": false, + "name": "module", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "action": { + "autoincrement": false, + "name": "action", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "target": { + "autoincrement": false, + "name": "target", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "autoincrement": false, + "name": "target_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "request_data": { + "autoincrement": false, + "name": "request_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_data": { + "autoincrement": false, + "name": "response_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "ip": { + "autoincrement": false, + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "autoincrement": false, + "name": "user_agent", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "duration": { + "autoincrement": false, + "name": "duration", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_msg": { + "autoincrement": false, + "name": "error_msg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_operation_logs_id": { + "name": "sys_operation_logs_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "idx_ip": { + "name": "idx_ip", + "columns": [ + "ip" + ], + "isUnique": false + }, + "idx_module_action": { + "name": "idx_module_action", + "columns": [ + "module", + "action" + ], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_target": { + "name": "idx_target", + "columns": [ + "target_id" + ], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sys_organizations": { + "name": "sys_organizations", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "autoincrement": false, + "name": "full_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "type": { + "autoincrement": false, + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "leader_id": { + "autoincrement": false, + "name": "leader_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "address": { + "autoincrement": false, + "name": "address", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "autoincrement": false, + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "version": { + "default": 1, + "autoincrement": false, + "name": "version", + "type": "int", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_organizations_id": { + "name": "sys_organizations_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "idx_leader_id": { + "name": "idx_leader_id", + "columns": [ + "leader_id" + ], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": [ + "name" + ], + "isUnique": false + }, + "idx_path": { + "name": "idx_path", + "columns": [ + "path" + ], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": [ + "pid" + ], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": [ + "pid", + "sort_order" + ], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_type": { + "name": "idx_type", + "columns": [ + "type" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": [ + "code", + "deleted_at" + ] + } + }, + "checkConstraint": {} + }, + "sys_permissions": { + "name": "sys_permissions", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "type": { + "autoincrement": false, + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "resource": { + "autoincrement": false, + "name": "resource", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "action": { + "autoincrement": false, + "name": "action", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "meta": { + "autoincrement": false, + "name": "meta", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_permissions_id": { + "name": "sys_permissions_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": [ + "pid" + ], + "isUnique": false + }, + "idx_resource_action": { + "name": "idx_resource_action", + "columns": [ + "resource", + "action" + ], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": [ + "pid", + "sort_order" + ], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_type": { + "name": "idx_type", + "columns": [ + "type" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": [ + "code", + "deleted_at" + ] + } + }, + "checkConstraint": {} + }, + "sys_role_permissions": { + "name": "sys_role_permissions", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "autoincrement": false, + "name": "role_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "autoincrement": false, + "name": "permission_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "is_half": { + "default": 0, + "autoincrement": false, + "name": "is_half", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_role_permissions_id": { + "name": "sys_role_permissions_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_is_half": { + "name": "idx_is_half", + "columns": [ + "is_half" + ], + "isUnique": false + }, + "idx_permission_id": { + "name": "idx_permission_id", + "columns": [ + "permission_id" + ], + "isUnique": false + }, + "idx_role_id": { + "name": "idx_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_role_permission": { + "name": "uk_role_permission", + "columns": [ + "role_id", + "permission_id" + ] + } + }, + "checkConstraint": {} + }, + "sys_roles": { + "name": "sys_roles", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "default": 0, + "autoincrement": false, + "name": "is_system", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "permissions_snapshot": { + "autoincrement": false, + "name": "permissions_snapshot", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "version": { + "default": 1, + "autoincrement": false, + "name": "version", + "type": "int", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_roles_id": { + "name": "sys_roles_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "idx_is_system": { + "name": "idx_is_system", + "columns": [ + "is_system" + ], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": [ + "name" + ], + "isUnique": false + }, + "idx_path": { + "name": "idx_path", + "columns": [ + "path" + ], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": [ + "pid" + ], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": [ + "pid", + "sort_order" + ], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": [ + "code", + "deleted_at" + ] + } + }, + "checkConstraint": {} + }, + "sys_tags": { + "name": "sys_tags", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "type": { + "default": "'user'", + "autoincrement": false, + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "color": { + "autoincrement": false, + "name": "color", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "default": 0, + "autoincrement": false, + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_tags_id": { + "name": "sys_tags_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": [ + "name" + ], + "isUnique": false + }, + "idx_type": { + "name": "idx_type", + "columns": [ + "type" + ], + "isUnique": false + }, + "idx_usage_count": { + "name": "idx_usage_count", + "columns": [ + "usage_count" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_name_type": { + "name": "uk_name_type", + "columns": [ + "name", + "type", + "deleted_at" + ] + } + }, + "checkConstraint": {} + }, + "sys_user_organizations": { + "name": "sys_user_organizations", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "autoincrement": false, + "name": "organization_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "default": 0, + "autoincrement": false, + "name": "is_primary", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "position": { + "autoincrement": false, + "name": "position", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "joined_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_user_organizations_id": { + "name": "sys_user_organizations_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_is_primary": { + "name": "idx_is_primary", + "columns": [ + "is_primary" + ], + "isUnique": false + }, + "idx_joined_at": { + "name": "idx_joined_at", + "columns": [ + "joined_at" + ], + "isUnique": false + }, + "idx_organization_id": { + "name": "idx_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_user_org": { + "name": "uk_user_org", + "columns": [ + "user_id", + "organization_id" + ] + } + }, + "checkConstraint": {} + }, + "sys_user_roles": { + "name": "sys_user_roles", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "autoincrement": false, + "name": "role_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expired_at": { + "autoincrement": false, + "name": "expired_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_user_roles_id": { + "name": "sys_user_roles_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "idx_expired_at": { + "name": "idx_expired_at", + "columns": [ + "expired_at" + ], + "isUnique": false + }, + "idx_role_id": { + "name": "idx_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_user_role": { + "name": "uk_user_role", + "columns": [ + "user_id", + "role_id" + ] + } + }, + "checkConstraint": {} + }, + "sys_user_tags": { + "name": "sys_user_tags", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "autoincrement": false, + "name": "tag_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_user_tags_id": { + "name": "sys_user_tags_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "idx_tag_id": { + "name": "idx_tag_id", + "columns": [ + "tag_id" + ], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_user_tag": { + "name": "uk_user_tag", + "columns": [ + "user_id", + "tag_id" + ] + } + }, + "checkConstraint": {} + }, + "sys_users": { + "name": "sys_users", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "username": { + "autoincrement": false, + "name": "username", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "email": { + "autoincrement": false, + "name": "email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "mobile": { + "autoincrement": false, + "name": "mobile", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "autoincrement": false, + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "autoincrement": false, + "name": "avatar", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "nickname": { + "autoincrement": false, + "name": "nickname", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "gender": { + "default": 0, + "autoincrement": false, + "name": "gender", + "type": "tinyint", + "primaryKey": false, + "notNull": false + }, + "birthday": { + "autoincrement": false, + "name": "birthday", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "bio": { + "autoincrement": false, + "name": "bio", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "login_count": { + "default": 0, + "autoincrement": false, + "name": "login_count", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "last_login_at": { + "autoincrement": false, + "name": "last_login_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "last_login_ip": { + "autoincrement": false, + "name": "last_login_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "failed_attempts": { + "default": 0, + "autoincrement": false, + "name": "failed_attempts", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "locked_until": { + "autoincrement": false, + "name": "locked_until", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "is_root": { + "default": 0, + "autoincrement": false, + "name": "is_root", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "version": { + "default": 1, + "autoincrement": false, + "name": "version", + "type": "int", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_users_id": { + "name": "sys_users_id", + "columns": [ + "id" + ] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "idx_is_root": { + "name": "idx_is_root", + "columns": [ + "is_root" + ], + "isUnique": false + }, + "idx_last_login": { + "name": "idx_last_login", + "columns": [ + "last_login_at" + ], + "isUnique": false + }, + "idx_mobile": { + "name": "idx_mobile", + "columns": [ + "mobile" + ], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_email": { + "name": "uk_email", + "columns": [ + "email", + "deleted_at" + ] + }, + "uk_username": { + "name": "uk_username", + "columns": [ + "username", + "deleted_at" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": { + "sys_dict_items": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_dict_types": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_operation_logs": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_organizations": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_permissions": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_role_permissions": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_roles": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_tags": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_user_organizations": { + "columns": { + "joined_at": { + "isDefaultAnExpression": true + }, + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_user_roles": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_user_tags": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_users": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + } + }, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..cb56d78 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1751620262784, + "tag": "0000_nostalgic_eternity", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/relations.ts b/drizzle/relations.ts new file mode 100644 index 0000000..80768e2 --- /dev/null +++ b/drizzle/relations.ts @@ -0,0 +1,3 @@ +import { relations } from "drizzle-orm/relations"; +import { } from "./schema"; + diff --git a/drizzle/schema.ts b/drizzle/schema.ts new file mode 100644 index 0000000..e3da6d6 --- /dev/null +++ b/drizzle/schema.ts @@ -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), +]); diff --git a/package.json b/package.json index 8948b79..145470e 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/constants/error-codes.ts b/src/constants/error-codes.ts new file mode 100644 index 0000000..c4c53b2 --- /dev/null +++ b/src/constants/error-codes.ts @@ -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 = { + // 成功 + [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 = { + // 成功 + [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]: '服务暂时不可用', +}; \ No newline at end of file diff --git a/src/eneities/customType.ts b/src/eneities/customType.ts new file mode 100644 index 0000000..f0e9509 --- /dev/null +++ b/src/eneities/customType.ts @@ -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); // 确保写入时为数字类型 + } +}); \ No newline at end of file diff --git a/src/eneities/index.ts b/src/eneities/index.ts new file mode 100644 index 0000000..e2a85a0 --- /dev/null +++ b/src/eneities/index.ts @@ -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), +]); diff --git a/src/modules/sample/example.controller.ts b/src/modules/example/example.controller.ts similarity index 90% rename from src/modules/sample/example.controller.ts rename to src/modules/example/example.controller.ts index 48655da..ac5df66 100644 --- a/src/modules/sample/example.controller.ts +++ b/src/modules/example/example.controller.ts @@ -1,53 +1,53 @@ -/** - * @file 样例控制器 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口控制器,演示完整的接口开发流程 - */ - -import { Elysia } from 'elysia'; -import { jwtAuthPlugin } from '@/plugins/jwt/jwt.plugins'; -import { GetUserByUsernameSchema } from './example.schema'; -import { GetUserByUsernameResponses } from './example.response'; -import { tags } from '@/modules/tags'; -import { exampleService } from './example.service'; - -/** - * 样例控制器 - * @description 提供样例接口的路由定义和请求处理逻辑 - * @modification hotok 2025-06-29 实现根据用户名查询用户接口 - */ -export const sampleController = new Elysia() - // 使用JWT认证插件 - .use(jwtAuthPlugin) - /** - * 根据用户名查询用户信息 - * @route GET /api/sample/user/:username - * @description 通过用户名查询用户的详细信息,需要JWT认证 - * @param username 用户名,路径参数,长度2-50字符 - * @returns 用户信息对象或错误响应 - * @modification hotok 2025-06-29 初始实现 - */ - .get( - '/user/:username', - ({ params, user }) => { - return exampleService.findUserByUsername({ params, user }); - }, - { - // 路径参数验证 - params: GetUserByUsernameSchema, - - // API文档配置 - detail: { - summary: '根据用户名查询用户信息', - description: - '通过用户名查询用户的详细信息,需要JWT身份认证。返回用户的基本信息,不包含敏感数据如密码。', - tags: [tags.user, tags.sample], - security: [{ bearerAuth: [] }], - }, - // 响应格式定义 - response: GetUserByUsernameResponses, - }, - ); +/** + * @file 样例控制器 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 样例接口控制器,演示完整的接口开发流程 + */ + +import { Elysia } from 'elysia'; +import { jwtAuthPlugin } from '@/plugins/jwt/jwt.plugins'; +import { GetUserByUsernameSchema } from './example.schema'; +import { GetUserByUsernameResponses } from './example.response'; +import { tags } from '@/modules/tags'; +import { exampleService } from './example.service'; + +/** + * 样例控制器 + * @description 提供样例接口的路由定义和请求处理逻辑 + * @modification hotok 2025-06-29 实现根据用户名查询用户接口 + */ +export const exampleController = new Elysia() + // 使用JWT认证插件 + .use(jwtAuthPlugin) + /** + * 根据用户名查询用户信息 + * @route GET /api/example/user/:username + * @description 通过用户名查询用户的详细信息,需要JWT认证 + * @param username 用户名,路径参数,长度2-50字符 + * @returns 用户信息对象或错误响应 + * @modification hotok 2025-06-29 初始实现 + */ + .get( + '/user/:username', + ({ params, user }) => { + return exampleService.findUserByUsername({ params, user }); + }, + { + // 路径参数验证 + params: GetUserByUsernameSchema, + + // API文档配置 + detail: { + summary: '根据用户名查询用户信息', + description: + '通过用户名查询用户的详细信息,需要JWT身份认证。返回用户的基本信息,不包含敏感数据如密码。', + tags: [tags.user, tags.example], + security: [{ bearerAuth: [] }], + }, + // 响应格式定义 + response: GetUserByUsernameResponses, + }, + ); diff --git a/src/modules/example/example.response.ts b/src/modules/example/example.response.ts new file mode 100644 index 0000000..c0daa66 --- /dev/null +++ b/src/modules/example/example.response.ts @@ -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], +}; \ No newline at end of file diff --git a/src/modules/sample/example.schema.ts b/src/modules/example/example.schema.ts similarity index 93% rename from src/modules/sample/example.schema.ts rename to src/modules/example/example.schema.ts index be29457..22e1777 100644 --- a/src/modules/sample/example.schema.ts +++ b/src/modules/example/example.schema.ts @@ -1,79 +1,79 @@ -/** - * @file 样例接口Schema定义 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口的请求参数和响应数据的TypeBox schema定义 - */ - -import { t, type Static } from 'elysia'; - -/** - * 根据用户名查询用户的请求参数Schema - */ -export const GetUserByUsernameSchema = t.Object({ - /** 用户名,必填,长度2-50字符 */ - username: t.String({ - minLength: 2, - maxLength: 50, - description: '用户名,用于查询用户信息', - examples: ['admin', 'testuser', 'zhangsan'], - }), -}); - -/** - * 用户信息返回数据Schema - */ -export const UserInfoSchema = t.Object({ - /** 用户ID */ - id: t.Number({ - description: '用户唯一标识ID', - examples: [1, 2, 100], - }), - /** 用户名 */ - username: t.String({ - description: '用户名', - examples: ['admin', 'testuser'], - }), - /** 邮箱 */ - email: t.String({ - description: '用户邮箱', - examples: ['admin@example.com', 'user@test.com'], - }), - /** 用户昵称 */ - nickname: t.Optional(t.String({ - description: '用户昵称', - examples: ['管理员', '测试用户'], - })), - /** 用户头像URL */ - avatar: t.Optional(t.String({ - description: '用户头像URL', - examples: ['https://example.com/avatar.jpg'], - })), - /** 用户状态:0-禁用,1-启用 */ - status: t.Number({ - description: '用户状态,0-禁用,1-启用', - examples: [0, 1], - }), - /** 创建时间 */ - createdAt: t.String({ - description: '用户创建时间', - examples: ['2024-06-29T10:30:00.000Z'], - }), - /** 更新时间 */ - updatedAt: t.String({ - description: '用户最后更新时间', - examples: ['2024-06-29T10:30:00.000Z'], - }), -}); - -/** - * 根据用户名查询用户的请求参数类型 - */ -export type GetUserByUsernameType = Static; - -/** - * 用户信息数据类型 - */ +/** + * @file 样例接口Schema定义 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 样例接口的请求参数和响应数据的TypeBox schema定义 + */ + +import { t, type Static } from 'elysia'; + +/** + * 根据用户名查询用户的请求参数Schema + */ +export const GetUserByUsernameSchema = t.Object({ + /** 用户名,必填,长度2-50字符 */ + username: t.String({ + minLength: 2, + maxLength: 50, + description: '用户名,用于查询用户信息', + examples: ['admin', 'testuser', 'zhangsan'], + }), +}); + +/** + * 用户信息返回数据Schema + */ +export const UserInfoSchema = t.Object({ + /** 用户ID */ + id: t.Number({ + description: '用户唯一标识ID', + examples: [1, 2, 100], + }), + /** 用户名 */ + username: t.String({ + description: '用户名', + examples: ['admin', 'testuser'], + }), + /** 邮箱 */ + email: t.String({ + description: '用户邮箱', + examples: ['admin@example.com', 'user@test.com'], + }), + /** 用户昵称 */ + nickname: t.Optional(t.String({ + description: '用户昵称', + examples: ['管理员', '测试用户'], + })), + /** 用户头像URL */ + avatar: t.Optional(t.String({ + description: '用户头像URL', + examples: ['https://example.com/avatar.jpg'], + })), + /** 用户状态:0-禁用,1-启用 */ + status: t.Number({ + description: '用户状态,0-禁用,1-启用', + examples: [0, 1], + }), + /** 创建时间 */ + createdAt: t.String({ + description: '用户创建时间', + examples: ['2024-06-29T10:30:00.000Z'], + }), + /** 更新时间 */ + updatedAt: t.String({ + description: '用户最后更新时间', + examples: ['2024-06-29T10:30:00.000Z'], + }), +}); + +/** + * 根据用户名查询用户的请求参数类型 + */ +export type GetUserByUsernameType = Static; + +/** + * 用户信息数据类型 + */ export type UserInfoType = Static; \ No newline at end of file diff --git a/src/modules/sample/example.service.ts b/src/modules/example/example.service.ts similarity index 92% rename from src/modules/sample/example.service.ts rename to src/modules/example/example.service.ts index a547fe7..b8ebc5d 100644 --- a/src/modules/sample/example.service.ts +++ b/src/modules/example/example.service.ts @@ -1,86 +1,86 @@ -/** - * @file 样例业务逻辑服务 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口的业务逻辑实现 - * - * 设计思路: - * 1. 接收用户名参数,对参数进行基础校验 - * 2. 使用Drizzle ORM查询数据库中的用户信息 - * 3. 处理查询结果:用户存在则返回用户信息,不存在则抛出业务异常 - * 4. 对敏感信息进行过滤,不返回密码哈希等敏感字段 - * 5. 统一异常处理,确保返回标准的错误响应格式 - * 6. 记录操作日志,便于系统监控和问题排查 - * - * 安全考虑: - * - 严格过滤敏感信息,不向客户端返回密码哈希 - * - 对查询参数进行SQL注入防护(Drizzle ORM自带防护) - * - 记录查询日志,便于安全审计 - */ - -import { eq } from 'drizzle-orm'; -import { db } from '@/plugins/drizzle/drizzle.service'; -import { users } from '@/plugins/drizzle/schema/users'; -import { ERROR_CODES } from '@/validators/global.response'; -import { type GetUserByUsernameType } from './example.schema'; -import type { JwtUserType } from '@/type/jwt.type'; - -/** - * 样例服务类 - * @description 提供用户相关的业务逻辑处理 - */ -export class ExampleService { - async findUserByUsername({ params, user }: { params: GetUserByUsernameType; user: JwtUserType }) { - const { username } = params; - user; - // 使用Drizzle ORM查询用户信息 - const userList = await db - .select({ - id: users.id, - username: users.username, - email: users.email, - nickname: users.nickname, - avatar: users.avatar, - status: users.status, - createdAt: users.createdAt, - updatedAt: users.updatedAt, - }) - .from(users) - .where(eq(users.username, username)) - .limit(1); - - // 检查查询结果 - if (!userList || userList.length === 0) { - return { - code: 400 as const, - message: '用户不存在', - data: null, - }; - } - - const userInfo = userList[0]; - - // 返回成功响应 - return { - code: ERROR_CODES.SUCCESS, - message: '查询用户成功', - data: { - id: userInfo.id, - username: userInfo.username, - email: userInfo.email, - nickname: userInfo.nickname || undefined, - avatar: userInfo.avatar || undefined, - status: userInfo.status, - createdAt: userInfo.createdAt.toISOString(), - updatedAt: userInfo.updatedAt.toISOString(), - }, - }; - } -} - -/** - * 导出样例服务实例 - */ -export const exampleService = new ExampleService(); +/** + * @file 样例业务逻辑服务 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 样例接口的业务逻辑实现 + * + * 设计思路: + * 1. 接收用户名参数,对参数进行基础校验 + * 2. 使用Drizzle ORM查询数据库中的用户信息 + * 3. 处理查询结果:用户存在则返回用户信息,不存在则抛出业务异常 + * 4. 对敏感信息进行过滤,不返回密码哈希等敏感字段 + * 5. 统一异常处理,确保返回标准的错误响应格式 + * 6. 记录操作日志,便于系统监控和问题排查 + * + * 安全考虑: + * - 严格过滤敏感信息,不向客户端返回密码哈希 + * - 对查询参数进行SQL注入防护(Drizzle ORM自带防护) + * - 记录查询日志,便于安全审计 + */ + +import { eq } from 'drizzle-orm'; +import { db } from '@/plugins/drizzle/drizzle.service'; +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'; + +/** + * 样例服务类 + * @description 提供用户相关的业务逻辑处理 + */ +export class ExampleService { + async findUserByUsername({ params, user }: { params: GetUserByUsernameType; user: JwtUserType }) { + const { username } = params; + user; + // 使用Drizzle ORM查询用户信息 + const userList = await db() + .select({ + id: users.id, + username: users.username, + email: users.email, + nickname: users.nickname, + avatar: users.avatar, + status: users.status, + createdAt: users.createdAt, + updatedAt: users.updatedAt, + }) + .from(users) + .where(eq(users.username, username)) + .limit(1); + + // 检查查询结果 + if (!userList || userList.length === 0) { + return { + code: 400 as const, + message: '用户不存在', + data: null, + }; + } + + const userInfo = userList[0]!; + + // 返回成功响应 + return { + code: ERROR_CODES.SUCCESS, + message: '查询用户成功', + data: { + id: userInfo.id, + username: userInfo.username, + email: userInfo.email, + nickname: userInfo.nickname || undefined, + avatar: userInfo.avatar || undefined, + status: userInfo.status, + createdAt: userInfo.createdAt.toISOString(), + updatedAt: userInfo.updatedAt.toISOString(), + }, + }; + } +} + +/** + * 导出样例服务实例 + */ +export const exampleService = new ExampleService(); diff --git a/src/modules/sample/example.test.ts b/src/modules/example/example.test.ts similarity index 85% rename from src/modules/sample/example.test.ts rename to src/modules/example/example.test.ts index e8ef24f..1be011b 100644 --- a/src/modules/sample/example.test.ts +++ b/src/modules/example/example.test.ts @@ -1,188 +1,188 @@ -/** - * @file 样例接口测试用例 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口的完整测试用例,覆盖正常、异常、边界场景 - */ - -import { describe, it, expect, beforeAll } from 'vitest'; -import { Elysia } from 'elysia'; -import { sampleController } from './example.controller'; -import { jwtPlugin } from '@/plugins/jwt/jwt.plugins'; - -// 创建测试应用实例 -const app = new Elysia() - .use(jwtPlugin) - .use(sampleController); - -// 测试用的JWT Token(需要根据实际情况生成) -let testToken = ''; - -describe('样例接口测试', () => { - beforeAll(async () => { - // 在实际测试中,这里应该通过登录接口获取有效token - // 这里为了演示,假设我们有一个有效的token - // 创建临时的JWT实例来生成测试token - const tempApp = new Elysia().use(jwtPlugin); - const context = { jwt: tempApp.derive().jwt }; - - testToken = await context.jwt.sign({ - userId: 1, - username: 'admin', - iat: Math.floor(Date.now() / 1000), - }); - }); - - describe('GET /api/sample/user/:username', () => { - it('应该成功查询存在的用户', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/user/admin', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('成功查询响应:', body); - - expect(res.status).toBe(200); - expect(body.code).toBe(0); - expect(body.message).toBe('查询用户成功'); - expect(body.data).toBeDefined(); - expect(typeof body.data.id).toBe('number'); - expect(typeof body.data.username).toBe('string'); - expect(typeof body.data.email).toBe('string'); - }); - - it('用户名过短应返回422', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/user/a', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('用户名过短响应:', body); - - expect(res.status).toBe(422); - expect(body.code).toBe(422); - expect(body.message).toMatch(/用户名/); - }); - - it('用户名过长应返回422', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/user/' + 'a'.repeat(51), { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('用户名过长响应:', body); - - expect(res.status).toBe(422); - expect(body.code).toBe(422); - expect(body.message).toMatch(/用户名/); - }); - - it('用户不存在应返回400', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/user/nonexistentuser12345', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('用户不存在响应:', body); - - expect(res.status).toBe(400); - expect(body.code).toBe(400); - expect(body.message).toBe('用户不存在'); - expect(body.data).toBeNull(); - }); - - it('缺少Authorization头应返回401', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/user/admin', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - console.log('缺少Authorization响应:', body); - - expect(res.status).toBe(401); - expect(body.code).toBe(401); - expect(body.message).toMatch(/Token|认证|授权/); - }); - - it('无效Token应返回401', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/user/admin', { - method: 'GET', - headers: { - Authorization: 'Bearer invalid_token_here', - }, - }), - ); - - const body = (await res.json()) as any; - console.log('无效Token响应:', body); - - expect(res.status).toBe(401); - expect(body.code).toBe(401); - expect(body.message).toMatch(/Token|认证|授权/); - }); - - it('错误的Authorization格式应返回401', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/user/admin', { - method: 'GET', - headers: { - Authorization: 'InvalidFormat token', - }, - }), - ); - - const body = (await res.json()) as any; - console.log('错误Authorization格式响应:', body); - - expect(res.status).toBe(401); - expect(body.code).toBe(401); - expect(body.message).toMatch(/Token|认证|授权/); - }); - }); - - describe('GET /api/sample/health', () => { - it('应该返回模块健康状态', async () => { - const res = await app.fetch( - new Request('http://localhost/sample/health', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - console.log('健康检查响应:', body); - - expect(res.status).toBe(200); - expect(body.code).toBe(0); - expect(body.message).toBe('样例模块运行正常'); - expect(body.data).toBeDefined(); - expect(body.data.module).toBe('sample'); - expect(body.data.status).toBe('healthy'); - expect(typeof body.data.timestamp).toBe('string'); - }); - }); +/** + * @file 样例接口测试用例 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 样例接口的完整测试用例,覆盖正常、异常、边界场景 + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Elysia } from 'elysia'; +import { exampleController } from './example.controller'; +import { jwtPlugin } from '@/plugins/jwt/jwt.plugins'; + +// 创建测试应用实例 +const app = new Elysia() + .use(jwtPlugin) + .use(exampleController); + +// 测试用的JWT Token(需要根据实际情况生成) +let testToken = ''; + +describe('样例接口测试', () => { + beforeAll(async () => { + // 在实际测试中,这里应该通过登录接口获取有效token + // 这里为了演示,假设我们有一个有效的token + // 创建临时的JWT实例来生成测试token + const tempApp = new Elysia().use(jwtPlugin); + const context = { jwt: tempApp.derive().jwt }; + + testToken = await context.jwt.sign({ + userId: 1, + username: 'admin', + iat: Math.floor(Date.now() / 1000), + }); + }); + + describe('GET /api/example/user/:username', () => { + it('应该成功查询存在的用户', async () => { + const res = await app.fetch( + new Request('http://localhost/example/user/admin', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${testToken}`, + }, + }), + ); + + const body = (await res.json()) as any; + console.log('成功查询响应:', body); + + expect(res.status).toBe(200); + expect(body.code).toBe(0); + expect(body.message).toBe('查询用户成功'); + expect(body.data).toBeDefined(); + expect(typeof body.data.id).toBe('number'); + expect(typeof body.data.username).toBe('string'); + expect(typeof body.data.email).toBe('string'); + }); + + it('用户名过短应返回422', async () => { + const res = await app.fetch( + new Request('http://localhost/example/user/a', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${testToken}`, + }, + }), + ); + + const body = (await res.json()) as any; + console.log('用户名过短响应:', body); + + expect(res.status).toBe(422); + expect(body.code).toBe(422); + expect(body.message).toMatch(/用户名/); + }); + + it('用户名过长应返回422', async () => { + const res = await app.fetch( + new Request('http://localhost/example/user/' + 'a'.repeat(51), { + method: 'GET', + headers: { + 'Authorization': `Bearer ${testToken}`, + }, + }), + ); + + const body = (await res.json()) as any; + console.log('用户名过长响应:', body); + + expect(res.status).toBe(422); + expect(body.code).toBe(422); + expect(body.message).toMatch(/用户名/); + }); + + it('用户不存在应返回400', async () => { + const res = await app.fetch( + new Request('http://localhost/example/user/nonexistentuser12345', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${testToken}`, + }, + }), + ); + + const body = (await res.json()) as any; + console.log('用户不存在响应:', body); + + expect(res.status).toBe(400); + expect(body.code).toBe(400); + expect(body.message).toBe('用户不存在'); + expect(body.data).toBeNull(); + }); + + it('缺少Authorization头应返回401', async () => { + const res = await app.fetch( + new Request('http://localhost/example/user/admin', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + console.log('缺少Authorization响应:', body); + + expect(res.status).toBe(401); + expect(body.code).toBe(401); + expect(body.message).toMatch(/Token|认证|授权/); + }); + + it('无效Token应返回401', async () => { + const res = await app.fetch( + new Request('http://localhost/example/user/admin', { + method: 'GET', + headers: { + Authorization: 'Bearer invalid_token_here', + }, + }), + ); + + const body = (await res.json()) as any; + console.log('无效Token响应:', body); + + expect(res.status).toBe(401); + expect(body.code).toBe(401); + expect(body.message).toMatch(/Token|认证|授权/); + }); + + it('错误的Authorization格式应返回401', async () => { + const res = await app.fetch( + new Request('http://localhost/example/user/admin', { + method: 'GET', + headers: { + Authorization: 'InvalidFormat token', + }, + }), + ); + + const body = (await res.json()) as any; + console.log('错误Authorization格式响应:', body); + + expect(res.status).toBe(401); + expect(body.code).toBe(401); + expect(body.message).toMatch(/Token|认证|授权/); + }); + }); + + describe('GET /api/example/health', () => { + it('应该返回模块健康状态', async () => { + const res = await app.fetch( + new Request('http://localhost/example/health', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + console.log('健康检查响应:', body); + + expect(res.status).toBe(200); + expect(body.code).toBe(0); + expect(body.message).toBe('样例模块运行正常'); + expect(body.data).toBeDefined(); + expect(body.data.module).toBe('example'); + expect(body.data.status).toBe('healthy'); + expect(typeof body.data.timestamp).toBe('string'); + }); + }); }); \ No newline at end of file diff --git a/src/modules/index.ts b/src/modules/index.ts index 6ef6b67..c9c55f0 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -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)); diff --git a/src/modules/sample/example.response.ts b/src/modules/sample/example.response.ts deleted file mode 100644 index 606630c..0000000 --- a/src/modules/sample/example.response.ts +++ /dev/null @@ -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(), - }), -}; \ No newline at end of file diff --git a/src/modules/tags.ts b/src/modules/tags.ts index f58d3b4..66afb13 100644 --- a/src/modules/tags.ts +++ b/src/modules/tags.ts @@ -21,7 +21,7 @@ export const tags = { /** 测试接口 */ test: 'Test', /** 样例接口 */ - sample: 'Sample', + example: 'example', /** 文件上传接口 */ upload: 'Upload', /** 系统管理接口 */ diff --git a/src/plugins/drizzle/drizzle.plugins.ts b/src/plugins/drizzle/drizzle.plugins.ts index d739b94..9d1a1da 100644 --- a/src/plugins/drizzle/drizzle.plugins.ts +++ b/src/plugins/drizzle/drizzle.plugins.ts @@ -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>; +export type DrizzleDB = Awaited>; diff --git a/src/plugins/drizzle/drizzle.service.ts b/src/plugins/drizzle/drizzle.service.ts index a20eee1..400b506 100644 --- a/src/plugins/drizzle/drizzle.service.ts +++ b/src/plugins/drizzle/drizzle.service.ts @@ -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 { + 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 { + // 防止重复初始化 + 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 { + 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 { + 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 { + 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; + }> { + 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 初始化失败'); - } -}; \ No newline at end of file +/** + * ============================================== + * 向后兼容导出 - 保持原有函数式API + * ============================================== + */ + +/** + * 创建并初始化Drizzle数据库连接 + * + * @description 向后兼容的初始化方法,内部调用drizzleService.initialize() + * @returns {Promise} 返回初始化后的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} 返回连接是否正常 + * + * @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} 返回关闭操作的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} 返回重新连接后的数据库实例 + * + * @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; \ No newline at end of file diff --git a/src/plugins/drizzle/schema/index.ts b/src/plugins/drizzle/schema/index.ts deleted file mode 100644 index 724a049..0000000 --- a/src/plugins/drizzle/schema/index.ts +++ /dev/null @@ -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'; \ No newline at end of file diff --git a/src/plugins/drizzle/schema/users.ts b/src/plugins/drizzle/schema/users.ts deleted file mode 100644 index 2254ef0..0000000 --- a/src/plugins/drizzle/schema/users.ts +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 612435c..5ec5483 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -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); diff --git a/src/plugins/jwt/jwt.service.ts b/src/plugins/jwt/jwt.service.ts index 864ba2d..a9d0f31 100644 --- a/src/plugins/jwt/jwt.service.ts +++ b/src/plugins/jwt/jwt.service.ts @@ -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服务类 diff --git a/src/plugins/redis/redis.plugins.ts b/src/plugins/redis/redis.plugins.ts new file mode 100644 index 0000000..0175a62 --- /dev/null +++ b/src/plugins/redis/redis.plugins.ts @@ -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(); +}); + diff --git a/src/plugins/redis/redis.service.ts b/src/plugins/redis/redis.service.ts new file mode 100644 index 0000000..64eb279 --- /dev/null +++ b/src/plugins/redis/redis.service.ts @@ -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 { + // 防止重复初始化 + 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 { + 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 { + 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 { + 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 { + 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} 返回初始化后的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} 返回连接是否正常 + * + * @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} 返回关闭操作的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} 返回重新连接后的客户端实例 + * + * @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; \ No newline at end of file diff --git a/types/config.type.ts b/src/type/config.type.ts similarity index 95% rename from types/config.type.ts rename to src/type/config.type.ts index 72e4a1a..e7e2b2a 100644 --- a/types/config.type.ts +++ b/src/type/config.type.ts @@ -1,75 +1,75 @@ -/** - * 数据库配置类型 - */ -export interface DbConfig { - /** 数据库主机地址 */ - host: string; - /** 数据库端口号 */ - port: number; - /** 数据库用户名 */ - user: string; - /** 数据库密码 */ - password: string; - /** 数据库名称 */ - database: string; -} - -/** - * JWT配置类型 - */ -export interface JwtConfig { - /** JWT签名密钥 */ - secret: string; - /** Token有效期 */ - exp: string; -} - -/** - * Redis配置类型 - */ -export interface RedisConfig { - /** Redis连接名称 */ - connectName: string; - /** Redis服务器主机地址 */ - host: string; - /** Redis服务器端口号 */ - port: number; - /** Redis用户名 */ - username: string; - /** Redis密码 */ - password: string; - /** Redis数据库索引 */ - database: number; -} -/** - * 日志配置接口 - * @interface LogConfig - */ -export interface LogConfigType { - /** 日志文件目录 */ - directory: string; - /** 是否输出到控制台 */ - console: boolean; - /** 单个日志文件最大大小 */ - maxSize: string; - /** 最大保留文件数 */ - maxFiles: string; - /** 日志等级 */ - level: string; -} - -/** - * 全局配置类型,包含所有配置项 - */ -export interface GlobalConfig { - /** 数据库配置 */ - db: DbConfig; - /** JWT配置 */ - jwt: JwtConfig; - /** Redis配置 */ - redis: RedisConfig; - /** 日志配置 */ - logger: LogConfigType; - /** 当前环境 */ - env: string; -} +/** + * 数据库配置类型 + */ +export interface DbConfig { + /** 数据库主机地址 */ + host: string; + /** 数据库端口号 */ + port: number; + /** 数据库用户名 */ + user: string; + /** 数据库密码 */ + password: string; + /** 数据库名称 */ + database: string; +} + +/** + * JWT配置类型 + */ +export interface JwtConfig { + /** JWT签名密钥 */ + secret: string; + /** Token有效期 */ + exp: string; +} + +/** + * Redis配置类型 + */ +export interface RedisConfig { + /** Redis连接名称 */ + connectName: string; + /** Redis服务器主机地址 */ + host: string; + /** Redis服务器端口号 */ + port: number; + /** Redis用户名 */ + username: string; + /** Redis密码 */ + password: string; + /** Redis数据库索引 */ + database: number; +} +/** + * 日志配置接口 + * @interface LogConfig + */ +export interface LogConfigType { + /** 日志文件目录 */ + directory: string; + /** 是否输出到控制台 */ + console: boolean; + /** 单个日志文件最大大小 */ + maxSize: string; + /** 最大保留文件数 */ + maxFiles: string; + /** 日志等级 */ + level: string; +} + +/** + * 全局配置类型,包含所有配置项 + */ +export interface GlobalConfig { + /** 数据库配置 */ + db: DbConfig; + /** JWT配置 */ + jwt: JwtConfig; + /** Redis配置 */ + redis: RedisConfig; + /** 日志配置 */ + logger: LogConfigType; + /** 当前环境 */ + env: string; +} diff --git a/types/drizzle.type.ts b/src/type/drizzle.type.ts similarity index 90% rename from types/drizzle.type.ts rename to src/type/drizzle.type.ts index c9d3ae6..0dbf46c 100644 --- a/types/drizzle.type.ts +++ b/src/type/drizzle.type.ts @@ -1,52 +1,52 @@ -/** - * @file Drizzle ORM类型定义 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 定义Drizzle ORM相关的类型,包括数据库实例和表类型 - */ - -import type { MySql2Database } from 'drizzle-orm/mysql2'; -import type * as schema from '../src/plugins/drizzle/schema'; - -/** - * Drizzle数据库实例类型 - */ -export type DrizzleDB = MySql2Database; - -/** - * 数据库表Schema类型 - */ -export type DatabaseSchema = typeof schema; - -/** - * 扩展Elysia Context,添加数据库实例 - */ -export interface DrizzleContext { - /** Drizzle数据库实例 */ - db: DrizzleDB; -} - -/** - * 数据库连接状态 - */ -export type ConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected'; - -/** - * 数据库连接信息 - */ -export interface DatabaseConnectionInfo { - /** 连接状态 */ - status: ConnectionStatus; - /** 连接主机 */ - host: string; - /** 连接端口 */ - port: number; - /** 数据库名称 */ - database: string; - /** 连接时间 */ - connectedAt?: Date; - /** 错误信息 */ - error?: string; +/** + * @file Drizzle ORM类型定义 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 定义Drizzle ORM相关的类型,包括数据库实例和表类型 + */ + +import type { MySql2Database } from 'drizzle-orm/mysql2'; +import type * as schema from '@/eneities'; + +/** + * Drizzle数据库实例类型 + */ +export type DrizzleDB = MySql2Database; + +/** + * 数据库表Schema类型 + */ +export type DatabaseSchema = typeof schema; + +/** + * 扩展Elysia Context,添加数据库实例 + */ +export interface DrizzleContext { + /** Drizzle数据库实例 */ + db: DrizzleDB; +} + +/** + * 数据库连接状态 + */ +export type ConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected'; + +/** + * 数据库连接信息 + */ +export interface DatabaseConnectionInfo { + /** 连接状态 */ + status: ConnectionStatus; + /** 连接主机 */ + host: string; + /** 连接端口 */ + port: number; + /** 数据库名称 */ + database: string; + /** 连接时间 */ + connectedAt?: Date; + /** 错误信息 */ + error?: string; } \ No newline at end of file diff --git a/types/logger.type.ts b/src/type/logger.type.ts similarity index 96% rename from types/logger.type.ts rename to src/type/logger.type.ts index 3f9a5ff..a4f6078 100644 --- a/types/logger.type.ts +++ b/src/type/logger.type.ts @@ -1,18 +1,18 @@ - -/** - * Logger类的类型定义 - */ -export interface LoggerInstance { - /** 调试级别日志 */ - debug(message: string | object): void; - /** 信息级别日志 */ - info(message: string | object): void; - /** 警告级别日志 */ - warn(message: string | object): void; - /** 错误级别日志 */ - error(error: Error): void; - /** HTTP级别日志 */ - http(message: string | object): void; - /** 详细级别日志 */ - verbose(message: string | object): void; + +/** + * Logger类的类型定义 + */ +export interface LoggerInstance { + /** 调试级别日志 */ + debug(message: string | object): void; + /** 信息级别日志 */ + info(message: string | object): void; + /** 警告级别日志 */ + warn(message: string | object): void; + /** 错误级别日志 */ + error(error: Error): void; + /** HTTP级别日志 */ + http(message: string | object): void; + /** 详细级别日志 */ + verbose(message: string | object): void; } \ No newline at end of file diff --git a/src/type/redis.type.ts b/src/type/redis.type.ts new file mode 100644 index 0000000..28ea158 --- /dev/null +++ b/src/type/redis.type.ts @@ -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; +} \ No newline at end of file diff --git a/src/utils/jwt.helper.ts b/src/utils/jwt.helper.ts index 19f4771..191f1ec 100644 --- a/src/utils/jwt.helper.ts +++ b/src/utils/jwt.helper.ts @@ -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用户信息 diff --git a/src/utils/response.helper.ts b/src/utils/response.helper.ts new file mode 100644 index 0000000..b3558dc --- /dev/null +++ b/src/utils/response.helper.ts @@ -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 { + /** 业务状态码 */ + code: ErrorCode; + /** 响应消息 */ + message: string; + /** 响应数据 */ + data: T; + /** 时间戳 */ + timestamp?: string; + /** 请求ID(可选,用于追踪) */ + requestId?: string; +} + +/** + * 成功响应构造函数 + * @param data 响应数据 + * @param message 自定义消息,默认使用标准成功消息 + * @returns 标准成功响应格式 + */ +export function successResponse( + data: T, + message: string = ERROR_CODE_MESSAGES[ERROR_CODES.SUCCESS] +): ApiResponse { + return { + code: ERROR_CODES.SUCCESS, + message, + data, + timestamp: new Date().toISOString(), + }; +} + +/** + * 错误响应构造函数 + * @param code 错误码 + * @param message 错误消息,如果不提供则使用默认消息 + * @param data 错误详情数据,默认为null + * @returns 标准错误响应格式 + */ +export function errorResponse( + code: ErrorCode, + message?: string, + data: T = null as T +): ApiResponse { + return { + code, + message: message || ERROR_CODE_MESSAGES[code] || '未知错误', + data, + timestamp: new Date().toISOString(), + }; +} + +/** + * 分页数据响应格式 + */ +export interface PaginatedData { + /** 数据列表 */ + 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( + items: T[], + total: number, + page: number, + pageSize: number, + message: string = '查询成功' +): ApiResponse> { + const totalPages = Math.ceil(total / pageSize); + + return successResponse>({ + 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'; + } +} \ No newline at end of file diff --git a/tasks/20250629-计划.md b/tasks/archive/20250629-计划.md similarity index 55% rename from tasks/20250629-计划.md rename to tasks/archive/20250629-计划.md index ada7f10..2197cc5 100644 --- a/tasks/20250629-计划.md +++ b/tasks/archive/20250629-计划.md @@ -1,3 +1,3 @@ -1. 协助ai完成接口 -2. 协助ai完成测试用例 +1. 协助ai完成接口 +2. 协助ai完成测试用例 3. 优化mdc关于drizzle和redis的使用 \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a5276bc..c00a3a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,7 +44,7 @@ "include": [ "src", "types/**/*.d.ts" - ], +, "drizzle.config.ts" ], "exclude": [ "node_modules", "dist"