service:完善用户服务功能
- 优化用户查询和管理逻辑 - 更新相关单元测试 - 完善内存模式用户服务实现
This commit is contained in:
@@ -1,20 +1,42 @@
|
|||||||
/**
|
/**
|
||||||
* 用户实体、DTO和服务的完整测试套件
|
* 用户实体、DTO和服务的完整测试套件
|
||||||
*
|
*
|
||||||
* 功能:
|
* 功能描述:
|
||||||
* - 测试Users实体的结构和装饰器
|
* - 测试Users实体的结构和装饰器配置
|
||||||
* - 测试CreateUserDto的验证规则
|
* - 测试CreateUserDto的数据验证规则和边界条件
|
||||||
* - 测试UsersService的所有CRUD操作
|
* - 测试UsersService的所有CRUD操作和业务逻辑
|
||||||
* - 验证数据类型和约束条件
|
* - 验证数据类型、约束条件和异常处理
|
||||||
|
* - 确保服务层与数据库交互的正确性
|
||||||
|
*
|
||||||
|
* 测试覆盖范围:
|
||||||
|
* - 实体字段映射和类型验证
|
||||||
|
* - DTO数据验证和错误处理
|
||||||
|
* - 服务方法的正常流程和异常流程
|
||||||
|
* - 数据库操作的模拟和验证
|
||||||
|
* - 业务规则和约束条件检查
|
||||||
|
*
|
||||||
|
* 测试策略:
|
||||||
|
* - 单元测试:独立测试每个方法的功能
|
||||||
|
* - 集成测试:测试DTO到Entity的完整流程
|
||||||
|
* - 异常测试:验证各种错误情况的处理
|
||||||
|
* - 边界测试:测试数据验证的边界条件
|
||||||
|
*
|
||||||
|
* 依赖模块:
|
||||||
|
* - Jest: 测试框架和断言库
|
||||||
|
* - NestJS Testing: 提供测试模块和依赖注入
|
||||||
|
* - class-validator: DTO验证测试
|
||||||
|
* - TypeORM: 数据库操作模拟
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
* @since 2025-12-17
|
* @since 2025-12-17
|
||||||
|
*
|
||||||
|
* @lastModified 2025-01-07 by moyin
|
||||||
|
* @lastChange 添加完整的测试注释体系,增强测试覆盖率和方法测试
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { Repository, DataSource } from 'typeorm';
|
|
||||||
import { ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
import { ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
import { plainToClass } from 'class-transformer';
|
import { plainToClass } from 'class-transformer';
|
||||||
@@ -25,10 +47,21 @@ import { UsersService } from './users.service';
|
|||||||
|
|
||||||
describe('Users Entity, DTO and Service Tests', () => {
|
describe('Users Entity, DTO and Service Tests', () => {
|
||||||
let service: UsersService;
|
let service: UsersService;
|
||||||
let repository: Repository<Users>;
|
|
||||||
let module: TestingModule;
|
let module: TestingModule;
|
||||||
|
|
||||||
// 模拟的Repository方法
|
/**
|
||||||
|
* 模拟的TypeORM Repository方法
|
||||||
|
*
|
||||||
|
* 功能:模拟数据库操作,避免真实数据库依赖
|
||||||
|
* 包含的方法:
|
||||||
|
* - save: 保存实体到数据库
|
||||||
|
* - find: 查询多个实体
|
||||||
|
* - findOne: 查询单个实体
|
||||||
|
* - delete: 删除实体
|
||||||
|
* - softRemove: 软删除实体
|
||||||
|
* - count: 统计实体数量
|
||||||
|
* - createQueryBuilder: 创建查询构建器
|
||||||
|
*/
|
||||||
const mockRepository = {
|
const mockRepository = {
|
||||||
save: jest.fn(),
|
save: jest.fn(),
|
||||||
find: jest.fn(),
|
find: jest.fn(),
|
||||||
@@ -39,22 +72,45 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
createQueryBuilder: jest.fn(),
|
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 = {
|
const mockUser: Users = {
|
||||||
id: BigInt(1),
|
id: BigInt(1),
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
|
email_verified: false,
|
||||||
phone: '+8613800138000',
|
phone: '+8613800138000',
|
||||||
password_hash: 'hashed_password',
|
password_hash: 'hashed_password',
|
||||||
nickname: '测试用户',
|
nickname: '测试用户',
|
||||||
github_id: 'github_123',
|
github_id: 'github_123',
|
||||||
avatar_url: 'https://example.com/avatar.jpg',
|
avatar_url: 'https://example.com/avatar.jpg',
|
||||||
role: 1,
|
role: 1,
|
||||||
email_verified: false,
|
status: 'active' as any, // UserStatus.ACTIVE
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试用的创建用户DTO数据
|
||||||
|
*
|
||||||
|
* 包含创建用户所需的基本字段:
|
||||||
|
* - 必填字段:username, nickname
|
||||||
|
* - 可选字段:email, phone, password_hash, github_id, avatar_url, role
|
||||||
|
*
|
||||||
|
* 用于测试:
|
||||||
|
* - 数据验证规则
|
||||||
|
* - 用户创建流程
|
||||||
|
* - DTO到Entity的转换
|
||||||
|
*/
|
||||||
const createUserDto: CreateUserDto = {
|
const createUserDto: CreateUserDto = {
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
@@ -66,6 +122,17 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
role: 1
|
role: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试前置设置
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 创建测试模块和依赖注入容器
|
||||||
|
* - 配置UsersService和模拟的Repository
|
||||||
|
* - 初始化测试环境
|
||||||
|
* - 清理之前测试的Mock状态
|
||||||
|
*
|
||||||
|
* 执行时机:每个测试用例执行前
|
||||||
|
*/
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
module = await Test.createTestingModule({
|
module = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -78,17 +145,47 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<UsersService>(UsersService);
|
service = module.get<UsersService>(UsersService);
|
||||||
repository = module.get<Repository<Users>>(getRepositoryToken(Users));
|
|
||||||
|
|
||||||
// 清理所有mock
|
// 清理所有mock状态,确保测试独立性
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试后置清理
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 关闭测试模块
|
||||||
|
* - 释放资源和内存
|
||||||
|
* - 防止测试间的状态污染
|
||||||
|
*
|
||||||
|
* 执行时机:每个测试用例执行后
|
||||||
|
*/
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await module.close();
|
await module.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Users实体测试组
|
||||||
|
*
|
||||||
|
* 测试目标:
|
||||||
|
* - 验证Users实体类的基本功能
|
||||||
|
* - 测试实体字段的设置和获取
|
||||||
|
* - 确保实体结构符合设计要求
|
||||||
|
*
|
||||||
|
* 测试内容:
|
||||||
|
* - 实体实例化和属性赋值
|
||||||
|
* - 字段类型和数据完整性
|
||||||
|
* - TypeORM装饰器的正确配置
|
||||||
|
*/
|
||||||
describe('Users Entity Tests', () => {
|
describe('Users Entity Tests', () => {
|
||||||
|
/**
|
||||||
|
* 测试用户实体的基本创建和属性设置
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - 实体可以正常实例化
|
||||||
|
* - 所有字段可以正确赋值
|
||||||
|
* - 字段值可以正确读取
|
||||||
|
*/
|
||||||
it('应该正确创建用户实体实例', () => {
|
it('应该正确创建用户实体实例', () => {
|
||||||
const user = new Users();
|
const user = new Users();
|
||||||
user.username = 'testuser';
|
user.username = 'testuser';
|
||||||
@@ -136,7 +233,31 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateUserDto数据验证测试组
|
||||||
|
*
|
||||||
|
* 测试目标:
|
||||||
|
* - 验证DTO的数据验证规则
|
||||||
|
* - 测试各种输入数据的验证结果
|
||||||
|
* - 确保数据完整性和业务规则
|
||||||
|
*
|
||||||
|
* 测试内容:
|
||||||
|
* - 有效数据的验证通过
|
||||||
|
* - 无效数据的验证失败
|
||||||
|
* - 必填字段的验证
|
||||||
|
* - 格式验证(邮箱、手机号等)
|
||||||
|
* - 长度限制验证
|
||||||
|
* - 数值范围验证
|
||||||
|
*/
|
||||||
describe('CreateUserDto Validation Tests', () => {
|
describe('CreateUserDto Validation Tests', () => {
|
||||||
|
/**
|
||||||
|
* 测试有效数据的验证
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - 包含所有必填字段的数据应该通过验证
|
||||||
|
* - 可选字段的数据格式正确
|
||||||
|
* - 验证错误数组应该为空
|
||||||
|
*/
|
||||||
it('应该通过有效数据的验证', async () => {
|
it('应该通过有效数据的验证', async () => {
|
||||||
const validData = {
|
const validData = {
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
@@ -155,6 +276,14 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试缺少必填字段时的验证失败
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - 缺少username和nickname时验证应该失败
|
||||||
|
* - 验证错误数组包含对应的字段错误
|
||||||
|
* - 错误信息准确指向缺失的字段
|
||||||
|
*/
|
||||||
it('应该拒绝缺少必填字段的数据', async () => {
|
it('应该拒绝缺少必填字段的数据', async () => {
|
||||||
const invalidData = {
|
const invalidData = {
|
||||||
email: 'test@example.com'
|
email: 'test@example.com'
|
||||||
@@ -173,6 +302,14 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
expect(nicknameError).toBeDefined();
|
expect(nicknameError).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试邮箱格式验证
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - 无效的邮箱格式应该被拒绝
|
||||||
|
* - 验证错误指向email字段
|
||||||
|
* - 错误信息提示格式不正确
|
||||||
|
*/
|
||||||
it('应该拒绝无效的邮箱格式', async () => {
|
it('应该拒绝无效的邮箱格式', async () => {
|
||||||
const invalidData = {
|
const invalidData = {
|
||||||
username: 'testuser',
|
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', () => {
|
describe('UsersService CRUD Tests', () => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create()方法测试组
|
||||||
|
*
|
||||||
|
* 测试目标:
|
||||||
|
* - 验证基础用户创建功能
|
||||||
|
* - 测试数据验证和异常处理
|
||||||
|
* - 确保数据库操作的正确性
|
||||||
|
*/
|
||||||
describe('create()', () => {
|
describe('create()', () => {
|
||||||
|
/**
|
||||||
|
* 测试成功创建用户的正常流程
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - Repository.save方法被正确调用
|
||||||
|
* - 返回值与期望的用户数据一致
|
||||||
|
* - 数据验证通过
|
||||||
|
*/
|
||||||
it('应该成功创建新用户', async () => {
|
it('应该成功创建新用户', async () => {
|
||||||
mockRepository.findOne.mockResolvedValue(null); // 没有重复用户
|
|
||||||
mockRepository.save.mockResolvedValue(mockUser);
|
mockRepository.save.mockResolvedValue(mockUser);
|
||||||
|
|
||||||
const result = await service.create(createUserDto);
|
const result = await service.create(createUserDto);
|
||||||
@@ -228,18 +400,17 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
expect(result).toEqual(mockUser);
|
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 () => {
|
it('应该在数据验证失败时抛出BadRequestException', async () => {
|
||||||
const invalidDto = { username: '', nickname: '' }; // 无效数据
|
const invalidDto = { username: '', nickname: '' }; // 无效数据
|
||||||
|
|
||||||
await expect(service.create(invalidDto as CreateUserDto)).rejects.toThrow(BadRequestException);
|
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()', () => {
|
describe('findAll()', () => {
|
||||||
@@ -310,17 +481,126 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findByEmail()', () => {
|
describe('findByGithubId()', () => {
|
||||||
it('应该根据邮箱返回用户', async () => {
|
it('应该根据GitHub ID返回用户', async () => {
|
||||||
mockRepository.findOne.mockResolvedValue(mockUser);
|
mockRepository.findOne.mockResolvedValue(mockUser);
|
||||||
|
|
||||||
const result = await service.findByEmail('test@example.com');
|
const result = await service.findByGithubId('github_123');
|
||||||
|
|
||||||
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
||||||
where: { email: 'test@example.com' }
|
where: { github_id: 'github_123' }
|
||||||
});
|
});
|
||||||
expect(result).toEqual(mockUser);
|
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()', () => {
|
describe('update()', () => {
|
||||||
@@ -435,7 +715,29 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 集成测试组
|
||||||
|
*
|
||||||
|
* 测试目标:
|
||||||
|
* - 验证DTO到Entity的完整数据流
|
||||||
|
* - 测试组件间的协作和集成
|
||||||
|
* - 确保端到端流程的正确性
|
||||||
|
*
|
||||||
|
* 测试内容:
|
||||||
|
* - DTO验证 → 实体创建 → 数据库保存的完整流程
|
||||||
|
* - 可选字段的默认值处理
|
||||||
|
* - 数据转换和映射的正确性
|
||||||
|
*/
|
||||||
describe('Integration Tests', () => {
|
describe('Integration Tests', () => {
|
||||||
|
/**
|
||||||
|
* 测试从DTO到Entity的完整数据流
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - DTO验证成功
|
||||||
|
* - 数据正确转换为Entity
|
||||||
|
* - 服务方法正确处理数据
|
||||||
|
* - 返回结果符合预期
|
||||||
|
*/
|
||||||
it('应该完成从DTO到Entity的完整流程', async () => {
|
it('应该完成从DTO到Entity的完整流程', async () => {
|
||||||
// 1. 验证DTO
|
// 1. 验证DTO
|
||||||
const dto = plainToClass(CreateUserDto, createUserDto);
|
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||||
@@ -474,19 +776,76 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误处理测试组
|
||||||
|
*
|
||||||
|
* 测试目标:
|
||||||
|
* - 验证各种异常情况的处理
|
||||||
|
* - 测试错误恢复和降级机制
|
||||||
|
* - 确保系统的健壮性和稳定性
|
||||||
|
*
|
||||||
|
* 测试内容:
|
||||||
|
* - 数据库连接错误处理
|
||||||
|
* - 并发操作冲突处理
|
||||||
|
* - 系统异常的统一处理
|
||||||
|
* - 搜索异常的降级处理
|
||||||
|
* - 各种操作失败的异常抛出
|
||||||
|
*
|
||||||
|
* 异常处理策略:
|
||||||
|
* - 业务异常:直接抛出对应的HTTP异常
|
||||||
|
* - 系统异常:转换为BadRequestException
|
||||||
|
* - 搜索异常:返回空结果而不抛出异常
|
||||||
|
*/
|
||||||
describe('Error Handling Tests', () => {
|
describe('Error Handling Tests', () => {
|
||||||
|
/**
|
||||||
|
* 测试数据库连接错误的处理
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - 数据库操作失败时抛出正确的异常
|
||||||
|
* - 异常类型为BadRequestException
|
||||||
|
* - 错误信息被正确记录
|
||||||
|
*/
|
||||||
it('应该正确处理数据库连接错误', async () => {
|
it('应该正确处理数据库连接错误', async () => {
|
||||||
mockRepository.save.mockRejectedValue(new Error('Database connection failed'));
|
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 () => {
|
it('应该正确处理并发创建冲突', async () => {
|
||||||
// 模拟并发情况:检查时不存在,保存时出现唯一约束错误
|
// 模拟并发情况:检查时不存在,保存时出现唯一约束错误
|
||||||
mockRepository.findOne.mockResolvedValue(null);
|
|
||||||
mockRepository.save.mockRejectedValue(new Error('Duplicate entry'));
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -9,9 +9,12 @@
|
|||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
* @since 2025-12-17
|
* @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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||||
import { Users } from './users.entity';
|
import { Users } from './users.entity';
|
||||||
@@ -22,6 +25,8 @@ import { plainToClass } from 'class-transformer';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
|
private readonly logger = new Logger(UsersService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Users)
|
@InjectRepository(Users)
|
||||||
private readonly usersRepository: Repository<Users>,
|
private readonly usersRepository: Repository<Users>,
|
||||||
@@ -35,6 +40,16 @@ export class UsersService {
|
|||||||
* @throws BadRequestException 当数据验证失败时
|
* @throws BadRequestException 当数据验证失败时
|
||||||
*/
|
*/
|
||||||
async create(createUserDto: CreateUserDto): Promise<Users> {
|
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log('开始创建用户', {
|
||||||
|
operation: 'create',
|
||||||
|
username: createUserDto.username,
|
||||||
|
email: createUserDto.email,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
// 验证DTO
|
// 验证DTO
|
||||||
const dto = plainToClass(CreateUserDto, createUserDto);
|
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||||
const validationErrors = await validate(dto);
|
const validationErrors = await validate(dto);
|
||||||
@@ -43,6 +58,14 @@ export class UsersService {
|
|||||||
const errorMessages = validationErrors.map(error =>
|
const errorMessages = validationErrors.map(error =>
|
||||||
Object.values(error.constraints || {}).join(', ')
|
Object.values(error.constraints || {}).join(', ')
|
||||||
).join('; ');
|
).join('; ');
|
||||||
|
|
||||||
|
this.logger.warn('用户创建失败:数据验证失败', {
|
||||||
|
operation: 'create',
|
||||||
|
username: createUserDto.username,
|
||||||
|
email: createUserDto.email,
|
||||||
|
validationErrors: errorMessages
|
||||||
|
});
|
||||||
|
|
||||||
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +83,38 @@ export class UsersService {
|
|||||||
user.status = createUserDto.status || UserStatus.ACTIVE;
|
user.status = createUserDto.status || UserStatus.ACTIVE;
|
||||||
|
|
||||||
// 保存到数据库
|
// 保存到数据库
|
||||||
return await this.usersRepository.save(user);
|
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('用户创建失败,请稍后重试');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,12 +126,29 @@ export class UsersService {
|
|||||||
* @throws BadRequestException 当数据验证失败时
|
* @throws BadRequestException 当数据验证失败时
|
||||||
*/
|
*/
|
||||||
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
|
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
// 检查用户名是否已存在
|
// 检查用户名是否已存在
|
||||||
if (createUserDto.username) {
|
if (createUserDto.username) {
|
||||||
const existingUser = await this.usersRepository.findOne({
|
const existingUser = await this.usersRepository.findOne({
|
||||||
where: { username: createUserDto.username }
|
where: { username: createUserDto.username }
|
||||||
});
|
});
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
|
this.logger.warn('用户创建失败:用户名已存在', {
|
||||||
|
operation: 'createWithDuplicateCheck',
|
||||||
|
username: createUserDto.username,
|
||||||
|
existingUserId: existingUser.id.toString()
|
||||||
|
});
|
||||||
throw new ConflictException('用户名已存在');
|
throw new ConflictException('用户名已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +159,11 @@ export class UsersService {
|
|||||||
where: { email: createUserDto.email }
|
where: { email: createUserDto.email }
|
||||||
});
|
});
|
||||||
if (existingEmail) {
|
if (existingEmail) {
|
||||||
|
this.logger.warn('用户创建失败:邮箱已存在', {
|
||||||
|
operation: 'createWithDuplicateCheck',
|
||||||
|
email: createUserDto.email,
|
||||||
|
existingUserId: existingEmail.id.toString()
|
||||||
|
});
|
||||||
throw new ConflictException('邮箱已存在');
|
throw new ConflictException('邮箱已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,6 +174,11 @@ export class UsersService {
|
|||||||
where: { phone: createUserDto.phone }
|
where: { phone: createUserDto.phone }
|
||||||
});
|
});
|
||||||
if (existingPhone) {
|
if (existingPhone) {
|
||||||
|
this.logger.warn('用户创建失败:手机号已存在', {
|
||||||
|
operation: 'createWithDuplicateCheck',
|
||||||
|
phone: createUserDto.phone,
|
||||||
|
existingUserId: existingPhone.id.toString()
|
||||||
|
});
|
||||||
throw new ConflictException('手机号已存在');
|
throw new ConflictException('手机号已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,12 +189,47 @@ export class UsersService {
|
|||||||
where: { github_id: createUserDto.github_id }
|
where: { github_id: createUserDto.github_id }
|
||||||
});
|
});
|
||||||
if (existingGithub) {
|
if (existingGithub) {
|
||||||
|
this.logger.warn('用户创建失败:GitHub ID已存在', {
|
||||||
|
operation: 'createWithDuplicateCheck',
|
||||||
|
github_id: createUserDto.github_id,
|
||||||
|
existingUserId: existingGithub.id.toString()
|
||||||
|
});
|
||||||
throw new ConflictException('GitHub ID已存在');
|
throw new ConflictException('GitHub ID已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用普通的创建方法
|
// 调用普通的创建方法
|
||||||
return await this.create(createUserDto);
|
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 更新后的用户实体
|
* @returns 更新后的用户实体
|
||||||
* @throws NotFoundException 当用户不存在时
|
* @throws NotFoundException 当用户不存在时
|
||||||
* @throws ConflictException 当更新的数据与其他用户冲突时
|
* @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> {
|
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
|
||||||
// 检查用户是否存在
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log('开始更新用户信息', {
|
||||||
|
operation: 'update',
|
||||||
|
userId: id.toString(),
|
||||||
|
updateFields: Object.keys(updateData),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检查用户是否存在 - 确保要更新的用户确实存在
|
||||||
const existingUser = await this.findOne(id);
|
const existingUser = await this.findOne(id);
|
||||||
|
|
||||||
// 检查更新数据的唯一性约束
|
// 2. 检查更新数据的唯一性约束 - 防止违反数据库唯一约束
|
||||||
|
|
||||||
|
// 2.1 检查用户名唯一性 - 只有当用户名确实发生变化时才检查
|
||||||
if (updateData.username && updateData.username !== existingUser.username) {
|
if (updateData.username && updateData.username !== existingUser.username) {
|
||||||
const usernameExists = await this.usersRepository.findOne({
|
const usernameExists = await this.usersRepository.findOne({
|
||||||
where: { username: updateData.username }
|
where: { username: updateData.username }
|
||||||
});
|
});
|
||||||
if (usernameExists) {
|
if (usernameExists) {
|
||||||
|
this.logger.warn('用户更新失败:用户名已存在', {
|
||||||
|
operation: 'update',
|
||||||
|
userId: id.toString(),
|
||||||
|
conflictUsername: updateData.username,
|
||||||
|
existingUserId: usernameExists.id.toString()
|
||||||
|
});
|
||||||
throw new ConflictException('用户名已存在');
|
throw new ConflictException('用户名已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2.2 检查邮箱唯一性 - 只有当邮箱确实发生变化时才检查
|
||||||
if (updateData.email && updateData.email !== existingUser.email) {
|
if (updateData.email && updateData.email !== existingUser.email) {
|
||||||
const emailExists = await this.usersRepository.findOne({
|
const emailExists = await this.usersRepository.findOne({
|
||||||
where: { email: updateData.email }
|
where: { email: updateData.email }
|
||||||
});
|
});
|
||||||
if (emailExists) {
|
if (emailExists) {
|
||||||
|
this.logger.warn('用户更新失败:邮箱已存在', {
|
||||||
|
operation: 'update',
|
||||||
|
userId: id.toString(),
|
||||||
|
conflictEmail: updateData.email,
|
||||||
|
existingUserId: emailExists.id.toString()
|
||||||
|
});
|
||||||
throw new ConflictException('邮箱已存在');
|
throw new ConflictException('邮箱已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2.3 检查手机号唯一性 - 只有当手机号确实发生变化时才检查
|
||||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||||
const phoneExists = await this.usersRepository.findOne({
|
const phoneExists = await this.usersRepository.findOne({
|
||||||
where: { phone: updateData.phone }
|
where: { phone: updateData.phone }
|
||||||
});
|
});
|
||||||
if (phoneExists) {
|
if (phoneExists) {
|
||||||
|
this.logger.warn('用户更新失败:手机号已存在', {
|
||||||
|
operation: 'update',
|
||||||
|
userId: id.toString(),
|
||||||
|
conflictPhone: updateData.phone,
|
||||||
|
existingUserId: phoneExists.id.toString()
|
||||||
|
});
|
||||||
throw new ConflictException('手机号已存在');
|
throw new ConflictException('手机号已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2.4 检查GitHub ID唯一性 - 只有当GitHub ID确实发生变化时才检查
|
||||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||||
const githubExists = await this.usersRepository.findOne({
|
const githubExists = await this.usersRepository.findOne({
|
||||||
where: { github_id: updateData.github_id }
|
where: { github_id: updateData.github_id }
|
||||||
});
|
});
|
||||||
if (githubExists) {
|
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已存在');
|
throw new ConflictException('GitHub ID已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户数据
|
// 3. 合并更新数据 - 使用Object.assign将新数据合并到现有实体
|
||||||
Object.assign(existingUser, updateData);
|
Object.assign(existingUser, updateData);
|
||||||
|
|
||||||
return await this.usersRepository.save(existingUser);
|
// 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 当用户不存在时
|
* @throws NotFoundException 当用户不存在时
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const result = await usersService.remove(BigInt(1));
|
||||||
|
* console.log(`删除了 ${result.affected} 个用户`);
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||||
// 检查用户是否存在
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log('开始删除用户', {
|
||||||
|
operation: 'remove',
|
||||||
|
userId: id.toString(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检查用户是否存在 - 确保要删除的用户确实存在
|
||||||
await this.findOne(id);
|
await this.findOne(id);
|
||||||
|
|
||||||
// 执行删除 - 使用where条件来处理bigint类型
|
// 2. 执行删除操作 - 使用where条件来处理bigint类型
|
||||||
const result = await this.usersRepository.delete({ id });
|
const result = await this.usersRepository.delete({ id });
|
||||||
|
|
||||||
return {
|
const deleteResult = {
|
||||||
affected: result.affected || 0,
|
affected: result.affected || 0,
|
||||||
message: `成功删除ID为 ${id} 的用户`
|
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[]> {
|
async search(keyword: string, limit: number = 20): Promise<Users[]> {
|
||||||
return await this.usersRepository
|
const startTime = Date.now();
|
||||||
.createQueryBuilder('user')
|
|
||||||
|
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', {
|
.where('user.username LIKE :keyword OR user.nickname LIKE :keyword', {
|
||||||
keyword: `%${keyword}%`
|
keyword: `%${keyword}%` // 前后加%实现模糊匹配
|
||||||
})
|
})
|
||||||
.orderBy('user.created_at', 'DESC')
|
.orderBy('user.created_at', 'DESC') // 按创建时间倒序
|
||||||
.limit(limit)
|
.limit(limit) // 限制返回数量
|
||||||
.getMany();
|
.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 [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,9 +19,12 @@
|
|||||||
* @author angjustinl
|
* @author angjustinl
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
* @since 2025-12-17
|
* @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 { Users } from './users.entity';
|
||||||
import { CreateUserDto } from './users.dto';
|
import { CreateUserDto } from './users.dto';
|
||||||
import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum';
|
import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum';
|
||||||
@@ -30,6 +33,7 @@ import { plainToClass } from 'class-transformer';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersMemoryService {
|
export class UsersMemoryService {
|
||||||
|
private readonly logger = new Logger(UsersMemoryService.name);
|
||||||
private users: Map<bigint, Users> = new Map();
|
private users: Map<bigint, Users> = new Map();
|
||||||
private currentId: bigint = BigInt(1);
|
private currentId: bigint = BigInt(1);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user