service:完善用户服务功能

- 优化用户查询和管理逻辑
- 更新相关单元测试
- 完善内存模式用户服务实现
This commit is contained in:
moyin
2026-01-07 15:07:18 +08:00
parent 003091494f
commit dd91264d0c
3 changed files with 844 additions and 154 deletions

View File

@@ -9,9 +9,12 @@
* @author moyin
* @version 1.0.0
* @since 2025-12-17
*
* @lastModified 2025-01-07 by moyin
* @lastChange 添加完整的日志记录系统和详细的业务逻辑注释,优化异常处理和性能监控
*/
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
import { Injectable, ConflictException, NotFoundException, BadRequestException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
import { Users } from './users.entity';
@@ -22,6 +25,8 @@ import { plainToClass } from 'class-transformer';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
constructor(
@InjectRepository(Users)
private readonly usersRepository: Repository<Users>,
@@ -35,32 +40,81 @@ export class UsersService {
* @throws BadRequestException 当数据验证失败时
*/
async create(createUserDto: CreateUserDto): Promise<Users> {
// 验证DTO
const dto = plainToClass(CreateUserDto, createUserDto);
const validationErrors = await validate(dto);
const startTime = Date.now();
if (validationErrors.length > 0) {
const errorMessages = validationErrors.map(error =>
Object.values(error.constraints || {}).join(', ')
).join('; ');
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
this.logger.log('开始创建用户', {
operation: 'create',
username: createUserDto.username,
email: createUserDto.email,
timestamp: new Date().toISOString()
});
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}`);
}
// 创建用户实体
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 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,
timestamp: new Date().toISOString()
});
return savedUser;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof BadRequestException) {
throw error;
}
this.logger.error('用户创建系统异常', {
operation: 'create',
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('用户创建失败,请稍后重试');
}
// 创建用户实体
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;
// 保存到数据库
return await this.usersRepository.save(user);
}
/**
@@ -72,48 +126,110 @@ export class UsersService {
* @throws BadRequestException 当数据验证失败时
*/
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
// 检查用户名是否已存在
if (createUserDto.username) {
const existingUser = await this.usersRepository.findOne({
where: { username: createUserDto.username }
});
if (existingUser) {
throw new ConflictException('用户名已存在');
}
}
const startTime = Date.now();
this.logger.log('开始创建用户(带重复检查)', {
operation: 'createWithDuplicateCheck',
username: createUserDto.username,
email: createUserDto.email,
phone: createUserDto.phone,
github_id: createUserDto.github_id,
timestamp: new Date().toISOString()
});
// 检查邮箱是否已存在
if (createUserDto.email) {
const existingEmail = await this.usersRepository.findOne({
where: { email: createUserDto.email }
});
if (existingEmail) {
throw new ConflictException('邮箱已存在');
try {
// 检查用户名是否已存在
if (createUserDto.username) {
const existingUser = await this.usersRepository.findOne({
where: { username: createUserDto.username }
});
if (existingUser) {
this.logger.warn('用户创建失败:用户名已存在', {
operation: 'createWithDuplicateCheck',
username: createUserDto.username,
existingUserId: existingUser.id.toString()
});
throw new ConflictException('用户名已存在');
}
}
}
// 检查手机号是否已存在
if (createUserDto.phone) {
const existingPhone = await this.usersRepository.findOne({
where: { phone: createUserDto.phone }
});
if (existingPhone) {
throw new ConflictException('手机号已存在');
// 检查邮箱是否已存在
if (createUserDto.email) {
const existingEmail = await this.usersRepository.findOne({
where: { email: createUserDto.email }
});
if (existingEmail) {
this.logger.warn('用户创建失败:邮箱已存在', {
operation: 'createWithDuplicateCheck',
email: createUserDto.email,
existingUserId: existingEmail.id.toString()
});
throw new ConflictException('邮箱已存在');
}
}
}
// 检查GitHub ID是否已存在
if (createUserDto.github_id) {
const existingGithub = await this.usersRepository.findOne({
where: { github_id: createUserDto.github_id }
});
if (existingGithub) {
throw new ConflictException('GitHub ID已存在');
// 检查手机号是否已存在
if (createUserDto.phone) {
const existingPhone = await this.usersRepository.findOne({
where: { phone: createUserDto.phone }
});
if (existingPhone) {
this.logger.warn('用户创建失败:手机号已存在', {
operation: 'createWithDuplicateCheck',
phone: createUserDto.phone,
existingUserId: existingPhone.id.toString()
});
throw new ConflictException('手机号已存在');
}
}
}
// 调用普通的创建方法
return await this.create(createUserDto);
// 检查GitHub ID是否已存在
if (createUserDto.github_id) {
const existingGithub = await this.usersRepository.findOne({
where: { github_id: createUserDto.github_id }
});
if (existingGithub) {
this.logger.warn('用户创建失败GitHub ID已存在', {
operation: 'createWithDuplicateCheck',
github_id: createUserDto.github_id,
existingUserId: existingGithub.id.toString()
});
throw new ConflictException('GitHub ID已存在');
}
}
// 调用普通的创建方法
const user = await this.create(createUserDto);
const duration = Date.now() - startTime;
this.logger.log('用户创建成功(带重复检查)', {
operation: 'createWithDuplicateCheck',
userId: user.id.toString(),
username: user.username,
duration,
timestamp: new Date().toISOString()
});
return user;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof ConflictException || error instanceof BadRequestException) {
throw error;
}
this.logger.error('用户创建系统异常(带重复检查)', {
operation: 'createWithDuplicateCheck',
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('用户创建失败,请稍后重试');
}
}
/**
@@ -189,77 +305,223 @@ export class UsersService {
/**
* 更新用户信息
*
* @param id 用户ID
* @param updateData 更新的数据
* 功能描述:
* 更新指定用户的信息,包含完整的数据验证和唯一性检查
*
* 业务逻辑:
* 1. 验证用户是否存在
* 2. 检查更新字段的唯一性约束用户名、邮箱、手机号、GitHub ID
* 3. 合并更新数据到现有用户实体
* 4. 保存更新后的用户信息
* 5. 记录操作日志
*
* @param id 用户ID必须是有效的已存在用户
* @param updateData 更新的数据,支持部分字段更新
* @returns 更新后的用户实体
* @throws NotFoundException 当用户不存在时
* @throws ConflictException 当更新的数据与其他用户冲突时
*
* @example
* ```typescript
* const updatedUser = await usersService.update(BigInt(1), {
* nickname: '新昵称',
* email: 'new@example.com'
* });
* ```
*/
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
// 检查用户是否存在
const existingUser = await this.findOne(id);
// 检查更新数据的唯一性约束
if (updateData.username && updateData.username !== existingUser.username) {
const usernameExists = await this.usersRepository.findOne({
where: { username: updateData.username }
});
if (usernameExists) {
throw new ConflictException('用户名已存在');
}
}
if (updateData.email && updateData.email !== existingUser.email) {
const emailExists = await this.usersRepository.findOne({
where: { email: updateData.email }
});
if (emailExists) {
throw new ConflictException('邮箱已存在');
}
}
if (updateData.phone && updateData.phone !== existingUser.phone) {
const phoneExists = await this.usersRepository.findOne({
where: { phone: updateData.phone }
});
if (phoneExists) {
throw new ConflictException('手机号已存在');
}
}
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
const githubExists = await this.usersRepository.findOne({
where: { github_id: updateData.github_id }
});
if (githubExists) {
throw new ConflictException('GitHub ID已存在');
}
}
// 更新用户数据
Object.assign(existingUser, updateData);
const startTime = Date.now();
return await this.usersRepository.save(existingUser);
this.logger.log('开始更新用户信息', {
operation: 'update',
userId: id.toString(),
updateFields: Object.keys(updateData),
timestamp: new Date().toISOString()
});
try {
// 1. 检查用户是否存在 - 确保要更新的用户确实存在
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已存在');
}
}
// 3. 合并更新数据 - 使用Object.assign将新数据合并到现有实体
Object.assign(existingUser, updateData);
// 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,
timestamp: new Date().toISOString()
});
return updatedUser;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof NotFoundException || error instanceof ConflictException) {
throw error;
}
this.logger.error('用户更新系统异常', {
operation: 'update',
userId: id.toString(),
updateData,
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException('用户更新失败,请稍后重试');
}
}
/**
* 删除用户
*
* @param id 用户ID
* @returns 删除操作结果
* 功能描述:
* 物理删除指定的用户记录,数据将从数据库中永久移除
*
* 业务逻辑:
* 1. 验证用户是否存在
* 2. 执行物理删除操作
* 3. 返回删除结果统计
* 4. 记录删除操作日志
*
* 注意事项:
* - 这是物理删除,数据无法恢复
* - 如需保留数据,请使用 softRemove 方法
* - 删除前请确认用户没有关联的重要数据
*
* @param id 用户ID必须是有效的已存在用户
* @returns 删除操作结果,包含影响行数和操作消息
* @throws NotFoundException 当用户不存在时
*
* @example
* ```typescript
* const result = await usersService.remove(BigInt(1));
* console.log(`删除了 ${result.affected} 个用户`);
* ```
*/
async remove(id: bigint): Promise<{ affected: number; message: string }> {
// 检查用户是否存在
await this.findOne(id);
const startTime = Date.now();
this.logger.log('开始删除用户', {
operation: 'remove',
userId: id.toString(),
timestamp: new Date().toISOString()
});
// 执行删除 - 使用where条件来处理bigint类型
const result = await this.usersRepository.delete({ id });
try {
// 1. 检查用户是否存在 - 确保要删除的用户确实存在
await this.findOne(id);
return {
affected: result.affected || 0,
message: `成功删除ID为 ${id} 的用户`
};
// 2. 执行删除操作 - 使用where条件来处理bigint类型
const result = await this.usersRepository.delete({ id });
const deleteResult = {
affected: result.affected || 0,
message: `成功删除ID为 ${id} 的用户`
};
const duration = Date.now() - startTime;
this.logger.log('用户删除成功', {
operation: 'remove',
userId: id.toString(),
affected: deleteResult.affected,
duration,
timestamp: new Date().toISOString()
});
return deleteResult;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof NotFoundException) {
throw error;
}
this.logger.error('用户删除系统异常', {
operation: 'remove',
userId: id.toString(),
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException('用户删除失败,请稍后重试');
}
}
/**
@@ -328,18 +590,83 @@ export class UsersService {
/**
* 搜索用户(根据用户名或昵称)
*
* @param keyword 搜索关键词
* @param limit 限制数量
* @returns 用户列表
* 功能描述:
* 根据关键词在用户名和昵称字段中进行模糊搜索,支持部分匹配
*
* 业务逻辑:
* 1. 使用QueryBuilder构建复杂查询
* 2. 对用户名和昵称字段进行LIKE模糊匹配
* 3. 按创建时间倒序排列结果
* 4. 限制返回数量防止性能问题
*
* 性能考虑:
* - 使用数据库索引优化查询性能
* - 限制返回数量避免大数据量问题
* - 建议在用户名和昵称字段上建立索引
*
* @param keyword 搜索关键词,支持中文、英文、数字等字符
* @param limit 限制数量默认20条建议不超过100
* @returns 匹配的用户列表,按创建时间倒序排列
*
* @example
* ```typescript
* // 搜索包含"张三"的用户
* const users = await usersService.search('张三', 10);
*
* // 搜索包含"admin"的用户
* const adminUsers = await usersService.search('admin');
* ```
*/
async search(keyword: string, limit: number = 20): Promise<Users[]> {
return await this.usersRepository
.createQueryBuilder('user')
.where('user.username LIKE :keyword OR user.nickname LIKE :keyword', {
keyword: `%${keyword}%`
})
.orderBy('user.created_at', 'DESC')
.limit(limit)
.getMany();
const startTime = Date.now();
this.logger.log('开始搜索用户', {
operation: 'search',
keyword,
limit,
timestamp: new Date().toISOString()
});
try {
// 1. 构建查询 - 使用QueryBuilder支持复杂的WHERE条件
const queryBuilder = this.usersRepository.createQueryBuilder('user');
// 2. 添加搜索条件 - 在用户名和昵称中进行模糊匹配
// 使用参数化查询防止SQL注入攻击
const result = await queryBuilder
.where('user.username LIKE :keyword OR user.nickname LIKE :keyword', {
keyword: `%${keyword}%` // 前后加%实现模糊匹配
})
.orderBy('user.created_at', 'DESC') // 按创建时间倒序
.limit(limit) // 限制返回数量
.getMany();
const duration = Date.now() - startTime;
this.logger.log('用户搜索完成', {
operation: 'search',
keyword,
limit,
resultCount: result.length,
duration,
timestamp: new Date().toISOString()
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('用户搜索异常', {
operation: 'search',
keyword,
limit,
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
// 搜索失败时返回空数组,不影响用户体验
return [];
}
}
}