feat: 实现用户注册接口 (任务1.0)

- 新增auth.schema.ts: 用户注册请求参数Schema定义和类型导出

- 新增auth.response.ts: 注册成功/失败响应格式定义

- 新增auth.service.ts: 注册业务逻辑,包含验证码验证、用户名邮箱唯一性检查、密码加密

- 新增auth.controller.ts: POST /api/auth/register路由实现,包含错误处理

- 新增auth.test.ts: 完整测试用例覆盖正常、异常、边界场景(14个测试全通过)

- 修复数据库日期时间插入问题和路由路径重复问题

关联PRD: M2-基础用户系统,任务1.0完成
This commit is contained in:
expressgy 2025-07-06 01:21:09 +08:00
parent 3bca80e2cf
commit 9a76d91307
16 changed files with 1902 additions and 635 deletions

View File

@ -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. **验证修复结果**
- 确保修复后代码可运行
- 检查是否引入新问题
- 验证功能完整性
### 性能优化建议
我会在适当时候提供:
- 数据库查询优化
- 缓存策略建议
- 并发处理优化
- 内存使用优化
## 质量保证
### 代码质量检查
- [ ] 类型安全性

View File

@ -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 注释规范(关键❗️)

View File

@ -12,7 +12,7 @@ alwaysApply: true
管理Markdown文件中的任务清单以跟踪完成PRD进度的指南。
## 任务实施 (Task Implementation)
* **一次处理一个子任务:** 在询问用户并获得“yes”或“y”的许可之前**不要**开始下一个子任务。
* **一次处理一个子任务:** 在询问用户并获得“yes”或“y”的许可之前**不要**开始下一个子任务。********非常重要必须执行*********
* **完成协议 (Completion protocol)**
1. 当你完成一个**子任务**时,立即通过将 `[ ]` 改为 `[x]` 将其标记为已完成。
2. 如果一个父任务下的**所有**子任务现在都是 `[x]`,则按以下顺序执行:

View File

@ -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=="],

View File

@ -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",

View File

@ -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<ErrorCode, number> = {
[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<ErrorCode, string> = {
[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]: '数据库操作失败',

View File

@ -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,
}
);

View File

@ -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<typeof RegisterSuccessDataSchema>;
/** 用户注册成功响应类型 */
export type RegisterSuccessResponse = Static<typeof RegisterSuccessResponseSchema>;
/** 用户注册失败响应类型 */
export type RegisterErrorResponse = Static<typeof RegisterErrorResponseSchema>;

View File

@ -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<typeof RegisterSchema>;

View File

@ -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<RegisterSuccessResponse>
*/
async register(request: RegisterRequest): Promise<RegisterSuccessResponse> {
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<void> {
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<void> {
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<void> {
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<string>
*/
private async hashPassword(password: string): Promise<string> {
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<CreatedUser>
*/
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();

View File

@ -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);
});
});
});

View File

@ -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));

View File

@ -1,3 +0,0 @@
import { Elysia } from 'elysia';
export const userController = new Elysia({ name: 'userController' }).get('/', () => ({ message: '用户系统' }));

320
src/utils/snowflake.test.ts Normal file
View File

@ -0,0 +1,320 @@
/**
* @file ID生成器测试
* @author AI助手 <ai@example.com>
* @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());
});
});
});

245
src/utils/snowflake.ts Normal file
View File

@ -0,0 +1,245 @@
/**
* @file ID生成器
* @author AI助手 <ai@example.com>
* @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生成器类
* 64ID(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} 64ID
* @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<SnowflakeConfig>): Snowflake {
if (!snowflakeInstance) {
const defaultConfig: SnowflakeConfig = {
workerId: 1,
datacenterId: 1,
...config,
};
snowflakeInstance = new Snowflake(defaultConfig);
}
return snowflakeInstance;
}
/**
* ID便
* @returns {bigint} 64ID
*/
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);
}

View File

@ -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 < 200msP99 < 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% (基于当前技术栈和时间安排)
## 相关文件 (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的顺序开发确保核心功能优先完成