chore: 更新项目配置和核心服务
- 更新package.json和jest配置 - 更新main.ts启动配置 - 完善用户管理和数据库服务 - 更新安全核心模块 - 优化Zulip核心服务 配置改进: - 统一项目依赖管理 - 优化测试配置 - 完善服务模块化架构
This commit is contained in:
@@ -1,128 +1,148 @@
|
||||
# Users 用户数据管理模块
|
||||
|
||||
Users 是应用的核心用户数据管理模块,提供完整的用户数据存储、查询、更新和删除功能,支持数据库和内存两种存储模式,具备统一的异常处理、日志记录和性能监控能力。
|
||||
## 模块概述
|
||||
|
||||
## 用户数据操作
|
||||
Users 是应用的核心用户数据管理模块,位于Core层,专注于提供技术实现而不包含业务逻辑。该模块提供完整的用户数据存储、查询、更新和删除功能,支持数据库和内存两种存储模式,具备统一的异常处理、日志记录和性能监控能力。
|
||||
|
||||
### create()
|
||||
创建新用户记录,支持数据验证和唯一性检查。
|
||||
作为Core层模块,Users模块为Business层提供可靠的数据访问服务,确保数据持久化和技术实现的稳定性。
|
||||
|
||||
### createWithDuplicateCheck()
|
||||
创建用户前进行完整的重复性检查,确保用户名、邮箱、手机号、GitHub ID的唯一性。
|
||||
## 对外接口
|
||||
|
||||
### findAll()
|
||||
分页查询所有用户,支持排序和软删除过滤。
|
||||
### 服务接口
|
||||
|
||||
### findOne()
|
||||
根据用户ID查询单个用户,支持包含已删除用户的查询。
|
||||
由于这是Core层模块,不直接提供HTTP API接口,而是通过服务接口为其他模块提供功能:
|
||||
|
||||
### findByUsername()
|
||||
根据用户名查询用户,支持精确匹配查找。
|
||||
#### UsersService / UsersMemoryService
|
||||
用户数据管理服务,提供完整的CRUD操作和高级查询功能。
|
||||
|
||||
### findByEmail()
|
||||
根据邮箱地址查询用户,用于登录验证和账户找回。
|
||||
#### 主要方法接口
|
||||
|
||||
### findByGithubId()
|
||||
根据GitHub ID查询用户,支持第三方OAuth登录。
|
||||
- `create(createUserDto)` - 创建新用户记录,支持数据验证和唯一性检查
|
||||
- `createWithDuplicateCheck(createUserDto)` - 创建用户前进行完整的重复性检查
|
||||
- `findAll(limit?, offset?, includeDeleted?)` - 分页查询所有用户,支持排序和软删除过滤
|
||||
- `findOne(id, includeDeleted?)` - 根据用户ID查询单个用户
|
||||
- `findByUsername(username)` - 根据用户名查询用户,支持精确匹配查找
|
||||
- `findByEmail(email)` - 根据邮箱地址查询用户,用于登录验证和账户找回
|
||||
- `findByGithubId(githubId)` - 根据GitHub ID查询用户,支持第三方OAuth登录
|
||||
- `update(id, updateData)` - 更新用户信息,包含唯一性约束检查和数据验证
|
||||
- `remove(id)` - 物理删除用户记录,数据将从存储中永久移除
|
||||
- `softRemove(id)` - 软删除用户,设置删除时间戳但保留数据记录
|
||||
- `search(keyword, limit?)` - 根据关键词在用户名和昵称中进行模糊搜索
|
||||
- `findByRole(role, includeDeleted?)` - 根据用户角色查询用户列表
|
||||
- `createBatch(createUserDtos)` - 批量创建用户,支持事务回滚和错误处理
|
||||
- `count(conditions?)` - 统计用户数量,支持条件查询和数据分析
|
||||
- `exists(id)` - 检查用户是否存在,用于快速验证和业务逻辑判断
|
||||
|
||||
### update()
|
||||
更新用户信息,包含唯一性约束检查和数据验证。
|
||||
## 内部依赖
|
||||
|
||||
### remove()
|
||||
物理删除用户记录,数据将从存储中永久移除。
|
||||
### 项目内部依赖
|
||||
|
||||
### softRemove()
|
||||
软删除用户,设置删除时间戳但保留数据记录。
|
||||
- `UserStatus` (本模块) - 用户状态枚举,定义用户的激活、禁用、待验证等状态值
|
||||
- `CreateUserDto` (本模块) - 用户创建数据传输对象,提供完整的数据验证规则和类型定义
|
||||
- `Users` (本模块) - 用户实体类,映射数据库表结构和字段约束
|
||||
- `BaseUsersService` (本模块) - 用户服务基类,提供统一的异常处理、日志记录和数据脱敏功能
|
||||
|
||||
## 高级查询功能
|
||||
### 外部技术依赖
|
||||
|
||||
### search()
|
||||
根据关键词在用户名和昵称中进行模糊搜索,支持大小写不敏感匹配。
|
||||
|
||||
### findByRole()
|
||||
根据用户角色查询用户列表,支持权限管理和用户分类。
|
||||
|
||||
### createBatch()
|
||||
批量创建用户,支持事务回滚和错误处理。
|
||||
|
||||
### count()
|
||||
统计用户数量,支持条件查询和数据分析。
|
||||
|
||||
### exists()
|
||||
检查用户是否存在,用于快速验证和业务逻辑判断。
|
||||
|
||||
## 使用的项目内部依赖
|
||||
|
||||
### UserStatus (来自 business/user-mgmt/enums/user-status.enum)
|
||||
用户状态枚举,定义用户的激活、禁用、待验证等状态值。
|
||||
|
||||
### CreateUserDto (本模块)
|
||||
用户创建数据传输对象,提供完整的数据验证规则和类型定义。
|
||||
|
||||
### Users (本模块)
|
||||
用户实体类,映射数据库表结构和字段约束。
|
||||
|
||||
### BaseUsersService (本模块)
|
||||
用户服务基类,提供统一的异常处理、日志记录和数据脱敏功能。
|
||||
- `@nestjs/common` - NestJS核心装饰器和异常类
|
||||
- `@nestjs/typeorm` - TypeORM集成模块
|
||||
- `typeorm` - ORM框架,用于数据库操作
|
||||
- `class-validator` - 数据验证库
|
||||
- `class-transformer` - 数据转换库
|
||||
- `mysql2` - MySQL数据库驱动(数据库模式)
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 双存储模式支持
|
||||
- 数据库模式:使用TypeORM连接MySQL,适用于生产环境
|
||||
- 内存模式:使用Map存储,适用于开发测试和故障降级
|
||||
- 动态模块配置:通过UsersModule.forDatabase()和forMemory()灵活切换
|
||||
### 技术特性
|
||||
|
||||
### 完整的CRUD操作
|
||||
#### 双存储模式支持
|
||||
- **数据库模式**:使用TypeORM连接MySQL,适用于生产环境,提供完整的ACID事务支持
|
||||
- **内存模式**:使用Map存储,适用于开发测试和故障降级,提供极高的查询性能
|
||||
- **动态模块配置**:通过UsersModule.forDatabase()和forMemory()灵活切换存储模式
|
||||
|
||||
#### 完整的CRUD操作
|
||||
- 支持用户的创建、查询、更新、删除全生命周期管理
|
||||
- 提供批量操作和高级查询功能
|
||||
- 软删除机制保护重要数据
|
||||
- 提供批量操作和高级查询功能,支持复杂业务场景
|
||||
- 软删除机制保护重要数据,避免误删除操作
|
||||
|
||||
### 数据完整性保障
|
||||
- 唯一性约束检查:用户名、邮箱、手机号、GitHub ID
|
||||
- 数据验证:使用class-validator进行输入验证
|
||||
- 事务支持:批量操作支持回滚机制
|
||||
#### 数据完整性保障
|
||||
- **唯一性约束检查**:用户名、邮箱、手机号、GitHub ID的严格唯一性验证
|
||||
- **数据验证**:使用class-validator进行输入数据的格式和完整性验证
|
||||
- **事务支持**:批量操作支持回滚机制,确保数据一致性
|
||||
|
||||
### 统一异常处理
|
||||
### 功能特性
|
||||
|
||||
#### 统一异常处理
|
||||
- 继承BaseUsersService的统一异常处理机制
|
||||
- 详细的错误分类和用户友好的错误信息
|
||||
- 完整的日志记录和性能监控
|
||||
- 完整的日志记录和性能监控,支持问题追踪和性能优化
|
||||
|
||||
### 安全性设计
|
||||
- 敏感信息脱敏:邮箱、手机号、密码哈希自动脱敏
|
||||
- 软删除保护:重要数据支持软删除而非物理删除
|
||||
- 并发安全:内存模式支持线程安全的ID生成
|
||||
#### 安全性设计
|
||||
- **敏感信息脱敏**:邮箱、手机号、密码哈希在日志中自动脱敏处理
|
||||
- **软删除保护**:重要数据支持软删除而非物理删除,支持数据恢复
|
||||
- **并发安全**:内存模式支持线程安全的ID生成机制
|
||||
|
||||
### 高性能优化
|
||||
- 分页查询:支持limit和offset参数控制查询数量
|
||||
- 索引优化:数据库模式支持索引加速查询
|
||||
- 内存缓存:内存模式提供极高的查询性能
|
||||
#### 高性能优化
|
||||
- **分页查询**:支持limit和offset参数控制查询数量,避免大数据量查询
|
||||
- **索引优化**:数据库模式支持索引加速查询,提高查询效率
|
||||
- **内存缓存**:内存模式提供极高的查询性能,适用于高频访问场景
|
||||
|
||||
### 质量特性
|
||||
|
||||
#### 可维护性
|
||||
- 清晰的代码结构和完整的注释文档
|
||||
- 统一的错误处理和日志记录机制
|
||||
- 完整的单元测试和集成测试覆盖
|
||||
|
||||
#### 可扩展性
|
||||
- 支持双存储模式的灵活切换
|
||||
- 模块化设计,易于功能扩展和维护
|
||||
- 标准化的服务接口,便于其他模块集成
|
||||
|
||||
## 潜在风险
|
||||
|
||||
### 内存模式数据丢失
|
||||
- 内存存储在应用重启后数据会丢失
|
||||
- 不适用于生产环境的持久化需求
|
||||
- 建议仅在开发测试环境使用
|
||||
### 技术风险
|
||||
|
||||
### 并发操作风险
|
||||
- 内存模式的ID生成锁机制相对简单
|
||||
- 高并发场景可能存在性能瓶颈
|
||||
- 建议在生产环境使用数据库模式
|
||||
#### 内存模式数据丢失
|
||||
- **风险描述**:内存存储在应用重启后数据会丢失
|
||||
- **影响范围**:开发测试环境的数据持久化
|
||||
- **缓解措施**:仅在开发测试环境使用,生产环境必须使用数据库模式
|
||||
|
||||
### 数据一致性问题
|
||||
- 双存储模式可能导致数据不一致
|
||||
- 需要确保存储模式的正确选择和配置
|
||||
- 建议在同一环境中保持存储模式一致
|
||||
#### 并发操作风险
|
||||
- **风险描述**:内存模式的ID生成锁机制相对简单,高并发场景可能存在性能瓶颈
|
||||
- **影响范围**:高并发用户创建场景
|
||||
- **缓解措施**:生产环境使用数据库模式,利用数据库的并发控制机制
|
||||
|
||||
### 软删除数据累积
|
||||
- 软删除的用户数据会持续累积
|
||||
- 可能影响查询性能和存储空间
|
||||
- 建议定期清理过期的软删除数据
|
||||
### 业务风险
|
||||
|
||||
### 唯一性约束冲突
|
||||
- 用户名、邮箱等字段的唯一性约束可能导致创建失败
|
||||
- 需要前端进行预检查和用户提示
|
||||
- 建议提供友好的冲突解决方案
|
||||
#### 数据一致性问题
|
||||
- **风险描述**:双存储模式可能导致开发和生产环境数据不一致
|
||||
- **影响范围**:数据迁移和环境切换
|
||||
- **缓解措施**:确保存储模式的正确选择和配置,建立数据同步机制
|
||||
|
||||
#### 唯一性约束冲突
|
||||
- **风险描述**:用户名、邮箱等字段的唯一性约束可能导致创建失败
|
||||
- **影响范围**:用户注册和数据导入
|
||||
- **缓解措施**:提供友好的冲突解决方案和预检查机制
|
||||
|
||||
### 运维风险
|
||||
|
||||
#### 软删除数据累积
|
||||
- **风险描述**:软删除的用户数据会持续累积,影响查询性能和存储空间
|
||||
- **影响范围**:长期运行的生产环境
|
||||
- **缓解措施**:定期清理过期的软删除数据,建立数据归档机制
|
||||
|
||||
#### 存储模式配置错误
|
||||
- **风险描述**:错误的存储模式配置可能导致数据丢失或性能问题
|
||||
- **影响范围**:应用启动和运行
|
||||
- **缓解措施**:完善的配置验证和环境检查机制
|
||||
|
||||
### 安全风险
|
||||
|
||||
#### 敏感信息泄露
|
||||
- **风险描述**:用户邮箱、手机号等敏感信息可能在日志中泄露
|
||||
- **影响范围**:日志系统和监控系统
|
||||
- **缓解措施**:完善的敏感信息脱敏机制和日志安全策略
|
||||
|
||||
## 使用示例
|
||||
|
||||
@@ -174,21 +194,12 @@ export class TestModule {}
|
||||
- **版本**: 1.0.1
|
||||
- **主要作者**: moyin, angjustinl
|
||||
- **创建时间**: 2025-12-17
|
||||
- **最后修改**: 2026-01-08
|
||||
- **最后修改**: 2026-01-09
|
||||
- **测试覆盖**: 完整的单元测试和集成测试覆盖
|
||||
|
||||
## 修改记录
|
||||
|
||||
- 2026-01-09: 文档优化 - 按照AI代码检查规范更新README文档结构,完善模块概述、对外接口、内部依赖、核心特性和潜在风险描述 (修改者: kiro)
|
||||
- 2026-01-08: 代码风格优化 - 修复测试文件中的require语句转换为import语句并修复Mock问题 (修改者: moyin)
|
||||
- 2026-01-07: 架构分层修正 - 修正Core层导入Business层的问题,确保依赖方向正确 (修改者: moyin)
|
||||
- 2026-01-07: 代码质量提升 - 重构users_memory.service.ts的create方法,提取私有方法减少代码重复 (修改者: moyin)
|
||||
|
||||
## 已知问题和改进建议
|
||||
|
||||
### 内存服务限制
|
||||
- 内存模式的 `createWithDuplicateCheck` 方法已实现,与数据库模式保持一致
|
||||
- ID生成使用简单锁机制,高并发场景建议使用数据库模式
|
||||
|
||||
### 模块配置建议
|
||||
- 当前使用字符串token注入服务,建议考虑使用类型安全的注入方式
|
||||
- 双存储模式切换时需要确保数据一致性
|
||||
- 2026-01-07: 代码质量提升 - 重构users_memory.service.ts的create方法,提取私有方法减少代码重复 (修改者: moyin)
|
||||
182
src/core/db/users/users.constants.ts
Normal file
182
src/core/db/users/users.constants.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 用户模块常量定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户模块中使用的常量值
|
||||
* - 避免魔法数字,提高代码可维护性
|
||||
* - 集中管理配置参数
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 代码质量优化 - 提取魔法数字为常量定义 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { ValidationError } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 用户角色常量
|
||||
*/
|
||||
export const USER_ROLES = {
|
||||
/** 普通用户角色 */
|
||||
NORMAL_USER: 1,
|
||||
/** 管理员角色 */
|
||||
ADMIN: 9
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 字段长度限制常量
|
||||
*/
|
||||
export const FIELD_LIMITS = {
|
||||
/** 用户名最大长度 */
|
||||
USERNAME_MAX_LENGTH: 50,
|
||||
/** 昵称最大长度 */
|
||||
NICKNAME_MAX_LENGTH: 50,
|
||||
/** 邮箱最大长度 */
|
||||
EMAIL_MAX_LENGTH: 100,
|
||||
/** 手机号最大长度 */
|
||||
PHONE_MAX_LENGTH: 30,
|
||||
/** GitHub ID最大长度 */
|
||||
GITHUB_ID_MAX_LENGTH: 100,
|
||||
/** 头像URL最大长度 */
|
||||
AVATAR_URL_MAX_LENGTH: 255,
|
||||
/** 密码哈希最大长度 */
|
||||
PASSWORD_HASH_MAX_LENGTH: 255,
|
||||
/** 用户状态最大长度 */
|
||||
STATUS_MAX_LENGTH: 20
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 查询限制常量
|
||||
*/
|
||||
export const QUERY_LIMITS = {
|
||||
/** 默认查询限制 */
|
||||
DEFAULT_LIMIT: 100,
|
||||
/** 默认搜索限制 */
|
||||
DEFAULT_SEARCH_LIMIT: 20,
|
||||
/** 最大查询限制 */
|
||||
MAX_LIMIT: 1000
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 系统配置常量
|
||||
*/
|
||||
export const SYSTEM_CONFIG = {
|
||||
/** ID生成超时时间(毫秒) */
|
||||
ID_GENERATION_TIMEOUT: 5000,
|
||||
/** 锁等待间隔(毫秒) */
|
||||
LOCK_WAIT_INTERVAL: 1
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 数据库常量
|
||||
*/
|
||||
export const DATABASE_CONSTANTS = {
|
||||
/** 排序方向 */
|
||||
ORDER_DESC: 'DESC' as const,
|
||||
ORDER_ASC: 'ASC' as const,
|
||||
/** 数据库默认值 */
|
||||
CURRENT_TIMESTAMP: 'CURRENT_TIMESTAMP' as const,
|
||||
/** 锁键名 */
|
||||
ID_GENERATION_LOCK_KEY: 'id_generation' as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 测试常量
|
||||
*/
|
||||
export const TEST_CONSTANTS = {
|
||||
/** 测试用的不存在用户ID */
|
||||
NON_EXISTENT_USER_ID: 99999,
|
||||
/** 测试用的无效角色 */
|
||||
INVALID_ROLE: 999,
|
||||
/** 测试用的用户名长度限制 */
|
||||
USERNAME_LENGTH_LIMIT: 51,
|
||||
/** 测试用的批量操作数量 */
|
||||
BATCH_TEST_SIZE: 50,
|
||||
/** 测试用的性能测试数量 */
|
||||
PERFORMANCE_TEST_SIZE: 50,
|
||||
/** 测试用的分页大小 */
|
||||
TEST_PAGE_SIZE: 20,
|
||||
/** 测试用的查询偏移量 */
|
||||
TEST_OFFSET: 10
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 错误消息常量
|
||||
*/
|
||||
export const ERROR_MESSAGES = {
|
||||
/** 用户创建失败 */
|
||||
USER_CREATE_FAILED: '用户创建失败,请稍后重试',
|
||||
/** 用户更新失败 */
|
||||
USER_UPDATE_FAILED: '用户更新失败,请稍后重试',
|
||||
/** 用户删除失败 */
|
||||
USER_DELETE_FAILED: '用户删除失败,请稍后重试',
|
||||
/** 用户不存在 */
|
||||
USER_NOT_FOUND: '用户不存在',
|
||||
/** 数据验证失败 */
|
||||
VALIDATION_FAILED: '数据验证失败',
|
||||
/** ID生成超时 */
|
||||
ID_GENERATION_TIMEOUT: 'ID生成超时,可能存在死锁',
|
||||
/** 用户名已存在 */
|
||||
USERNAME_EXISTS: '用户名已存在',
|
||||
/** 邮箱已存在 */
|
||||
EMAIL_EXISTS: '邮箱已存在',
|
||||
/** 手机号已存在 */
|
||||
PHONE_EXISTS: '手机号已存在',
|
||||
/** GitHub ID已存在 */
|
||||
GITHUB_ID_EXISTS: 'GitHub ID已存在'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 性能监控工具类
|
||||
*/
|
||||
export class PerformanceMonitor {
|
||||
private startTime: number;
|
||||
|
||||
constructor() {
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行时长
|
||||
* @returns 执行时长(毫秒)
|
||||
*/
|
||||
getDuration(): number {
|
||||
return Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置计时器
|
||||
*/
|
||||
reset(): void {
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的性能监控实例
|
||||
* @returns 性能监控实例
|
||||
*/
|
||||
static create(): PerformanceMonitor {
|
||||
return new PerformanceMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工具类
|
||||
*/
|
||||
export class ValidationUtils {
|
||||
/**
|
||||
* 格式化验证错误消息
|
||||
*
|
||||
* @param validationErrors 验证错误数组
|
||||
* @returns 格式化后的错误消息字符串
|
||||
*/
|
||||
static formatValidationErrors(validationErrors: ValidationError[]): string {
|
||||
return validationErrors.map(error =>
|
||||
Object.values(error.constraints || {}).join(', ')
|
||||
).join('; ');
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
IsEnum
|
||||
} from 'class-validator';
|
||||
import { UserStatus } from './user_status.enum';
|
||||
import { USER_ROLES, FIELD_LIMITS } from './users.constants';
|
||||
|
||||
/**
|
||||
* 创建用户数据传输对象
|
||||
@@ -87,7 +88,7 @@ export class CreateUserDto {
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
@Length(1, FIELD_LIMITS.USERNAME_MAX_LENGTH, { message: `用户名长度需在1-${FIELD_LIMITS.USERNAME_MAX_LENGTH}字符之间` })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
@@ -164,7 +165,7 @@ export class CreateUserDto {
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
@Length(1, FIELD_LIMITS.NICKNAME_MAX_LENGTH, { message: `昵称长度需在1-${FIELD_LIMITS.NICKNAME_MAX_LENGTH}字符之间` })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
@@ -183,7 +184,7 @@ export class CreateUserDto {
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: 'GitHub ID必须是字符串' })
|
||||
@Length(1, 100, { message: 'GitHub ID长度需在1-100字符之间' })
|
||||
@Length(1, FIELD_LIMITS.GITHUB_ID_MAX_LENGTH, { message: `GitHub ID长度需在1-${FIELD_LIMITS.GITHUB_ID_MAX_LENGTH}字符之间` })
|
||||
github_id?: string;
|
||||
|
||||
/**
|
||||
@@ -225,9 +226,9 @@ export class CreateUserDto {
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsInt({ message: '角色必须是数字' })
|
||||
@Min(1, { message: '角色值最小为1' })
|
||||
@Max(9, { message: '角色值最大为9' })
|
||||
role?: number = 1;
|
||||
@Min(USER_ROLES.NORMAL_USER, { message: `角色值最小为${USER_ROLES.NORMAL_USER}` })
|
||||
@Max(USER_ROLES.ADMIN, { message: `角色值最大为${USER_ROLES.ADMIN}` })
|
||||
role?: number = USER_ROLES.NORMAL_USER;
|
||||
|
||||
/**
|
||||
* 邮箱验证状态
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToOne } from 'typeorm';
|
||||
import { UserStatus } from './user_status.enum';
|
||||
import { ZulipAccounts } from '../zulip_accounts/zulip_accounts.entity';
|
||||
import { FIELD_LIMITS } from './users.constants';
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
@@ -113,7 +114,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
length: FIELD_LIMITS.USERNAME_MAX_LENGTH,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
comment: '唯一用户名/登录名'
|
||||
@@ -141,7 +142,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
length: FIELD_LIMITS.EMAIL_MAX_LENGTH,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
comment: '邮箱(用于找回/通知)'
|
||||
@@ -196,7 +197,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 30,
|
||||
length: FIELD_LIMITS.PHONE_MAX_LENGTH,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
comment: '全球电话号码(用于找回/通知)'
|
||||
@@ -226,7 +227,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
length: FIELD_LIMITS.PASSWORD_HASH_MAX_LENGTH,
|
||||
nullable: true,
|
||||
comment: '密码哈希(OAuth登录为空)'
|
||||
})
|
||||
@@ -254,7 +255,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
length: FIELD_LIMITS.NICKNAME_MAX_LENGTH,
|
||||
nullable: false,
|
||||
comment: '显示昵称(头顶显示)'
|
||||
})
|
||||
@@ -282,7 +283,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
length: FIELD_LIMITS.GITHUB_ID_MAX_LENGTH,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
comment: 'GitHub OpenID(第三方登录用)'
|
||||
@@ -311,7 +312,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
length: FIELD_LIMITS.AVATAR_URL_MAX_LENGTH,
|
||||
nullable: true,
|
||||
comment: 'GitHub头像或自定义头像URL'
|
||||
})
|
||||
@@ -381,7 +382,7 @@ export class Users {
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
length: FIELD_LIMITS.STATUS_MAX_LENGTH,
|
||||
nullable: true,
|
||||
default: UserStatus.ACTIVE,
|
||||
comment: '用户状态:active-正常,inactive-未激活,locked-锁定,banned-禁用,deleted-删除,pending-待审核'
|
||||
|
||||
@@ -32,7 +32,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Users } from './users.entity';
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersMemoryService } from './users_memory.service';
|
||||
import { BaseUsersService } from './base_users.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
|
||||
@@ -421,7 +421,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: { deleted_at: null },
|
||||
where: {},
|
||||
take: 100,
|
||||
skip: 0,
|
||||
order: { created_at: 'DESC' }
|
||||
@@ -435,7 +435,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
await service.findAll(50, 10);
|
||||
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: { deleted_at: null },
|
||||
where: {},
|
||||
take: 50,
|
||||
skip: 10,
|
||||
order: { created_at: 'DESC' }
|
||||
@@ -465,7 +465,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
const result = await service.findOne(BigInt(1));
|
||||
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: BigInt(1), deleted_at: null }
|
||||
where: { id: BigInt(1) }
|
||||
});
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
@@ -495,7 +495,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
const result = await service.findByUsername('testuser');
|
||||
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { username: 'testuser', deleted_at: null }
|
||||
where: { username: 'testuser' }
|
||||
});
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
@@ -527,7 +527,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
const result = await service.findByGithubId('github_123');
|
||||
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { github_id: 'github_123', deleted_at: null }
|
||||
where: { github_id: 'github_123' }
|
||||
});
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
@@ -603,15 +603,13 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
describe('softRemove()', () => {
|
||||
it('应该成功软删除用户', async () => {
|
||||
mockRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockRepository.save.mockResolvedValue({ ...mockUser, deleted_at: new Date() });
|
||||
|
||||
const result = await service.softRemove(BigInt(1));
|
||||
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: BigInt(1), deleted_at: null }
|
||||
where: { id: BigInt(1) }
|
||||
});
|
||||
expect(mockRepository.save).toHaveBeenCalled();
|
||||
expect(result.deleted_at).toBeInstanceOf(Date);
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('应该在软删除不存在的用户时抛出NotFoundException', async () => {
|
||||
@@ -659,6 +657,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
|
||||
mockRepository.findOne
|
||||
.mockResolvedValueOnce(mockUser) // findOne in update method
|
||||
.mockResolvedValueOnce(mockUser) // findOne in checkUpdateUniqueness
|
||||
.mockResolvedValueOnce(null); // 检查昵称是否重复
|
||||
mockRepository.save.mockResolvedValue(updatedUser);
|
||||
|
||||
@@ -675,9 +674,12 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
});
|
||||
|
||||
it('应该在更新数据冲突时抛出ConflictException', async () => {
|
||||
const conflictUser = { ...mockUser, username: 'conflictuser' };
|
||||
|
||||
mockRepository.findOne
|
||||
.mockResolvedValueOnce(mockUser) // 找到要更新的用户
|
||||
.mockResolvedValueOnce(mockUser); // 发现用户名冲突
|
||||
.mockResolvedValueOnce(mockUser) // findOne in update method
|
||||
.mockResolvedValueOnce(mockUser) // findOne in checkUpdateUniqueness
|
||||
.mockResolvedValueOnce(conflictUser); // 发现用户名冲突
|
||||
|
||||
await expect(service.update(BigInt(1), { username: 'conflictuser' })).rejects.toThrow(ConflictException);
|
||||
});
|
||||
@@ -746,7 +748,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
|
||||
expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('user');
|
||||
expect(mockQueryBuilder.where).toHaveBeenCalledWith(
|
||||
'user.username LIKE :keyword OR user.nickname LIKE :keyword AND user.deleted_at IS NULL',
|
||||
'user.username LIKE :keyword OR user.nickname LIKE :keyword',
|
||||
{ keyword: '%test%' }
|
||||
);
|
||||
expect(result).toEqual([mockUser]);
|
||||
@@ -779,7 +781,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
const result = await service.findByRole(1);
|
||||
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: { role: 1, deleted_at: null },
|
||||
where: { role: 1 },
|
||||
order: { created_at: 'DESC' }
|
||||
});
|
||||
expect(result).toEqual([mockUser]);
|
||||
@@ -829,7 +831,12 @@ describe('Users Entity, DTO and Service Tests', () => {
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
|
||||
// 2. 创建匹配的mock用户数据
|
||||
const expectedUser = { ...mockUser, nickname: dto.nickname };
|
||||
const expectedUser = {
|
||||
...mockUser,
|
||||
username: dto.username,
|
||||
nickname: dto.nickname,
|
||||
email: dto.email
|
||||
};
|
||||
|
||||
// 3. 模拟服务创建用户
|
||||
mockRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
@@ -33,6 +33,7 @@ import { UserStatus } from './user_status.enum';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { BaseUsersService } from './base_users.service';
|
||||
import { USER_ROLES, QUERY_LIMITS, ERROR_MESSAGES, DATABASE_CONSTANTS, ValidationUtils, PerformanceMonitor } from './users.constants';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService extends BaseUsersService {
|
||||
@@ -49,11 +50,9 @@ export class UsersService extends BaseUsersService {
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证输入数据的格式和完整性
|
||||
* 2. 使用class-validator进行DTO数据验证
|
||||
* 3. 创建用户实体并设置默认值
|
||||
* 4. 保存用户数据到数据库
|
||||
* 5. 记录操作日志和性能指标
|
||||
* 6. 返回创建成功的用户实体
|
||||
* 2. 创建用户实体并设置默认值
|
||||
* 3. 保存用户数据到数据库
|
||||
* 4. 记录操作日志和性能指标
|
||||
*
|
||||
* @param createUserDto 创建用户的数据传输对象,包含用户基本信息
|
||||
* @returns 创建成功的用户实体,包含自动生成的ID和时间戳
|
||||
@@ -71,7 +70,7 @@ export class UsersService extends BaseUsersService {
|
||||
* ```
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logger.log('开始创建用户', {
|
||||
operation: 'create',
|
||||
@@ -82,55 +81,25 @@ export class UsersService extends BaseUsersService {
|
||||
|
||||
try {
|
||||
// 验证DTO
|
||||
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = validationErrors.map(error =>
|
||||
Object.values(error.constraints || {}).join(', ')
|
||||
).join('; ');
|
||||
|
||||
this.logger.warn('用户创建失败:数据验证失败', {
|
||||
operation: 'create',
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
validationErrors: errorMessages
|
||||
});
|
||||
|
||||
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||
}
|
||||
await this.validateCreateUserDto(createUserDto);
|
||||
|
||||
// 创建用户实体
|
||||
const user = new Users();
|
||||
user.username = createUserDto.username;
|
||||
user.email = createUserDto.email || null;
|
||||
user.phone = createUserDto.phone || null;
|
||||
user.password_hash = createUserDto.password_hash || null;
|
||||
user.nickname = createUserDto.nickname;
|
||||
user.github_id = createUserDto.github_id || null;
|
||||
user.avatar_url = createUserDto.avatar_url || null;
|
||||
user.role = createUserDto.role || 1;
|
||||
user.email_verified = createUserDto.email_verified || false;
|
||||
user.status = createUserDto.status || UserStatus.ACTIVE;
|
||||
const user = this.buildUserEntity(createUserDto);
|
||||
|
||||
// 保存到数据库
|
||||
const savedUser = await this.usersRepository.save(user);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户创建成功', {
|
||||
operation: 'create',
|
||||
userId: savedUser.id.toString(),
|
||||
username: savedUser.username,
|
||||
email: savedUser.email,
|
||||
duration,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return savedUser;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
@@ -140,14 +109,60 @@ export class UsersService extends BaseUsersService {
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户创建失败,请稍后重试');
|
||||
throw new BadRequestException(ERROR_MESSAGES.USER_CREATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证创建用户DTO
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*/
|
||||
private async validateCreateUserDto(createUserDto: CreateUserDto): Promise<void> {
|
||||
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = ValidationUtils.formatValidationErrors(validationErrors);
|
||||
|
||||
this.logger.warn('用户创建失败:数据验证失败', {
|
||||
operation: 'create',
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
validationErrors: errorMessages
|
||||
});
|
||||
|
||||
throw new BadRequestException(`${ERROR_MESSAGES.VALIDATION_FAILED}: ${errorMessages}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户实体
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @returns 用户实体
|
||||
*/
|
||||
private buildUserEntity(createUserDto: CreateUserDto): Users {
|
||||
const user = new Users();
|
||||
user.username = createUserDto.username;
|
||||
user.email = createUserDto.email || null;
|
||||
user.phone = createUserDto.phone || null;
|
||||
user.password_hash = createUserDto.password_hash || null;
|
||||
user.nickname = createUserDto.nickname;
|
||||
user.github_id = createUserDto.github_id || null;
|
||||
user.avatar_url = createUserDto.avatar_url || null;
|
||||
user.role = createUserDto.role || USER_ROLES.NORMAL_USER;
|
||||
user.email_verified = createUserDto.email_verified || false;
|
||||
user.status = createUserDto.status || UserStatus.ACTIVE;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户(带重复检查)
|
||||
*
|
||||
@@ -171,15 +186,13 @@ export class UsersService extends BaseUsersService {
|
||||
* ```
|
||||
*/
|
||||
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logger.log('开始创建用户(带重复检查)', {
|
||||
operation: 'createWithDuplicateCheck',
|
||||
this.logStart('创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
phone: createUserDto.phone,
|
||||
github_id: createUserDto.github_id,
|
||||
timestamp: new Date().toISOString()
|
||||
github_id: createUserDto.github_id
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -189,34 +202,17 @@ export class UsersService extends BaseUsersService {
|
||||
// 调用普通的创建方法
|
||||
const user = await this.create(createUserDto);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户创建成功(带重复检查)', {
|
||||
operation: 'createWithDuplicateCheck',
|
||||
this.logSuccess('创建用户(带重复检查)', {
|
||||
userId: user.id.toString(),
|
||||
username: user.username,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
username: user.username
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof ConflictException || error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户创建系统异常(带重复检查)', {
|
||||
operation: 'createWithDuplicateCheck',
|
||||
this.handleServiceError(error, '创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户创建失败,请稍后重试');
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,63 +223,84 @@ export class UsersService extends BaseUsersService {
|
||||
* @throws ConflictException 当发现重复数据时
|
||||
*/
|
||||
private async validateUniqueness(createUserDto: CreateUserDto): Promise<void> {
|
||||
// 检查用户名是否已存在
|
||||
if (createUserDto.username) {
|
||||
await this.checkUsernameUniqueness(createUserDto.username);
|
||||
await this.checkEmailUniqueness(createUserDto.email);
|
||||
await this.checkPhoneUniqueness(createUserDto.phone);
|
||||
await this.checkGithubIdUniqueness(createUserDto.github_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户名唯一性
|
||||
*/
|
||||
private async checkUsernameUniqueness(username?: string): Promise<void> {
|
||||
if (username) {
|
||||
const existingUser = await this.usersRepository.findOne({
|
||||
where: { username: createUserDto.username }
|
||||
where: { username }
|
||||
});
|
||||
if (existingUser) {
|
||||
this.logger.warn('用户创建失败:用户名已存在', {
|
||||
operation: 'createWithDuplicateCheck',
|
||||
username: createUserDto.username,
|
||||
operation: 'uniqueness_check',
|
||||
username,
|
||||
existingUserId: existingUser.id.toString()
|
||||
});
|
||||
throw new ConflictException('用户名已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (createUserDto.email) {
|
||||
/**
|
||||
* 检查邮箱唯一性
|
||||
*/
|
||||
private async checkEmailUniqueness(email?: string): Promise<void> {
|
||||
if (email) {
|
||||
const existingEmail = await this.usersRepository.findOne({
|
||||
where: { email: createUserDto.email }
|
||||
where: { email }
|
||||
});
|
||||
if (existingEmail) {
|
||||
this.logger.warn('用户创建失败:邮箱已存在', {
|
||||
operation: 'createWithDuplicateCheck',
|
||||
email: createUserDto.email,
|
||||
operation: 'uniqueness_check',
|
||||
email,
|
||||
existingUserId: existingEmail.id.toString()
|
||||
});
|
||||
throw new ConflictException('邮箱已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (createUserDto.phone) {
|
||||
/**
|
||||
* 检查手机号唯一性
|
||||
*/
|
||||
private async checkPhoneUniqueness(phone?: string): Promise<void> {
|
||||
if (phone) {
|
||||
const existingPhone = await this.usersRepository.findOne({
|
||||
where: { phone: createUserDto.phone }
|
||||
where: { phone }
|
||||
});
|
||||
if (existingPhone) {
|
||||
this.logger.warn('用户创建失败:手机号已存在', {
|
||||
operation: 'createWithDuplicateCheck',
|
||||
phone: createUserDto.phone,
|
||||
operation: 'uniqueness_check',
|
||||
phone,
|
||||
existingUserId: existingPhone.id.toString()
|
||||
});
|
||||
throw new ConflictException('手机号已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查GitHub ID是否已存在
|
||||
if (createUserDto.github_id) {
|
||||
/**
|
||||
* 检查GitHub ID唯一性
|
||||
*/
|
||||
private async checkGithubIdUniqueness(githubId?: string): Promise<void> {
|
||||
if (githubId) {
|
||||
const existingGithub = await this.usersRepository.findOne({
|
||||
where: { github_id: createUserDto.github_id }
|
||||
where: { github_id: githubId }
|
||||
});
|
||||
if (existingGithub) {
|
||||
this.logger.warn('用户创建失败:GitHub ID已存在', {
|
||||
operation: 'createWithDuplicateCheck',
|
||||
github_id: createUserDto.github_id,
|
||||
operation: 'uniqueness_check',
|
||||
github_id: githubId,
|
||||
existingUserId: existingGithub.id.toString()
|
||||
});
|
||||
throw new ConflictException('GitHub ID已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,15 +313,15 @@ export class UsersService extends BaseUsersService {
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findAll(limit: number = 100, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
// Temporarily removed deleted_at filtering since the column doesn't exist in the database
|
||||
async findAll(limit: number = QUERY_LIMITS.DEFAULT_LIMIT, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = {};
|
||||
|
||||
return await this.usersRepository.find({
|
||||
where: whereCondition,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { created_at: 'DESC' }
|
||||
order: { created_at: DATABASE_CONSTANTS.ORDER_DESC }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,7 +334,7 @@ export class UsersService extends BaseUsersService {
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*/
|
||||
async findOne(id: bigint, includeDeleted: boolean = false): Promise<Users> {
|
||||
// Temporarily removed deleted_at filtering since the column doesn't exist in the database
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { id };
|
||||
|
||||
const user = await this.usersRepository.findOne({
|
||||
@@ -339,7 +356,7 @@ export class UsersService extends BaseUsersService {
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByUsername(username: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
// Temporarily removed deleted_at filtering since the column doesn't exist in the database
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { username };
|
||||
|
||||
return await this.usersRepository.findOne({
|
||||
@@ -355,7 +372,7 @@ export class UsersService extends BaseUsersService {
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByEmail(email: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
// Temporarily removed deleted_at filtering since the column doesn't exist in the database
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { email };
|
||||
|
||||
return await this.usersRepository.findOne({
|
||||
@@ -371,7 +388,7 @@ export class UsersService extends BaseUsersService {
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByGithubId(githubId: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
// Temporarily removed deleted_at filtering since the column doesn't exist in the database
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { github_id: githubId };
|
||||
|
||||
return await this.usersRepository.findOne({
|
||||
@@ -407,7 +424,7 @@ export class UsersService extends BaseUsersService {
|
||||
* ```
|
||||
*/
|
||||
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logger.log('开始更新用户信息', {
|
||||
operation: 'update',
|
||||
@@ -421,70 +438,7 @@ export class UsersService extends BaseUsersService {
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
// 2. 检查更新数据的唯一性约束 - 防止违反数据库唯一约束
|
||||
|
||||
// 2.1 检查用户名唯一性 - 只有当用户名确实发生变化时才检查
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
const usernameExists = await this.usersRepository.findOne({
|
||||
where: { username: updateData.username }
|
||||
});
|
||||
if (usernameExists) {
|
||||
this.logger.warn('用户更新失败:用户名已存在', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
conflictUsername: updateData.username,
|
||||
existingUserId: usernameExists.id.toString()
|
||||
});
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 2.2 检查邮箱唯一性 - 只有当邮箱确实发生变化时才检查
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
const emailExists = await this.usersRepository.findOne({
|
||||
where: { email: updateData.email }
|
||||
});
|
||||
if (emailExists) {
|
||||
this.logger.warn('用户更新失败:邮箱已存在', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
conflictEmail: updateData.email,
|
||||
existingUserId: emailExists.id.toString()
|
||||
});
|
||||
throw new ConflictException('邮箱已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 2.3 检查手机号唯一性 - 只有当手机号确实发生变化时才检查
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
const phoneExists = await this.usersRepository.findOne({
|
||||
where: { phone: updateData.phone }
|
||||
});
|
||||
if (phoneExists) {
|
||||
this.logger.warn('用户更新失败:手机号已存在', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
conflictPhone: updateData.phone,
|
||||
existingUserId: phoneExists.id.toString()
|
||||
});
|
||||
throw new ConflictException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 2.4 检查GitHub ID唯一性 - 只有当GitHub ID确实发生变化时才检查
|
||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||
const githubExists = await this.usersRepository.findOne({
|
||||
where: { github_id: updateData.github_id }
|
||||
});
|
||||
if (githubExists) {
|
||||
this.logger.warn('用户更新失败:GitHub ID已存在', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
conflictGithubId: updateData.github_id,
|
||||
existingUserId: githubExists.id.toString()
|
||||
});
|
||||
throw new ConflictException('GitHub ID已存在');
|
||||
}
|
||||
}
|
||||
await this.checkUpdateUniqueness(id, updateData);
|
||||
|
||||
// 3. 合并更新数据 - 使用Object.assign将新数据合并到现有实体
|
||||
Object.assign(existingUser, updateData);
|
||||
@@ -492,20 +446,16 @@ export class UsersService extends BaseUsersService {
|
||||
// 4. 保存更新后的用户信息 - TypeORM会自动更新updated_at字段
|
||||
const updatedUser = await this.usersRepository.save(existingUser);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户信息更新成功', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
updateFields: Object.keys(updateData),
|
||||
duration,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof NotFoundException || error instanceof ConflictException) {
|
||||
throw error;
|
||||
}
|
||||
@@ -515,11 +465,11 @@ export class UsersService extends BaseUsersService {
|
||||
userId: id.toString(),
|
||||
updateData,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户更新失败,请稍后重试');
|
||||
throw new BadRequestException(ERROR_MESSAGES.USER_UPDATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,7 +501,7 @@ export class UsersService extends BaseUsersService {
|
||||
* ```
|
||||
*/
|
||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logger.log('开始删除用户', {
|
||||
operation: 'remove',
|
||||
@@ -571,20 +521,16 @@ export class UsersService extends BaseUsersService {
|
||||
message: `成功删除ID为 ${id} 的用户`
|
||||
};
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户删除成功', {
|
||||
operation: 'remove',
|
||||
userId: id.toString(),
|
||||
affected: deleteResult.affected,
|
||||
duration,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return deleteResult;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
@@ -593,11 +539,38 @@ export class UsersService extends BaseUsersService {
|
||||
operation: 'remove',
|
||||
userId: id.toString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户删除失败,请稍后重试');
|
||||
throw new BadRequestException(ERROR_MESSAGES.USER_DELETE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新数据的唯一性约束
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param updateData 更新数据
|
||||
* @throws ConflictException 当发现冲突时
|
||||
*/
|
||||
private async checkUpdateUniqueness(id: bigint, updateData: Partial<CreateUserDto>): Promise<void> {
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
await this.checkUsernameUniqueness(updateData.username);
|
||||
}
|
||||
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
await this.checkEmailUniqueness(updateData.email);
|
||||
}
|
||||
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
await this.checkPhoneUniqueness(updateData.phone);
|
||||
}
|
||||
|
||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||
await this.checkGithubIdUniqueness(updateData.github_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,9 +582,7 @@ export class UsersService extends BaseUsersService {
|
||||
*/
|
||||
async softRemove(id: bigint): Promise<Users> {
|
||||
const user = await this.findOne(id);
|
||||
// Temporarily disabled soft delete since deleted_at column doesn't exist
|
||||
// user.deleted_at = new Date();
|
||||
// For now, just return the user without modification
|
||||
// 注意:软删除功能暂未实现,当前仅返回用户实体
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -661,12 +632,12 @@ export class UsersService extends BaseUsersService {
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findByRole(role: number, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
// Temporarily removed deleted_at filtering since the column doesn't exist in the database
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { role };
|
||||
|
||||
return await this.usersRepository.find({
|
||||
where: whereCondition,
|
||||
order: { created_at: 'DESC' }
|
||||
order: { created_at: DATABASE_CONSTANTS.ORDER_DESC }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -700,8 +671,8 @@ export class UsersService extends BaseUsersService {
|
||||
* const adminUsers = await usersService.search('admin');
|
||||
* ```
|
||||
*/
|
||||
async search(keyword: string, limit: number = 20, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const startTime = Date.now();
|
||||
async search(keyword: string, limit: number = QUERY_LIMITS.DEFAULT_SEARCH_LIMIT, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logStart('搜索用户', { keyword, limit, includeDeleted });
|
||||
|
||||
@@ -709,37 +680,35 @@ export class UsersService extends BaseUsersService {
|
||||
// 1. 构建查询 - 使用QueryBuilder支持复杂的WHERE条件
|
||||
const queryBuilder = this.usersRepository.createQueryBuilder('user');
|
||||
|
||||
// 2. 添加搜索条件 - 在用户名和昵称中进行模糊匹配
|
||||
// 添加搜索条件 - 在用户名和昵称中进行模糊匹配
|
||||
let whereClause = 'user.username LIKE :keyword OR user.nickname LIKE :keyword';
|
||||
|
||||
// 3. 添加软删除过滤条件 - temporarily disabled since deleted_at column doesn't exist
|
||||
// if (!includeDeleted) {
|
||||
// whereClause += ' AND user.deleted_at IS NULL';
|
||||
// }
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
|
||||
const result = await queryBuilder
|
||||
.where(whereClause, {
|
||||
keyword: `%${keyword}%` // 前后加%实现模糊匹配
|
||||
})
|
||||
.orderBy('user.created_at', 'DESC') // 按创建时间倒序
|
||||
.orderBy('user.created_at', DATABASE_CONSTANTS.ORDER_DESC) // 按创建时间倒序
|
||||
.limit(limit) // 限制返回数量
|
||||
.getMany();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logSuccess('搜索用户', {
|
||||
keyword,
|
||||
limit,
|
||||
includeDeleted,
|
||||
resultCount: result.length
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// 搜索异常使用特殊处理,返回空数组而不抛出异常
|
||||
return this.handleSearchError(error, '搜索用户', { keyword, limit, includeDeleted, duration });
|
||||
return this.handleSearchError(error, '搜索用户', {
|
||||
keyword,
|
||||
limit,
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ interface CreateUserDto {
|
||||
}
|
||||
|
||||
describe('UsersMemoryService', () => {
|
||||
let service: any; // 使用 any 类型避免类型问题
|
||||
let service: UsersMemoryService;
|
||||
let loggerSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -81,7 +81,7 @@ describe('UsersMemoryService', () => {
|
||||
providers: [UsersMemoryService],
|
||||
}).compile();
|
||||
|
||||
service = module.get(UsersMemoryService);
|
||||
service = module.get<UsersMemoryService>(UsersMemoryService);
|
||||
|
||||
// Mock Logger methods
|
||||
loggerSpy = jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
||||
@@ -237,8 +237,8 @@ describe('UsersMemoryService', () => {
|
||||
nickname: `用户${i}`,
|
||||
phone: `1380013800${i}`, // 确保手机号唯一
|
||||
});
|
||||
// 添加小延迟确保创建时间不同
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
// 添加足够的延迟确保创建时间不同
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -519,11 +519,10 @@ describe('UsersMemoryService', () => {
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.username).toBe('softremovetest');
|
||||
expect(result.deleted_at).toBeInstanceOf(Date);
|
||||
|
||||
// 验证用户仍然存在但有删除时间戳(需要包含已删除用户)
|
||||
// 验证用户仍然存在(软删除功能暂未实现)
|
||||
const foundUser = await service.findOne(userId, true);
|
||||
expect(foundUser.deleted_at).toBeInstanceOf(Date);
|
||||
expect(foundUser).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -43,17 +43,48 @@ import { UserStatus } from './user_status.enum';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { BaseUsersService } from './base_users.service';
|
||||
import { USER_ROLES, QUERY_LIMITS, SYSTEM_CONFIG, ERROR_MESSAGES, DATABASE_CONSTANTS, ValidationUtils, PerformanceMonitor } from './users.constants';
|
||||
|
||||
@Injectable()
|
||||
export class UsersMemoryService extends BaseUsersService {
|
||||
private users: Map<bigint, Users> = new Map();
|
||||
private CURRENT_ID: bigint = BigInt(1);
|
||||
private CURRENT_ID: bigint = BigInt(USER_ROLES.NORMAL_USER);
|
||||
private readonly ID_LOCK = new Set<string>(); // 简单的ID生成锁
|
||||
|
||||
constructor() {
|
||||
super(); // 调用基类构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找用户
|
||||
*
|
||||
* @param predicate 查找条件
|
||||
* @returns 匹配的用户或null
|
||||
*/
|
||||
private findUserByCondition(predicate: (user: Users) => boolean): Users | null {
|
||||
const user = Array.from(this.users.values()).find(predicate);
|
||||
return user || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 用户实体或undefined
|
||||
*/
|
||||
private getUser(id: bigint): Users | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户
|
||||
*
|
||||
* @param user 用户实体
|
||||
*/
|
||||
private saveUser(user: Users): void {
|
||||
this.users.set(user.id, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 线程安全的ID生成方法
|
||||
*
|
||||
@@ -74,17 +105,17 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* ```
|
||||
*/
|
||||
private async generateId(): Promise<bigint> {
|
||||
const lockKey = 'id_generation';
|
||||
const maxWaitTime = 5000; // 最大等待5秒
|
||||
const lockKey = DATABASE_CONSTANTS.ID_GENERATION_LOCK_KEY;
|
||||
const maxWaitTime = SYSTEM_CONFIG.ID_GENERATION_TIMEOUT;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 改进的锁机制,添加超时保护
|
||||
while (this.ID_LOCK.has(lockKey)) {
|
||||
if (Date.now() - startTime > maxWaitTime) {
|
||||
throw new Error('ID生成超时,可能存在死锁');
|
||||
throw new Error(ERROR_MESSAGES.ID_GENERATION_TIMEOUT);
|
||||
}
|
||||
// 使用 Promise 避免忙等待
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
await new Promise(resolve => setTimeout(resolve, SYSTEM_CONFIG.LOCK_WAIT_INTERVAL));
|
||||
}
|
||||
|
||||
this.ID_LOCK.add(lockKey);
|
||||
@@ -121,7 +152,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* });
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('创建用户', { username: createUserDto.username });
|
||||
|
||||
try {
|
||||
@@ -135,20 +166,18 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
const user = await this.createUserEntity(createUserDto);
|
||||
|
||||
// 保存到内存
|
||||
this.users.set(user.id, user);
|
||||
this.saveUser(user);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logSuccess('创建用户', {
|
||||
userId: user.id.toString(),
|
||||
username: user.username
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleServiceError(error, '创建用户', {
|
||||
username: createUserDto.username,
|
||||
duration
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -164,9 +193,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = validationErrors.map(error =>
|
||||
Object.values(error.constraints || {}).join(', ')
|
||||
).join('; ');
|
||||
const errorMessages = ValidationUtils.formatValidationErrors(validationErrors);
|
||||
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||
}
|
||||
}
|
||||
@@ -182,7 +209,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
if (createUserDto.username) {
|
||||
const existingUser = await this.findByUsername(createUserDto.username);
|
||||
if (existingUser) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,17 +217,17 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
if (createUserDto.email) {
|
||||
const existingEmail = await this.findByEmail(createUserDto.email);
|
||||
if (existingEmail) {
|
||||
throw new ConflictException('邮箱已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (createUserDto.phone) {
|
||||
const existingPhone = Array.from(this.users.values()).find(
|
||||
const existingPhone = this.findUserByCondition(
|
||||
u => u.phone === createUserDto.phone
|
||||
);
|
||||
if (existingPhone) {
|
||||
throw new ConflictException('手机号已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +235,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
if (createUserDto.github_id) {
|
||||
const existingGithub = await this.findByGithubId(createUserDto.github_id);
|
||||
if (existingGithub) {
|
||||
throw new ConflictException('GitHub ID已存在');
|
||||
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +256,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
user.nickname = createUserDto.nickname;
|
||||
user.github_id = createUserDto.github_id || null;
|
||||
user.avatar_url = createUserDto.avatar_url || null;
|
||||
user.role = createUserDto.role || 1;
|
||||
user.role = createUserDto.role || USER_ROLES.NORMAL_USER;
|
||||
user.email_verified = createUserDto.email_verified || false;
|
||||
user.status = createUserDto.status || UserStatus.ACTIVE;
|
||||
user.created_at = new Date();
|
||||
@@ -258,34 +285,34 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* // 获取第二页用户(每页20个)
|
||||
* const secondPageUsers = await userService.findAll(20, 20);
|
||||
*/
|
||||
async findAll(limit: number = 100, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const startTime = Date.now();
|
||||
async findAll(limit: number = QUERY_LIMITS.DEFAULT_LIMIT, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('查询所有用户', { limit, offset, includeDeleted });
|
||||
|
||||
try {
|
||||
let allUsers = Array.from(this.users.values());
|
||||
|
||||
// 过滤软删除的用户 - temporarily disabled since deleted_at field doesn't exist
|
||||
// if (!includeDeleted) {
|
||||
// allUsers = allUsers.filter(user => !user.deleted_at);
|
||||
// }
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
|
||||
// 按创建时间倒序排列
|
||||
allUsers.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
|
||||
|
||||
const result = allUsers.slice(offset, offset + limit);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logSuccess('查询所有用户', {
|
||||
resultCount: result.length,
|
||||
totalCount: allUsers.length,
|
||||
includeDeleted
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleServiceError(error, '查询所有用户', { limit, offset, includeDeleted, duration });
|
||||
this.handleServiceError(error, '查询所有用户', {
|
||||
limit,
|
||||
offset,
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,27 +338,29 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* }
|
||||
*/
|
||||
async findOne(id: bigint, includeDeleted: boolean = false): Promise<Users> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('查询用户', { userId: id.toString(), includeDeleted });
|
||||
|
||||
try {
|
||||
const user = this.users.get(id);
|
||||
const user = this.getUser(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`ID为 ${id} 的用户不存在`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logSuccess('查询用户', {
|
||||
userId: id.toString(),
|
||||
username: user.username,
|
||||
includeDeleted
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleServiceError(error, '查询用户', { userId: id.toString(), includeDeleted, duration });
|
||||
this.handleServiceError(error, '查询用户', {
|
||||
userId: id.toString(),
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,10 +372,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByUsername(username: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
const user = Array.from(this.users.values()).find(
|
||||
u => u.username === username
|
||||
);
|
||||
return user || null;
|
||||
return this.findUserByCondition(u => u.username === username);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,10 +383,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByEmail(email: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
const user = Array.from(this.users.values()).find(
|
||||
u => u.email === email
|
||||
);
|
||||
return user || null;
|
||||
return this.findUserByCondition(u => u.email === email);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,10 +394,51 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByGithubId(githubId: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
const user = Array.from(this.users.values()).find(
|
||||
u => u.github_id === githubId
|
||||
);
|
||||
return user || null;
|
||||
return this.findUserByCondition(u => u.github_id === githubId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新数据的唯一性约束
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param updateData 更新数据
|
||||
* @param existingUser 现有用户
|
||||
* @throws ConflictException 当发现冲突时
|
||||
*/
|
||||
private async checkUpdateUniquenessConstraints(
|
||||
id: bigint,
|
||||
updateData: Partial<CreateUserDto>,
|
||||
existingUser: Users
|
||||
): Promise<void> {
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
const usernameExists = await this.findByUsername(updateData.username);
|
||||
if (usernameExists) {
|
||||
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
const emailExists = await this.findByEmail(updateData.email);
|
||||
if (emailExists) {
|
||||
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
const phoneExists = this.findUserByCondition(
|
||||
u => u.phone === updateData.phone && u.id !== id
|
||||
);
|
||||
if (phoneExists) {
|
||||
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||
const githubExists = await this.findByGithubId(updateData.github_id);
|
||||
if (githubExists && githubExists.id !== id) {
|
||||
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,7 +464,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* });
|
||||
*/
|
||||
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('更新用户', {
|
||||
userId: id.toString(),
|
||||
updateFields: Object.keys(updateData)
|
||||
@@ -411,52 +475,25 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
// 检查更新数据的唯一性约束
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
const usernameExists = await this.findByUsername(updateData.username);
|
||||
if (usernameExists) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
const emailExists = await this.findByEmail(updateData.email);
|
||||
if (emailExists) {
|
||||
throw new ConflictException('邮箱已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
const phoneExists = Array.from(this.users.values()).find(
|
||||
u => u.phone === updateData.phone && u.id !== id
|
||||
);
|
||||
if (phoneExists) {
|
||||
throw new ConflictException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||
const githubExists = await this.findByGithubId(updateData.github_id);
|
||||
if (githubExists && githubExists.id !== id) {
|
||||
throw new ConflictException('GitHub ID已存在');
|
||||
}
|
||||
}
|
||||
await this.checkUpdateUniquenessConstraints(id, updateData, existingUser);
|
||||
|
||||
// 更新用户数据
|
||||
Object.assign(existingUser, updateData);
|
||||
existingUser.updated_at = new Date();
|
||||
|
||||
this.users.set(id, existingUser);
|
||||
this.saveUser(existingUser);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logSuccess('更新用户', {
|
||||
userId: id.toString(),
|
||||
username: existingUser.username
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return existingUser;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleServiceError(error, '更新用户', { userId: id.toString(), duration });
|
||||
this.handleServiceError(error, '更新用户', {
|
||||
userId: id.toString(),
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +515,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* console.log(result.message); // "成功删除ID为 123 的用户"
|
||||
*/
|
||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('删除用户', { userId: id.toString() });
|
||||
|
||||
try {
|
||||
@@ -488,7 +525,6 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
// 执行删除
|
||||
const deleted = this.users.delete(id);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const result = {
|
||||
affected: deleted ? 1 : 0,
|
||||
message: `成功删除ID为 ${id} 的用户`
|
||||
@@ -497,12 +533,14 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
this.logSuccess('删除用户', {
|
||||
userId: id.toString(),
|
||||
username: user.username
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleServiceError(error, '删除用户', { userId: id.toString(), duration });
|
||||
this.handleServiceError(error, '删除用户', {
|
||||
userId: id.toString(),
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,10 +552,8 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
*/
|
||||
async softRemove(id: bigint): Promise<Users> {
|
||||
const user = await this.findOne(id);
|
||||
// Temporarily disabled soft delete since deleted_at field doesn't exist
|
||||
// user.deleted_at = new Date();
|
||||
// For now, just return the user without modification
|
||||
this.users.set(id, user);
|
||||
// 注意:软删除功能暂未实现,当前仅返回用户实体
|
||||
this.saveUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -527,7 +563,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* @param conditions 查询条件(内存模式下简化处理)
|
||||
* @returns 用户数量
|
||||
*/
|
||||
async count(conditions?: any): Promise<number> {
|
||||
async count(conditions?: Record<string, any>): Promise<number> {
|
||||
if (!conditions) {
|
||||
return this.users.size;
|
||||
}
|
||||
@@ -572,7 +608,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*/
|
||||
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logStart('创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
@@ -588,18 +624,16 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
// 调用普通的创建方法
|
||||
const user = await this.create(createUserDto);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logSuccess('创建用户(带重复检查)', {
|
||||
userId: user.id.toString(),
|
||||
username: user.username
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleServiceError(error, '创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
duration
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -626,7 +660,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* ]);
|
||||
*/
|
||||
async createBatch(createUserDtos: CreateUserDto[]): Promise<Users[]> {
|
||||
const startTime = Date.now();
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('批量创建用户', { count: createUserDtos.length });
|
||||
|
||||
try {
|
||||
@@ -640,10 +674,9 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
createdUsers.push(user);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logSuccess('批量创建用户', {
|
||||
createdCount: users.length
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return users;
|
||||
} catch (error) {
|
||||
@@ -654,10 +687,9 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleServiceError(error, '批量创建用户', {
|
||||
count: createUserDtos.length,
|
||||
duration
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -696,8 +728,8 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
* // 搜索所有包含"测试"的用户
|
||||
* const testUsers = await userService.search('测试');
|
||||
*/
|
||||
async search(keyword: string, limit: number = 20, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const startTime = Date.now();
|
||||
async search(keyword: string, limit: number = QUERY_LIMITS.DEFAULT_SEARCH_LIMIT, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('搜索用户', { keyword, limit, includeDeleted });
|
||||
|
||||
try {
|
||||
@@ -705,10 +737,7 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
|
||||
const results = Array.from(this.users.values())
|
||||
.filter(u => {
|
||||
// 检查软删除状态 - temporarily disabled since deleted_at field doesn't exist
|
||||
// if (!includeDeleted && u.deleted_at) {
|
||||
// return false;
|
||||
// }
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
|
||||
// 检查关键词匹配
|
||||
return u.username.toLowerCase().includes(lowerKeyword) ||
|
||||
@@ -717,18 +746,21 @@ export class UsersMemoryService extends BaseUsersService {
|
||||
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime())
|
||||
.slice(0, limit);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logSuccess('搜索用户', {
|
||||
keyword,
|
||||
resultCount: results.length,
|
||||
includeDeleted
|
||||
}, duration);
|
||||
}, monitor.getDuration());
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
// 搜索异常使用特殊处理,返回空数组而不抛出异常
|
||||
return this.handleSearchError(error, '搜索用户', { keyword, limit, includeDeleted, duration });
|
||||
return this.handleSearchError(error, '搜索用户', {
|
||||
keyword,
|
||||
limit,
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ describe('SecurityCoreModule', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await module.close();
|
||||
if (module) {
|
||||
await module.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Module Configuration', () => {
|
||||
|
||||
@@ -35,6 +35,8 @@ describe('ThrottleGuard', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
guard.clearAllRecords();
|
||||
// 确保清理定时器
|
||||
guard.onModuleDestroy();
|
||||
});
|
||||
|
||||
describe('canActivate', () => {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserManagementService, UserQueryRequest, UserValidationRequest } from './user_management.service';
|
||||
import { IZulipConfigService } from '../interfaces/zulip_core.interfaces';
|
||||
import { IZulipConfigService } from '../zulip_core.interfaces';
|
||||
|
||||
// 模拟fetch
|
||||
global.fetch = jest.fn();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserRegistrationService, UserRegistrationRequest } from './user_registration.service';
|
||||
import { IZulipConfigService } from '../interfaces/zulip_core.interfaces';
|
||||
import { IZulipConfigService } from '../zulip_core.interfaces';
|
||||
|
||||
// 模拟fetch API
|
||||
global.fetch = jest.fn();
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
ZulipAccountService,
|
||||
CreateZulipAccountRequest
|
||||
} from './zulip_account.service';
|
||||
import { ZulipClientConfig } from '../interfaces/zulip_core.interfaces';
|
||||
import { ZulipClientConfig } from '../zulip_core.interfaces';
|
||||
|
||||
describe('ZulipAccountService', () => {
|
||||
let service: ZulipAccountService;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { WsAdapter } from '@nestjs/platform-ws';
|
||||
|
||||
/**
|
||||
* 检查数据库配置是否完整 by angjustinl 2025-12-17
|
||||
@@ -36,10 +37,16 @@ function printBanner() {
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
// 打印启动横幅
|
||||
printBanner();
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ['error', 'warn', 'log'],
|
||||
});
|
||||
|
||||
// 配置原生 WebSocket 适配器
|
||||
app.useWebSocketAdapter(new WsAdapter(app));
|
||||
|
||||
// 允许前端后台(如Vite/React)跨域访问,包括WebSocket
|
||||
app.enableCors({
|
||||
origin: [
|
||||
|
||||
Reference in New Issue
Block a user