diff --git a/.cursor/rules/ai-development-workflow.mdc b/.cursor/rules/ai-development-workflow.mdc index 534590b..e2129ec 100644 --- a/.cursor/rules/ai-development-workflow.mdc +++ b/.cursor/rules/ai-development-workflow.mdc @@ -269,117 +269,6 @@ alwaysApply: true - 验证响应格式一致性 - 检查测试覆盖率 -## 交互模式 - -### 快速开发模式 ⚡ -适用于:标准CRUD操作、常见业务场景 - -**你只需要说:** -``` -"帮我实现用户模块的CRUD接口" -"添加产品管理功能" -"实现订单状态查询" -``` - -**AI会自动:** -- 按照规范创建完整的5个文件 -- 集成到现有路由系统 -- 提供完整的类型安全 -- 包含基础测试用例 - -### 定制开发模式 🔧 -适用于:复杂业务逻辑、特殊需求 - -**你需要提供:** -- 详细的业务规则 -- 特殊的验证要求 -- 复杂的数据关联 -- 性能要求 - -**AI会:** -- 详细分析需求 -- 提供设计方案 -- 征求确认后实现 -- 优化性能和安全性 - -## 我擅长处理的场景 - -### ✅ 高效处理 -- 标准REST API开发 -- CRUD操作实现 -- 数据验证和类型安全 -- 错误处理和响应格式 -- JWT认证集成 -- 数据库操作(Drizzle ORM) -- Redis缓存集成 -- 测试用例编写 -- API文档生成 - -### ⚡ 批量操作 -- 多文件同时创建/修改 -- 路由批量注册 -- 类型定义批量导出 -- 测试用例批量生成 - -### 🔍 代码分析 -- 现有代码结构理解 -- 依赖关系分析 -- 潜在问题识别 -- 优化建议提供 - -## 沟通最佳实践 - -### 清晰描述需求 -``` -❌ "做一个用户功能" -✅ "实现用户注册、登录、个人信息查询和修改功能,需要JWT认证" - -❌ "这个接口有问题" -✅ "用户登录接口返回401错误,但是用户名密码正确" -``` - -### 提供上下文信息 -``` -✅ "在现有的用户模块基础上添加头像上传功能" -✅ "这个接口需要管理员权限才能访问" -✅ "数据需要缓存到Redis,缓存时间1小时" -``` - -### 明确期望结果 -``` -✅ "创建完整的用户CRUD接口,包含测试" -✅ "只需要修改现有的查询接口,添加分页功能" -✅ "优化这个接口的性能,响应时间控制在100ms内" -``` - -## 错误处理和调试 - -### 当代码出现问题时 - -1. **我会主动分析** - - 检查类型错误 - - 验证语法正确性 - - 确认导入导出关系 - -2. **提供修复方案** - - 直接修复简单问题 - - 解释复杂问题的原因 - - 提供多种解决方案 - -3. **验证修复结果** - - 确保修复后代码可运行 - - 检查是否引入新问题 - - 验证功能完整性 - -### 性能优化建议 - -我会在适当时候提供: -- 数据库查询优化 -- 缓存策略建议 -- 并发处理优化 -- 内存使用优化 - -## 质量保证 ### 代码质量检查 - [ ] 类型安全性 diff --git a/.cursor/rules/elysia-rules.mdc b/.cursor/rules/elysia-rules.mdc index ed114f6..ee7b398 100644 --- a/.cursor/rules/elysia-rules.mdc +++ b/.cursor/rules/elysia-rules.mdc @@ -1,5 +1,5 @@ --- -description: "全局规则" +description: "Bun Elysia框架业务开发规则" globs: ["**/*"] alwaysApply: true --- @@ -83,6 +83,7 @@ src/modules/ │ ├── example.service.ts # 业务逻辑 │ ├── example.controller.ts # 路由控制器 │ └── example.test.ts # 测试用例 +│ └── example.testDocs.ts # 测试用例文档 ├── health/ # 健康检查模块 │ ├── health.controller.ts │ └── health.service.ts @@ -178,6 +179,7 @@ src/modules/[module]/ ├── [module].service.ts # 3️⃣ 业务逻辑实现 ├── [module].controller.ts # 4️⃣ 路由控制器 └── [module].test.ts # 5️⃣ 测试用例 +└── [module].testDoc.ts # 5️⃣ 测试用例文档 ``` ### 1.2 文件命名约定 @@ -497,6 +499,10 @@ describe('User API', () => { }); ``` +### 7.2 测试用例文档 + +- 先设计测试用例文档,再根据文档编写测试用例 + ## 8. AI助手协作规范 ### 8.1 注释规范(关键❗️) diff --git a/.cursor/rules/global-rules/process-list-task.mdc b/.cursor/rules/global-rules/process-list-task.mdc index 1fa9d14..8e48779 100644 --- a/.cursor/rules/global-rules/process-list-task.mdc +++ b/.cursor/rules/global-rules/process-list-task.mdc @@ -12,7 +12,7 @@ alwaysApply: true 管理Markdown文件中的任务清单以跟踪完成PRD进度的指南。 ## 任务实施 (Task Implementation) -* **一次处理一个子任务:** 在询问用户并获得“yes”或“y”的许可之前,**不要**开始下一个子任务。 +* **一次处理一个子任务:** 在询问用户并获得“yes”或“y”的许可之前,**不要**开始下一个子任务。********非常重要必须执行********* * **完成协议 (Completion protocol):** 1. 当你完成一个**子任务**时,立即通过将 `[ ]` 改为 `[x]` 将其标记为已完成。 2. 如果一个父任务下的**所有**子任务现在都是 `[x]`,则按以下顺序执行: diff --git a/bun.lock b/bun.lock index de623c8..cc03ff1 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@elysiajs/jwt": "^1.3.1", "@elysiajs/swagger": "^1.3.0", "@types/ua-parser-js": "^0.7.39", + "bcrypt": "^6.0.0", "canvas": "^3.1.2", "chalk": "^5.4.1", "drizzle-orm": "^0.44.2", @@ -21,6 +22,7 @@ "winston-daily-rotate-file": "^5.0.0", }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/bun": "^1.0.25", "@types/nodemailer": "^6.4.17", "@types/redis": "^4.0.11", @@ -197,6 +199,8 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "https://registry.npmmirror.com/@tokenizer/token/-/token-0.3.0.tgz", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/bcrypt": ["@types/bcrypt@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ=="], + "@types/bun": ["@types/bun@1.2.17", "https://registry.npmmirror.com/@types/bun/-/bun-1.2.17.tgz", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], "@types/chai": ["@types/chai@5.2.2", "https://registry.npmmirror.com/@types/chai/-/chai-5.2.2.tgz", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], @@ -279,6 +283,8 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "brace-expansion": ["brace-expansion@1.1.12", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -567,10 +573,12 @@ "node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="], - "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-addon-api": ["node-addon-api@8.4.0", "", {}, "sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg=="], "node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "nodemailer": ["nodemailer@7.0.4", "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.4.tgz", {}, "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw=="], "object-hash": ["object-hash@3.0.0", "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], @@ -773,6 +781,8 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "canvas/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "color/color-convert": ["color-convert@1.9.3", "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "eslint/chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], diff --git a/package.json b/package.json index b162337..ce80159 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "bun": ">=1.0.25" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/bun": "^1.0.25", "@types/nodemailer": "^6.4.17", "@types/redis": "^4.0.11", @@ -31,6 +32,7 @@ "@elysiajs/jwt": "^1.3.1", "@elysiajs/swagger": "^1.3.0", "@types/ua-parser-js": "^0.7.39", + "bcrypt": "^6.0.0", "canvas": "^3.1.2", "chalk": "^5.4.1", "drizzle-orm": "^0.44.2", @@ -45,7 +47,7 @@ "winston-daily-rotate-file": "^5.0.0" }, "scripts": { - "dev": "bun --watch --env-file=.env --hot src/server.ts", + "dev": "bun --env-file=.env src/server.ts", "start": "bun --env-file=.env.prod src/server.ts", "test": "bun test", "test:watch": "bun test --watch", diff --git a/src/constants/error-codes.ts b/src/constants/error-codes.ts index c4c53b2..e2f3f47 100644 --- a/src/constants/error-codes.ts +++ b/src/constants/error-codes.ts @@ -31,6 +31,29 @@ export const ERROR_CODES = { TOKEN_INVALID: 'TOKEN_INVALID', // Token无效 INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS', // 权限不足 + // 用户注册相关错误 + USERNAME_EXISTS: 'USERNAME_EXISTS', // 用户名已存在 + EMAIL_EXISTS: 'EMAIL_EXISTS', // 邮箱已存在 + PASSWORD_MISMATCH: 'PASSWORD_MISMATCH', // 密码不匹配 + CAPTCHA_ERROR: 'CAPTCHA_ERROR', // 验证码错误 + EMAIL_SEND_FAILED: 'EMAIL_SEND_FAILED', // 邮件发送失败 + + // 用户激活相关错误 + INVALID_ACTIVATION_TOKEN: 'INVALID_ACTIVATION_TOKEN', // 激活令牌无效 + ALREADY_ACTIVATED: 'ALREADY_ACTIVATED', // 已经激活 + + // 用户登录相关错误 + INVALID_PASSWORD: 'INVALID_PASSWORD', // 密码错误 + ACCOUNT_NOT_ACTIVATED: 'ACCOUNT_NOT_ACTIVATED', // 账号未激活 + ACCOUNT_LOCKED: 'ACCOUNT_LOCKED', // 账号被锁定 + TOO_MANY_FAILED_ATTEMPTS: 'TOO_MANY_FAILED_ATTEMPTS', // 失败次数过多 + + // 密码重置相关错误 + INVALID_RESET_TOKEN: 'INVALID_RESET_TOKEN', // 重置令牌无效 + + // 系统状态 + NOT_IMPLEMENTED: 'NOT_IMPLEMENTED', // 功能未实现 + // 服务器错误 5xx INTERNAL_ERROR: 'INTERNAL_ERROR', // 内部服务器错误 DATABASE_ERROR: 'DATABASE_ERROR', // 数据库错误 @@ -69,6 +92,29 @@ export const ERROR_CODE_TO_HTTP_STATUS: Record = { [ERROR_CODES.TOKEN_INVALID]: 401, [ERROR_CODES.INSUFFICIENT_PERMISSIONS]: 403, + // 用户注册相关错误 + [ERROR_CODES.USERNAME_EXISTS]: 409, + [ERROR_CODES.EMAIL_EXISTS]: 409, + [ERROR_CODES.PASSWORD_MISMATCH]: 400, + [ERROR_CODES.CAPTCHA_ERROR]: 400, + [ERROR_CODES.EMAIL_SEND_FAILED]: 500, + + // 用户激活相关错误 + [ERROR_CODES.INVALID_ACTIVATION_TOKEN]: 400, + [ERROR_CODES.ALREADY_ACTIVATED]: 409, + + // 用户登录相关错误 + [ERROR_CODES.INVALID_PASSWORD]: 401, + [ERROR_CODES.ACCOUNT_NOT_ACTIVATED]: 403, + [ERROR_CODES.ACCOUNT_LOCKED]: 403, + [ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: 429, + + // 密码重置相关错误 + [ERROR_CODES.INVALID_RESET_TOKEN]: 400, + + // 系统状态 + [ERROR_CODES.NOT_IMPLEMENTED]: 501, + // 服务器错误 5xx [ERROR_CODES.INTERNAL_ERROR]: 500, [ERROR_CODES.DATABASE_ERROR]: 500, @@ -102,6 +148,29 @@ export const ERROR_CODE_MESSAGES: Record = { [ERROR_CODES.TOKEN_INVALID]: '访问令牌无效', [ERROR_CODES.INSUFFICIENT_PERMISSIONS]: '权限不足', + // 用户注册相关错误 + [ERROR_CODES.USERNAME_EXISTS]: '用户名已存在', + [ERROR_CODES.EMAIL_EXISTS]: '邮箱已被注册', + [ERROR_CODES.PASSWORD_MISMATCH]: '两次输入的密码不一致', + [ERROR_CODES.CAPTCHA_ERROR]: '验证码错误或已过期', + [ERROR_CODES.EMAIL_SEND_FAILED]: '邮件发送失败', + + // 用户激活相关错误 + [ERROR_CODES.INVALID_ACTIVATION_TOKEN]: '激活令牌无效或已过期', + [ERROR_CODES.ALREADY_ACTIVATED]: '账号已经激活', + + // 用户登录相关错误 + [ERROR_CODES.INVALID_PASSWORD]: '密码错误', + [ERROR_CODES.ACCOUNT_NOT_ACTIVATED]: '账号未激活', + [ERROR_CODES.ACCOUNT_LOCKED]: '账号已被锁定', + [ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: '登录失败次数过多', + + // 密码重置相关错误 + [ERROR_CODES.INVALID_RESET_TOKEN]: '重置令牌无效或已过期', + + // 系统状态 + [ERROR_CODES.NOT_IMPLEMENTED]: '功能未实现', + // 服务器错误 [ERROR_CODES.INTERNAL_ERROR]: '服务器内部错误', [ERROR_CODES.DATABASE_ERROR]: '数据库操作失败', diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..7b8f705 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,63 @@ +/** + * @file 认证模块Controller层实现 + * @author AI Assistant + * @date 2024-12-19 + * @description 认证模块的路由控制器,处理用户注册相关请求 + */ + +import { Elysia } from 'elysia'; +import { RegisterSchema } from './auth.schema'; +import { RegisterResponses } from './auth.response'; +import { authService } from './auth.service'; +import { tags } from '@/modules/tags'; +import { Logger } from '@/plugins/logger/logger.service'; +import { ERROR_CODES } from '@/constants/error-codes'; +import { BusinessError } from '@/utils/response.helper'; + +/** + * 认证控制器 + * @description 处理用户认证相关的HTTP请求 + */ +export const authController = new Elysia() + /** + * 用户注册接口 + * @route POST /api/auth/register + * @description 用户注册,包含验证码验证、用户名邮箱唯一性检查等 + * @param body RegisterRequest 注册请求参数 + * @returns RegisterSuccessResponse | RegisterErrorResponse + */ + .post( + '/register', + async ({ body, set }) => { + try { + return await authService.register(body); + } catch (error) { + if (error instanceof BusinessError) { + set.status = 400; + return { + code: error.code, + message: error.message, + data: null, + }; + } + + Logger.error(error as Error); + set.status = 500; + return { + code: ERROR_CODES.INTERNAL_ERROR, + message: '服务器内部错误', + data: null, + }; + } + }, + { + body: RegisterSchema, + detail: { + summary: '用户注册', + description: '用户注册接口,需要提供用户名、邮箱、密码和验证码', + tags: [tags.auth], + operationId: 'registerUser', + }, + response: RegisterResponses, + } + ); \ No newline at end of file diff --git a/src/modules/auth/auth.response.ts b/src/modules/auth/auth.response.ts new file mode 100644 index 0000000..8185e89 --- /dev/null +++ b/src/modules/auth/auth.response.ts @@ -0,0 +1,102 @@ +/** + * @file 认证模块响应格式定义 + * @author AI Assistant + * @date 2024-12-19 + * @description 定义认证模块的用户注册响应格式 + */ + +import { t, type Static } from 'elysia'; +import { globalResponseWrapperSchema } from '@/validators/global.response'; + +/** + * 用户注册成功响应数据Schema + * @description 用户注册成功后返回的用户信息 + */ +export const RegisterSuccessDataSchema = t.Object({ + /** 用户ID */ + id: t.Number({ + description: '用户ID', + examples: [1, 2, 3] + }), + /** 用户名 */ + username: t.String({ + description: '用户名', + examples: ['admin', 'testuser'] + }), + /** 邮箱地址 */ + email: t.String({ + description: '邮箱地址', + examples: ['user@example.com'] + }), + /** 账号状态 */ + status: t.String({ + description: '账号状态', + examples: ['pending', 'active'] + }), + /** 创建时间 */ + createdAt: t.String({ + description: '创建时间', + examples: ['2024-12-19T10:30:00Z'] + }) +}); + +/** + * 用户注册成功响应Schema + * @description 用户注册成功的完整响应格式 + */ +export const RegisterSuccessResponseSchema = globalResponseWrapperSchema(RegisterSuccessDataSchema); + +/** + * 用户注册失败响应Schema + * @description 用户注册失败的错误响应格式 + */ +export const RegisterErrorResponseSchema = t.Object({ + /** 错误代码 */ + code: t.Union([ + t.Literal('VALIDATION_ERROR'), + t.Literal('USERNAME_EXISTS'), + t.Literal('EMAIL_EXISTS'), + t.Literal('CAPTCHA_ERROR'), + t.Literal('INTERNAL_ERROR') + ], { + description: '错误代码', + examples: ['VALIDATION_ERROR', 'USERNAME_EXISTS', 'EMAIL_EXISTS'] + }), + /** 错误信息 */ + message: t.String({ + description: '错误信息', + examples: ['用户名已存在', '邮箱已被注册', '验证码错误'] + }), + /** 错误数据 */ + data: t.Null({ + description: '错误时数据为null' + }) +}); + +/** + * 用户注册接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const RegisterResponses = { + 200: RegisterSuccessResponseSchema, + 400: RegisterErrorResponseSchema, + 500: t.Object({ + code: t.Literal('INTERNAL_ERROR'), + message: t.String({ + description: '内部服务器错误', + examples: ['服务器内部错误'] + }), + data: t.Null() + }) +}; + +// ========== TypeScript类型导出 ========== + +/** 用户注册成功响应数据类型 */ +export type RegisterSuccessData = Static; + +/** 用户注册成功响应类型 */ +export type RegisterSuccessResponse = Static; + +/** 用户注册失败响应类型 */ +export type RegisterErrorResponse = Static; \ No newline at end of file diff --git a/src/modules/auth/auth.schema.ts b/src/modules/auth/auth.schema.ts new file mode 100644 index 0000000..b4a4d1e --- /dev/null +++ b/src/modules/auth/auth.schema.ts @@ -0,0 +1,51 @@ +/** + * @file 认证模块Schema定义 + * @author AI Assistant + * @date 2024-12-19 + * @description 定义认证模块的用户注册Schema + */ + +import { t, type Static } from 'elysia'; + +/** + * 用户注册Schema + * @description 用户注册请求参数验证规则,基于sys_users表结构 + */ +export const RegisterSchema = t.Object({ + /** 用户名,2-50字符,对应sys_users.username */ + username: t.String({ + minLength: 2, + maxLength: 50, + description: '用户名,2-50字符', + examples: ['admin', 'testuser'] + }), + /** 邮箱地址,对应sys_users.email */ + email: t.String({ + format: 'email', + maxLength: 100, + description: '邮箱地址', + examples: ['user@example.com'] + }), + /** 密码,6-50字符 */ + password: t.String({ + minLength: 6, + maxLength: 50, + description: '密码,6-50字符', + examples: ['password123'] + }), + /** 图形验证码 */ + captcha: t.String({ + minLength: 4, + maxLength: 6, + description: '图形验证码', + examples: ['a1b2'] + }), + /** 验证码会话ID */ + captchaId: t.String({ + description: '验证码会话ID', + examples: ['uuid-string-here'] + }) +}); + +/** 用户注册请求类型 */ +export type RegisterRequest = Static; \ No newline at end of file diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..a6c38f4 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,232 @@ +/** + * @file 认证模块Service层实现 + * @author AI Assistant + * @date 2024-12-19 + * @description 认证模块的用户注册业务逻辑实现 + */ + +import bcrypt from 'bcrypt'; +import { eq } from 'drizzle-orm'; +import { db } from '@/plugins/drizzle/drizzle.service'; +import { sysUsers } from '@/eneities'; +import { captchaService } from '@/modules/captcha/captcha.service'; +import { Logger } from '@/plugins/logger/logger.service'; +import { ERROR_CODES } from '@/constants/error-codes'; +import { successResponse, errorResponse, BusinessError } from '@/utils/response.helper'; +import { nextId } from '@/utils/snowflake'; +import type { RegisterRequest } from './auth.schema'; +import type { + RegisterSuccessResponse, + RegisterErrorResponse +} from './auth.response'; + +/** + * 认证服务类 + * @description 处理用户注册、登录等认证相关业务逻辑 + */ +export class AuthService { + /** bcrypt加密成本因子 */ + private readonly BCRYPT_ROUNDS = 12; + + /** + * 用户注册 + * @param request 用户注册请求参数 + * @returns Promise + */ + async register(request: RegisterRequest): Promise { + try { + Logger.info(`用户注册请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`); + + const { username, email, password, captcha, captchaId } = request; + + // 1. 验证验证码 + await this.validateCaptcha(captcha, captchaId); + + // 2. 检查用户名是否已存在 + await this.checkUsernameExists(username); + + // 3. 检查邮箱是否已存在 + await this.checkEmailExists(email); + + // 4. 密码加密 + const passwordHash = await this.hashPassword(password); + + // 5. 创建用户记录 + const newUser = await this.createUser({ + username, + email, + passwordHash + }); + + Logger.info(`用户注册成功:${newUser.id} - ${newUser.username}`); + + return successResponse({ + id: newUser.id, + username: newUser.username, + email: newUser.email, + status: newUser.status, + createdAt: newUser.createdAt + }, '用户注册成功'); + + } catch (error) { + Logger.error(new Error(`用户注册失败:${error}`)); + + if (error instanceof BusinessError) { + throw error; + } + + throw new BusinessError('注册失败,请稍后重试', ERROR_CODES.INTERNAL_ERROR); + } + } + + /** + * 验证验证码 + * @param captcha 验证码 + * @param captchaId 验证码ID + */ + private async validateCaptcha(captcha: string, captchaId: string): Promise { + try { + const result = await captchaService.verifyCaptcha({ + captchaId, + captchaCode: captcha + }); + + if (!result.data?.valid) { + throw new BusinessError( + result.data?.message || '验证码验证失败', + ERROR_CODES.CAPTCHA_ERROR + ); + } + } catch (error) { + if (error instanceof BusinessError) { + throw error; + } + throw new BusinessError('验证码验证失败', ERROR_CODES.CAPTCHA_ERROR); + } + } + + /** + * 检查用户名是否已存在 + * @param username 用户名 + */ + private async checkUsernameExists(username: string): Promise { + try { + const existingUser = await db().select({ id: sysUsers.id }) + .from(sysUsers) + .where(eq(sysUsers.username, username)) + .limit(1); + + if (existingUser.length > 0) { + throw new BusinessError('用户名已存在', ERROR_CODES.USERNAME_EXISTS); + } + } catch (error) { + if (error instanceof BusinessError) { + throw error; + } + Logger.error(new Error(`检查用户名失败:${error}`)); + throw new BusinessError('用户名检查失败', ERROR_CODES.INTERNAL_ERROR); + } + } + + /** + * 检查邮箱是否已存在 + * @param email 邮箱地址 + */ + private async checkEmailExists(email: string): Promise { + try { + const existingUser = await db().select({ id: sysUsers.id }) + .from(sysUsers) + .where(eq(sysUsers.email, email)) + .limit(1); + + if (existingUser.length > 0) { + throw new BusinessError('邮箱已被注册', ERROR_CODES.EMAIL_EXISTS); + } + } catch (error) { + if (error instanceof BusinessError) { + throw error; + } + Logger.error(new Error(`检查邮箱失败:${error}`)); + throw new BusinessError('邮箱检查失败', ERROR_CODES.INTERNAL_ERROR); + } + } + + /** + * 密码加密 + * @param password 原始密码 + * @returns Promise 加密后的密码哈希 + */ + private async hashPassword(password: string): Promise { + try { + return await bcrypt.hash(password, this.BCRYPT_ROUNDS); + } catch (error) { + Logger.error(new Error(`密码加密失败:${error}`)); + throw new BusinessError('密码加密失败', ERROR_CODES.INTERNAL_ERROR); + } + } + + /** + * 创建用户记录 + * @param userData 用户数据 + * @returns Promise 创建的用户信息 + */ + private async createUser(userData: { + username: string; + email: string; + passwordHash: string; + }): Promise<{ + id: number; + username: string; + email: string; + status: string; + createdAt: string; + }> { + try { + const { username, email, passwordHash } = userData; + + const userId = Number(nextId()); + + const [insertResult] = await db().insert(sysUsers).values({ + id: userId, + username, + email, + passwordHash, + status: 'pending' // 新注册用户状态为待激活 + }); + + // 查询刚创建的用户信息 + const [newUser] = await db().select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + createdAt: sysUsers.createdAt + }) + .from(sysUsers) + .where(eq(sysUsers.id, userId)) + .limit(1); + + if (!newUser) { + throw new Error('创建用户后查询失败'); + } + + return { + id: Number(newUser.id), + username: newUser.username, + email: newUser.email, + status: newUser.status, + createdAt: newUser.createdAt + }; + } catch (error) { + console.log(error); + Logger.error(error as Error); + throw new BusinessError('创建用户失败', ERROR_CODES.INTERNAL_ERROR); + } + } +} + +/** + * 导出认证服务单例实例 + * @description 供controller层使用的认证服务实例 + */ +export const authService = new AuthService(); \ No newline at end of file diff --git a/src/modules/auth/auth.test.ts b/src/modules/auth/auth.test.ts new file mode 100644 index 0000000..c4ef2f6 --- /dev/null +++ b/src/modules/auth/auth.test.ts @@ -0,0 +1,445 @@ +/** + * @file 认证模块测试用例 + * @author AI Assistant + * @date 2024-12-19 + * @description 认证模块用户注册功能的完整测试用例,覆盖正常、异常、边界场景 + */ + +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; +import { Elysia } from 'elysia'; +import { authController } from './auth.controller'; +import { captchaController } from '@/modules/captcha/captcha.controller'; +import { plugins } from '@/plugins/index'; +import { redisService } from '@/plugins/redis/redis.service'; +import { drizzleService } from '@/plugins/drizzle/drizzle.service'; +import { sysUsers } from '@/eneities'; +import { eq } from 'drizzle-orm'; +import type { RegisterRequest } from './auth.schema'; + +// 创建测试应用实例 +const testApp = new Elysia({ prefix: '/api' }) + .use(plugins) + .group('/auth', (app) => app.use(authController)) + .group('/captcha', (app) => app.use(captchaController)); + +describe('认证模块测试', () => { + let captchaId: string; + let validCaptchaCode: string; + + beforeAll(async () => { + // 初始化数据库和Redis服务 + try { + await drizzleService.initialize(); + await redisService.initialize(); + } catch (error) { + console.warn('服务初始化失败,跳过相关测试:', error); + } + }); + + afterAll(async () => { + // 清理测试数据和关闭连接 + try { + // 删除测试用户 + await drizzleService.db.delete(sysUsers) + .where(eq(sysUsers.username, 'testuser')); + await drizzleService.db.delete(sysUsers) + .where(eq(sysUsers.email, 'test@example.com')); + + await redisService.close(); + await drizzleService.close(); + } catch (error) { + console.warn('服务关闭失败:', error); + } + }); + + beforeEach(async () => { + // 每个测试前生成新的验证码 + try { + const captchaResponse = await testApp + .handle(new Request('http://localhost/api/captcha/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'image', + length: 4, + expireTime: 300 + }), + })); + + if (captchaResponse.status === 200) { + const captchaResult = await captchaResponse.json() as any; + captchaId = captchaResult.data.id; + // 模拟已知验证码(在实际测试中可能需要直接从Redis获取) + validCaptchaCode = 'TEST'; + + // 直接在Redis中设置已知的验证码用于测试 + await redisService.setex( + `captcha:${captchaId}`, + 300, + JSON.stringify({ + id: captchaId, + code: 'test', // 小写存储 + type: 'image', + image: 'test-image-data', + expireTime: Date.now() + 300000, + createdAt: Date.now() + }) + ); + } + } catch (error) { + console.warn('验证码生成失败:', error); + captchaId = 'test-captcha-id'; + validCaptchaCode = 'TEST'; + } + }); + + describe('POST /api/auth/register - 用户注册', () => { + it('应该成功注册新用户', async () => { + const payload: RegisterRequest = { + username: 'testuser', + email: 'test@example.com', + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(200); + const result = await response.json() as any; + expect(result.code).toBe('SUCCESS'); + expect(result.message).toBe('用户注册成功'); + expect(result.data).toBeDefined(); + expect(result.data.id).toBeDefined(); + expect(result.data.username).toBe('testuser'); + expect(result.data.email).toBe('test@example.com'); + expect(result.data.status).toBe('pending'); + expect(result.data.createdAt).toBeDefined(); + }); + + it('用户名已存在应返回409错误', async () => { + // 先注册一个用户 + const firstPayload: RegisterRequest = { + username: 'existinguser', + email: 'existing@example.com', + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(firstPayload), + })); + + // 重新生成验证码 + try { + const captchaResponse = await testApp + .handle(new Request('http://localhost/api/captcha/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'image', + length: 4, + expireTime: 300 + }), + })); + + if (captchaResponse.status === 200) { + const captchaResult = await captchaResponse.json() as any; + captchaId = captchaResult.data.id; + validCaptchaCode = 'TEST'; + + // 直接在Redis中设置已知的验证码用于测试 + await redisService.setex( + `captcha:${captchaId}`, + 300, + JSON.stringify({ + id: captchaId, + code: 'test', // 小写存储 + type: 'image', + image: 'test-image-data', + expireTime: Date.now() + 300000, + createdAt: Date.now() + }) + ); + } + } catch (error) { + captchaId = 'test-captcha-id'; + validCaptchaCode = 'TEST'; + } + + // 尝试用相同用户名注册 + const duplicatePayload: RegisterRequest = { + username: 'existinguser', + email: 'different@example.com', + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(duplicatePayload), + })); + + expect(response.status).toBe(400); + const result = await response.json() as any; + expect(result.code).toBe('USERNAME_EXISTS'); + expect(result.message).toBe('用户名已存在'); + expect(result.data).toBeNull(); + }); + + it('邮箱已存在应返回409错误', async () => { + const duplicateEmailPayload: RegisterRequest = { + username: 'newuser', + email: 'test@example.com', // 使用之前注册的邮箱 + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(duplicateEmailPayload), + })); + + expect(response.status).toBe(400); + const result = await response.json() as any; + expect(result.code).toBe('EMAIL_EXISTS'); + expect(result.message).toBe('邮箱已被注册'); + expect(result.data).toBeNull(); + }); + + it('验证码错误应返回400错误', async () => { + const payload: RegisterRequest = { + username: 'newuser2', + email: 'newuser2@example.com', + password: 'password123', + captcha: 'WRONG', + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + const result = await response.json() as any; + expect(result.code).toBe('CAPTCHA_ERROR'); + expect(result.message).toContain('验证码'); + expect(result.data).toBeNull(); + }); + + it('验证码ID不存在应返回400错误', async () => { + const payload: RegisterRequest = { + username: 'newuser3', + email: 'newuser3@example.com', + password: 'password123', + captcha: 'TEST', + captchaId: 'nonexistent-captcha-id' + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + const result = await response.json() as any; + expect(result.code).toBe('CAPTCHA_ERROR'); + expect(result.data).toBeNull(); + }); + }); + + describe('参数验证测试', () => { + it('用户名过短应返回400错误', async () => { + const payload = { + username: 'a', // 1个字符,小于最小长度2 + email: 'test@example.com', + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + + it('用户名过长应返回400错误', async () => { + const payload = { + username: 'a'.repeat(51), // 51个字符,超过最大长度50 + email: 'test@example.com', + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + + it('邮箱格式无效应返回400错误', async () => { + const payload = { + username: 'testuser', + email: 'invalid-email', // 无效邮箱格式 + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + + it('密码过短应返回400错误', async () => { + const payload = { + username: 'testuser', + email: 'test@example.com', + password: '12345', // 5个字符,小于最小长度6 + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + + it('验证码过短应返回400错误', async () => { + const payload = { + username: 'testuser', + email: 'test@example.com', + password: 'password123', + captcha: '123', // 3个字符,小于最小长度4 + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + + it('缺少必填字段应返回400错误', async () => { + const payload = { + username: 'testuser', + // 缺少 email + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + }); + + describe('边界条件测试', () => { + it('用户名长度正好为最小值应成功', async () => { + const payload: RegisterRequest = { + username: 'xy', // 2个字符,正好等于最小长度 + email: 'min2@example.com', + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(200); + }); + + it('用户名长度正好为最大值应成功', async () => { + const payload: RegisterRequest = { + username: 'b'.repeat(50), // 50个字符,正好等于最大长度 + email: 'max2@example.com', + password: 'password123', + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(200); + }); + + it('密码长度正好为最小值应成功', async () => { + const payload: RegisterRequest = { + username: 'minpass2', + email: 'minpass2@example.com', + password: '123456', // 6个字符,正好等于最小长度 + captcha: validCaptchaCode, + captchaId: captchaId + }; + + const response = await testApp + .handle(new Request('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(200); + }); + }); +}); \ No newline at end of file diff --git a/src/modules/index.ts b/src/modules/index.ts index 18b787d..4ff03c3 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -9,10 +9,11 @@ import { Elysia } from 'elysia'; import { healthController } from './health/health.controller'; -import { userController } from './user/user.controller'; +// import { userController } from './user/user.controller'; import { testController } from './test/test.controller'; import { exampleController } from './example/example.controller'; import { captchaController } from './captcha/captcha.controller'; +import { authController } from './auth/auth.controller'; /** * 主路由控制器 - API 路由总入口 @@ -27,12 +28,14 @@ export const controllers = new Elysia({ version: '1.0.0', })) // 用户系统接口 - .group('/user', (app) => app.use(userController)) + // .group('/user', (app) => app.use(userController)) // 验证性接口 .group('/test', (app) => app.use(testController)) // 健康检查接口 .group('/health', (app) => app.use(healthController)) // 样例接口 .group('/example', (app) => app.use(exampleController)) + // 认证接口 + .group('/auth', (app) => app.use(authController)) // 验证码接口 .group('/captcha', (app) => app.use(captchaController)); diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts deleted file mode 100644 index b50728c..0000000 --- a/src/modules/user/user.controller.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Elysia } from 'elysia'; - -export const userController = new Elysia({ name: 'userController' }).get('/', () => ({ message: '用户系统' })); diff --git a/src/utils/snowflake.test.ts b/src/utils/snowflake.test.ts new file mode 100644 index 0000000..3b8a3a9 --- /dev/null +++ b/src/utils/snowflake.test.ts @@ -0,0 +1,320 @@ +/** + * @file 雪花ID生成器测试 + * @author AI助手 + * @date 2024-12-30 + * @lastEditor AI助手 + * @lastEditTime 2024-12-30 + * @description 雪花ID生成器的单元测试,验证ID生成、解析、配置等功能 + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Snowflake, getSnowflake, nextId, parseId, createSnowflake, type SnowflakeConfig } from './snowflake'; + +describe('Snowflake ID Generator', () => { + let snowflake: Snowflake; + + beforeEach(() => { + // 创建新的雪花ID实例 + snowflake = new Snowflake({ workerId: 1, datacenterId: 1 }); + }); + + describe('Constructor', () => { + it('应该成功创建雪花ID生成器实例', () => { + const config: SnowflakeConfig = { + workerId: 1, + datacenterId: 1, + }; + const instance = new Snowflake(config); + expect(instance).toBeInstanceOf(Snowflake); + }); + + it('应该使用默认配置创建实例', () => { + const config: SnowflakeConfig = { + workerId: 1, + datacenterId: 1, + }; + const instance = new Snowflake(config); + const instanceConfig = instance.getConfig(); + + expect(instanceConfig.workerId).toBe(1); + expect(instanceConfig.datacenterId).toBe(1); + expect(instanceConfig.sequence).toBe(0); + expect(instanceConfig.epoch).toBe(1577836800000); // 2020-01-01 00:00:00 UTC + }); + + it('应该使用自定义配置创建实例', () => { + const customEpoch = 1609459200000; // 2021-01-01 00:00:00 UTC + const config: SnowflakeConfig = { + workerId: 5, + datacenterId: 3, + sequence: 100, + epoch: customEpoch, + }; + const instance = new Snowflake(config); + const instanceConfig = instance.getConfig(); + + expect(instanceConfig.workerId).toBe(5); + expect(instanceConfig.datacenterId).toBe(3); + expect(instanceConfig.sequence).toBe(100); + expect(instanceConfig.epoch).toBe(customEpoch); + }); + + it('应该验证workerId范围', () => { + expect(() => { + new Snowflake({ workerId: -1, datacenterId: 1 }); + }).toThrow('Worker ID must be between 0 and 31'); + + expect(() => { + new Snowflake({ workerId: 32, datacenterId: 1 }); + }).toThrow('Worker ID must be between 0 and 31'); + }); + + it('应该验证datacenterId范围', () => { + expect(() => { + new Snowflake({ workerId: 1, datacenterId: -1 }); + }).toThrow('Datacenter ID must be between 0 and 31'); + + expect(() => { + new Snowflake({ workerId: 1, datacenterId: 32 }); + }).toThrow('Datacenter ID must be between 0 and 31'); + }); + }); + + describe('ID Generation', () => { + it('应该生成唯一的ID', () => { + const id1 = snowflake.nextId(); + const id2 = snowflake.nextId(); + + expect(id1).not.toBe(id2); + expect(typeof id1).toBe('bigint'); + expect(typeof id2).toBe('bigint'); + }); + + it('应该生成递增的ID', () => { + const ids: bigint[] = []; + for (let i = 0; i < 10; i++) { + ids.push(snowflake.nextId()); + } + + for (let i = 1; i < ids.length; i++) { + expect(ids[i]).toBeGreaterThan(ids[i - 1]); + } + }); + + it('应该在同一毫秒内递增序列号', () => { + // 模拟同一毫秒 + const originalDateNow = Date.now; + Date.now = () => 1609459200000; + + const id1 = snowflake.nextId(); + const id2 = snowflake.nextId(); + + const parsed1 = Snowflake.parseId(id1); + const parsed2 = Snowflake.parseId(id2); + + expect(parsed1.timestamp).toBe(parsed2.timestamp); + expect(parsed2.sequence).toBe(parsed1.sequence + 1); + + // 恢复原始函数 + Date.now = originalDateNow; + }); + + it('应该在不同毫秒间重置序列号', () => { + let callCount = 0; + const originalDateNow = Date.now; + Date.now = () => { + callCount++; + return 1609459200000 + callCount; // 每次调用递增1毫秒 + }; + + const id1 = snowflake.nextId(); + const id2 = snowflake.nextId(); + + const parsed1 = Snowflake.parseId(id1); + const parsed2 = Snowflake.parseId(id2); + + expect(parsed1.timestamp).toBeLessThan(parsed2.timestamp); + expect(parsed2.sequence).toBe(0); // 序列号应该重置 + + // 恢复原始函数 + Date.now = originalDateNow; + }); + }); + + describe('ID Parsing', () => { + it('应该正确解析生成的ID', () => { + const id = snowflake.nextId(); + const parsed = Snowflake.parseId(id); + + expect(parsed.workerId).toBe(1); + expect(parsed.datacenterId).toBe(1); + expect(parsed.sequence).toBeGreaterThanOrEqual(0); + expect(parsed.timestamp).toBeGreaterThan(0); + expect(parsed.createdAt).toBeInstanceOf(Date); + }); + + it('应该解析自定义配置生成的ID', () => { + const customSnowflake = new Snowflake({ + workerId: 10, + datacenterId: 20, + epoch: 1609459200000, + }); + + const id = customSnowflake.nextId(); + const parsed = Snowflake.parseId(id); + + expect(parsed.workerId).toBe(10); + expect(parsed.datacenterId).toBe(20); + }); + + it('应该正确计算创建时间', () => { + const id = snowflake.nextId(); + const parsed = Snowflake.parseId(id); + const now = new Date(); + + // 创建时间应该在合理范围内(前后1秒) + const timeDiff = Math.abs(parsed.createdAt.getTime() - now.getTime()); + expect(timeDiff).toBeLessThan(1000); + }); + }); + + describe('Singleton Pattern', () => { + it('应该返回相同的单例实例', () => { + const instance1 = getSnowflake(); + const instance2 = getSnowflake(); + + expect(instance1).toBe(instance2); + }); + + it('应该使用默认配置创建单例', () => { + const instance = getSnowflake(); + const config = instance.getConfig(); + + expect(config.workerId).toBe(1); + expect(config.datacenterId).toBe(1); + }); + + it('应该使用自定义配置创建单例', () => { + const customInstance = getSnowflake({ workerId: 5, datacenterId: 5 }); + const config = customInstance.getConfig(); + + expect(config.workerId).toBe(5); + expect(config.datacenterId).toBe(5); + }); + }); + + describe('Utility Functions', () => { + it('nextId函数应该生成ID', () => { + const id = nextId(); + expect(typeof id).toBe('bigint'); + expect(id).toBeGreaterThan(0n); + }); + + it('parseId函数应该解析ID', () => { + const id = nextId(); + const parsed = parseId(id); + + expect(parsed).toHaveProperty('timestamp'); + expect(parsed).toHaveProperty('datacenterId'); + expect(parsed).toHaveProperty('workerId'); + expect(parsed).toHaveProperty('sequence'); + expect(parsed).toHaveProperty('createdAt'); + }); + + it('createSnowflake函数应该创建新实例', () => { + const instance1 = createSnowflake({ workerId: 1, datacenterId: 1 }); + const instance2 = createSnowflake({ workerId: 2, datacenterId: 2 }); + + expect(instance1).not.toBe(instance2); + + const config1 = instance1.getConfig(); + const config2 = instance2.getConfig(); + + expect(config1.workerId).toBe(1); + expect(config2.workerId).toBe(2); + }); + }); + + describe('Performance Tests', () => { + it('应该能够快速生成大量ID', () => { + const startTime = Date.now(); + const ids: bigint[] = []; + + // 生成1000个ID + for (let i = 0; i < 1000; i++) { + ids.push(snowflake.nextId()); + } + + const endTime = Date.now(); + const duration = endTime - startTime; + + expect(ids.length).toBe(1000); + expect(duration).toBeLessThan(100); // 应该在100ms内完成 + + // 验证所有ID都是唯一的 + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(1000); + }); + + it('应该能够处理序列号溢出', () => { + // 模拟快速生成ID,触发序列号溢出 + const originalDateNow = Date.now; + Date.now = () => 1609459200000; + + const ids: bigint[] = []; + for (let i = 0; i < 5000; i++) { + ids.push(snowflake.nextId()); + } + + // 验证所有ID都是唯一的 + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(5000); + + // 恢复原始函数 + Date.now = originalDateNow; + }); + }); + + describe('Edge Cases', () => { + it('应该处理最大配置值', () => { + const maxSnowflake = new Snowflake({ + workerId: 31, + datacenterId: 31, + }); + + const id = maxSnowflake.nextId(); + const parsed = Snowflake.parseId(id); + + expect(parsed.workerId).toBe(31); + expect(parsed.datacenterId).toBe(31); + }); + + it('应该处理最小配置值', () => { + const minSnowflake = new Snowflake({ + workerId: 0, + datacenterId: 0, + }); + + const id = minSnowflake.nextId(); + const parsed = Snowflake.parseId(id); + + expect(parsed.workerId).toBe(0); + expect(parsed.datacenterId).toBe(0); + }); + + it('应该处理自定义epoch', () => { + const customEpoch = Date.now(); + const customSnowflake = new Snowflake({ + workerId: 1, + datacenterId: 1, + epoch: customEpoch, + }); + + const id = customSnowflake.nextId(); + const parsed = Snowflake.parseId(id); + + // 由于parseId使用默认epoch,时间戳会有差异 + expect(parsed.createdAt.getTime()).toBeLessThan(Date.now()); + }); + }); +}); \ No newline at end of file diff --git a/src/utils/snowflake.ts b/src/utils/snowflake.ts new file mode 100644 index 0000000..c67503a --- /dev/null +++ b/src/utils/snowflake.ts @@ -0,0 +1,245 @@ +/** + * @file 雪花ID生成器 + * @author AI助手 + * @date 2024-12-30 + * @lastEditor AI助手 + * @lastEditTime 2024-12-30 + * @description 基于Twitter雪花算法的分布式ID生成器,支持自定义workerId和datacenterId + */ + +/** + * 雪花ID生成器配置接口 + * @property {number} workerId - 工作机器ID (0-31) + * @property {number} datacenterId - 数据中心ID (0-31) + * @property {number} sequence - 序列号起始值 + * @property {number} epoch - 起始时间戳 (毫秒) + */ +export interface SnowflakeConfig { + /** 工作机器ID,范围0-31 */ + workerId: number; + /** 数据中心ID,范围0-31 */ + datacenterId: number; + /** 序列号起始值,默认0 */ + sequence?: number; + /** 起始时间戳,默认2020-01-01 00:00:00 UTC */ + epoch?: number; +} + +/** + * 雪花ID生成器类 + * 生成64位长整型ID,格式:时间戳(41位) + 数据中心ID(5位) + 工作机器ID(5位) + 序列号(12位) + */ +export class Snowflake { + /** 工作机器ID位数 */ + private static readonly WORKER_ID_BITS = 5; + /** 数据中心ID位数 */ + private static readonly DATACENTER_ID_BITS = 5; + /** 序列号位数 */ + private static readonly SEQUENCE_BITS = 12; + + /** 最大工作机器ID */ + private static readonly MAX_WORKER_ID = (1 << Snowflake.WORKER_ID_BITS) - 1; + /** 最大数据中心ID */ + private static readonly MAX_DATACENTER_ID = (1 << Snowflake.DATACENTER_ID_BITS) - 1; + /** 最大序列号 */ + private static readonly MAX_SEQUENCE = (1 << Snowflake.SEQUENCE_BITS) - 1; + + /** 工作机器ID左移位数 */ + private static readonly WORKER_ID_SHIFT = Snowflake.SEQUENCE_BITS; + /** 数据中心ID左移位数 */ + private static readonly DATACENTER_ID_SHIFT = Snowflake.SEQUENCE_BITS + Snowflake.WORKER_ID_BITS; + /** 时间戳左移位数 */ + private static readonly TIMESTAMP_LEFT_SHIFT = Snowflake.SEQUENCE_BITS + Snowflake.WORKER_ID_BITS + Snowflake.DATACENTER_ID_BITS; + + /** 工作机器ID */ + private readonly workerId: number; + /** 数据中心ID */ + private readonly datacenterId: number; + /** 起始时间戳 */ + private readonly epoch: number; + /** 当前序列号 */ + private sequence: number; + /** 上次生成ID的时间戳 */ + private lastTimestamp: number; + + /** + * 构造函数 + * @param config 雪花ID配置 + * @throws {Error} 当workerId或datacenterId超出范围时抛出错误 + */ + constructor(config: SnowflakeConfig) { + // 验证workerId范围 + if (config.workerId < 0 || config.workerId > Snowflake.MAX_WORKER_ID) { + throw new Error(`Worker ID must be between 0 and ${Snowflake.MAX_WORKER_ID}`); + } + + // 验证datacenterId范围 + if (config.datacenterId < 0 || config.datacenterId > Snowflake.MAX_DATACENTER_ID) { + throw new Error(`Datacenter ID must be between 0 and ${Snowflake.MAX_DATACENTER_ID}`); + } + + this.workerId = config.workerId; + this.datacenterId = config.datacenterId; + this.sequence = config.sequence || 0; + this.epoch = config.epoch || 1577836800000; // 2020-01-01 00:00:00 UTC + this.lastTimestamp = -1; + } + + /** + * 生成下一个雪花ID + * @returns {bigint} 64位雪花ID + * @throws {Error} 当系统时钟回拨时抛出错误 + */ + public nextId(): bigint { + let timestamp = this.getCurrentTimestamp(); + + // 检查时钟回拨 + if (timestamp < this.lastTimestamp) { + const timeDiff = this.lastTimestamp - timestamp; + throw new Error(`Clock moved backwards. Refusing to generate id for ${timeDiff} milliseconds`); + } + + // 如果是同一毫秒内,递增序列号 + if (timestamp === this.lastTimestamp) { + this.sequence = (this.sequence + 1) & Snowflake.MAX_SEQUENCE; + + // 如果序列号溢出,等待下一毫秒 + if (this.sequence === 0) { + timestamp = this.waitNextMillis(this.lastTimestamp); + } + } else { + // 不同毫秒,重置序列号 + this.sequence = 0; + } + + this.lastTimestamp = timestamp; + + // 生成雪花ID + const id = ((BigInt(timestamp - this.epoch) << BigInt(Snowflake.TIMESTAMP_LEFT_SHIFT)) | + (BigInt(this.datacenterId) << BigInt(Snowflake.DATACENTER_ID_SHIFT)) | + (BigInt(this.workerId) << BigInt(Snowflake.WORKER_ID_SHIFT)) | + BigInt(this.sequence)); + + return id; + } + + /** + * 解析雪花ID,返回各个组成部分 + * @param id 雪花ID + * @returns {object} 解析结果 + */ + public static parseId(id: bigint): { + timestamp: number; + datacenterId: number; + workerId: number; + sequence: number; + createdAt: Date; + } { + const timestamp = Number((id >> BigInt(Snowflake.TIMESTAMP_LEFT_SHIFT)) & BigInt((1 << 41) - 1)); + const datacenterId = Number((id >> BigInt(Snowflake.DATACENTER_ID_SHIFT)) & BigInt(Snowflake.MAX_DATACENTER_ID)); + const workerId = Number((id >> BigInt(Snowflake.WORKER_ID_SHIFT)) & BigInt(Snowflake.MAX_WORKER_ID)); + const sequence = Number(id & BigInt(Snowflake.MAX_SEQUENCE)); + + // 计算创建时间(使用默认epoch) + const epoch = 1577836800000; // 2020-01-01 00:00:00 UTC + const createdAt = new Date(epoch + timestamp); + + return { + timestamp, + datacenterId, + workerId, + sequence, + createdAt, + }; + } + + /** + * 获取当前时间戳(毫秒) + * @returns {number} 当前时间戳 + */ + private getCurrentTimestamp(): number { + return Date.now(); + } + + /** + * 等待下一毫秒 + * @param lastTimestamp 上次时间戳 + * @returns {number} 新的时间戳 + */ + private waitNextMillis(lastTimestamp: number): number { + let timestamp = this.getCurrentTimestamp(); + while (timestamp <= lastTimestamp) { + timestamp = this.getCurrentTimestamp(); + } + return timestamp; + } + + /** + * 获取配置信息 + * @returns {object} 当前配置 + */ + public getConfig(): { + workerId: number; + datacenterId: number; + sequence: number; + epoch: number; + lastTimestamp: number; + } { + return { + workerId: this.workerId, + datacenterId: this.datacenterId, + sequence: this.sequence, + epoch: this.epoch, + lastTimestamp: this.lastTimestamp, + }; + } +} + +/** + * 雪花ID生成器单例实例 + * 使用默认配置:workerId=1, datacenterId=1 + */ +let snowflakeInstance: Snowflake | null = null; + +/** + * 获取雪花ID生成器单例实例 + * @param config 可选配置,如果不提供则使用默认配置 + * @returns {Snowflake} 雪花ID生成器实例 + */ +export function getSnowflake(config?: Partial): Snowflake { + if (!snowflakeInstance) { + const defaultConfig: SnowflakeConfig = { + workerId: 1, + datacenterId: 1, + ...config, + }; + snowflakeInstance = new Snowflake(defaultConfig); + } + return snowflakeInstance; +} + +/** + * 生成下一个雪花ID(便捷方法) + * @returns {bigint} 64位雪花ID + */ +export function nextId(): bigint { + return getSnowflake().nextId(); +} + +/** + * 解析雪花ID(便捷方法) + * @param id 雪花ID + * @returns {object} 解析结果 + */ +export function parseId(id: bigint) { + return Snowflake.parseId(id); +} + +/** + * 创建自定义配置的雪花ID生成器 + * @param config 雪花ID配置 + * @returns {Snowflake} 新的雪花ID生成器实例 + */ +export function createSnowflake(config: SnowflakeConfig): Snowflake { + return new Snowflake(config); +} \ No newline at end of file diff --git a/tasks/M2-基础用户系统-开发任务计划.md b/tasks/M2-基础用户系统-开发任务计划.md index 6a07e5e..9b9f13e 100644 --- a/tasks/M2-基础用户系统-开发任务计划.md +++ b/tasks/M2-基础用户系统-开发任务计划.md @@ -1,517 +1,350 @@ # M2 - 基础用户系统 - 开发任务计划 -## 📋 任务概览 - -基于接口设计文档和开发PRD,本计划将M2基础用户系统开发分为4个阶段,共70+个详细子任务,预计4周完成。 - -### 🎯 总体目标 -- 完成70+个详细开发子任务 -- 建立完整的RBAC权限体系 -- 实现安全的用户认证系统 -- 构建可扩展的系统基础架构 - -### 📊 任务统计 -- **总任务数**:70+ 个子任务 -- **P0核心任务**:35个 (必须完成) -- **P1重要任务**:25个 (优先完成) -- **P2优化任务**:10个 (时间允许时完成) - -## 📅 第一阶段:数据库设计与基础服务 (第1周,5天) - -### 🗄️ 数据库表结构设计 (第1-2天) -**预估工时**:16小时 - -#### 核心任务: -- [x] **创建用户表 sys_users** `(4h)` - - 设计字段、索引、约束 - - 支持软删除、乐观锁 - -- [x] **创建角色表 sys_roles** `(3h)` - - 支持树形结构、权限快照 - -- [x] **创建权限表 sys_permissions** `(3h)` - - 支持层级权限、资源权限 - -- [x] **创建关联表** `(3h)` - - user_roles、role_permissions、user_organizations等 - -- [x] **创建系统表** `(2h)` - - 字典表、标签表、日志表 - -- [x] **编写数据库迁移脚本和基础数据** `(1h)` - -### 🔧 基础服务配置 (第3天) -**预估工时**:8小时 - -#### 核心任务: -- [x] **完善数据库连接配置** `(2h)` - - 连接池、事务管理 - -- [x] **优化Redis配置** `(2h)` - - 缓存策略、session管理 - -- [x] **完善JWT配置** `(2h)` - - 密钥管理、过期时间、RefreshToken - -- [x] **集成邮件发送服务** `(1h)` - - SMTP配置、邮件模板 - -- [x] **集成验证码服务** `(1h)` - - [x] 图形验证码生成、验证 - -### 🔐 用户注册功能 (第4天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现注册参数验证** `(1h)` - - 用户名格式、邮箱格式、密码强度 - -- [ ] **实现唯一性检查** `(1h)` - - 用户名唯一、邮箱唯一 - -- [ ] **实现密码加密** `(1h)` - - bcrypt加密、盐值生成 - -- [ ] **实现激活机制** `(2h)` - - 生成激活令牌、Redis存储 - -- [ ] **实现注册邮件发送** `(2h)` - - 激活邮件模板、异步发送 - -- [ ] **编写注册功能测试** `(1h)` - -### 🎫 用户激活与登录功能 (第5天) -**预估工时**:8小时 - -#### 激活功能子任务: -- [ ] **实现激活令牌验证** `(1h)` - - Redis查询、过期检查 - -- [ ] **实现用户状态更新** `(1h)` - - 激活状态、数据库更新 - -- [ ] **发送欢迎邮件** `(1h)` - - 欢迎邮件模板、用户引导 - -- [ ] **编写激活功能测试** `(1h)` - -#### 登录功能子任务: -- [ ] **实现多种登录方式** `(1h)` - - 用户名登录、邮箱登录 - -- [ ] **实现密码验证** `(1h)` - - bcrypt验证、用户状态检查 - -- [ ] **实现JWT生成** `(1h)` - - AccessToken、RefreshToken生成 - -- [ ] **实现登录失败限制** `(1h)` - - 失败次数统计、账号锁定 - -## 📅 第二阶段:用户管理与安全功能 (第2周,5天) - -### 🛡️ JWT认证中间件 (第6天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现JWT令牌验证** `(2h)` - - 签名验证、过期检查 - -- [ ] **实现用户信息提取** `(2h)` - - 从Token解析用户ID、角色 - -- [ ] **实现Token黑名单** `(2h)` - - Redis黑名单、退出登录处理 - -- [ ] **开发JWT中间件** `(2h)` - - 请求拦截、权限注入 - -### 🔄 Token管理功能 (第7天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现RefreshToken验证** `(2h)` - - Redis验证、用户状态检查 - -- [ ] **实现新Token生成** `(2h)` - - AccessToken刷新、RefreshToken轮转 - -- [ ] **实现Token撤销** `(2h)` - - 加入黑名单、清除RefreshToken - -- [ ] **实现缓存清除** `(2h)` - - 用户缓存、权限缓存清理 - -### 👤 用户信息管理 (第8天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现基本信息查询** `(2h)` - - 用户基本信息、状态信息 - -- [ ] **实现角色信息查询** `(2h)` - - 用户角色、权限信息 - -- [ ] **实现组织信息查询** `(1h)` - - 用户组织、职位信息 - -- [ ] **实现用户信息缓存** `(2h)` - - Redis缓存、缓存更新策略 - -- [ ] **编写用户信息API测试** `(1h)` - -### 📋 用户列表与CRUD (第9天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现分页查询** `(2h)` - - 页码参数、分页算法、数据返回格式 - -- [ ] **实现多条件筛选** `(2h)` - - 状态筛选、角色筛选、时间范围 - -- [ ] **实现关键词搜索** `(1h)` - - 用户名、邮箱、手机号、昵称搜索 - -- [ ] **实现排序功能** `(1h)` - - 创建时间、最后登录、用户名排序 - -- [ ] **实现用户CRUD操作** `(2h)` - - 创建用户、更新信息、软删除 - -### 🔒 密码管理功能 (第10天) -**预估工时**:8小时 - -#### 密码修改子任务: -- [ ] **实现原密码验证** `(1h)` - - 当前密码检查、权限验证 - -- [ ] **实现新密码规则** `(1h)` - - 密码强度、确认密码检查 - -- [ ] **实现密码更新** `(1h)` - - bcrypt加密、数据库更新 - -- [ ] **实现密码修改后处理** `(1h)` - - 清除所有Token、发送通知 - -#### 密码重置子任务: -- [ ] **实现重置申请邮箱验证** `(1h)` - - 邮箱存在性、用户状态检查 - -- [ ] **实现验证码验证** `(1h)` - - 图形验证码检查、防刷机制 - -- [ ] **实现重置令牌生成** `(1h)` - - 6位数字码、Redis存储、过期时间 - -- [ ] **实现重置邮件发送** `(1h)` - - 邮件模板、发送频率限制 - -## 📅 第三阶段:角色权限系统 (第3周,5天) - -### 🏷️ 角色管理功能 (第11-12天) -**预估工时**:16小时 - -#### 角色基础功能: -- [ ] **实现角色树形结构** `(3h)` - - 父子关系、层级路径、深度计算 - -- [ ] **实现角色基本操作** `(3h)` - - 创建、更新、删除、查询 - -- [ ] **实现角色权限分配** `(4h)` - - 权限选择、继承检查 - -- [ ] **实现权限快照** `(3h)` - - JSON存储、快照更新策略 - -- [ ] **实现权限继承** `(3h)` - - 上下级权限合并、继承规则 - -### 🔐 权限验证中间件 (第13-14天) -**预估工时**:16小时 - -#### 权限验证核心: -- [ ] **实现认证检查** `(2h)` - - JWT验证、用户状态检查 - -- [ ] **实现RBAC权限检查** `(4h)` - - 角色权限查询、权限匹配 - -- [ ] **实现权限缓存** `(3h)` - - Redis缓存用户权限、缓存刷新 - -- [ ] **实现数据权限过滤** `(4h)` - - 基于角色的数据范围控制 - -- [ ] **实现用户角色分配** `(3h)` - - 角色添加、移除、有效期设置 - -### 🧪 权限系统测试 (第15天) -**预估工时**:8小时 - -#### 测试任务: -- [ ] **权限验证准确性测试** `(3h)` -- [ ] **角色继承测试** `(2h)` -- [ ] **权限缓存测试** `(2h)` -- [ ] **性能压力测试** `(1h)` - -## 📅 第四阶段:系统完善与优化 (第4周,5天) - -### 🏢 组织架构模块 (第16天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现组织树形结构** `(2h)` - - 父子关系、层级路径、深度管理 - -- [ ] **实现组织CRUD** `(2h)` - - 创建、更新、删除、查询组织 - -- [ ] **实现用户组织关系** `(2h)` - - 用户加入、离开、职位设置 - -- [ ] **实现组织权限** `(2h)` - - 组织级数据权限、层级权限 - -### 📚 字典标签系统 (第17天) -**预估工时**:8小时 - -#### 字典管理: -- [ ] **实现字典类型管理** `(2h)` - - 字典类型CRUD、系统字典保护 - -- [ ] **实现字典项管理** `(2h)` - - 字典项CRUD、排序、状态管理 - -- [ ] **实现字典缓存策略** `(2h)` - - Redis缓存、懒加载、缓存刷新 - -- [ ] **实现字典前端API** `(2h)` - - 字典数据接口、国际化支持 - -### 🏷️ 标签系统 (第18天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现标签CRUD** `(2h)` - - 标签创建、更新、删除、查询 - -- [ ] **实现标签分类** `(2h)` - - 标签类型、颜色管理、分组显示 - -- [ ] **实现标签使用统计** `(2h)` - - 使用次数统计、热门标签排行 - -- [ ] **实现标签推荐** `(2h)` - - 基于使用频率的标签推荐算法 - -### 📋 日志审计系统 (第19天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **实现操作日志中间件** `(2h)` - - 自动记录用户操作、请求参数 - -- [ ] **实现操作日志查询** `(2h)` - - 日志列表、筛选、搜索功能 - -- [ ] **实现日志统计分析** `(2h)` - - 操作频率、用户行为分析 - -- [ ] **实现安全监控** `(2h)` - - 敏感操作监控、异常行为告警 - -### 🚀 性能优化与部署 (第20天) -**预估工时**:8小时 - -#### 详细子任务: -- [ ] **数据库性能优化** `(2h)` - - 索引优化、查询优化、慢查询分析 - -- [ ] **缓存策略优化** `(2h)` - - 缓存命中率优化、缓存更新策略 - -- [ ] **API响应优化** `(2h)` - - 响应时间优化、并发处理优化 - -- [ ] **部署准备** `(2h)` - - 生产环境配置、监控配置、文档完善 - -## 📊 详细任务优先级分类 - -### 🔴 P0 - 核心功能 (必须完成,35个任务) -**数据库基础**: -- 用户表、角色表、权限表、关联表创建 -- 基础服务配置 - -**认证核心**: -- 用户注册、激活、登录 -- JWT认证中间件 -- Token管理 - -**用户管理**: -- 用户信息管理、列表查询 -- 基本CRUD操作 - -**权限控制**: -- 角色管理、权限分配 -- 权限验证中间件 - -### 🟡 P1 - 重要功能 (优先完成,25个任务) -**安全功能**: -- 密码重置、验证码系统 -- 登录失败限制 - -**角色权限**: -- 权限继承、权限缓存 -- 用户角色管理 - -**系统管理**: -- 组织架构、字典管理 -- 标签系统 - -### 🟢 P2 - 优化功能 (时间允许时完成,10个任务) -**增强功能**: -- 操作日志、安全监控 -- 性能优化 -- 高级搜索功能 - -## 🔗 任务依赖关系图 - -```mermaid -graph TD - A[数据库表设计] --> B[基础服务配置] - B --> C[用户注册功能] - B --> D[验证码系统] - C --> E[用户激活功能] - D --> F[用户登录功能] - F --> G[JWT认证中间件] - G --> H[Token管理] - G --> I[用户信息管理] - I --> J[用户列表查询] - J --> K[角色管理] - K --> L[权限验证中间件] - L --> M[组织管理] - M --> N[字典标签系统] - N --> O[日志审计] - O --> P[性能优化] -``` - -## 📈 详细质量标准 - -### 代码质量指标 -- [ ] **TypeScript严格模式**:无any类型,严格类型检查 -- [ ] **ESLint检查**:0警告,代码风格统一 -- [ ] **单元测试覆盖率**:> 80%,核心业务逻辑100% -- [ ] **集成测试**:完整业务流程端到端测试 - -### 性能质量指标 -- [ ] **API响应时间**:P95 < 200ms,P99 < 500ms -- [ ] **数据库查询**:无N+1问题,慢查询 < 100ms -- [ ] **Redis缓存**:命中率 > 90%,内存使用合理 -- [ ] **并发支持**:1000+并发用户,无性能退化 - -### 安全质量指标 -- [ ] **密码安全**:bcrypt成本因子12,强密码策略 -- [ ] **JWT安全**:安全密钥,合理过期时间 -- [ ] **输入验证**:所有输入严格校验,防止注入攻击 -- [ ] **权限控制**:RBAC模型正确实现,无权限绕过 - -## 🚨 详细风险控制 - -### 技术风险及应对 -| 风险项 | 风险等级 | 影响 | 应对策略 | -|--------|----------|------|----------| -| 数据库性能瓶颈 | 中 | 高 | 提前压力测试,索引优化,读写分离准备 | -| JWT安全性问题 | 低 | 高 | RefreshToken轮转,黑名单机制,安全审计 | -| 缓存一致性问题 | 中 | 中 | 设计缓存更新策略,版本控制,监控机制 | -| 权限设计复杂度 | 中 | 中 | 分阶段实现,充分测试,文档完善 | - -### 进度风险及应对 -| 风险项 | 风险等级 | 影响 | 应对策略 | -|--------|----------|------|----------| -| 需求变更 | 中 | 中 | 敏捷开发,版本控制,需求冻结期 | -| 技术难点 | 低 | 中 | 技术预研,备选方案,专家咨询 | -| 测试时间不足 | 中 | 高 | 并行开发测试,自动化测试,持续集成 | -| 第三方服务依赖 | 低 | 中 | 本地Mock,降级方案,多供应商备选 | - -## 📋 阶段性验收标准 - -### 第一阶段验收 (第1周末) -**功能验收**: -- [ ] 用户注册流程完整可用 -- [ ] 邮箱激活功能正常 -- [ ] 用户登录功能稳定 -- [ ] 基础数据库表结构完善 - -**质量验收**: -- [ ] 单元测试覆盖率 > 70% -- [ ] 接口响应时间 < 300ms -- [ ] 无明显安全漏洞 - -### 第二阶段验收 (第2周末) -**功能验收**: -- [ ] 用户管理功能完整 -- [ ] 密码管理功能可用 -- [ ] JWT认证机制稳定 -- [ ] 基础权限控制实现 - -**质量验收**: -- [ ] 单元测试覆盖率 > 75% -- [ ] 集成测试覆盖主要流程 -- [ ] 接口文档完整准确 - -### 第三阶段验收 (第3周末) -**功能验收**: -- [ ] 完整RBAC权限体系 -- [ ] 角色管理功能完善 -- [ ] 权限验证准确无误 -- [ ] 数据权限控制有效 - -**质量验收**: -- [ ] 单元测试覆盖率 > 80% -- [ ] 权限测试100%通过 -- [ ] 性能测试达标 - -### 第四阶段验收 (第4周末) -**功能验收**: -- [ ] 系统功能完整 -- [ ] 组织架构可用 -- [ ] 字典标签系统完善 -- [ ] 日志审计功能正常 - -**质量验收**: -- [ ] 所有测试用例通过 -- [ ] 生产环境部署成功 -- [ ] 监控告警正常 -- [ ] 文档完整可用 - -## 📅 每日工作计划建议 - -### 典型工作日安排 (8小时) -- **09:00-10:30**:核心开发任务 (1.5h) -- **10:30-10:45**:休息 -- **10:45-12:00**:核心开发任务 (1.25h) -- **12:00-13:00**:午餐休息 -- **13:00-14:30**:核心开发任务 (1.5h) -- **14:30-14:45**:休息 -- **14:45-16:15**:开发任务/代码审查 (1.5h) -- **16:15-16:30**:休息 -- **16:30-17:30**:测试编写/文档更新 (1h) -- **17:30-18:00**:每日总结/明日规划 (0.5h) - -### 关键节点提醒 -- **每日**:更新TODO进度,提交代码 -- **每周五**:阶段性总结,风险评估 -- **里程碑**:功能验收,质量检查 -- **每2周**:技术债务清理,重构优化 - ---- - -**预计总工作量**:160工时 (4周 × 40工时/周) -**项目优先级**:P0 (最高优先级) -**团队规模建议**:1-2人 -**技术难度评级**:中等偏上 -**成功概率评估**:85% (基于当前技术栈和时间安排) \ No newline at end of file +## 相关文件 (Relevant Files) + +### 认证模块 (Auth) +- `src/modules/auth/auth.schema.ts` - 认证模块Schema定义 +- `src/modules/auth/auth.response.ts` - 认证模块响应格式定义 +- `src/modules/auth/auth.service.ts` - 认证模块Service层实现 +- `src/modules/auth/auth.controller.ts` - 认证模块Controller层实现 +- `src/modules/auth/auth.test.ts` - 认证模块测试用例 + +### 用户管理模块 (User) +- `src/modules/user/user.schema.ts` - 用户模块Schema定义(已存在) +- `src/modules/user/user.response.ts` - 用户模块响应格式定义(已存在) +- `src/modules/user/user.service.ts` - 用户模块Service层实现(已存在) +- `src/modules/user/user.controller.ts` - 用户模块Controller层实现(需更新) +- `src/modules/user/user.test.ts` - 用户模块测试用例 + +### 角色权限模块 (Role) +- `src/modules/role/role.schema.ts` - 角色模块Schema定义 +- `src/modules/role/role.response.ts` - 角色模块响应格式定义 +- `src/modules/role/role.service.ts` - 角色模块Service层实现 +- `src/modules/role/role.controller.ts` - 角色模块Controller层实现 +- `src/modules/role/role.test.ts` - 角色模块测试用例 + +### 权限管理模块 (Permission) +- `src/modules/permission/permission.schema.ts` - 权限模块Schema定义 +- `src/modules/permission/permission.response.ts` - 权限模块响应格式定义 +- `src/modules/permission/permission.service.ts` - 权限模块Service层实现 +- `src/modules/permission/permission.controller.ts` - 权限模块Controller层实现 +- `src/modules/permission/permission.test.ts` - 权限模块测试用例 + +### 组织架构模块 (Organization) +- `src/modules/organization/organization.schema.ts` - 组织模块Schema定义 +- `src/modules/organization/organization.response.ts` - 组织模块响应格式定义 +- `src/modules/organization/organization.service.ts` - 组织模块Service层实现 +- `src/modules/organization/organization.controller.ts` - 组织模块Controller层实现 +- `src/modules/organization/organization.test.ts` - 组织模块测试用例 + +### 系统基础模块 (System) +- `src/modules/system/dict/dict.schema.ts` - 字典模块Schema定义 +- `src/modules/system/dict/dict.response.ts` - 字典模块响应格式定义 +- `src/modules/system/dict/dict.service.ts` - 字典模块Service层实现 +- `src/modules/system/dict/dict.controller.ts` - 字典模块Controller层实现 +- `src/modules/system/dict/dict.test.ts` - 字典模块测试用例 +- `src/modules/system/tag/tag.schema.ts` - 标签模块Schema定义 +- `src/modules/system/tag/tag.response.ts` - 标签模块响应格式定义 +- `src/modules/system/tag/tag.service.ts` - 标签模块Service层实现 +- `src/modules/system/tag/tag.controller.ts` - 标签模块Controller层实现 +- `src/modules/system/tag/tag.test.ts` - 标签模块测试用例 +- `src/modules/system/log/log.schema.ts` - 日志模块Schema定义 +- `src/modules/system/log/log.response.ts` - 日志模块响应格式定义 +- `src/modules/system/log/log.service.ts` - 日志模块Service层实现 +- `src/modules/system/log/log.controller.ts` - 日志模块Controller层实现 +- `src/modules/system/log/log.test.ts` - 日志模块测试用例 + +### 备注 (Notes) +- 验证码功能已有captcha模块可直接集成 +- 遵循Elysia开发规范,每个接口都要有完整的5个文件 +- 按照PRD优先级:P0 > P1 > P2 顺序开发 + +## 任务 (Tasks) + +### 🔐 认证模块 (Auth Module) - P0优先级 + +- [x] 1.0 POST /auth/register - 用户注册接口 + - [x] 1.1 创建auth.schema.ts - 定义用户注册Schema + - [x] 1.2 创建auth.response.ts - 定义注册响应格式 + - [x] 1.3 创建auth.service.ts - 实现注册业务逻辑 + - [x] 1.4 创建auth.controller.ts - 实现注册路由 + - [x] 1.5 创建auth.test.ts - 编写注册测试用例 + +- [ ] 2.0 POST /auth/activate - 邮箱激活接口 + - [ ] 2.1 扩展auth.schema.ts - 定义激活Schema + - [ ] 2.2 扩展auth.response.ts - 定义激活响应格式 + - [ ] 2.3 扩展auth.service.ts - 实现激活业务逻辑 + - [ ] 2.4 扩展auth.controller.ts - 实现激活路由 + - [ ] 2.5 扩展auth.test.ts - 编写激活测试用例 + +- [ ] 3.0 POST /auth/login - 用户登录接口 + - [ ] 3.1 扩展auth.schema.ts - 定义登录Schema + - [ ] 3.2 扩展auth.response.ts - 定义登录响应格式 + - [ ] 3.3 扩展auth.service.ts - 实现登录业务逻辑 + - [ ] 3.4 扩展auth.controller.ts - 实现登录路由 + - [ ] 3.5 扩展auth.test.ts - 编写登录测试用例 + +- [ ] 4.0 POST /auth/refresh - Token刷新接口 + - [ ] 4.1 扩展auth.schema.ts - 定义刷新Schema + - [ ] 4.2 扩展auth.response.ts - 定义刷新响应格式 + - [ ] 4.3 扩展auth.service.ts - 实现刷新业务逻辑 + - [ ] 4.4 扩展auth.controller.ts - 实现刷新路由 + - [ ] 4.5 扩展auth.test.ts - 编写刷新测试用例 + +- [ ] 5.0 POST /auth/logout - 退出登录接口 + - [ ] 5.1 扩展auth.schema.ts - 定义退出Schema + - [ ] 5.2 扩展auth.response.ts - 定义退出响应格式 + - [ ] 5.3 扩展auth.service.ts - 实现退出业务逻辑 + - [ ] 5.4 扩展auth.controller.ts - 实现退出路由 + - [ ] 5.5 扩展auth.test.ts - 编写退出测试用例 + +- [ ] 6.0 POST /auth/password/reset-request - 找回密码接口 + - [ ] 6.1 扩展auth.schema.ts - 定义找回密码Schema + - [ ] 6.2 扩展auth.response.ts - 定义找回密码响应格式 + - [ ] 6.3 扩展auth.service.ts - 实现找回密码业务逻辑 + - [ ] 6.4 扩展auth.controller.ts - 实现找回密码路由 + - [ ] 6.5 扩展auth.test.ts - 编写找回密码测试用例 + +- [ ] 7.0 POST /auth/password/reset-confirm - 重置密码接口 + - [ ] 7.1 扩展auth.schema.ts - 定义重置密码Schema + - [ ] 7.2 扩展auth.response.ts - 定义重置密码响应格式 + - [ ] 7.3 扩展auth.service.ts - 实现重置密码业务逻辑 + - [ ] 7.4 扩展auth.controller.ts - 实现重置密码路由 + - [ ] 7.5 扩展auth.test.ts - 编写重置密码测试用例 + +- [ ] 8.0 GET /auth/captcha - 图形验证码接口 + - [ ] 8.1 扩展auth.schema.ts - 定义验证码Schema + - [ ] 8.2 扩展auth.response.ts - 定义验证码响应格式 + - [ ] 8.3 扩展auth.service.ts - 集成验证码服务 + - [ ] 8.4 扩展auth.controller.ts - 实现验证码路由 + - [ ] 8.5 扩展auth.test.ts - 编写验证码测试用例 + +### 👤 用户管理模块 (User Module) - P0优先级 + +- [ ] 9.0 GET /users/me - 获取当前用户信息接口 + - [ ] 9.1 扩展user.schema.ts - 定义当前用户Schema + - [ ] 9.2 扩展user.response.ts - 定义当前用户响应格式 + - [ ] 9.3 扩展user.service.ts - 实现当前用户业务逻辑 + - [ ] 9.4 更新user.controller.ts - 实现当前用户路由 + - [ ] 9.5 创建user.test.ts - 编写当前用户测试用例 + +- [ ] 10.0 GET /users - 用户列表查询接口 + - [ ] 10.1 扩展user.schema.ts - 定义用户列表Schema + - [ ] 10.2 扩展user.response.ts - 定义用户列表响应格式 + - [ ] 10.3 扩展user.service.ts - 实现用户列表业务逻辑 + - [ ] 10.4 扩展user.controller.ts - 实现用户列表路由 + - [ ] 10.5 扩展user.test.ts - 编写用户列表测试用例 + +- [ ] 11.0 POST /users - 创建用户接口 + - [ ] 11.1 扩展user.schema.ts - 定义创建用户Schema + - [ ] 11.2 扩展user.response.ts - 定义创建用户响应格式 + - [ ] 11.3 扩展user.service.ts - 实现创建用户业务逻辑 + - [ ] 11.4 扩展user.controller.ts - 实现创建用户路由 + - [ ] 11.5 扩展user.test.ts - 编写创建用户测试用例 + +- [ ] 12.0 PUT /users/{id} - 更新用户信息接口 + - [ ] 12.1 扩展user.schema.ts - 定义更新用户Schema + - [ ] 12.2 扩展user.response.ts - 定义更新用户响应格式 + - [ ] 12.3 扩展user.service.ts - 实现更新用户业务逻辑 + - [ ] 12.4 扩展user.controller.ts - 实现更新用户路由 + - [ ] 12.5 扩展user.test.ts - 编写更新用户测试用例 + +- [ ] 13.0 DELETE /users/{id} - 删除用户接口 + - [ ] 13.1 扩展user.schema.ts - 定义删除用户Schema + - [ ] 13.2 扩展user.response.ts - 定义删除用户响应格式 + - [ ] 13.3 扩展user.service.ts - 实现删除用户业务逻辑 + - [ ] 13.4 扩展user.controller.ts - 实现删除用户路由 + - [ ] 13.5 扩展user.test.ts - 编写删除用户测试用例 + +- [ ] 14.0 PUT /users/me/password - 修改密码接口 + - [ ] 14.1 扩展user.schema.ts - 定义修改密码Schema + - [ ] 14.2 扩展user.response.ts - 定义修改密码响应格式 + - [ ] 14.3 扩展user.service.ts - 实现修改密码业务逻辑 + - [ ] 14.4 扩展user.controller.ts - 实现修改密码路由 + - [ ] 14.5 扩展user.test.ts - 编写修改密码测试用例 + +- [ ] 15.0 GET /users/{id} - 用户详情接口 + - [ ] 15.1 扩展user.schema.ts - 定义用户详情Schema + - [ ] 15.2 扩展user.response.ts - 定义用户详情响应格式 + - [ ] 15.3 扩展user.service.ts - 实现用户详情业务逻辑 + - [ ] 15.4 扩展user.controller.ts - 实现用户详情路由 + - [ ] 15.5 扩展user.test.ts - 编写用户详情测试用例 + +- [ ] 16.0 POST /users/batch - 批量操作接口 + - [ ] 16.1 扩展user.schema.ts - 定义批量操作Schema + - [ ] 16.2 扩展user.response.ts - 定义批量操作响应格式 + - [ ] 16.3 扩展user.service.ts - 实现批量操作业务逻辑 + - [ ] 16.4 扩展user.controller.ts - 实现批量操作路由 + - [ ] 16.5 扩展user.test.ts - 编写批量操作测试用例 + +### 🎭 角色权限模块 (Role Module) - P0优先级 + +- [ ] 17.0 GET /roles - 角色列表接口 + - [ ] 17.1 创建role.schema.ts - 定义角色Schema + - [ ] 17.2 创建role.response.ts - 定义角色响应格式 + - [ ] 17.3 创建role.service.ts - 实现角色业务逻辑 + - [ ] 17.4 创建role.controller.ts - 实现角色路由 + - [ ] 17.5 创建role.test.ts - 编写角色测试用例 + +- [ ] 18.0 POST /roles - 创建角色接口 + - [ ] 18.1 扩展role.schema.ts - 定义创建角色Schema + - [ ] 18.2 扩展role.response.ts - 定义创建角色响应格式 + - [ ] 18.3 扩展role.service.ts - 实现创建角色业务逻辑 + - [ ] 18.4 扩展role.controller.ts - 实现创建角色路由 + - [ ] 18.5 扩展role.test.ts - 编写创建角色测试用例 + +- [ ] 19.0 PUT /roles/{id} - 更新角色接口 + - [ ] 19.1 扩展role.schema.ts - 定义更新角色Schema + - [ ] 19.2 扩展role.response.ts - 定义更新角色响应格式 + - [ ] 19.3 扩展role.service.ts - 实现更新角色业务逻辑 + - [ ] 19.4 扩展role.controller.ts - 实现更新角色路由 + - [ ] 19.5 扩展role.test.ts - 编写更新角色测试用例 + +- [ ] 20.0 DELETE /roles/{id} - 删除角色接口 + - [ ] 20.1 扩展role.schema.ts - 定义删除角色Schema + - [ ] 20.2 扩展role.response.ts - 定义删除角色响应格式 + - [ ] 20.3 扩展role.service.ts - 实现删除角色业务逻辑 + - [ ] 20.4 扩展role.controller.ts - 实现删除角色路由 + - [ ] 20.5 扩展role.test.ts - 编写删除角色测试用例 + +- [ ] 21.0 GET /permissions - 权限列表接口 + - [ ] 21.1 创建permission.schema.ts - 定义权限Schema + - [ ] 21.2 创建permission.response.ts - 定义权限响应格式 + - [ ] 21.3 创建permission.service.ts - 实现权限业务逻辑 + - [ ] 21.4 创建permission.controller.ts - 实现权限路由 + - [ ] 21.5 创建permission.test.ts - 编写权限测试用例 + +- [ ] 22.0 POST /roles/{id}/permissions - 权限分配接口 + - [ ] 22.1 扩展role.schema.ts - 定义权限分配Schema + - [ ] 22.2 扩展role.response.ts - 定义权限分配响应格式 + - [ ] 22.3 扩展role.service.ts - 实现权限分配业务逻辑 + - [ ] 22.4 扩展role.controller.ts - 实现权限分配路由 + - [ ] 22.5 扩展role.test.ts - 编写权限分配测试用例 + +- [ ] 23.0 POST /users/{id}/roles - 用户角色分配接口 + - [ ] 23.1 扩展user.schema.ts - 定义用户角色分配Schema + - [ ] 23.2 扩展user.response.ts - 定义用户角色分配响应格式 + - [ ] 23.3 扩展user.service.ts - 实现用户角色分配业务逻辑 + - [ ] 23.4 扩展user.controller.ts - 实现用户角色分配路由 + - [ ] 23.5 扩展user.test.ts - 编写用户角色分配测试用例 + +### 🏢 组织架构模块 (Organization Module) - P1优先级 + +- [ ] 24.0 GET /organizations - 组织列表接口 + - [ ] 24.1 创建organization.schema.ts - 定义组织Schema + - [ ] 24.2 创建organization.response.ts - 定义组织响应格式 + - [ ] 24.3 创建organization.service.ts - 实现组织业务逻辑 + - [ ] 24.4 创建organization.controller.ts - 实现组织路由 + - [ ] 24.5 创建organization.test.ts - 编写组织测试用例 + +- [ ] 25.0 POST /organizations - 创建组织接口 + - [ ] 25.1 扩展organization.schema.ts - 定义创建组织Schema + - [ ] 25.2 扩展organization.response.ts - 定义创建组织响应格式 + - [ ] 25.3 扩展organization.service.ts - 实现创建组织业务逻辑 + - [ ] 25.4 扩展organization.controller.ts - 实现创建组织路由 + - [ ] 25.5 扩展organization.test.ts - 编写创建组织测试用例 + +- [ ] 26.0 PUT /organizations/{id} - 更新组织接口 + - [ ] 26.1 扩展organization.schema.ts - 定义更新组织Schema + - [ ] 26.2 扩展organization.response.ts - 定义更新组织响应格式 + - [ ] 26.3 扩展organization.service.ts - 实现更新组织业务逻辑 + - [ ] 26.4 扩展organization.controller.ts - 实现更新组织路由 + - [ ] 26.5 扩展organization.test.ts - 编写更新组织测试用例 + +- [ ] 27.0 DELETE /organizations/{id} - 删除组织接口 + - [ ] 27.1 扩展organization.schema.ts - 定义删除组织Schema + - [ ] 27.2 扩展organization.response.ts - 定义删除组织响应格式 + - [ ] 27.3 扩展organization.service.ts - 实现删除组织业务逻辑 + - [ ] 27.4 扩展organization.controller.ts - 实现删除组织路由 + - [ ] 27.5 扩展organization.test.ts - 编写删除组织测试用例 + +- [ ] 28.0 POST /users/{id}/organizations - 用户组织关系接口 + - [ ] 28.1 扩展user.schema.ts - 定义用户组织关系Schema + - [ ] 28.2 扩展user.response.ts - 定义用户组织关系响应格式 + - [ ] 28.3 扩展user.service.ts - 实现用户组织关系业务逻辑 + - [ ] 28.4 扩展user.controller.ts - 实现用户组织关系路由 + - [ ] 28.5 扩展user.test.ts - 编写用户组织关系测试用例 + +### 🗂️ 系统基础模块 (System Module) - P1优先级 + +- [ ] 29.0 字典类型管理 - CRUD /dict-types + - [ ] 29.1 创建dict.schema.ts - 定义字典类型Schema + - [ ] 29.2 创建dict.response.ts - 定义字典类型响应格式 + - [ ] 29.3 创建dict.service.ts - 实现字典类型业务逻辑 + - [ ] 29.4 创建dict.controller.ts - 实现字典类型路由 + - [ ] 29.5 创建dict.test.ts - 编写字典类型测试用例 + +- [ ] 30.0 字典项管理 - CRUD /dict-items + - [ ] 30.1 扩展dict.schema.ts - 定义字典项Schema + - [ ] 30.2 扩展dict.response.ts - 定义字典项响应格式 + - [ ] 30.3 扩展dict.service.ts - 实现字典项业务逻辑 + - [ ] 30.4 扩展dict.controller.ts - 实现字典项路由 + - [ ] 30.5 扩展dict.test.ts - 编写字典项测试用例 + +- [ ] 31.0 标签管理 - CRUD /tags + - [ ] 31.1 创建tag.schema.ts - 定义标签Schema + - [ ] 31.2 创建tag.response.ts - 定义标签响应格式 + - [ ] 31.3 创建tag.service.ts - 实现标签业务逻辑 + - [ ] 31.4 创建tag.controller.ts - 实现标签路由 + - [ ] 31.5 创建tag.test.ts - 编写标签测试用例 + +- [ ] 32.0 操作日志 - GET /logs/operations + - [ ] 32.1 创建log.schema.ts - 定义操作日志Schema + - [ ] 32.2 创建log.response.ts - 定义操作日志响应格式 + - [ ] 32.3 创建log.service.ts - 实现操作日志业务逻辑 + - [ ] 32.4 创建log.controller.ts - 实现操作日志路由 + - [ ] 32.5 创建log.test.ts - 编写操作日志测试用例 + +- [ ] 33.0 登录日志 - GET /logs/logins + - [ ] 33.1 扩展log.schema.ts - 定义登录日志Schema + - [ ] 33.2 扩展log.response.ts - 定义登录日志响应格式 + - [ ] 33.3 扩展log.service.ts - 实现登录日志业务逻辑 + - [ ] 33.4 扩展log.controller.ts - 实现登录日志路由 + - [ ] 33.5 扩展log.test.ts - 编写登录日志测试用例 + +### 🔧 基础设施完善 + +- [ ] 34.0 JWT认证中间件 + - [ ] 34.1 创建JWT认证插件 + - [ ] 34.2 实现Token黑名单管理 + - [ ] 34.3 实现RefreshToken机制 + - [ ] 34.4 集成权限验证中间件 + - [ ] 34.5 编写认证中间件测试 + +- [ ] 35.0 路由模块集成 + - [ ] 35.1 更新src/modules/index.ts - 集成所有模块 + - [ ] 35.2 更新src/app.ts - 注册所有路由 + - [ ] 35.3 更新Swagger标签定义 + - [ ] 35.4 完善API文档 + - [ ] 35.5 集成测试验证 + +## 开发优先级说明 + +### 第一阶段(P0):基础认证和用户管理 +- **认证模块**:用户注册、激活、登录、刷新、退出(任务1-5) +- **用户管理模块**:当前用户、用户列表、用户CRUD、密码管理(任务9-14) +- **完成目标**:具备基本的用户认证和管理功能 + +### 第二阶段(P0):角色权限系统 +- **角色管理**:角色CRUD、权限分配(任务17-20) +- **权限管理**:权限列表、权限分配、用户角色分配(任务21-23) +- **完成目标**:具备完整的RBAC权限控制体系 + +### 第三阶段(P1):扩展功能 +- **密码管理**:找回密码、重置密码(任务6-7) +- **验证码系统**:图形验证码(任务8) +- **用户扩展**:用户详情、批量操作(任务15-16) +- **组织架构**:组织管理、用户组织关系(任务24-28) + +### 第四阶段(P1-P2):系统完善 +- **系统基础**:字典、标签、日志管理(任务29-33) +- **基础设施**:JWT中间件、路由集成(任务34-35) +- **完成目标**:系统功能完整,可投入生产使用 + +## 备注说明 + +1. **已完成部分**:用户模块的Schema、Response、Service已基本完成,可直接使用 +2. **验证码集成**:现有captcha模块可直接集成到认证流程中 +3. **开发规范**:严格按照Elysia开发规范,每个接口都要有完整的5个子任务 +4. **测试要求**:每个模块都要有完整的测试用例,确保功能正确性 +5. **优先级管理**:按照P0 > P1 > P2的顺序开发,确保核心功能优先完成 \ No newline at end of file