diff --git a/src/core/db/users/users.dto.ts b/src/core/db/users/users.dto.ts new file mode 100644 index 0000000..ec9f493 --- /dev/null +++ b/src/core/db/users/users.dto.ts @@ -0,0 +1,59 @@ +// src/user/dto/create-user.dto.ts +import { + IsString, + IsEmail, + IsPhoneNumber, + IsInt, + Min, + Max, + IsOptional, + Length, + IsNotEmpty +} from 'class-validator' + +export class CreateUserDto { + // 用户名:必填、字符串、长度1-50 + @IsString() + @IsNotEmpty({ message: '用户名不能为空' }) + @Length(1, 50, { message: '用户名长度需在1-50字符之间' }) + username: string; + + // 邮箱:可选、合法邮箱格式 + @IsOptional() + @IsEmail({}, { message: '邮箱格式不正确' }) + email?: string; + + // 手机号:可选、合法手机号格式(支持全球号码) + @IsOptional() + @IsPhoneNumber(null, { message: '手机号格式不正确' }) + phone?: string; + + // 密码哈希:可选(OAuth登录为空) + @IsOptional() + @IsString({ message: '密码哈希必须是字符串' }) + password_hash?: string; + + // 昵称:必填、字符串、长度1-50 + @IsString() + @IsNotEmpty({ message: '昵称不能为空' }) + @Length(1, 50, { message: '昵称长度需在1-50字符之间' }) + nickname: string; + + // GitHub ID:可选、字符串、长度1-100 + @IsOptional() + @IsString({ message: 'GitHub ID必须是字符串' }) + @Length(1, 100, { message: 'GitHub ID长度需在1-100字符之间' }) + github_id?: string; + + // 头像URL:可选、字符串 + @IsOptional() + @IsString({ message: '头像URL必须是字符串' }) + avatar_url?: string; + + // 角色:可选、数字、1(普通)或9(管理员) + @IsOptional() + @IsInt({ message: '角色必须是数字' }) + @Min(1, { message: '角色值最小为1' }) + @Max(9, { message: '角色值最大为9' }) + role?: number = 1; // 默认普通用户 +} \ No newline at end of file diff --git a/src/core/db/users/users.entity.ts b/src/core/db/users/users.entity.ts new file mode 100644 index 0000000..a8a29b7 --- /dev/null +++ b/src/core/db/users/users.entity.ts @@ -0,0 +1,106 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('users') // 对应数据库表名 +export class Users { + // id:bigint、主键、非空、唯一、自增 + @PrimaryGeneratedColumn({ + type: 'bigint', + comment: '主键ID' + }) + id: bigint; + + // username:varchar(50)、非空、唯一 + @Column({ + type: 'varchar', + length: 50, + nullable: false, + unique: true, + comment: '唯一用户名/登录名' + }) + username: string; + + // email:varchar(100)、允许空、唯一 + @Column({ + type: 'varchar', + length: 100, + nullable: true, + unique: true, + comment: '邮箱(用于找回/通知)' + }) + email: string; + + // phone:varchar(30)、允许空、唯一 + @Column({ + type: 'varchar', + length: 30, + nullable: true, + unique: true, + comment: '全球电话号码(用于找回/通知)' + }) + phone: string; + + // password_hash:varchar(255)、允许空 + @Column({ + type: 'varchar', + length: 255, + nullable: true, + comment: '密码哈希(OAuth登录为空)' + }) + password_hash: string; + + // nickname:varchar(50)、非空 + @Column({ + type: 'varchar', + length: 50, + nullable: false, + comment: '显示昵称(头顶显示)' + }) + nickname: string; + + // github_id:varchar(100)、允许空、唯一 + @Column({ + type: 'varchar', + length: 100, + nullable: true, + unique: true, + comment: 'GitHub OpenID(第三方登录用)' + }) + github_id: string; + + // avatar_url:varchar(255)、允许空 + @Column({ + type: 'varchar', + length: 255, + nullable: true, + comment: 'GitHub头像或自定义头像URL' + }) + avatar_url: string; + + // role:tinyint、非空、默认1 + @Column({ + type: 'tinyint', + nullable: false, + default: 1, + comment: '角色:1-普通,9-管理员' + }) + role: number; + + // created_at:datetime、非空、默认当前时间 + @CreateDateColumn({ + type: 'datetime', + nullable: false, + default: () => 'CURRENT_TIMESTAMP', + comment: '注册时间' + }) + created_at: Date; + + // updated_at:datetime、非空、默认当前时间 + @UpdateDateColumn({ + type: 'datetime', + nullable: false, + default: () => 'CURRENT_TIMESTAMP', + onUpdate: 'CURRENT_TIMESTAMP', // 数据库更新时自动刷新时间 + comment: '更新时间' + }) + updated_at: Date; +} \ No newline at end of file diff --git a/src/core/db/users/users.module.ts b/src/core/db/users/users.module.ts new file mode 100644 index 0000000..fef0d77 --- /dev/null +++ b/src/core/db/users/users.module.ts @@ -0,0 +1,26 @@ +/** + * 用户模块 + * + * 功能描述: + * - 整合用户相关的实体、服务和控制器 + * - 配置TypeORM实体和Repository + * - 导出用户服务供其他模块使用 + * + * @author moyin + * @version 1.0.0 + * @since 2024-12-17 + */ + +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Users } from './users.entity'; +import { UsersService } from './users.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Users]) + ], + providers: [UsersService], + exports: [UsersService, TypeOrmModule], +}) +export class UsersModule {} \ No newline at end of file diff --git a/src/core/db/users/users.service.spec.ts b/src/core/db/users/users.service.spec.ts new file mode 100644 index 0000000..b1f1f96 --- /dev/null +++ b/src/core/db/users/users.service.spec.ts @@ -0,0 +1,491 @@ +/** + * 用户实体、DTO和服务的完整测试套件 + * + * 功能: + * - 测试Users实体的结构和装饰器 + * - 测试CreateUserDto的验证规则 + * - 测试UsersService的所有CRUD操作 + * - 验证数据类型和约束条件 + * + * @author moyin + * @version 1.0.0 + * @since 2024-12-17 + */ + +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'; + +import { Users } from './users.entity'; +import { CreateUserDto } from './users.dto'; +import { UsersService } from './users.service'; + +describe('Users Entity, DTO and Service Tests', () => { + let service: UsersService; + let repository: Repository; + let module: TestingModule; + + // 模拟的Repository方法 + const mockRepository = { + save: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + delete: jest.fn(), + softRemove: jest.fn(), + count: jest.fn(), + createQueryBuilder: jest.fn(), + }; + + // 测试数据 + const mockUser: Users = { + id: BigInt(1), + username: 'testuser', + email: 'test@example.com', + phone: '+8613800138000', + password_hash: 'hashed_password', + nickname: '测试用户', + github_id: 'github_123', + avatar_url: 'https://example.com/avatar.jpg', + role: 1, + created_at: new Date(), + updated_at: new Date(), + }; + + const createUserDto: CreateUserDto = { + username: 'testuser', + email: 'test@example.com', + phone: '+8613800138000', + password_hash: 'hashed_password', + nickname: '测试用户', + github_id: 'github_123', + avatar_url: 'https://example.com/avatar.jpg', + role: 1 + }; + + beforeEach(async () => { + module = await Test.createTestingModule({ + providers: [ + UsersService, + { + provide: getRepositoryToken(Users), + useValue: mockRepository, + }, + ], + }).compile(); + + service = module.get(UsersService); + repository = module.get>(getRepositoryToken(Users)); + + // 清理所有mock + jest.clearAllMocks(); + }); + + afterEach(async () => { + await module.close(); + }); + + describe('Users Entity Tests', () => { + it('应该正确创建用户实体实例', () => { + const user = new Users(); + user.username = 'testuser'; + user.email = 'test@example.com'; + user.phone = '+8613800138000'; + user.password_hash = 'hashed_password'; + user.nickname = '测试用户'; + user.github_id = 'github_123'; + user.avatar_url = 'https://example.com/avatar.jpg'; + user.role = 1; + user.created_at = new Date(); + user.updated_at = new Date(); + + expect(user.username).toBe('testuser'); + expect(user.email).toBe('test@example.com'); + expect(user.nickname).toBe('测试用户'); + expect(user.role).toBe(1); + }); + + it('应该能够设置和获取所有属性', () => { + const user = new Users(); + + // 验证可以设置和获取所有属性 + user.username = 'testuser'; + user.email = 'test@example.com'; + user.phone = '+8613800138000'; + user.password_hash = 'hashed_password'; + user.nickname = '测试用户'; + user.github_id = 'github_123'; + user.avatar_url = 'https://example.com/avatar.jpg'; + user.role = 1; + user.created_at = new Date(); + user.updated_at = new Date(); + + expect(user.username).toBe('testuser'); + expect(user.email).toBe('test@example.com'); + expect(user.phone).toBe('+8613800138000'); + expect(user.password_hash).toBe('hashed_password'); + expect(user.nickname).toBe('测试用户'); + expect(user.github_id).toBe('github_123'); + expect(user.avatar_url).toBe('https://example.com/avatar.jpg'); + expect(user.role).toBe(1); + expect(user.created_at).toBeInstanceOf(Date); + expect(user.updated_at).toBeInstanceOf(Date); + }); + }); + + describe('CreateUserDto Validation Tests', () => { + it('应该通过有效数据的验证', async () => { + const validData = { + username: 'testuser', + email: 'test@example.com', + phone: '+8613800138000', + password_hash: 'hashed_password', + nickname: '测试用户', + github_id: 'github_123', + avatar_url: 'https://example.com/avatar.jpg', + role: 1 + }; + + const dto = plainToClass(CreateUserDto, validData); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + }); + + it('应该拒绝缺少必填字段的数据', async () => { + const invalidData = { + email: 'test@example.com' + // 缺少 username 和 nickname + }; + + const dto = plainToClass(CreateUserDto, invalidData); + const errors = await validate(dto); + + expect(errors.length).toBeGreaterThan(0); + + const usernameError = errors.find(error => error.property === 'username'); + const nicknameError = errors.find(error => error.property === 'nickname'); + + expect(usernameError).toBeDefined(); + expect(nicknameError).toBeDefined(); + }); + + it('应该拒绝无效的邮箱格式', async () => { + const invalidData = { + username: 'testuser', + nickname: '测试用户', + email: 'invalid-email' + }; + + const dto = plainToClass(CreateUserDto, invalidData); + const errors = await validate(dto); + + const emailError = errors.find(error => error.property === 'email'); + expect(emailError).toBeDefined(); + }); + + it('应该拒绝超长的用户名', async () => { + const invalidData = { + username: 'a'.repeat(51), // 超过50字符 + nickname: '测试用户' + }; + + const dto = plainToClass(CreateUserDto, invalidData); + const errors = await validate(dto); + + const usernameError = errors.find(error => error.property === 'username'); + expect(usernameError).toBeDefined(); + }); + + it('应该拒绝无效的角色值', async () => { + const invalidData = { + username: 'testuser', + nickname: '测试用户', + role: 10 // 超出1-9范围 + }; + + const dto = plainToClass(CreateUserDto, invalidData); + const errors = await validate(dto); + + const roleError = errors.find(error => error.property === 'role'); + expect(roleError).toBeDefined(); + }); + }); + + describe('UsersService CRUD Tests', () => { + + describe('create()', () => { + it('应该成功创建新用户', async () => { + mockRepository.findOne.mockResolvedValue(null); // 没有重复用户 + mockRepository.save.mockResolvedValue(mockUser); + + const result = await service.create(createUserDto); + + expect(mockRepository.save).toHaveBeenCalled(); + expect(result).toEqual(mockUser); + }); + + it('应该在用户名重复时抛出ConflictException', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在 + + await expect(service.create(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); + }); + }); + + describe('findAll()', () => { + it('应该返回用户列表', async () => { + const mockUsers = [mockUser]; + mockRepository.find.mockResolvedValue(mockUsers); + + const result = await service.findAll(); + + expect(mockRepository.find).toHaveBeenCalledWith({ + take: 100, + skip: 0, + order: { created_at: 'DESC' } + }); + expect(result).toEqual(mockUsers); + }); + + it('应该支持分页参数', async () => { + mockRepository.find.mockResolvedValue([]); + + await service.findAll(50, 10); + + expect(mockRepository.find).toHaveBeenCalledWith({ + take: 50, + skip: 10, + order: { created_at: 'DESC' } + }); + }); + }); + + describe('findOne()', () => { + it('应该根据ID返回用户', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + + const result = await service.findOne(BigInt(1)); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: BigInt(1) } + }); + expect(result).toEqual(mockUser); + }); + + it('应该在用户不存在时抛出NotFoundException', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.findOne(BigInt(999))).rejects.toThrow(NotFoundException); + }); + }); + + describe('findByUsername()', () => { + it('应该根据用户名返回用户', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + + const result = await service.findByUsername('testuser'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { username: 'testuser' } + }); + expect(result).toEqual(mockUser); + }); + + it('应该在用户不存在时返回null', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const result = await service.findByUsername('nonexistent'); + + expect(result).toBeNull(); + }); + }); + + describe('findByEmail()', () => { + it('应该根据邮箱返回用户', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + + const result = await service.findByEmail('test@example.com'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { email: 'test@example.com' } + }); + expect(result).toEqual(mockUser); + }); + }); + + describe('update()', () => { + it('应该成功更新用户信息', async () => { + const updatedUser = { ...mockUser, nickname: '更新后的昵称' }; + + mockRepository.findOne + .mockResolvedValueOnce(mockUser) // findOne in update method + .mockResolvedValueOnce(null); // 检查昵称是否重复 + mockRepository.save.mockResolvedValue(updatedUser); + + const result = await service.update(BigInt(1), { nickname: '更新后的昵称' }); + + expect(mockRepository.save).toHaveBeenCalled(); + expect(result.nickname).toBe('更新后的昵称'); + }); + + it('应该在更新不存在的用户时抛出NotFoundException', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.update(BigInt(999), { nickname: '新昵称' })).rejects.toThrow(NotFoundException); + }); + + it('应该在更新数据冲突时抛出ConflictException', async () => { + mockRepository.findOne + .mockResolvedValueOnce(mockUser) // 找到要更新的用户 + .mockResolvedValueOnce(mockUser); // 发现用户名冲突 + + await expect(service.update(BigInt(1), { username: 'conflictuser' })).rejects.toThrow(ConflictException); + }); + }); + + describe('remove()', () => { + it('应该成功删除用户', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + mockRepository.delete.mockResolvedValue({ affected: 1 }); + + const result = await service.remove(BigInt(1)); + + expect(mockRepository.delete).toHaveBeenCalledWith({ id: BigInt(1) }); + expect(result.affected).toBe(1); + expect(result.message).toContain('成功删除'); + }); + + it('应该在删除不存在的用户时抛出NotFoundException', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.remove(BigInt(999))).rejects.toThrow(NotFoundException); + }); + }); + + describe('count()', () => { + it('应该返回用户总数', async () => { + mockRepository.count.mockResolvedValue(10); + + const result = await service.count(); + + expect(mockRepository.count).toHaveBeenCalled(); + expect(result).toBe(10); + }); + }); + + describe('exists()', () => { + it('应该在用户存在时返回true', async () => { + mockRepository.count.mockResolvedValue(1); + + const result = await service.exists(BigInt(1)); + + expect(result).toBe(true); + }); + + it('应该在用户不存在时返回false', async () => { + mockRepository.count.mockResolvedValue(0); + + const result = await service.exists(BigInt(999)); + + expect(result).toBe(false); + }); + }); + + describe('search()', () => { + it('应该根据关键词搜索用户', async () => { + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockUser]), + }; + + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + + const result = await service.search('test'); + + expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('user'); + expect(mockQueryBuilder.where).toHaveBeenCalled(); + expect(result).toEqual([mockUser]); + }); + }); + + describe('findByRole()', () => { + it('应该根据角色查询用户', async () => { + mockRepository.find.mockResolvedValue([mockUser]); + + const result = await service.findByRole(1); + + expect(mockRepository.find).toHaveBeenCalledWith({ + where: { role: 1 }, + order: { created_at: 'DESC' } + }); + expect(result).toEqual([mockUser]); + }); + }); + }); + + describe('Integration Tests', () => { + it('应该完成从DTO到Entity的完整流程', async () => { + // 1. 验证DTO + const dto = plainToClass(CreateUserDto, createUserDto); + const validationErrors = await validate(dto); + expect(validationErrors).toHaveLength(0); + + // 2. 创建匹配的mock用户数据 + const expectedUser = { ...mockUser, nickname: dto.nickname }; + + // 3. 模拟服务创建用户 + mockRepository.findOne.mockResolvedValue(null); + mockRepository.save.mockResolvedValue(expectedUser); + + const result = await service.create(dto); + + // 4. 验证结果 + expect(result).toBeDefined(); + expect(result.username).toBe(dto.username); + expect(result.nickname).toBe(dto.nickname); + }); + + it('应该正确处理可选字段', async () => { + const minimalDto: CreateUserDto = { + username: 'minimaluser', + nickname: '最小用户' + }; + + const dto = plainToClass(CreateUserDto, minimalDto); + const validationErrors = await validate(dto); + expect(validationErrors).toHaveLength(0); + + // 验证可选字段的默认值处理 + expect(dto.role).toBe(1); // 默认角色 + expect(dto.email).toBeUndefined(); + expect(dto.phone).toBeUndefined(); + }); + }); + + describe('Error Handling Tests', () => { + it('应该正确处理数据库连接错误', async () => { + mockRepository.save.mockRejectedValue(new Error('Database connection failed')); + + await expect(service.create(createUserDto)).rejects.toThrow('Database connection failed'); + }); + + it('应该正确处理并发创建冲突', async () => { + // 模拟并发情况:检查时不存在,保存时出现唯一约束错误 + mockRepository.findOne.mockResolvedValue(null); + mockRepository.save.mockRejectedValue(new Error('Duplicate entry')); + + await expect(service.create(createUserDto)).rejects.toThrow('Duplicate entry'); + }); + }); +}); \ No newline at end of file diff --git a/src/core/db/users/users.service.ts b/src/core/db/users/users.service.ts new file mode 100644 index 0000000..fb7fea3 --- /dev/null +++ b/src/core/db/users/users.service.ts @@ -0,0 +1,330 @@ +/** + * 用户服务类 + * + * 功能描述: + * - 提供用户的增删改查操作 + * - 处理用户数据的业务逻辑 + * - 数据验证和错误处理 + * + * @author moyin + * @version 1.0.0 + * @since 2024-12-17 + */ + +import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, FindOptionsWhere } from 'typeorm'; +import { Users } from './users.entity'; +import { CreateUserDto } from './users.dto'; +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; + +@Injectable() +export class UsersService { + constructor( + @InjectRepository(Users) + private readonly usersRepository: Repository, + ) {} + + /** + * 创建新用户 + * + * @param createUserDto 创建用户的数据传输对象 + * @returns 创建的用户实体 + * @throws ConflictException 当用户名、邮箱或手机号已存在时 + * @throws BadRequestException 当数据验证失败时 + */ + async create(createUserDto: CreateUserDto): Promise { + // 验证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('; '); + throw new BadRequestException(`数据验证失败: ${errorMessages}`); + } + + // 检查用户名是否已存在 + if (createUserDto.username) { + const existingUser = await this.usersRepository.findOne({ + where: { username: createUserDto.username } + }); + if (existingUser) { + throw new ConflictException('用户名已存在'); + } + } + + // 检查邮箱是否已存在 + if (createUserDto.email) { + const existingEmail = await this.usersRepository.findOne({ + where: { email: createUserDto.email } + }); + if (existingEmail) { + throw new ConflictException('邮箱已存在'); + } + } + + // 检查手机号是否已存在 + if (createUserDto.phone) { + const existingPhone = await this.usersRepository.findOne({ + where: { phone: createUserDto.phone } + }); + if (existingPhone) { + 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已存在'); + } + } + + // 创建用户实体 + 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; + + // 保存到数据库 + return await this.usersRepository.save(user); + } + + /** + * 查询所有用户 + * + * @param limit 限制返回数量,默认100 + * @param offset 偏移量,默认0 + * @returns 用户列表 + */ + async findAll(limit: number = 100, offset: number = 0): Promise { + return await this.usersRepository.find({ + take: limit, + skip: offset, + order: { created_at: 'DESC' } + }); + } + + /** + * 根据ID查询用户 + * + * @param id 用户ID + * @returns 用户实体 + * @throws NotFoundException 当用户不存在时 + */ + async findOne(id: bigint): Promise { + const user = await this.usersRepository.findOne({ + where: { id } + }); + + if (!user) { + throw new NotFoundException(`ID为 ${id} 的用户不存在`); + } + + return user; + } + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @returns 用户实体或null + */ + async findByUsername(username: string): Promise { + return await this.usersRepository.findOne({ + where: { username } + }); + } + + /** + * 根据邮箱查询用户 + * + * @param email 邮箱 + * @returns 用户实体或null + */ + async findByEmail(email: string): Promise { + return await this.usersRepository.findOne({ + where: { email } + }); + } + + /** + * 根据GitHub ID查询用户 + * + * @param githubId GitHub ID + * @returns 用户实体或null + */ + async findByGithubId(githubId: string): Promise { + return await this.usersRepository.findOne({ + where: { github_id: githubId } + }); + } + + /** + * 更新用户信息 + * + * @param id 用户ID + * @param updateData 更新的数据 + * @returns 更新后的用户实体 + * @throws NotFoundException 当用户不存在时 + * @throws ConflictException 当更新的数据与其他用户冲突时 + */ + 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); + + return await this.usersRepository.save(existingUser); + } + + /** + * 删除用户 + * + * @param id 用户ID + * @returns 删除操作结果 + * @throws NotFoundException 当用户不存在时 + */ + async remove(id: bigint): Promise<{ affected: number; message: string }> { + // 检查用户是否存在 + await this.findOne(id); + + // 执行删除 - 使用where条件来处理bigint类型 + const result = await this.usersRepository.delete({ id }); + + return { + affected: result.affected || 0, + message: `成功删除ID为 ${id} 的用户` + }; + } + + /** + * 软删除用户(如果需要保留数据) + * 注意:需要在实体中添加 @DeleteDateColumn 装饰器 + * + * @param id 用户ID + * @returns 软删除操作结果 + */ + async softRemove(id: bigint): Promise { + const user = await this.findOne(id); + return await this.usersRepository.softRemove(user); + } + + /** + * 统计用户数量 + * + * @param conditions 查询条件 + * @returns 用户数量 + */ + async count(conditions?: FindOptionsWhere): Promise { + return await this.usersRepository.count({ where: conditions }); + } + + /** + * 检查用户是否存在 + * + * @param id 用户ID + * @returns 是否存在 + */ + async exists(id: bigint): Promise { + const count = await this.usersRepository.count({ where: { id } }); + return count > 0; + } + + /** + * 批量创建用户 + * + * @param createUserDtos 用户数据数组 + * @returns 创建的用户列表 + */ + async createBatch(createUserDtos: CreateUserDto[]): Promise { + const users: Users[] = []; + + for (const dto of createUserDtos) { + const user = await this.create(dto); + users.push(user); + } + + return users; + } + + /** + * 根据角色查询用户 + * + * @param role 角色值 + * @returns 用户列表 + */ + async findByRole(role: number): Promise { + return await this.usersRepository.find({ + where: { role }, + order: { created_at: 'DESC' } + }); + } + + /** + * 搜索用户(根据用户名或昵称) + * + * @param keyword 搜索关键词 + * @param limit 限制数量 + * @returns 用户列表 + */ + 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(); + } +} \ No newline at end of file