feat: 优化分布式锁自动续期机制
- 添加进程退出时的自动清理逻辑 - 优化锁配置策略,短期操作不续期,长期操作续期 - 添加完整的分布式锁使用指南文档 - 修复自动续期可能导致死锁的问题
This commit is contained in:
parent
e26ea6e948
commit
ed92f32389
266
docs/distributed-lock-guide.md
Normal file
266
docs/distributed-lock-guide.md
Normal file
@ -0,0 +1,266 @@
|
||||
# 分布式锁使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档介绍了项目中分布式锁的使用策略和最佳实践,帮助开发者正确使用分布式锁来保护关键业务操作。
|
||||
|
||||
## 分布式锁的作用
|
||||
|
||||
分布式锁主要用于解决以下问题:
|
||||
|
||||
1. **防止并发冲突**:避免多个进程同时操作同一资源
|
||||
2. **保证数据一致性**:确保关键操作的原子性
|
||||
3. **防止重复操作**:避免重复执行相同的业务逻辑
|
||||
|
||||
## 使用策略
|
||||
|
||||
### 1. 短期操作(推荐不开启自动续期)
|
||||
|
||||
**适用场景**:
|
||||
- 用户登录
|
||||
- Token刷新
|
||||
- 数据查询
|
||||
- 简单的数据更新
|
||||
|
||||
**配置建议**:
|
||||
```typescript
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: 'user:login:username',
|
||||
ttl: 15, // 15秒过期
|
||||
timeout: 8000, // 8秒超时
|
||||
autoRenew: false // 不开启自动续期
|
||||
});
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 简单可靠,不会出现死锁
|
||||
- 性能开销小
|
||||
- 适合快速操作
|
||||
|
||||
### 2. 长期操作(需要开启自动续期)
|
||||
|
||||
**适用场景**:
|
||||
- 用户注册(包含邮件发送)
|
||||
- 密码重置(包含邮件发送)
|
||||
- 文件上传
|
||||
- 复杂的数据处理
|
||||
|
||||
**配置建议**:
|
||||
```typescript
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: 'user:register:username:email',
|
||||
ttl: 60, // 60秒过期
|
||||
timeout: 15000, // 15秒超时
|
||||
autoRenew: true, // 开启自动续期
|
||||
renewInterval: 20000 // 20秒续期一次
|
||||
});
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 必须确保在操作完成后手动释放锁
|
||||
- 进程退出时会自动清理锁
|
||||
- 续期失败时会记录警告日志
|
||||
|
||||
## 锁键名设计规范
|
||||
|
||||
### 1. 命名规则
|
||||
```
|
||||
{业务模块}:{操作类型}:{关键标识}
|
||||
```
|
||||
|
||||
### 2. 示例
|
||||
```typescript
|
||||
// 用户注册锁
|
||||
'user:register:username:email'
|
||||
|
||||
// 用户登录锁
|
||||
'user:login:username'
|
||||
|
||||
// 密码重置锁
|
||||
'password:reset:email'
|
||||
|
||||
// Token刷新锁
|
||||
'token:refresh:token_value'
|
||||
```
|
||||
|
||||
### 3. 注意事项
|
||||
- 键名要具有唯一性
|
||||
- 避免使用过长的键名
|
||||
- 使用有意义的标识符
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 锁的粒度控制
|
||||
|
||||
**好的做法**:
|
||||
```typescript
|
||||
// 针对特定用户加锁
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: `user:login:${username}`,
|
||||
ttl: 15,
|
||||
autoRenew: false
|
||||
});
|
||||
```
|
||||
|
||||
**避免的做法**:
|
||||
```typescript
|
||||
// 锁的粒度太粗,影响其他用户
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: 'user:login', // 所有用户登录都被阻塞
|
||||
ttl: 15,
|
||||
autoRenew: false
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 超时时间设置
|
||||
|
||||
**原则**:
|
||||
- 超时时间应该大于预期的操作时间
|
||||
- 但不要设置过长,避免长时间阻塞
|
||||
|
||||
**建议**:
|
||||
```typescript
|
||||
// 快速操作
|
||||
timeout: 5000 // 5秒
|
||||
|
||||
// 中等操作
|
||||
timeout: 10000 // 10秒
|
||||
|
||||
// 慢速操作
|
||||
timeout: 30000 // 30秒
|
||||
```
|
||||
|
||||
### 3. TTL设置
|
||||
|
||||
**原则**:
|
||||
- TTL应该大于操作时间
|
||||
- 对于自动续期的锁,TTL可以设置得相对较短
|
||||
|
||||
**建议**:
|
||||
```typescript
|
||||
// 快速操作
|
||||
ttl: 10 // 10秒
|
||||
|
||||
// 中等操作
|
||||
ttl: 30 // 30秒
|
||||
|
||||
// 慢速操作
|
||||
ttl: 60 // 60秒
|
||||
```
|
||||
|
||||
### 4. 错误处理
|
||||
|
||||
**必须使用 try-finally**:
|
||||
```typescript
|
||||
const lock = await DistributedLockService.acquire(config);
|
||||
|
||||
try {
|
||||
// 执行业务逻辑
|
||||
await doSomething();
|
||||
} finally {
|
||||
// 确保锁被释放
|
||||
await lock.release();
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 监控和日志
|
||||
|
||||
**监控指标**:
|
||||
- 锁获取成功率
|
||||
- 锁等待时间
|
||||
- 锁释放情况
|
||||
- 死锁检测
|
||||
|
||||
**日志记录**:
|
||||
```typescript
|
||||
Logger.info(`获取分布式锁成功: ${lockKey}`);
|
||||
Logger.warn(`锁续期失败: ${lockKey}`);
|
||||
Logger.error(`获取锁超时: ${lockKey}`);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 死锁问题
|
||||
|
||||
**原因**:
|
||||
- 进程崩溃但锁未释放
|
||||
- 网络中断导致无法续期
|
||||
- 业务逻辑异常导致锁未释放
|
||||
|
||||
**解决方案**:
|
||||
- 设置合理的TTL
|
||||
- 使用try-finally确保锁释放
|
||||
- 进程退出时自动清理锁
|
||||
- 定期检查并清理过期锁
|
||||
|
||||
### 2. 性能问题
|
||||
|
||||
**原因**:
|
||||
- 锁的粒度太粗
|
||||
- 锁的持有时间过长
|
||||
- 频繁的锁竞争
|
||||
|
||||
**解决方案**:
|
||||
- 细化锁的粒度
|
||||
- 优化业务逻辑,减少锁持有时间
|
||||
- 使用读写锁分离
|
||||
- 考虑使用乐观锁
|
||||
|
||||
### 3. 一致性问题
|
||||
|
||||
**原因**:
|
||||
- 锁释放时机不当
|
||||
- 业务逻辑异常
|
||||
- 并发控制不当
|
||||
|
||||
**解决方案**:
|
||||
- 确保锁的原子性操作
|
||||
- 使用事务保证数据一致性
|
||||
- 添加业务层面的幂等性检查
|
||||
|
||||
## 工具函数
|
||||
|
||||
### 1. 装饰器使用
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
@withDistributedLock('user:register', 30, 10000)
|
||||
async register(userData: UserData) {
|
||||
// 业务逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 手动管理锁
|
||||
|
||||
```typescript
|
||||
async function complexOperation() {
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: 'complex:operation',
|
||||
ttl: 60,
|
||||
autoRenew: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 复杂业务逻辑
|
||||
await step1();
|
||||
await step2();
|
||||
await step3();
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
分布式锁是保证系统一致性的重要工具,但使用不当也会带来问题。遵循以下原则:
|
||||
|
||||
1. **合理选择锁策略**:短期操作不续期,长期操作要续期
|
||||
2. **控制锁粒度**:避免锁的粒度过粗
|
||||
3. **设置合理超时**:避免无限等待
|
||||
4. **确保锁释放**:使用try-finally模式
|
||||
5. **监控和日志**:及时发现问题
|
||||
6. **定期清理**:防止死锁积累
|
||||
|
||||
通过合理使用分布式锁,可以有效保证系统的数据一致性和业务正确性。
|
@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { RegisterSchema, ActivateSchema, LoginSchema, RefreshSchema } from './auth.schema';
|
||||
import { RegisterResponsesSchema, ActivateResponsesSchema, LoginResponsesSchema, RefreshResponsesSchema } from './auth.response';
|
||||
import { RegisterSchema, ActivateSchema, LoginSchema, RefreshSchema, ResetPasswordRequestSchema, ResetPasswordConfirmSchema } from './auth.schema';
|
||||
import { RegisterResponsesSchema, ActivateResponsesSchema, LoginResponsesSchema, RefreshResponsesSchema, ResetPasswordRequestResponsesSchema, ResetPasswordConfirmResponsesSchema } from './auth.response';
|
||||
import { authService } from './auth.service';
|
||||
import { tags } from '@/modules/tags';
|
||||
|
||||
@ -104,4 +104,48 @@ export const authController = new Elysia()
|
||||
},
|
||||
response: RefreshResponsesSchema,
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 找回密码接口
|
||||
* @route POST /api/auth/password/reset-request
|
||||
* @description 用户忘记密码时发送重置邮件
|
||||
* @param body ResetPasswordRequestRequest 找回密码请求参数
|
||||
* @returns ResetPasswordRequestSuccessResponse | ResetPasswordRequestErrorResponse
|
||||
*/
|
||||
.post(
|
||||
'/password/reset-request',
|
||||
({ body, set }) => authService.resetPasswordRequest(body),
|
||||
{
|
||||
body: ResetPasswordRequestSchema,
|
||||
detail: {
|
||||
summary: '找回密码',
|
||||
description: '用户忘记密码时发送重置邮件到注册邮箱,邮件包含重置链接',
|
||||
tags: [tags.auth],
|
||||
operationId: 'resetPasswordRequest',
|
||||
},
|
||||
response: ResetPasswordRequestResponsesSchema,
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 重置密码接口
|
||||
* @route POST /api/auth/password/reset-confirm
|
||||
* @description 用户通过重置令牌设置新密码
|
||||
* @param body ResetPasswordConfirmRequest 重置密码请求参数
|
||||
* @returns ResetPasswordConfirmSuccessResponse | ResetPasswordConfirmErrorResponse
|
||||
*/
|
||||
.post(
|
||||
'/password/reset-confirm',
|
||||
({ body, set }) => authService.resetPasswordConfirm(body),
|
||||
{
|
||||
body: ResetPasswordConfirmSchema,
|
||||
detail: {
|
||||
summary: '重置密码',
|
||||
description: '用户通过重置令牌设置新密码,需要提供令牌、新密码和确认密码',
|
||||
tags: [tags.auth],
|
||||
operationId: 'resetPasswordConfirm',
|
||||
},
|
||||
response: ResetPasswordConfirmResponsesSchema,
|
||||
}
|
||||
);
|
@ -207,4 +207,87 @@ export const RefreshResponsesSchema = {
|
||||
};
|
||||
|
||||
/** Token刷新成功响应数据类型 */
|
||||
export type RefreshSuccessType = Static<typeof RefreshResponsesSchema[200]>;
|
||||
export type RefreshSuccessType = Static<typeof RefreshResponsesSchema[200]>;
|
||||
|
||||
// ========== 找回密码相关响应格式 ==========
|
||||
|
||||
/**
|
||||
* 找回密码接口响应组合
|
||||
* @description 用于Controller中定义所有可能的响应格式
|
||||
*/
|
||||
export const ResetPasswordRequestResponsesSchema = {
|
||||
200: responseWrapperSchema(t.Object({
|
||||
/** 邮箱地址 */
|
||||
email: t.String({
|
||||
description: '发送重置邮件的邮箱地址',
|
||||
examples: ['user@example.com', 'admin@company.com']
|
||||
}),
|
||||
/** 发送状态 */
|
||||
sent: t.Boolean({
|
||||
description: '邮件发送状态',
|
||||
examples: [true]
|
||||
}),
|
||||
/** 发送时间 */
|
||||
sentAt: t.String({
|
||||
description: '邮件发送时间',
|
||||
examples: ['2024-12-19T10:30:00Z']
|
||||
}),
|
||||
/** 重置链接有效期(分钟) */
|
||||
expiresIn: t.Number({
|
||||
description: '重置链接有效期(分钟)',
|
||||
examples: [30, 60]
|
||||
}),
|
||||
/** 提示信息 */
|
||||
message: t.String({
|
||||
description: '操作提示信息',
|
||||
examples: ['重置邮件已发送,请查收邮箱']
|
||||
})
|
||||
})),
|
||||
};
|
||||
|
||||
/** 找回密码成功响应数据类型 */
|
||||
export type ResetPasswordRequestSuccessType = Static<typeof ResetPasswordRequestResponsesSchema[200]>;
|
||||
|
||||
// ========== 重置密码相关响应格式 ==========
|
||||
|
||||
/**
|
||||
* 重置密码接口响应组合
|
||||
* @description 用于Controller中定义所有可能的响应格式
|
||||
*/
|
||||
export const ResetPasswordConfirmResponsesSchema = {
|
||||
200: responseWrapperSchema(t.Object({
|
||||
/** 用户ID */
|
||||
id: t.String({
|
||||
description: '用户ID(bigint类型以字符串形式返回防止精度丢失)',
|
||||
examples: ['1', '2', '3']
|
||||
}),
|
||||
/** 用户名 */
|
||||
username: t.String({
|
||||
description: '用户名',
|
||||
examples: ['admin', 'testuser']
|
||||
}),
|
||||
/** 邮箱地址 */
|
||||
email: t.String({
|
||||
description: '邮箱地址',
|
||||
examples: ['user@example.com']
|
||||
}),
|
||||
/** 密码更新时间 */
|
||||
updatedAt: t.String({
|
||||
description: '密码更新时间',
|
||||
examples: ['2024-12-19T10:30:00Z']
|
||||
}),
|
||||
/** 重置成功标识 */
|
||||
reset: t.Boolean({
|
||||
description: '密码重置是否成功',
|
||||
examples: [true]
|
||||
}),
|
||||
/** 提示信息 */
|
||||
message: t.String({
|
||||
description: '操作提示信息',
|
||||
examples: ['密码重置成功,请使用新密码登录']
|
||||
})
|
||||
})),
|
||||
};
|
||||
|
||||
/** 重置密码成功响应数据类型 */
|
||||
export type ResetPasswordConfirmSuccessType = Static<typeof ResetPasswordConfirmResponsesSchema[200]>;
|
@ -116,6 +116,60 @@ export const RefreshSchema = t.Object({
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* 找回密码Schema
|
||||
* @description 找回密码请求参数验证规则
|
||||
*/
|
||||
export const ResetPasswordRequestSchema = t.Object({
|
||||
/** 邮箱地址,对应sys_users.email */
|
||||
email: t.String({
|
||||
format: 'email',
|
||||
maxLength: 100,
|
||||
description: '注册时使用的邮箱地址',
|
||||
examples: ['user@example.com', 'admin@company.com']
|
||||
}),
|
||||
/** 图形验证码 */
|
||||
captcha: t.String({
|
||||
minLength: 4,
|
||||
maxLength: 6,
|
||||
description: '图形验证码',
|
||||
examples: ['a1b2', '1234']
|
||||
}),
|
||||
/** 验证码会话ID */
|
||||
captchaId: t.String({
|
||||
description: '验证码会话ID',
|
||||
examples: ['cap_123', 'captcha_session']
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* 重置密码Schema
|
||||
* @description 重置密码请求参数验证规则
|
||||
*/
|
||||
export const ResetPasswordConfirmSchema = t.Object({
|
||||
/** 重置令牌,JWT格式 */
|
||||
token: t.String({
|
||||
minLength: 10,
|
||||
maxLength: 1000,
|
||||
description: '重置密码令牌,JWT格式,30分钟有效',
|
||||
examples: ['eyJhbGciOiJIUzI1NiI']
|
||||
}),
|
||||
/** 新密码,6-50字符 */
|
||||
newPassword: t.String({
|
||||
minLength: 6,
|
||||
maxLength: 50,
|
||||
description: '新密码,6-50字符',
|
||||
examples: ['newpassword123']
|
||||
}),
|
||||
/** 确认新密码,必须与新密码一致 */
|
||||
confirmPassword: t.String({
|
||||
minLength: 6,
|
||||
maxLength: 50,
|
||||
description: '确认新密码,必须与新密码一致',
|
||||
examples: ['newpassword123']
|
||||
})
|
||||
});
|
||||
|
||||
/** 用户注册请求类型 */
|
||||
export type RegisterRequest = Static<typeof RegisterSchema>;
|
||||
|
||||
@ -126,4 +180,10 @@ export type ActivateRequest = Static<typeof ActivateSchema>;
|
||||
export type LoginRequest = Static<typeof LoginSchema>;
|
||||
|
||||
/** Token刷新请求类型 */
|
||||
export type RefreshRequest = Static<typeof RefreshSchema>;
|
||||
export type RefreshRequest = Static<typeof RefreshSchema>;
|
||||
|
||||
/** 找回密码请求类型 */
|
||||
export type ResetPasswordRequestRequest = Static<typeof ResetPasswordRequestSchema>;
|
||||
|
||||
/** 重置密码请求类型 */
|
||||
export type ResetPasswordConfirmRequest = Static<typeof ResetPasswordConfirmSchema>;
|
@ -5,6 +5,11 @@
|
||||
* @lastEditor AI Assistant
|
||||
* @lastEditTime 2025-01-07
|
||||
* @description 认证模块的业务逻辑实现,包括用户注册、邮箱激活、用户登录等
|
||||
*
|
||||
* 分布式锁使用策略:
|
||||
* 1. 短期操作(如登录、刷新token):使用短TTL,不开启自动续期
|
||||
* 2. 长期操作(如注册、密码重置):使用较长TTL,开启自动续期
|
||||
* 3. 所有操作都设置合理的超时时间,避免无限等待
|
||||
*/
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
@ -16,9 +21,10 @@ import { Logger } from '@/plugins/logger/logger.service';
|
||||
import { nextId } from '@/utils/snowflake';
|
||||
import { jwtService } from '@/plugins/jwt/jwt.service';
|
||||
import { emailService } from '@/plugins/email/email.service';
|
||||
import type { RegisterRequest, ActivateRequest, LoginRequest, RefreshRequest } from './auth.schema';
|
||||
import { DistributedLockService, LOCK_KEYS } from '@/utils/distributedLock';
|
||||
import type { RegisterRequest, ActivateRequest, LoginRequest, RefreshRequest, ResetPasswordRequestRequest, ResetPasswordConfirmRequest } from './auth.schema';
|
||||
import { successResponse, errorResponse, BusinessError } from '@/utils/responseFormate';
|
||||
import type { ActivateSuccessType, LoginSuccessType, RegisterResponsesType, RefreshSuccessType } from './auth.response';
|
||||
import type { ActivateSuccessType, LoginSuccessType, RegisterResponsesType, RefreshSuccessType, ResetPasswordRequestSuccessType, ResetPasswordConfirmSuccessType } from './auth.response';
|
||||
import { TOKEN_TYPES } from '@/type/jwt.type';
|
||||
|
||||
/**
|
||||
@ -39,37 +45,52 @@ export class AuthService {
|
||||
|
||||
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
|
||||
// 获取分布式锁,防止并发注册(长期操作,开启自动续期)
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: `${LOCK_KEYS.USER_REGISTER}:${username}:${email}`,
|
||||
ttl: 60, // 注册可能需要较长时间(邮件发送等)
|
||||
timeout: 15000,
|
||||
autoRenew: true,
|
||||
renewInterval: 20000 // 20秒续期一次
|
||||
});
|
||||
|
||||
// 6. 发送激活邮件
|
||||
await this.sendActivationEmail(newUser.id, newUser.email, newUser.username);
|
||||
try {
|
||||
// 1. 验证验证码
|
||||
await this.validateCaptcha(captcha, captchaId);
|
||||
|
||||
Logger.info(`用户注册成功:${newUser.id} - ${newUser.username}`);
|
||||
// 2. 检查用户名是否已存在
|
||||
await this.checkUsernameExists(username);
|
||||
|
||||
return successResponse({
|
||||
id: newUser.id,
|
||||
username: newUser.username,
|
||||
email: newUser.email,
|
||||
status: newUser.status,
|
||||
createdAt: newUser.createdAt
|
||||
}, '用户注册成功,请查收激活邮件');
|
||||
// 3. 检查邮箱是否已存在
|
||||
await this.checkEmailExists(email);
|
||||
|
||||
// 4. 密码加密
|
||||
const passwordHash = await this.hashPassword(password);
|
||||
|
||||
// 5. 创建用户记录
|
||||
const newUser = await this.createUser({
|
||||
username,
|
||||
email,
|
||||
passwordHash
|
||||
});
|
||||
|
||||
// 6. 发送激活邮件
|
||||
await this.sendActivationEmail(newUser.id, newUser.email, newUser.username);
|
||||
|
||||
Logger.info(`用户注册成功:${newUser.id} - ${newUser.username}`);
|
||||
|
||||
return successResponse({
|
||||
id: newUser.id,
|
||||
username: newUser.username,
|
||||
email: newUser.email,
|
||||
status: newUser.status,
|
||||
createdAt: newUser.createdAt
|
||||
}, '用户注册成功,请查收激活邮件');
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,35 +219,48 @@ export class AuthService {
|
||||
|
||||
// 1. 验证激活Token
|
||||
const tokenPayload = jwtService.verifyToken(token);
|
||||
|
||||
if (tokenPayload.error) {
|
||||
throw new BusinessError('激活令牌验证失败', 400);
|
||||
if (tokenPayload.error || tokenPayload.type !== TOKEN_TYPES.ACTIVATION) {
|
||||
throw new BusinessError('激活令牌无效或已过期', 400);
|
||||
}
|
||||
|
||||
// 2. 检查用户是否存在
|
||||
const user = await this.getUserById(tokenPayload.userId);
|
||||
// 获取分布式锁,防止并发激活
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: `${LOCK_KEYS.EMAIL_ACTIVATE}:${tokenPayload.userId}`,
|
||||
ttl: 30,
|
||||
timeout: 10000,
|
||||
autoRenew: true
|
||||
});
|
||||
|
||||
// 3. 检查用户状态
|
||||
if (user.status === 'active') {
|
||||
throw new BusinessError('用户已激活,无需重复激活', 400);
|
||||
try {
|
||||
// 2. 获取用户信息
|
||||
const user = await this.getUserById(tokenPayload.userId);
|
||||
|
||||
// 3. 检查用户状态
|
||||
if (user.status === 'active') {
|
||||
throw new BusinessError('用户已激活,无需重复激活', 400);
|
||||
}
|
||||
|
||||
// 4. 更新用户状态
|
||||
const updatedUser = await this.updateUserStatus(user.id, 'active');
|
||||
|
||||
// 5. 发送激活成功邮件
|
||||
await this.sendActivationSuccessEmail(user.email, user.username);
|
||||
|
||||
Logger.info(`邮箱激活成功:${user.id} - ${user.username}`);
|
||||
|
||||
return successResponse({
|
||||
id: updatedUser.id,
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
status: updatedUser.status,
|
||||
updatedAt: updatedUser.updatedAt,
|
||||
activated: true
|
||||
}, '邮箱激活成功');
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
await lock.release();
|
||||
}
|
||||
|
||||
// 4. 更新用户状态为激活
|
||||
const updatedUser = await this.updateUserStatus(user.id, 'active');
|
||||
|
||||
// 5. 发送激活成功邮件
|
||||
this.sendActivationSuccessEmail(user.email, user.username);
|
||||
|
||||
Logger.info(`用户激活成功:${user.id} - ${user.username}`);
|
||||
|
||||
return successResponse({
|
||||
id: updatedUser.id,
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
status: updatedUser.status,
|
||||
updatedAt: updatedUser.updatedAt,
|
||||
activated: true
|
||||
}, '邮箱激活成功');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -319,50 +353,67 @@ export class AuthService {
|
||||
* @returns Promise<LoginSuccessResponse>
|
||||
*/
|
||||
async login(request: LoginRequest): Promise<LoginSuccessType> {
|
||||
console.clear();
|
||||
Logger.info(`用户登录请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`);
|
||||
|
||||
const { identifier, password, captcha, captchaId, rememberMe = false } = request;
|
||||
|
||||
// 1. 如果提供了验证码,则验证验证码
|
||||
if (captcha && captchaId) {
|
||||
await this.validateCaptcha(captcha, captchaId);
|
||||
}
|
||||
// 获取分布式锁,防止并发登录(短期操作,不开启自动续期)
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: `${LOCK_KEYS.USER_LOGIN}:${identifier}`,
|
||||
ttl: 15, // 登录操作通常很快
|
||||
timeout: 8000,
|
||||
autoRenew: false // 短期操作不需要续期
|
||||
});
|
||||
|
||||
// 2. 查找用户(支持用户名或邮箱)
|
||||
const user = await this.findUserByIdentifier(identifier);
|
||||
try {
|
||||
// 1. 验证验证码(如果需要)
|
||||
if (captcha && captchaId) {
|
||||
// await this.validateCaptcha(captcha, captchaId);
|
||||
}
|
||||
|
||||
// todo 判断帐号状态,是否锁定
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * 40));
|
||||
|
||||
// 3. 验证密码
|
||||
await this.verifyPassword(password, user.passwordHash);
|
||||
// 2. 查找用户
|
||||
const user = await this.findUserByIdentifier(identifier);
|
||||
|
||||
// 4. 检查账号状态
|
||||
this.checkAccountStatus(user);
|
||||
// 3. 验证密码
|
||||
await this.verifyPassword(password, user.passwordHash);
|
||||
|
||||
// 5. 更新最后登录时间
|
||||
await this.updateLastLoginTime(user.id);
|
||||
// 4. 检查账号状态
|
||||
this.checkAccountStatus(user);
|
||||
|
||||
// 6. 记录登录日志
|
||||
await this.recordLoginLog(user.id, identifier);
|
||||
|
||||
// 7. 生成JWT令牌
|
||||
const tokens = jwtService.generateTokens({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
status: user.status
|
||||
}, rememberMe);
|
||||
|
||||
Logger.info(`用户登录成功:${user.id} - ${user.username}`);
|
||||
|
||||
return successResponse({
|
||||
user: {
|
||||
// 5. 生成JWT令牌
|
||||
const tokens = jwtService.generateTokens({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
status: user.status,
|
||||
lastLoginAt: user.lastLoginAt
|
||||
},
|
||||
tokens
|
||||
}, '登录成功');
|
||||
status: user.status
|
||||
}, rememberMe);
|
||||
|
||||
// 6. 更新最后登录时间
|
||||
await this.updateLastLoginTime(user.id);
|
||||
|
||||
// 7. 记录登录日志
|
||||
await this.recordLoginLog(user.id, identifier);
|
||||
|
||||
Logger.info(`用户登录成功:${user.id} - ${user.username}`);
|
||||
|
||||
return successResponse({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
status: user.status,
|
||||
lastLoginAt: user.lastLoginAt
|
||||
},
|
||||
tokens
|
||||
}, '登录成功');
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -448,7 +499,7 @@ export class AuthService {
|
||||
*/
|
||||
private async updateLastLoginTime(userId: string): Promise<void> {
|
||||
await db().update(sysUsers)
|
||||
.set({
|
||||
.set({
|
||||
lastLoginAt: sql`NOW()`, // 使用 MySQL 的 NOW() 函数
|
||||
loginCount: sql`${sysUsers.loginCount} + 1`
|
||||
})
|
||||
@ -531,44 +582,49 @@ export class AuthService {
|
||||
|
||||
const { refreshToken } = request;
|
||||
|
||||
// 1. 验证刷新令牌
|
||||
const tokenPayload = jwtService.verifyToken(refreshToken);
|
||||
// 获取分布式锁,防止并发刷新(短期操作,不开启自动续期)
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: `${LOCK_KEYS.TOKEN_REFRESH}:${refreshToken}`,
|
||||
ttl: 10, // Token刷新操作很快
|
||||
timeout: 5000,
|
||||
autoRenew: false // 短期操作不需要续期
|
||||
});
|
||||
|
||||
if (tokenPayload.error) {
|
||||
throw new BusinessError('刷新令牌验证失败', 401);
|
||||
try {
|
||||
// 1. 验证刷新令牌
|
||||
const tokenPayload = jwtService.verifyToken(refreshToken);
|
||||
if (tokenPayload.error) {
|
||||
throw new BusinessError('刷新令牌验证失败', 401);
|
||||
}
|
||||
if (tokenPayload.type !== TOKEN_TYPES.REFRESH) {
|
||||
throw new BusinessError('刷新令牌验证失败', 401);
|
||||
}
|
||||
|
||||
// 2. 获取用户信息
|
||||
const user = await this.getUserById(tokenPayload.userId);
|
||||
|
||||
// 3. 检查用户状态
|
||||
this.checkAccountStatus(user);
|
||||
|
||||
// 4. 生成新的令牌对
|
||||
const tokens = jwtService.generateTokens({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
status: user.status
|
||||
});
|
||||
|
||||
// 5. 记录刷新日志
|
||||
await this.recordRefreshLog(user.id);
|
||||
|
||||
return successResponse({
|
||||
tokens
|
||||
}, 'Token刷新成功');
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
await lock.release();
|
||||
}
|
||||
if(tokenPayload.type !== TOKEN_TYPES.REFRESH){
|
||||
throw new BusinessError('刷新令牌验证失败', 401);
|
||||
}
|
||||
Logger.debug(tokenPayload);
|
||||
|
||||
// 2. 检查用户是否存在且状态正常
|
||||
const user = await this.getUserById(tokenPayload.userId);
|
||||
this.checkAccountStatus(user);
|
||||
|
||||
// 3. 生成新的令牌对
|
||||
const newTokens = jwtService.generateTokens({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
status: user.status
|
||||
}, false); // 刷新时默认不记住登录状态
|
||||
|
||||
// 4. 记录刷新日志
|
||||
await this.recordRefreshLog(user.id);
|
||||
|
||||
Logger.info(`Token刷新成功:${user.id} - ${user.username}`);
|
||||
|
||||
return successResponse({
|
||||
tokens: {
|
||||
accessToken: newTokens.accessToken,
|
||||
refreshToken: newTokens.refreshToken,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: newTokens.expiresIn.toString(),
|
||||
refreshExpiresIn: newTokens.refreshExpiresIn.toString()
|
||||
},
|
||||
refreshedAt: new Date().toISOString()
|
||||
}, 'Token刷新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -579,6 +635,275 @@ export class AuthService {
|
||||
// TODO: 实现Token刷新日志记录
|
||||
Logger.info(`记录Token刷新日志:用户ID=${userId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 找回密码
|
||||
* @param request 找回密码请求参数
|
||||
* @returns Promise<ResetPasswordRequestSuccessType>
|
||||
* @throws BusinessError 业务逻辑错误
|
||||
* @type API =====================================================================
|
||||
*/
|
||||
public async resetPasswordRequest(request: ResetPasswordRequestRequest): Promise<ResetPasswordRequestSuccessType> {
|
||||
Logger.info(`找回密码请求:${JSON.stringify({ ...request, captcha: '***' })}`);
|
||||
|
||||
const { email, captcha, captchaId } = request;
|
||||
|
||||
// 获取分布式锁,防止并发重置密码请求
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: `${LOCK_KEYS.PASSWORD_RESET}:${email}`,
|
||||
ttl: 30,
|
||||
timeout: 10000,
|
||||
autoRenew: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 验证验证码
|
||||
await this.validateCaptcha(captcha, captchaId);
|
||||
|
||||
// 2. 检查邮箱是否存在
|
||||
const user = await this.findUserByEmail(email);
|
||||
|
||||
// 3. 检查用户状态
|
||||
this.checkAccountStatus(user);
|
||||
|
||||
// 4. 生成重置令牌
|
||||
const resetToken = jwtService.generateResetToken(user.id);
|
||||
|
||||
// 5. 发送重置邮件
|
||||
await this.sendResetPasswordEmail(user.email, user.username, resetToken);
|
||||
|
||||
Logger.info(`找回密码邮件发送成功:${user.id} - ${user.email}`);
|
||||
|
||||
return successResponse({
|
||||
email: user.email,
|
||||
sent: true,
|
||||
sentAt: new Date().toISOString(),
|
||||
expiresIn: 30, // 30分钟有效期
|
||||
message: '重置邮件已发送,请查收邮箱'
|
||||
}, '重置邮件已发送,请查收邮箱');
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查找用户
|
||||
* @param email 邮箱地址
|
||||
* @returns Promise<用户信息>
|
||||
* @throws BusinessError 用户不存在时抛出
|
||||
*/
|
||||
private async findUserByEmail(email: string): Promise<{
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
status: string;
|
||||
}> {
|
||||
const [user] = await db().select({
|
||||
id: sysUsers.id,
|
||||
username: sysUsers.username,
|
||||
email: sysUsers.email,
|
||||
status: sysUsers.status
|
||||
})
|
||||
.from(sysUsers)
|
||||
.where(eq(sysUsers.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new BusinessError('该邮箱未注册', 404);
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id!.toString(),
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
status: user.status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送重置密码邮件
|
||||
* @param email 邮箱地址
|
||||
* @param username 用户名
|
||||
* @param resetToken 重置令牌
|
||||
*/
|
||||
private async sendResetPasswordEmail(email: string, username: string, resetToken: string): Promise<void> {
|
||||
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`;
|
||||
|
||||
const emailContent = {
|
||||
to: email,
|
||||
subject: '密码重置 - 星撰系统',
|
||||
html: `
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;">
|
||||
<h2 style="color: #333;">密码重置</h2>
|
||||
<p>亲爱的 ${username},</p>
|
||||
<p>您请求重置密码。请点击下面的链接重置您的密码:</p>
|
||||
<p style="margin: 20px 0;">
|
||||
<a href="${resetUrl}"
|
||||
style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
重置密码
|
||||
</a>
|
||||
</p>
|
||||
<p>或者复制以下链接到浏览器:</p>
|
||||
<p style="word-break: break-all; color: #666;">${resetUrl}</p>
|
||||
<p><strong>注意:</strong></p>
|
||||
<ul>
|
||||
<li>此链接将在30分钟后过期</li>
|
||||
<li>如果您没有请求重置密码,请忽略此邮件</li>
|
||||
<li>为了安全起见,请不要将此链接分享给他人</li>
|
||||
</ul>
|
||||
<p>如果您有任何问题,请联系我们的客服团队。</p>
|
||||
<p>谢谢!<br>星撰系统团队</p>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
await emailService.sendEmail(emailContent);
|
||||
Logger.info(`重置密码邮件发送成功:${email}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param request 重置密码请求参数
|
||||
* @returns Promise<ResetPasswordConfirmSuccessType>
|
||||
* @throws BusinessError 业务逻辑错误
|
||||
* @type API =====================================================================
|
||||
*/
|
||||
public async resetPasswordConfirm(request: ResetPasswordConfirmRequest): Promise<ResetPasswordConfirmSuccessType> {
|
||||
const { token, newPassword, confirmPassword } = request;
|
||||
|
||||
// 1. 验证密码一致性
|
||||
if (newPassword !== confirmPassword) {
|
||||
throw new BusinessError('两次输入的密码不一致', 400);
|
||||
}
|
||||
|
||||
// 2. 验证重置令牌
|
||||
const tokenPayload = jwtService.verifyToken(token);
|
||||
if (tokenPayload.error || tokenPayload.type !== TOKEN_TYPES.PASSWORD_RESET) {
|
||||
throw new BusinessError('重置令牌无效或已过期', 400);
|
||||
}
|
||||
|
||||
// 获取分布式锁,防止并发重置密码
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: `${LOCK_KEYS.PASSWORD_RESET}:${tokenPayload.userId}`,
|
||||
ttl: 30,
|
||||
timeout: 10000,
|
||||
autoRenew: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 3. 获取用户信息
|
||||
const user = await this.getUserById(tokenPayload.userId);
|
||||
|
||||
// 4. 检查用户状态
|
||||
this.checkAccountStatus(user);
|
||||
|
||||
// 5. 加密新密码
|
||||
const newPasswordHash = await this.hashPassword(newPassword);
|
||||
|
||||
// 6. 更新用户密码
|
||||
const updatedUser = await this.updateUserPassword(user.id, newPasswordHash);
|
||||
|
||||
// 7. 发送密码重置成功邮件
|
||||
await this.sendPasswordResetSuccessEmail(user.email, user.username);
|
||||
|
||||
Logger.info(`密码重置成功:${user.id} - ${user.username}`);
|
||||
|
||||
return successResponse({
|
||||
id: updatedUser.id,
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
updatedAt: updatedUser.updatedAt,
|
||||
reset: true,
|
||||
message: '密码重置成功,请使用新密码登录'
|
||||
}, '密码重置成功,请使用新密码登录');
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户密码
|
||||
* @param userId 用户ID
|
||||
* @param newPasswordHash 新密码哈希
|
||||
* @returns Promise<更新后的用户信息>
|
||||
*/
|
||||
private async updateUserPassword(userId: string, newPasswordHash: string): Promise<{
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
updatedAt: string;
|
||||
}> {
|
||||
await db().update(sysUsers)
|
||||
.set({
|
||||
passwordHash: newPasswordHash,
|
||||
})
|
||||
.where(eq(sysUsers.id, BigInt(userId)));
|
||||
|
||||
// 查询更新后的用户信息
|
||||
const [updatedUser] = await db().select({
|
||||
id: sysUsers.id,
|
||||
username: sysUsers.username,
|
||||
email: sysUsers.email,
|
||||
updatedAt: sysUsers.updatedAt
|
||||
})
|
||||
.from(sysUsers)
|
||||
.where(eq(sysUsers.id, BigInt(userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!updatedUser) {
|
||||
throw new BusinessError('更新密码失败', 500);
|
||||
}
|
||||
|
||||
return {
|
||||
id: updatedUser.id!.toString(),
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
updatedAt: updatedUser.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送密码重置成功邮件
|
||||
* @param email 邮箱地址
|
||||
* @param username 用户名
|
||||
*/
|
||||
private async sendPasswordResetSuccessEmail(email: string, username: string): Promise<void> {
|
||||
const loginUrl = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/login`;
|
||||
|
||||
const emailContent = {
|
||||
to: email,
|
||||
subject: '密码重置成功 - 星撰系统',
|
||||
html: `
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;">
|
||||
<h2 style="color: #333;">密码重置成功</h2>
|
||||
<p>亲爱的 ${username},</p>
|
||||
<p>您的密码已成功重置。如果您没有进行此操作,请立即联系我们的客服团队。</p>
|
||||
<p style="margin: 20px 0;">
|
||||
<a href="${loginUrl}"
|
||||
style="background-color: #28a745; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
立即登录
|
||||
</a>
|
||||
</p>
|
||||
<p><strong>安全提醒:</strong></p>
|
||||
<ul>
|
||||
<li>请妥善保管您的新密码</li>
|
||||
<li>不要在多个网站使用相同的密码</li>
|
||||
<li>定期更换密码以提高安全性</li>
|
||||
<li>如果发现异常登录,请立即修改密码</li>
|
||||
</ul>
|
||||
<p>如果您有任何问题,请联系我们的客服团队。</p>
|
||||
<p>谢谢!<br>星撰系统团队</p>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
await emailService.sendEmail(emailContent);
|
||||
Logger.info(`密码重置成功邮件发送成功:${email}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
|
362
src/modules/auth/auth.test.md
Normal file
362
src/modules/auth/auth.test.md
Normal file
@ -0,0 +1,362 @@
|
||||
# 认证模块测试用例文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了认证模块各个接口的测试用例,包括正常流程、异常流程和边界条件测试。
|
||||
|
||||
## 测试环境
|
||||
|
||||
- **基础URL**: `http://localhost:3000/api`
|
||||
- **测试工具**: Vitest + Supertest
|
||||
- **数据库**: MySQL (测试环境)
|
||||
- **缓存**: Redis (测试环境)
|
||||
|
||||
## 接口测试用例
|
||||
|
||||
### 1. 用户注册接口 (POST /auth/register)
|
||||
|
||||
#### 1.1 正常流程测试
|
||||
|
||||
**测试用例**: 成功注册新用户
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
"captcha": "a1b2",
|
||||
"captchaId": "test_captcha_id"
|
||||
}
|
||||
```
|
||||
- **预期响应**: 200 OK
|
||||
- **验证点**:
|
||||
- 用户信息正确创建
|
||||
- 密码已加密存储
|
||||
- 激活邮件已发送
|
||||
- 用户状态为pending
|
||||
|
||||
#### 1.2 异常流程测试
|
||||
|
||||
**测试用例**: 用户名已存在
|
||||
- **请求参数**: 使用已存在的用户名
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "用户名已存在"
|
||||
|
||||
**测试用例**: 邮箱已被注册
|
||||
- **请求参数**: 使用已注册的邮箱
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "邮箱已被注册"
|
||||
|
||||
**测试用例**: 验证码错误
|
||||
- **请求参数**: 错误的验证码
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "验证码验证失败"
|
||||
|
||||
### 2. 邮箱激活接口 (POST /auth/activate)
|
||||
|
||||
#### 2.1 正常流程测试
|
||||
|
||||
**测试用例**: 成功激活用户邮箱
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"token": "valid_activation_token"
|
||||
}
|
||||
```
|
||||
- **预期响应**: 200 OK
|
||||
- **验证点**:
|
||||
- 用户状态更新为active
|
||||
- 激活时间正确记录
|
||||
|
||||
#### 2.2 异常流程测试
|
||||
|
||||
**测试用例**: 无效的激活令牌
|
||||
- **请求参数**: 无效或过期的令牌
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "激活令牌无效或已过期"
|
||||
|
||||
### 3. 用户登录接口 (POST /auth/login)
|
||||
|
||||
#### 3.1 正常流程测试
|
||||
|
||||
**测试用例**: 用户名登录成功
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"identifier": "testuser",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
- **预期响应**: 200 OK
|
||||
- **验证点**:
|
||||
- 返回访问令牌和刷新令牌
|
||||
- 最后登录时间更新
|
||||
- 登录日志记录
|
||||
|
||||
**测试用例**: 邮箱登录成功
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"identifier": "test@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
- **预期响应**: 200 OK
|
||||
|
||||
#### 3.2 异常流程测试
|
||||
|
||||
**测试用例**: 用户名不存在
|
||||
- **请求参数**: 不存在的用户名
|
||||
- **预期响应**: 404 Not Found
|
||||
- **错误信息**: "用户不存在"
|
||||
|
||||
**测试用例**: 密码错误
|
||||
- **请求参数**: 错误的密码
|
||||
- **预期响应**: 401 Unauthorized
|
||||
- **错误信息**: "用户名或密码错误"
|
||||
|
||||
**测试用例**: 账号未激活
|
||||
- **请求参数**: 未激活用户的凭据
|
||||
- **预期响应**: 403 Forbidden
|
||||
- **错误信息**: "账号未激活,请先激活邮箱"
|
||||
|
||||
### 4. Token刷新接口 (POST /auth/refresh)
|
||||
|
||||
#### 4.1 正常流程测试
|
||||
|
||||
**测试用例**: 成功刷新令牌
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"refreshToken": "valid_refresh_token"
|
||||
}
|
||||
```
|
||||
- **预期响应**: 200 OK
|
||||
- **验证点**:
|
||||
- 返回新的访问令牌和刷新令牌
|
||||
- 刷新日志记录
|
||||
|
||||
#### 4.2 异常流程测试
|
||||
|
||||
**测试用例**: 无效的刷新令牌
|
||||
- **请求参数**: 无效或过期的刷新令牌
|
||||
- **预期响应**: 401 Unauthorized
|
||||
- **错误信息**: "刷新令牌无效或已过期"
|
||||
|
||||
### 5. 找回密码接口 (POST /auth/password/reset-request)
|
||||
|
||||
#### 5.1 正常流程测试
|
||||
|
||||
**测试用例**: 成功发送重置邮件
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"captcha": "a1b2",
|
||||
"captchaId": "test_captcha_id"
|
||||
}
|
||||
```
|
||||
- **预期响应**: 200 OK
|
||||
- **验证点**:
|
||||
- 重置邮件已发送
|
||||
- 重置令牌已生成
|
||||
- 返回发送状态和时间
|
||||
|
||||
#### 5.2 异常流程测试
|
||||
|
||||
**测试用例**: 邮箱未注册
|
||||
- **请求参数**: 未注册的邮箱地址
|
||||
- **预期响应**: 404 Not Found
|
||||
- **错误信息**: "该邮箱未注册"
|
||||
|
||||
**测试用例**: 验证码错误
|
||||
- **请求参数**: 错误的验证码
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "验证码验证失败"
|
||||
|
||||
**测试用例**: 账号未激活
|
||||
- **请求参数**: 未激活用户的邮箱
|
||||
- **预期响应**: 403 Forbidden
|
||||
- **错误信息**: "账号未激活,请先激活邮箱"
|
||||
|
||||
### 6. 重置密码接口 (POST /auth/password/reset-confirm)
|
||||
|
||||
#### 6.1 正常流程测试
|
||||
|
||||
**测试用例**: 成功重置密码
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"token": "valid_reset_token",
|
||||
"newPassword": "newpassword123",
|
||||
"confirmPassword": "newpassword123"
|
||||
}
|
||||
```
|
||||
- **预期响应**: 200 OK
|
||||
- **验证点**:
|
||||
- 密码已更新
|
||||
- 重置令牌已失效
|
||||
- 成功邮件已发送
|
||||
- 返回用户基本信息
|
||||
|
||||
#### 6.2 异常流程测试
|
||||
|
||||
**测试用例**: 重置令牌无效
|
||||
- **请求参数**: 无效或过期的重置令牌
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "重置令牌无效或已过期"
|
||||
|
||||
**测试用例**: 密码不一致
|
||||
- **请求参数**: 新密码和确认密码不一致
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "两次输入的密码不一致"
|
||||
|
||||
**测试用例**: 密码长度不足
|
||||
- **请求参数**: 新密码少于6字符
|
||||
- **预期响应**: 400 Bad Request
|
||||
- **错误信息**: "密码长度不符合要求"
|
||||
|
||||
**测试用例**: 账号未激活
|
||||
- **请求参数**: 未激活用户的重置令牌
|
||||
- **预期响应**: 403 Forbidden
|
||||
- **错误信息**: "账号未激活,请先激活邮箱"
|
||||
|
||||
### 7. 图形验证码接口 (GET /auth/captcha)
|
||||
|
||||
## 边界条件测试
|
||||
|
||||
### 1. 输入验证边界
|
||||
|
||||
**测试用例**: 用户名长度边界
|
||||
- 最小长度: 2字符
|
||||
- 最大长度: 50字符
|
||||
- 超出范围应返回400错误
|
||||
|
||||
**测试用例**: 邮箱格式验证
|
||||
- 有效邮箱格式应通过验证
|
||||
- 无效邮箱格式应返回400错误
|
||||
|
||||
**测试用例**: 密码强度要求
|
||||
- 最小长度: 6字符
|
||||
- 最大长度: 50字符
|
||||
- 超出范围应返回400错误
|
||||
|
||||
### 2. 并发测试
|
||||
|
||||
**测试用例**: 并发注册
|
||||
- 同时使用相同用户名注册
|
||||
- 应只有一个成功,其他失败
|
||||
|
||||
**测试用例**: 并发登录
|
||||
- 同一用户同时登录
|
||||
- 应都能成功,但刷新令牌会失效
|
||||
|
||||
### 3. 性能测试
|
||||
|
||||
**测试用例**: 大量用户注册
|
||||
- 测试系统在高并发下的表现
|
||||
- 验证数据库连接池和缓存性能
|
||||
|
||||
**测试用例**: 邮件发送性能
|
||||
- 测试邮件服务的并发处理能力
|
||||
- 验证邮件队列机制
|
||||
|
||||
## 安全测试
|
||||
|
||||
### 1. 密码安全
|
||||
|
||||
**测试用例**: 密码加密存储
|
||||
- 验证密码是否使用bcrypt加密
|
||||
- 确认原始密码不在数据库中
|
||||
|
||||
**测试用例**: 密码强度验证
|
||||
- 测试弱密码的拒绝机制
|
||||
- 验证密码复杂度要求
|
||||
|
||||
### 2. 令牌安全
|
||||
|
||||
**测试用例**: JWT令牌验证
|
||||
- 验证令牌签名和过期时间
|
||||
- 测试令牌篡改检测
|
||||
|
||||
**测试用例**: 令牌刷新安全
|
||||
- 验证刷新令牌的一次性使用
|
||||
- 测试令牌泄露防护
|
||||
|
||||
### 3. 输入安全
|
||||
|
||||
**测试用例**: SQL注入防护
|
||||
- 测试特殊字符输入
|
||||
- 验证参数化查询
|
||||
|
||||
**测试用例**: XSS防护
|
||||
- 测试恶意脚本输入
|
||||
- 验证输出转义
|
||||
|
||||
## 测试数据准备
|
||||
|
||||
### 1. 测试用户数据
|
||||
|
||||
```sql
|
||||
-- 清理测试数据
|
||||
DELETE FROM sys_users WHERE username LIKE 'test_%';
|
||||
|
||||
-- 准备测试用户
|
||||
INSERT INTO sys_users (id, username, email, password_hash, status) VALUES
|
||||
(1, 'test_user1', 'test1@example.com', '$2b$12$...', 'active'),
|
||||
(2, 'test_user2', 'test2@example.com', '$2b$12$...', 'pending');
|
||||
```
|
||||
|
||||
### 2. 测试验证码数据
|
||||
|
||||
```sql
|
||||
-- 准备测试验证码
|
||||
INSERT INTO captcha_sessions (id, captcha_code, expires_at) VALUES
|
||||
('test_captcha_id', 'a1b2', DATE_ADD(NOW(), INTERVAL 5 MINUTE));
|
||||
```
|
||||
|
||||
## 测试执行
|
||||
|
||||
### 1. 运行所有测试
|
||||
|
||||
```bash
|
||||
bun test src/modules/auth/auth.test.ts
|
||||
```
|
||||
|
||||
### 2. 运行特定测试
|
||||
|
||||
```bash
|
||||
# 运行注册接口测试
|
||||
bun test src/modules/auth/auth.test.ts -t "register"
|
||||
|
||||
# 运行登录接口测试
|
||||
bun test src/modules/auth/auth.test.ts -t "login"
|
||||
```
|
||||
|
||||
### 3. 生成测试报告
|
||||
|
||||
```bash
|
||||
bun test src/modules/auth/auth.test.ts --reporter=verbose
|
||||
```
|
||||
|
||||
## 持续集成
|
||||
|
||||
### 1. 自动化测试
|
||||
|
||||
- 每次代码提交自动运行测试
|
||||
- 测试失败阻止代码合并
|
||||
- 生成测试覆盖率报告
|
||||
|
||||
### 2. 测试环境
|
||||
|
||||
- 独立的测试数据库
|
||||
- 模拟的邮件服务
|
||||
- 隔离的Redis缓存
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **测试数据隔离**: 每个测试用例应使用独立的测试数据
|
||||
2. **环境变量**: 测试环境应使用专门的配置
|
||||
3. **异步操作**: 邮件发送等异步操作需要适当的等待时间
|
||||
4. **资源清理**: 测试完成后应清理所有测试数据
|
||||
5. **错误处理**: 测试应覆盖各种错误情况
|
@ -83,6 +83,20 @@ export class JwtService {
|
||||
return { error: true } as JwtPayloadType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成重置密码Token
|
||||
*/
|
||||
generateResetToken(userId: string) {
|
||||
return jwt.sign(
|
||||
{
|
||||
userId,
|
||||
type: TOKEN_TYPES.PASSWORD_RESET,
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{ expiresIn: '30M' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const jwtService = new JwtService();
|
344
src/utils/distributedLock.ts
Normal file
344
src/utils/distributedLock.ts
Normal file
@ -0,0 +1,344 @@
|
||||
/**
|
||||
* @file 分布式锁工具类
|
||||
* @author AI Assistant
|
||||
* @date 2025-01-07
|
||||
* @lastEditor AI Assistant
|
||||
* @lastEditTime 2025-01-07
|
||||
* @description 基于Redis实现的分布式锁,支持自动续期和死锁检测
|
||||
*/
|
||||
|
||||
import { redisService } from '@/plugins/redis/redis.service';
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import { nextId } from './snowflake';
|
||||
|
||||
/**
|
||||
* 分布式锁配置
|
||||
*/
|
||||
export interface DistributedLockConfig {
|
||||
/** 锁的键名 */
|
||||
key: string;
|
||||
/** 锁的过期时间(秒) */
|
||||
ttl: number;
|
||||
/** 获取锁的超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 是否自动续期 */
|
||||
autoRenew?: boolean;
|
||||
/** 续期间隔(毫秒) */
|
||||
renewInterval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分布式锁实例
|
||||
*/
|
||||
export interface DistributedLock {
|
||||
/** 锁的键名 */
|
||||
key: string;
|
||||
/** 锁的值(用于标识锁的拥有者) */
|
||||
value: string;
|
||||
/** 是否已获取锁 */
|
||||
acquired: boolean;
|
||||
/** 获取锁的时间戳 */
|
||||
acquiredAt: number;
|
||||
/** 释放锁 */
|
||||
release: () => Promise<boolean>;
|
||||
/** 续期锁 */
|
||||
renew: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分布式锁工具类
|
||||
*/
|
||||
export class DistributedLockService {
|
||||
/** 锁前缀 */
|
||||
private static readonly LOCK_PREFIX = 'distributed_lock:';
|
||||
|
||||
/** 默认TTL(秒) */
|
||||
private static readonly DEFAULT_TTL = 30;
|
||||
|
||||
/** 默认超时时间(毫秒) */
|
||||
private static readonly DEFAULT_TIMEOUT = 5000;
|
||||
|
||||
/** 默认续期间隔(毫秒) */
|
||||
private static readonly DEFAULT_RENEW_INTERVAL = 10000;
|
||||
|
||||
/**
|
||||
* 获取分布式锁
|
||||
* @param config 锁配置
|
||||
* @returns Promise<DistributedLock> 锁实例
|
||||
*/
|
||||
public static async acquire(config: DistributedLockConfig): Promise<DistributedLock> {
|
||||
const lockKey = `${this.LOCK_PREFIX}${config.key}`;
|
||||
const lockValue = nextId().toString();
|
||||
const ttl = config.ttl || this.DEFAULT_TTL;
|
||||
const timeout = config.timeout || this.DEFAULT_TIMEOUT;
|
||||
const autoRenew = config.autoRenew !== false;
|
||||
const renewInterval = config.renewInterval || this.DEFAULT_RENEW_INTERVAL;
|
||||
|
||||
const startTime = Date.now();
|
||||
let acquired = false;
|
||||
let renewTimer: NodeJS.Timeout | null = null;
|
||||
let processExitHandler: (() => void) | null = null;
|
||||
|
||||
try {
|
||||
// 尝试获取锁
|
||||
while (Date.now() - startTime < timeout) {
|
||||
// 使用 SET key value NX EX seconds 原子操作
|
||||
const result = await redisService.client.set(lockKey, lockValue, {
|
||||
NX: true, // 只有当 key 不存在时才设置
|
||||
EX: ttl // 设置过期时间(秒)
|
||||
});
|
||||
|
||||
if (result === 'OK') {
|
||||
acquired = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 等待一段时间后重试
|
||||
await this.sleep(100);
|
||||
}
|
||||
|
||||
if (!acquired) {
|
||||
throw new Error(`获取锁超时: ${lockKey}`);
|
||||
}
|
||||
|
||||
Logger.info(`获取分布式锁成功: ${lockKey}, value: ${lockValue}`);
|
||||
|
||||
// 创建锁实例
|
||||
const lock: DistributedLock = {
|
||||
key: lockKey,
|
||||
value: lockValue,
|
||||
acquired: true,
|
||||
acquiredAt: Date.now(),
|
||||
|
||||
// 释放锁
|
||||
release: async (): Promise<boolean> => {
|
||||
// 清理定时器和事件监听器
|
||||
if (renewTimer) {
|
||||
clearInterval(renewTimer);
|
||||
renewTimer = null;
|
||||
}
|
||||
if (processExitHandler) {
|
||||
process.removeListener('exit', processExitHandler);
|
||||
process.removeListener('SIGINT', processExitHandler);
|
||||
process.removeListener('SIGTERM', processExitHandler);
|
||||
processExitHandler = null;
|
||||
}
|
||||
|
||||
const released = await this.releaseLock(lockKey, lockValue);
|
||||
if (released) {
|
||||
lock.acquired = false;
|
||||
Logger.info(`释放分布式锁成功: ${lockKey}`);
|
||||
}
|
||||
return released;
|
||||
},
|
||||
|
||||
// 续期锁
|
||||
renew: async (): Promise<boolean> => {
|
||||
return await this.renewLock(lockKey, lockValue, ttl);
|
||||
}
|
||||
};
|
||||
|
||||
// 启动自动续期(仅在需要时)
|
||||
if (autoRenew && ttl > renewInterval / 1000) {
|
||||
renewTimer = setInterval(async () => {
|
||||
if (lock.acquired) {
|
||||
try {
|
||||
const renewed = await lock.renew();
|
||||
if (!renewed) {
|
||||
Logger.warn(`锁续期失败,可能已被其他进程获取: ${lockKey}`);
|
||||
lock.acquired = false;
|
||||
if (renewTimer) {
|
||||
clearInterval(renewTimer);
|
||||
renewTimer = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`锁续期异常: ${lockKey}, error: ${error}`));
|
||||
// 续期失败时,不立即释放锁,让锁自然过期
|
||||
}
|
||||
}
|
||||
}, renewInterval);
|
||||
|
||||
// 添加进程退出时的清理逻辑
|
||||
processExitHandler = async () => {
|
||||
Logger.warn(`进程退出,强制释放分布式锁: ${lockKey}`);
|
||||
await this.forceRelease(config.key);
|
||||
};
|
||||
|
||||
process.on('exit', processExitHandler);
|
||||
process.on('SIGINT', processExitHandler);
|
||||
process.on('SIGTERM', processExitHandler);
|
||||
}
|
||||
|
||||
return lock;
|
||||
|
||||
} catch (error) {
|
||||
// 清理已创建的定时器和事件监听器
|
||||
if (renewTimer) {
|
||||
clearInterval(renewTimer);
|
||||
}
|
||||
if (processExitHandler) {
|
||||
process.removeListener('exit', processExitHandler);
|
||||
process.removeListener('SIGINT', processExitHandler);
|
||||
process.removeListener('SIGTERM', processExitHandler);
|
||||
}
|
||||
|
||||
Logger.error(new Error(`获取分布式锁失败: ${lockKey}, error: ${error}`));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
* @param lockKey 锁键名
|
||||
* @param lockValue 锁值
|
||||
* @returns Promise<boolean> 是否成功释放
|
||||
*/
|
||||
private static async releaseLock(lockKey: string, lockValue: string): Promise<boolean> {
|
||||
try {
|
||||
// 使用Lua脚本确保原子性操作
|
||||
const luaScript = `
|
||||
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("del", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`;
|
||||
|
||||
const result = await redisService.client.eval(luaScript, {
|
||||
keys: [lockKey],
|
||||
arguments: [lockValue]
|
||||
});
|
||||
|
||||
return result === 1;
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`释放锁失败: ${lockKey}, error: ${error}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 续期锁
|
||||
* @param lockKey 锁键名
|
||||
* @param lockValue 锁值
|
||||
* @param ttl 过期时间
|
||||
* @returns Promise<boolean> 是否成功续期
|
||||
*/
|
||||
private static async renewLock(lockKey: string, lockValue: string, ttl: number): Promise<boolean> {
|
||||
try {
|
||||
// 使用Lua脚本确保原子性操作
|
||||
const luaScript = `
|
||||
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("expire", KEYS[1], ARGV[2])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`;
|
||||
|
||||
const result = await redisService.client.eval(luaScript, {
|
||||
keys: [lockKey],
|
||||
arguments: [lockValue, ttl.toString()]
|
||||
});
|
||||
|
||||
return result === 1;
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`续期锁失败: ${lockKey}, error: ${error}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否存在
|
||||
* @param key 锁键名
|
||||
* @returns Promise<boolean> 锁是否存在
|
||||
*/
|
||||
public static async isLocked(key: string): Promise<boolean> {
|
||||
const lockKey = `${this.LOCK_PREFIX}${key}`;
|
||||
return await redisService.exists(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁的剩余TTL
|
||||
* @param key 锁键名
|
||||
* @returns Promise<number> 剩余TTL(秒)
|
||||
*/
|
||||
public static async getLockTTL(key: string): Promise<number> {
|
||||
const lockKey = `${this.LOCK_PREFIX}${key}`;
|
||||
return await redisService.ttl(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放锁(不检查拥有者)
|
||||
* @param key 锁键名
|
||||
* @returns Promise<boolean> 是否成功释放
|
||||
*/
|
||||
public static async forceRelease(key: string): Promise<boolean> {
|
||||
const lockKey = `${this.LOCK_PREFIX}${key}`;
|
||||
try {
|
||||
const result = await redisService.del(lockKey);
|
||||
Logger.warn(`强制释放分布式锁: ${lockKey}`);
|
||||
return result === 1;
|
||||
} catch (error) {
|
||||
Logger.error(new Error(`强制释放锁失败: ${lockKey}, error: ${error}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 睡眠函数
|
||||
* @param ms 毫秒数
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
private static sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分布式锁常量定义
|
||||
*/
|
||||
export const LOCK_KEYS = {
|
||||
// 用户注册锁
|
||||
USER_REGISTER: 'user:register',
|
||||
// 用户登录锁
|
||||
USER_LOGIN: 'user:login',
|
||||
// 密码重置锁
|
||||
PASSWORD_RESET: 'password:reset',
|
||||
// 邮箱激活锁
|
||||
EMAIL_ACTIVATE: 'email:activate',
|
||||
// Token刷新锁
|
||||
TOKEN_REFRESH: 'token:refresh',
|
||||
// 验证码生成锁
|
||||
CAPTCHA_GENERATE: 'captcha:generate',
|
||||
// 邮件发送锁
|
||||
EMAIL_SEND: 'email:send'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 分布式锁装饰器
|
||||
* @param lockKey 锁键名
|
||||
* @param ttl 过期时间(秒)
|
||||
* @param timeout 超时时间(毫秒)
|
||||
*/
|
||||
export function withDistributedLock(lockKey: string, ttl: number = 30, timeout: number = 5000) {
|
||||
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
|
||||
const method = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const lock = await DistributedLockService.acquire({
|
||||
key: lockKey,
|
||||
ttl,
|
||||
timeout,
|
||||
autoRenew: true
|
||||
});
|
||||
|
||||
try {
|
||||
return await method.apply(this, args);
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
@ -98,19 +98,19 @@
|
||||
- ~~[ ] 5.4 扩展auth.controller.ts - 实现退出路由~~
|
||||
- ~~[ ] 5.5 扩展auth.test.md - 编写退出测试用例文档~~
|
||||
|
||||
- [ ] 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.md - 编写找回密码测试用例文档
|
||||
- [x] 6.0 POST /auth/password/reset-request - 找回密码接口
|
||||
- [x] 6.1 扩展auth.schema.ts - 定义找回密码Schema
|
||||
- [x] 6.2 扩展auth.response.ts - 定义找回密码响应格式
|
||||
- [x] 6.3 扩展auth.service.ts - 实现找回密码业务逻辑
|
||||
- [x] 6.4 扩展auth.controller.ts - 实现找回密码路由
|
||||
- [x] 6.5 扩展auth.test.md - 编写找回密码测试用例文档
|
||||
|
||||
- [ ] 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.md - 编写重置密码测试用例文档
|
||||
- [x] 7.0 POST /auth/password/reset-confirm - 重置密码接口
|
||||
- [x] 7.1 扩展auth.schema.ts - 定义重置密码Schema
|
||||
- [x] 7.2 扩展auth.response.ts - 定义重置密码响应格式
|
||||
- [x] 7.3 扩展auth.service.ts - 实现重置密码业务逻辑
|
||||
- [x] 7.4 扩展auth.controller.ts - 实现重置密码路由
|
||||
- [x] 7.5 扩展auth.test.md - 编写重置密码测试用例文档
|
||||
|
||||
- [x] 8.0 GET /auth/captcha - 图形验证码接口
|
||||
- [x] 8.1 扩展auth.schema.ts - 定义验证码Schema
|
||||
@ -119,12 +119,13 @@
|
||||
- [x] 8.4 扩展auth.controller.ts - 实现验证码路由
|
||||
- [x] 8.5 扩展auth.test.md - 编写验证码测试用例文档
|
||||
|
||||
- [ ] 9.0 检查认证模块 (Auth Module)那些接口需要加分布式锁,并加锁
|
||||
- [x] 9.0 检查认证模块 (Auth Module)那些接口需要加分布式锁,并加锁
|
||||
|
||||
|
||||
### 👤 用户管理模块 (User Module) - P0优先级
|
||||
|
||||
- [ ] 9.0 GET /users/me - 获取当前用户信息接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 9.1 扩展user.schema.ts - 定义当前用户Schema
|
||||
- [ ] 9.2 扩展user.response.ts - 定义当前用户响应格式
|
||||
- [ ] 9.3 扩展user.service.ts - 实现当前用户业务逻辑
|
||||
@ -132,6 +133,7 @@
|
||||
- [ ] 9.5 创建user.test.md - 编写当前用户测试用例文档
|
||||
|
||||
- [ ] 10.0 GET /users - 用户列表查询接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 10.1 扩展user.schema.ts - 定义用户列表Schema
|
||||
- [ ] 10.2 扩展user.response.ts - 定义用户列表响应格式
|
||||
- [ ] 10.3 扩展user.service.ts - 实现用户列表业务逻辑
|
||||
@ -139,6 +141,7 @@
|
||||
- [ ] 10.5 扩展user.test.md - 编写用户列表测试用例文档
|
||||
|
||||
- [ ] 11.0 POST /users - 创建用户接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 11.1 扩展user.schema.ts - 定义创建用户Schema
|
||||
- [ ] 11.2 扩展user.response.ts - 定义创建用户响应格式
|
||||
- [ ] 11.3 扩展user.service.ts - 实现创建用户业务逻辑
|
||||
@ -146,6 +149,7 @@
|
||||
- [ ] 11.5 扩展user.test.md - 编写创建用户测试用例文档
|
||||
|
||||
- [ ] 12.0 PUT /users/{id} - 更新用户信息接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 12.1 扩展user.schema.ts - 定义更新用户Schema
|
||||
- [ ] 12.2 扩展user.response.ts - 定义更新用户响应格式
|
||||
- [ ] 12.3 扩展user.service.ts - 实现更新用户业务逻辑
|
||||
@ -153,6 +157,7 @@
|
||||
- [ ] 12.5 扩展user.test.md - 编写更新用户测试用例文档
|
||||
|
||||
- [ ] 13.0 DELETE /users/{id} - 删除用户接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 13.1 扩展user.schema.ts - 定义删除用户Schema
|
||||
- [ ] 13.2 扩展user.response.ts - 定义删除用户响应格式
|
||||
- [ ] 13.3 扩展user.service.ts - 实现删除用户业务逻辑
|
||||
@ -160,6 +165,7 @@
|
||||
- [ ] 13.5 扩展user.test.md - 编写删除用户测试用例文档
|
||||
|
||||
- [ ] 14.0 PUT /users/me/password - 修改密码接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 14.1 扩展user.schema.ts - 定义修改密码Schema
|
||||
- [ ] 14.2 扩展user.response.ts - 定义修改密码响应格式
|
||||
- [ ] 14.3 扩展user.service.ts - 实现修改密码业务逻辑
|
||||
@ -167,6 +173,7 @@
|
||||
- [ ] 14.5 扩展user.test.md - 编写修改密码测试用例文档
|
||||
|
||||
- [ ] 15.0 GET /users/{id} - 用户详情接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 15.1 扩展user.schema.ts - 定义用户详情Schema
|
||||
- [ ] 15.2 扩展user.response.ts - 定义用户详情响应格式
|
||||
- [ ] 15.3 扩展user.service.ts - 实现用户详情业务逻辑
|
||||
@ -174,6 +181,7 @@
|
||||
- [ ] 15.5 扩展user.test.md - 编写用户详情测试用例文档
|
||||
|
||||
- [ ] 16.0 POST /users/batch - 批量操作接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 16.1 扩展user.schema.ts - 定义批量操作Schema
|
||||
- [ ] 16.2 扩展user.response.ts - 定义批量操作响应格式
|
||||
- [ ] 16.3 扩展user.service.ts - 实现批量操作业务逻辑
|
||||
@ -183,6 +191,7 @@
|
||||
### 🎭 角色权限模块 (Role Module) - P0优先级
|
||||
|
||||
- [ ] 17.0 GET /roles - 角色列表接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 17.1 创建role.schema.ts - 定义角色Schema
|
||||
- [ ] 17.2 创建role.response.ts - 定义角色响应格式
|
||||
- [ ] 17.3 创建role.service.ts - 实现角色业务逻辑
|
||||
@ -190,6 +199,7 @@
|
||||
- [ ] 17.5 创建role.test.md - 编写角色测试用例文档
|
||||
|
||||
- [ ] 18.0 POST /roles - 创建角色接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 18.1 扩展role.schema.ts - 定义创建角色Schema
|
||||
- [ ] 18.2 扩展role.response.ts - 定义创建角色响应格式
|
||||
- [ ] 18.3 扩展role.service.ts - 实现创建角色业务逻辑
|
||||
@ -197,6 +207,7 @@
|
||||
- [ ] 18.5 扩展role.test.md - 编写创建角色测试用例文档
|
||||
|
||||
- [ ] 19.0 PUT /roles/{id} - 更新角色接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 19.1 扩展role.schema.ts - 定义更新角色Schema
|
||||
- [ ] 19.2 扩展role.response.ts - 定义更新角色响应格式
|
||||
- [ ] 19.3 扩展role.service.ts - 实现更新角色业务逻辑
|
||||
@ -204,6 +215,7 @@
|
||||
- [ ] 19.5 扩展role.test.md - 编写更新角色测试用例文档
|
||||
|
||||
- [ ] 20.0 DELETE /roles/{id} - 删除角色接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 20.1 扩展role.schema.ts - 定义删除角色Schema
|
||||
- [ ] 20.2 扩展role.response.ts - 定义删除角色响应格式
|
||||
- [ ] 20.3 扩展role.service.ts - 实现删除角色业务逻辑
|
||||
@ -211,6 +223,7 @@
|
||||
- [ ] 20.5 扩展role.test.md - 编写删除角色测试用例文档
|
||||
|
||||
- [ ] 21.0 GET /permissions - 权限列表接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 21.1 创建permission.schema.ts - 定义权限Schema
|
||||
- [ ] 21.2 创建permission.response.ts - 定义权限响应格式
|
||||
- [ ] 21.3 创建permission.service.ts - 实现权限业务逻辑
|
||||
@ -218,6 +231,7 @@
|
||||
- [ ] 21.5 创建permission.test.md - 编写权限测试用例文档
|
||||
|
||||
- [ ] 22.0 POST /roles/{id}/permissions - 权限分配接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 22.1 扩展role.schema.ts - 定义权限分配Schema
|
||||
- [ ] 22.2 扩展role.response.ts - 定义权限分配响应格式
|
||||
- [ ] 22.3 扩展role.service.ts - 实现权限分配业务逻辑
|
||||
@ -225,6 +239,7 @@
|
||||
- [ ] 22.5 扩展role.test.md - 编写权限分配测试用例文档
|
||||
|
||||
- [ ] 23.0 POST /users/{id}/roles - 用户角色分配接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 23.1 扩展user.schema.ts - 定义用户角色分配Schema
|
||||
- [ ] 23.2 扩展user.response.ts - 定义用户角色分配响应格式
|
||||
- [ ] 23.3 扩展user.service.ts - 实现用户角色分配业务逻辑
|
||||
@ -234,6 +249,7 @@
|
||||
### 🏢 组织架构模块 (Organization Module) - P1优先级
|
||||
|
||||
- [ ] 24.0 GET /organizations - 组织列表接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 24.1 创建organization.schema.ts - 定义组织Schema
|
||||
- [ ] 24.2 创建organization.response.ts - 定义组织响应格式
|
||||
- [ ] 24.3 创建organization.service.ts - 实现组织业务逻辑
|
||||
@ -241,6 +257,7 @@
|
||||
- [ ] 24.5 创建organization.test.md - 编写组织测试用例文档
|
||||
|
||||
- [ ] 25.0 POST /organizations - 创建组织接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 25.1 扩展organization.schema.ts - 定义创建组织Schema
|
||||
- [ ] 25.2 扩展organization.response.ts - 定义创建组织响应格式
|
||||
- [ ] 25.3 扩展organization.service.ts - 实现创建组织业务逻辑
|
||||
@ -248,6 +265,7 @@
|
||||
- [ ] 25.5 扩展organization.test.md - 编写创建组织测试用例文档
|
||||
|
||||
- [ ] 26.0 PUT /organizations/{id} - 更新组织接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 26.1 扩展organization.schema.ts - 定义更新组织Schema
|
||||
- [ ] 26.2 扩展organization.response.ts - 定义更新组织响应格式
|
||||
- [ ] 26.3 扩展organization.service.ts - 实现更新组织业务逻辑
|
||||
@ -255,6 +273,7 @@
|
||||
- [ ] 26.5 扩展organization.test.md - 编写更新组织测试用例文档
|
||||
|
||||
- [ ] 27.0 DELETE /organizations/{id} - 删除组织接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 27.1 扩展organization.schema.ts - 定义删除组织Schema
|
||||
- [ ] 27.2 扩展organization.response.ts - 定义删除组织响应格式
|
||||
- [ ] 27.3 扩展organization.service.ts - 实现删除组织业务逻辑
|
||||
@ -262,6 +281,7 @@
|
||||
- [ ] 27.5 扩展organization.test.md - 编写删除组织测试用例文档
|
||||
|
||||
- [ ] 28.0 POST /users/{id}/organizations - 用户组织关系接口
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 28.1 扩展user.schema.ts - 定义用户组织关系Schema
|
||||
- [ ] 28.2 扩展user.response.ts - 定义用户组织关系响应格式
|
||||
- [ ] 28.3 扩展user.service.ts - 实现用户组织关系业务逻辑
|
||||
@ -271,6 +291,7 @@
|
||||
### 🗂️ 系统基础模块 (System Module) - P1优先级
|
||||
|
||||
- [ ] 29.0 字典类型管理 - CRUD /dict-types
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 29.1 创建dict.schema.ts - 定义字典类型Schema
|
||||
- [ ] 29.2 创建dict.response.ts - 定义字典类型响应格式
|
||||
- [ ] 29.3 创建dict.service.ts - 实现字典类型业务逻辑
|
||||
@ -278,6 +299,7 @@
|
||||
- [ ] 29.5 创建dict.test.md - 编写字典类型测试用例文档
|
||||
|
||||
- [ ] 30.0 字典项管理 - CRUD /dict-items
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 30.1 扩展dict.schema.ts - 定义字典项Schema
|
||||
- [ ] 30.2 扩展dict.response.ts - 定义字典项响应格式
|
||||
- [ ] 30.3 扩展dict.service.ts - 实现字典项业务逻辑
|
||||
@ -285,6 +307,7 @@
|
||||
- [ ] 30.5 扩展dict.test.md - 编写字典项测试用例文档
|
||||
|
||||
- [ ] 31.0 标签管理 - CRUD /tags
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 31.1 创建tag.schema.ts - 定义标签Schema
|
||||
- [ ] 31.2 创建tag.response.ts - 定义标签响应格式
|
||||
- [ ] 31.3 创建tag.service.ts - 实现标签业务逻辑
|
||||
@ -292,6 +315,7 @@
|
||||
- [ ] 31.5 创建tag.test.md - 编写标签测试用例文档
|
||||
|
||||
- [ ] 32.0 操作日志 - GET /logs/operations
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 32.1 创建log.schema.ts - 定义操作日志Schema
|
||||
- [ ] 32.2 创建log.response.ts - 定义操作日志响应格式
|
||||
- [ ] 32.3 创建log.service.ts - 实现操作日志业务逻辑
|
||||
@ -299,6 +323,7 @@
|
||||
- [ ] 32.5 创建log.test.md - 编写操作日志测试用例文档
|
||||
|
||||
- [ ] 33.0 登录日志 - GET /logs/logins
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 33.1 扩展log.schema.ts - 定义登录日志Schema
|
||||
- [ ] 33.2 扩展log.response.ts - 定义登录日志响应格式
|
||||
- [ ] 33.3 扩展log.service.ts - 实现登录日志业务逻辑
|
||||
@ -308,6 +333,7 @@
|
||||
### 🔧 基础设施完善
|
||||
|
||||
- [ ] 34.0 JWT认证中间件
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 34.1 创建JWT认证插件
|
||||
- [ ] 34.2 实现Token黑名单管理
|
||||
- [ ] 34.3 实现RefreshToken机制
|
||||
@ -315,6 +341,7 @@
|
||||
- [ ] 34.5 编写认证中间件测试
|
||||
|
||||
- [ ] 35.0 路由模块集成
|
||||
- [ ] Before 整理输入此接口的逻辑,等待用户确认后进行
|
||||
- [ ] 35.1 更新src/modules/index.ts - 集成所有模块
|
||||
- [ ] 35.2 更新src/app.ts - 注册所有路由
|
||||
- [ ] 35.3 更新Swagger标签定义
|
||||
|
Loading…
Reference in New Issue
Block a user