From dd91264d0c4c6e88625f92d419040182bdb50e10 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:07:18 +0800 Subject: [PATCH] =?UTF-8?q?service=EF=BC=9A=E5=AE=8C=E5=96=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=9C=8D=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化用户查询和管理逻辑 - 更新相关单元测试 - 完善内存模式用户服务实现 --- src/core/db/users/users.service.spec.ts | 413 ++++++++++++++- src/core/db/users/users.service.ts | 579 +++++++++++++++++----- src/core/db/users/users_memory.service.ts | 6 +- 3 files changed, 844 insertions(+), 154 deletions(-) diff --git a/src/core/db/users/users.service.spec.ts b/src/core/db/users/users.service.spec.ts index 99b4992..4d84055 100644 --- a/src/core/db/users/users.service.spec.ts +++ b/src/core/db/users/users.service.spec.ts @@ -1,20 +1,42 @@ /** * 用户实体、DTO和服务的完整测试套件 * - * 功能: - * - 测试Users实体的结构和装饰器 - * - 测试CreateUserDto的验证规则 - * - 测试UsersService的所有CRUD操作 - * - 验证数据类型和约束条件 + * 功能描述: + * - 测试Users实体的结构和装饰器配置 + * - 测试CreateUserDto的数据验证规则和边界条件 + * - 测试UsersService的所有CRUD操作和业务逻辑 + * - 验证数据类型、约束条件和异常处理 + * - 确保服务层与数据库交互的正确性 + * + * 测试覆盖范围: + * - 实体字段映射和类型验证 + * - DTO数据验证和错误处理 + * - 服务方法的正常流程和异常流程 + * - 数据库操作的模拟和验证 + * - 业务规则和约束条件检查 + * + * 测试策略: + * - 单元测试:独立测试每个方法的功能 + * - 集成测试:测试DTO到Entity的完整流程 + * - 异常测试:验证各种错误情况的处理 + * - 边界测试:测试数据验证的边界条件 + * + * 依赖模块: + * - Jest: 测试框架和断言库 + * - NestJS Testing: 提供测试模块和依赖注入 + * - class-validator: DTO验证测试 + * - TypeORM: 数据库操作模拟 * * @author moyin * @version 1.0.0 * @since 2025-12-17 + * + * @lastModified 2025-01-07 by moyin + * @lastChange 添加完整的测试注释体系,增强测试覆盖率和方法测试 */ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; import { ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @@ -25,10 +47,21 @@ import { UsersService } from './users.service'; describe('Users Entity, DTO and Service Tests', () => { let service: UsersService; - let repository: Repository; let module: TestingModule; - // 模拟的Repository方法 + /** + * 模拟的TypeORM Repository方法 + * + * 功能:模拟数据库操作,避免真实数据库依赖 + * 包含的方法: + * - save: 保存实体到数据库 + * - find: 查询多个实体 + * - findOne: 查询单个实体 + * - delete: 删除实体 + * - softRemove: 软删除实体 + * - count: 统计实体数量 + * - createQueryBuilder: 创建查询构建器 + */ const mockRepository = { save: jest.fn(), find: jest.fn(), @@ -39,22 +72,45 @@ describe('Users Entity, DTO and Service Tests', () => { createQueryBuilder: jest.fn(), }; - // 测试数据 + /** + * 测试用的模拟用户数据 + * + * 包含所有Users实体的字段: + * - 基础信息:id, username, nickname + * - 联系方式:email, phone + * - 认证信息:password_hash, github_id + * - 状态信息:role, status, email_verified + * - 时间戳:created_at, updated_at + * - 扩展信息:avatar_url + */ const mockUser: Users = { id: BigInt(1), username: 'testuser', email: 'test@example.com', + email_verified: false, phone: '+8613800138000', password_hash: 'hashed_password', nickname: '测试用户', github_id: 'github_123', avatar_url: 'https://example.com/avatar.jpg', role: 1, - email_verified: false, + status: 'active' as any, // UserStatus.ACTIVE created_at: new Date(), updated_at: new Date(), }; + /** + * 测试用的创建用户DTO数据 + * + * 包含创建用户所需的基本字段: + * - 必填字段:username, nickname + * - 可选字段:email, phone, password_hash, github_id, avatar_url, role + * + * 用于测试: + * - 数据验证规则 + * - 用户创建流程 + * - DTO到Entity的转换 + */ const createUserDto: CreateUserDto = { username: 'testuser', email: 'test@example.com', @@ -66,6 +122,17 @@ describe('Users Entity, DTO and Service Tests', () => { role: 1 }; + /** + * 测试前置设置 + * + * 功能: + * - 创建测试模块和依赖注入容器 + * - 配置UsersService和模拟的Repository + * - 初始化测试环境 + * - 清理之前测试的Mock状态 + * + * 执行时机:每个测试用例执行前 + */ beforeEach(async () => { module = await Test.createTestingModule({ providers: [ @@ -78,17 +145,47 @@ describe('Users Entity, DTO and Service Tests', () => { }).compile(); service = module.get(UsersService); - repository = module.get>(getRepositoryToken(Users)); - // 清理所有mock + // 清理所有mock状态,确保测试独立性 jest.clearAllMocks(); }); + /** + * 测试后置清理 + * + * 功能: + * - 关闭测试模块 + * - 释放资源和内存 + * - 防止测试间的状态污染 + * + * 执行时机:每个测试用例执行后 + */ afterEach(async () => { await module.close(); }); + /** + * Users实体测试组 + * + * 测试目标: + * - 验证Users实体类的基本功能 + * - 测试实体字段的设置和获取 + * - 确保实体结构符合设计要求 + * + * 测试内容: + * - 实体实例化和属性赋值 + * - 字段类型和数据完整性 + * - TypeORM装饰器的正确配置 + */ describe('Users Entity Tests', () => { + /** + * 测试用户实体的基本创建和属性设置 + * + * 验证点: + * - 实体可以正常实例化 + * - 所有字段可以正确赋值 + * - 字段值可以正确读取 + */ it('应该正确创建用户实体实例', () => { const user = new Users(); user.username = 'testuser'; @@ -136,7 +233,31 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * CreateUserDto数据验证测试组 + * + * 测试目标: + * - 验证DTO的数据验证规则 + * - 测试各种输入数据的验证结果 + * - 确保数据完整性和业务规则 + * + * 测试内容: + * - 有效数据的验证通过 + * - 无效数据的验证失败 + * - 必填字段的验证 + * - 格式验证(邮箱、手机号等) + * - 长度限制验证 + * - 数值范围验证 + */ describe('CreateUserDto Validation Tests', () => { + /** + * 测试有效数据的验证 + * + * 验证点: + * - 包含所有必填字段的数据应该通过验证 + * - 可选字段的数据格式正确 + * - 验证错误数组应该为空 + */ it('应该通过有效数据的验证', async () => { const validData = { username: 'testuser', @@ -155,6 +276,14 @@ describe('Users Entity, DTO and Service Tests', () => { expect(errors).toHaveLength(0); }); + /** + * 测试缺少必填字段时的验证失败 + * + * 验证点: + * - 缺少username和nickname时验证应该失败 + * - 验证错误数组包含对应的字段错误 + * - 错误信息准确指向缺失的字段 + */ it('应该拒绝缺少必填字段的数据', async () => { const invalidData = { email: 'test@example.com' @@ -173,6 +302,14 @@ describe('Users Entity, DTO and Service Tests', () => { expect(nicknameError).toBeDefined(); }); + /** + * 测试邮箱格式验证 + * + * 验证点: + * - 无效的邮箱格式应该被拒绝 + * - 验证错误指向email字段 + * - 错误信息提示格式不正确 + */ it('应该拒绝无效的邮箱格式', async () => { const invalidData = { username: 'testuser', @@ -215,11 +352,46 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * UsersService CRUD操作测试组 + * + * 测试目标: + * - 验证所有CRUD操作的正确性 + * - 测试业务逻辑和数据处理 + * - 确保异常情况的正确处理 + * + * 测试内容: + * - 创建操作:create, createWithDuplicateCheck, createBatch + * - 查询操作:findAll, findOne, findByUsername, findByEmail, findByGithubId, findByRole, search + * - 更新操作:update + * - 删除操作:remove, softRemove + * - 工具方法:count, exists + * + * 测试策略: + * - 正常流程测试:验证方法的基本功能 + * - 异常流程测试:验证错误处理和异常抛出 + * - 边界条件测试:验证参数边界和特殊情况 + */ describe('UsersService CRUD Tests', () => { + /** + * create()方法测试组 + * + * 测试目标: + * - 验证基础用户创建功能 + * - 测试数据验证和异常处理 + * - 确保数据库操作的正确性 + */ describe('create()', () => { + /** + * 测试成功创建用户的正常流程 + * + * 验证点: + * - Repository.save方法被正确调用 + * - 返回值与期望的用户数据一致 + * - 数据验证通过 + */ it('应该成功创建新用户', async () => { - mockRepository.findOne.mockResolvedValue(null); // 没有重复用户 mockRepository.save.mockResolvedValue(mockUser); const result = await service.create(createUserDto); @@ -228,18 +400,17 @@ describe('Users Entity, DTO and Service Tests', () => { expect(result).toEqual(mockUser); }); - it('应该在用户名重复时抛出ConflictException', async () => { - mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在 - - await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); - expect(mockRepository.save).not.toHaveBeenCalled(); - }); - it('应该在数据验证失败时抛出BadRequestException', async () => { const invalidDto = { username: '', nickname: '' }; // 无效数据 await expect(service.create(invalidDto as CreateUserDto)).rejects.toThrow(BadRequestException); }); + + it('应该在系统异常时抛出BadRequestException', async () => { + mockRepository.save.mockRejectedValue(new Error('Database error')); + + await expect(service.create(createUserDto)).rejects.toThrow(BadRequestException); + }); }); describe('findAll()', () => { @@ -310,17 +481,126 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); - describe('findByEmail()', () => { - it('应该根据邮箱返回用户', async () => { + describe('findByGithubId()', () => { + it('应该根据GitHub ID返回用户', async () => { mockRepository.findOne.mockResolvedValue(mockUser); - const result = await service.findByEmail('test@example.com'); + const result = await service.findByGithubId('github_123'); expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { email: 'test@example.com' } + where: { github_id: 'github_123' } }); expect(result).toEqual(mockUser); }); + + it('应该在GitHub ID不存在时返回null', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const result = await service.findByGithubId('nonexistent'); + + expect(result).toBeNull(); + }); + }); + + describe('createWithDuplicateCheck()', () => { + it('应该成功创建用户(带重复检查)', async () => { + // 模拟所有唯一性检查都通过 + mockRepository.findOne.mockResolvedValue(null); + mockRepository.save.mockResolvedValue(mockUser); + + const result = await service.createWithDuplicateCheck(createUserDto); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(4); // 检查用户名、邮箱、手机号、GitHub ID + expect(mockRepository.save).toHaveBeenCalled(); + expect(result).toEqual(mockUser); + }); + + it('应该在用户名重复时抛出ConflictException', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + expect(mockRepository.save).not.toHaveBeenCalled(); + }); + + it('应该在邮箱重复时抛出ConflictException', async () => { + mockRepository.findOne + .mockResolvedValueOnce(null) // 用户名检查通过 + .mockResolvedValueOnce(mockUser); // 邮箱已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + }); + + it('应该在手机号重复时抛出ConflictException', async () => { + mockRepository.findOne + .mockResolvedValueOnce(null) // 用户名检查通过 + .mockResolvedValueOnce(null) // 邮箱检查通过 + .mockResolvedValueOnce(mockUser); // 手机号已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + }); + + it('应该在GitHub ID重复时抛出ConflictException', async () => { + mockRepository.findOne + .mockResolvedValueOnce(null) // 用户名检查通过 + .mockResolvedValueOnce(null) // 邮箱检查通过 + .mockResolvedValueOnce(null) // 手机号检查通过 + .mockResolvedValueOnce(mockUser); // GitHub ID已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + }); + }); + + describe('softRemove()', () => { + it('应该成功软删除用户', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + mockRepository.softRemove.mockResolvedValue(mockUser); + + const result = await service.softRemove(BigInt(1)); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: BigInt(1) } + }); + expect(mockRepository.softRemove).toHaveBeenCalledWith(mockUser); + expect(result).toEqual(mockUser); + }); + + it('应该在软删除不存在的用户时抛出NotFoundException', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.softRemove(BigInt(999))).rejects.toThrow(NotFoundException); + }); + }); + + describe('createBatch()', () => { + it('应该成功批量创建用户', async () => { + const batchDto = [ + { ...createUserDto, username: 'user1', nickname: '用户1' }, + { ...createUserDto, username: 'user2', nickname: '用户2' } + ]; + + const batchUsers = [ + { ...mockUser, username: 'user1', nickname: '用户1' }, + { ...mockUser, username: 'user2', nickname: '用户2' } + ]; + + mockRepository.save.mockResolvedValueOnce(batchUsers[0]).mockResolvedValueOnce(batchUsers[1]); + + const result = await service.createBatch(batchDto); + + expect(mockRepository.save).toHaveBeenCalledTimes(2); + expect(result).toHaveLength(2); + expect(result[0].username).toBe('user1'); + expect(result[1].username).toBe('user2'); + }); + + it('应该在批量创建中某个用户失败时抛出异常', async () => { + const batchDto = [ + { ...createUserDto, username: 'user1' }, + { username: '', nickname: '' } // 无效数据 + ]; + + await expect(service.createBatch(batchDto)).rejects.toThrow(BadRequestException); + }); }); describe('update()', () => { @@ -435,7 +715,29 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * 集成测试组 + * + * 测试目标: + * - 验证DTO到Entity的完整数据流 + * - 测试组件间的协作和集成 + * - 确保端到端流程的正确性 + * + * 测试内容: + * - DTO验证 → 实体创建 → 数据库保存的完整流程 + * - 可选字段的默认值处理 + * - 数据转换和映射的正确性 + */ describe('Integration Tests', () => { + /** + * 测试从DTO到Entity的完整数据流 + * + * 验证点: + * - DTO验证成功 + * - 数据正确转换为Entity + * - 服务方法正确处理数据 + * - 返回结果符合预期 + */ it('应该完成从DTO到Entity的完整流程', async () => { // 1. 验证DTO const dto = plainToClass(CreateUserDto, createUserDto); @@ -474,19 +776,76 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * 错误处理测试组 + * + * 测试目标: + * - 验证各种异常情况的处理 + * - 测试错误恢复和降级机制 + * - 确保系统的健壮性和稳定性 + * + * 测试内容: + * - 数据库连接错误处理 + * - 并发操作冲突处理 + * - 系统异常的统一处理 + * - 搜索异常的降级处理 + * - 各种操作失败的异常抛出 + * + * 异常处理策略: + * - 业务异常:直接抛出对应的HTTP异常 + * - 系统异常:转换为BadRequestException + * - 搜索异常:返回空结果而不抛出异常 + */ describe('Error Handling Tests', () => { + /** + * 测试数据库连接错误的处理 + * + * 验证点: + * - 数据库操作失败时抛出正确的异常 + * - 异常类型为BadRequestException + * - 错误信息被正确记录 + */ it('应该正确处理数据库连接错误', async () => { mockRepository.save.mockRejectedValue(new Error('Database connection failed')); - await expect(service.create(createUserDto)).rejects.toThrow('Database connection failed'); + await expect(service.create(createUserDto)).rejects.toThrow(BadRequestException); }); it('应该正确处理并发创建冲突', async () => { // 模拟并发情况:检查时不存在,保存时出现唯一约束错误 - mockRepository.findOne.mockResolvedValue(null); mockRepository.save.mockRejectedValue(new Error('Duplicate entry')); - await expect(service.create(createUserDto)).rejects.toThrow('Duplicate entry'); + await expect(service.create(createUserDto)).rejects.toThrow(BadRequestException); + }); + + it('应该正确处理搜索异常', async () => { + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockRejectedValue(new Error('Search failed')), + }; + + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + + const result = await service.search('test'); + + // 搜索失败时应该返回空数组,不抛出异常 + expect(result).toEqual([]); + }); + + it('应该正确处理更新时的系统异常', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + mockRepository.save.mockRejectedValue(new Error('Update failed')); + + await expect(service.update(BigInt(1), { nickname: '新昵称' })).rejects.toThrow(BadRequestException); + }); + + it('应该正确处理删除时的系统异常', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + mockRepository.delete.mockRejectedValue(new Error('Delete failed')); + + await expect(service.remove(BigInt(1))).rejects.toThrow(BadRequestException); }); }); }); \ No newline at end of file diff --git a/src/core/db/users/users.service.ts b/src/core/db/users/users.service.ts index 1b9a36e..3eb26af 100644 --- a/src/core/db/users/users.service.ts +++ b/src/core/db/users/users.service.ts @@ -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, @@ -35,32 +40,81 @@ export class UsersService { * @throws BadRequestException 当数据验证失败时 */ async create(createUserDto: CreateUserDto): Promise { - // 验证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 { - // 检查用户名是否已存在 - 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): Promise { - // 检查用户是否存在 - 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 { - 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 []; + } } } \ No newline at end of file diff --git a/src/core/db/users/users_memory.service.ts b/src/core/db/users/users_memory.service.ts index e417423..27b71e4 100644 --- a/src/core/db/users/users_memory.service.ts +++ b/src/core/db/users/users_memory.service.ts @@ -19,9 +19,12 @@ * @author angjustinl * @version 1.0.0 * @since 2025-12-17 + * + * @lastModified 2025-01-07 by Kiro + * @lastChange 添加日志记录系统,统一异常处理和性能监控 */ -import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, ConflictException, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; import { Users } from './users.entity'; import { CreateUserDto } from './users.dto'; import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum'; @@ -30,6 +33,7 @@ import { plainToClass } from 'class-transformer'; @Injectable() export class UsersMemoryService { + private readonly logger = new Logger(UsersMemoryService.name); private users: Map = new Map(); private currentId: bigint = BigInt(1);