feat: 实现完整的用户管理系统和日志配置优化 #1
@@ -28,12 +28,16 @@
|
||||
"@nestjs/platform-express": "^10.4.20",
|
||||
"@nestjs/platform-socket.io": "^10.4.20",
|
||||
"@nestjs/schedule": "^4.1.2",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^10.4.20",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"mysql2": "^3.16.0",
|
||||
"nestjs-pino": "^4.5.0",
|
||||
"pino": "^10.1.0",
|
||||
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"rxjs": "^7.8.2"
|
||||
"rxjs": "^7.8.2",
|
||||
"typeorm": "^0.3.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { LoggerModule } from './core/utils/logger/logger.module';
|
||||
import { UsersModule } from './core/db/users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -11,6 +13,17 @@ import { LoggerModule } from './core/utils/logger/logger.module';
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
LoggerModule,
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'mysql',
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT),
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
synchronize: false,
|
||||
}),
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
59
src/core/db/users/users.dto.ts
Normal file
59
src/core/db/users/users.dto.ts
Normal file
@@ -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; // 默认普通用户
|
||||
}
|
||||
106
src/core/db/users/users.entity.ts
Normal file
106
src/core/db/users/users.entity.ts
Normal file
@@ -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;
|
||||
}
|
||||
26
src/core/db/users/users.module.ts
Normal file
26
src/core/db/users/users.module.ts
Normal file
@@ -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 {}
|
||||
491
src/core/db/users/users.service.spec.ts
Normal file
491
src/core/db/users/users.service.spec.ts
Normal file
@@ -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<Users>;
|
||||
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>(UsersService);
|
||||
repository = module.get<Repository<Users>>(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');
|
||||
});
|
||||
});
|
||||
});
|
||||
330
src/core/db/users/users.service.ts
Normal file
330
src/core/db/users/users.service.ts
Normal file
@@ -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<Users>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*
|
||||
* @param createUserDto 创建用户的数据传输对象
|
||||
* @returns 创建的用户实体
|
||||
* @throws ConflictException 当用户名、邮箱或手机号已存在时
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||
// 验证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<Users[]> {
|
||||
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<Users> {
|
||||
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<Users | null> {
|
||||
return await this.usersRepository.findOne({
|
||||
where: { username }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByEmail(email: string): Promise<Users | null> {
|
||||
return await this.usersRepository.findOne({
|
||||
where: { email }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据GitHub ID查询用户
|
||||
*
|
||||
* @param githubId GitHub ID
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByGithubId(githubId: string): Promise<Users | null> {
|
||||
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<CreateUserDto>): Promise<Users> {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
// 检查更新数据的唯一性约束
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
const usernameExists = await this.usersRepository.findOne({
|
||||
where: { username: updateData.username }
|
||||
});
|
||||
if (usernameExists) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
const emailExists = await this.usersRepository.findOne({
|
||||
where: { email: updateData.email }
|
||||
});
|
||||
if (emailExists) {
|
||||
throw new ConflictException('邮箱已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
const phoneExists = await this.usersRepository.findOne({
|
||||
where: { phone: updateData.phone }
|
||||
});
|
||||
if (phoneExists) {
|
||||
throw new ConflictException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||
const githubExists = await this.usersRepository.findOne({
|
||||
where: { github_id: updateData.github_id }
|
||||
});
|
||||
if (githubExists) {
|
||||
throw new ConflictException('GitHub ID已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户数据
|
||||
Object.assign(existingUser, updateData);
|
||||
|
||||
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<Users> {
|
||||
const user = await this.findOne(id);
|
||||
return await this.usersRepository.softRemove(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户数量
|
||||
*
|
||||
* @param conditions 查询条件
|
||||
* @returns 用户数量
|
||||
*/
|
||||
async count(conditions?: FindOptionsWhere<Users>): Promise<number> {
|
||||
return await this.usersRepository.count({ where: conditions });
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
async exists(id: bigint): Promise<boolean> {
|
||||
const count = await this.usersRepository.count({ where: { id } });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建用户
|
||||
*
|
||||
* @param createUserDtos 用户数据数组
|
||||
* @returns 创建的用户列表
|
||||
*/
|
||||
async createBatch(createUserDtos: CreateUserDto[]): Promise<Users[]> {
|
||||
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<Users[]> {
|
||||
return await this.usersRepository.find({
|
||||
where: { role },
|
||||
order: { created_at: 'DESC' }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索用户(根据用户名或昵称)
|
||||
*
|
||||
* @param keyword 搜索关键词
|
||||
* @param limit 限制数量
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async search(keyword: string, limit: number = 20): Promise<Users[]> {
|
||||
return await this.usersRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.username LIKE :keyword OR user.nickname LIKE :keyword', {
|
||||
keyword: `%${keyword}%`
|
||||
})
|
||||
.orderBy('user.created_at', 'DESC')
|
||||
.limit(limit)
|
||||
.getMany();
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@
|
||||
* - AppLoggerService: 应用日志服务
|
||||
* - ScheduleModule: 定时任务模块
|
||||
*
|
||||
* @author 开发团队
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-13
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@@ -7,9 +7,9 @@
|
||||
* - 根据环境自动调整日志策略
|
||||
* - 提供日志文件清理和归档功能
|
||||
*
|
||||
* @author 开发团队
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-13
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
@@ -82,7 +82,7 @@ export class LoggerConfigFactory {
|
||||
},
|
||||
|
||||
// 自定义错误响应消息
|
||||
customErrorMessage: (req: any, res: any, err: any) => {
|
||||
customErrorMessage: (req: any, _res: any, err: any) => {
|
||||
return `${req.method} ${req.url} failed: ${err.message}`;
|
||||
},
|
||||
},
|
||||
@@ -152,20 +152,7 @@ export class LoggerConfigFactory {
|
||||
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
||||
ignore: 'pid,hostname',
|
||||
messageFormat: '{app} [{level}] {msg}',
|
||||
customPrettifiers: {
|
||||
time: (timestamp: any) => `🕐 ${timestamp}`,
|
||||
level: (logLevel: any) => {
|
||||
const levelEmojis: Record<number, string> = {
|
||||
10: '🔍', // trace
|
||||
20: '🐛', // debug
|
||||
30: '📝', // info
|
||||
40: '⚠️', // warn
|
||||
50: '❌', // error
|
||||
60: '💀', // fatal
|
||||
};
|
||||
return `${levelEmojis[logLevel] || '📝'} ${logLevel}`;
|
||||
},
|
||||
},
|
||||
// 移除 customPrettifiers 以避免 Worker 线程序列化问题
|
||||
},
|
||||
level: logLevel,
|
||||
},
|
||||
@@ -231,13 +218,13 @@ export class LoggerConfigFactory {
|
||||
/**
|
||||
* 自定义日志级别判断
|
||||
*
|
||||
* @param req HTTP 请求对象
|
||||
* @param _req HTTP 请求对象
|
||||
* @param res HTTP 响应对象
|
||||
* @param err 错误对象
|
||||
* @returns 日志级别
|
||||
* @private
|
||||
*/
|
||||
private static customLogLevel(req: any, res: any, err: any) {
|
||||
private static customLogLevel(_req: any, res: any, err: any) {
|
||||
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||
return 'warn';
|
||||
} else if (res.statusCode >= 500 || err) {
|
||||
@@ -255,7 +242,7 @@ export class LoggerConfigFactory {
|
||||
* @private
|
||||
*/
|
||||
private static generateRequestId(): string {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
* - PinoLoggerModule: Pino 日志模块
|
||||
* - AppLoggerService: 应用日志服务
|
||||
*
|
||||
* @author 开发团队
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-13
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
@@ -23,7 +23,7 @@ import { LoggerModule as PinoLoggerModule } from 'nestjs-pino';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { AppLoggerService } from './logger.service';
|
||||
import { LoggerConfigFactory } from './logger.config';
|
||||
import { LogManagementService } from './log-management.service';
|
||||
import { LogManagementService } from './log_management.service';
|
||||
|
||||
/**
|
||||
* 日志模块类
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-13
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-13
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, Inject, Optional } from '@nestjs/common';
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
// 全局启用校验管道(核心配置)
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true, // 过滤掉 DTO 中未定义的字段(比如传了个 `age` 但 DTO 里没有,会自动忽略)
|
||||
forbidNonWhitelisted: true, // 若传了未定义的字段,直接报错(防止传多余参数)
|
||||
transform: true, // 自动把入参转为 DTO 对应的类型(比如前端传的字符串数字 `'1'` 转为数字 `1`)
|
||||
}),
|
||||
);
|
||||
await app.listen(3000);
|
||||
console.log('Pixel Game Server is running on http://localhost:3000');
|
||||
}
|
||||
|
||||
95
test-users-functionality.ts
Normal file
95
test-users-functionality.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 用户功能测试脚本
|
||||
*
|
||||
* 使用方法:npx ts-node test-users-functionality.ts
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './src/app.module';
|
||||
import { UsersService } from './src/core/db/users/users.service';
|
||||
import { CreateUserDto } from './src/core/db/users/users.dto';
|
||||
|
||||
async function testUsersFunctionality() {
|
||||
console.log('🚀 启动用户功能测试...\n');
|
||||
|
||||
try {
|
||||
// 创建NestJS应用
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: false, // 禁用日志以保持输出清洁
|
||||
});
|
||||
|
||||
// 获取用户服务
|
||||
const usersService = app.get(UsersService);
|
||||
console.log('✅ 成功获取UsersService实例');
|
||||
|
||||
// 测试数据
|
||||
const testUserDto: CreateUserDto = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
email: `test_${Date.now()}@example.com`,
|
||||
phone: `+86138${Date.now().toString().slice(-8)}`,
|
||||
password_hash: 'hashed_password_123',
|
||||
nickname: '功能测试用户',
|
||||
github_id: `github_${Date.now()}`,
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
role: 1
|
||||
};
|
||||
|
||||
console.log('\n📝 测试创建用户...');
|
||||
const createdUser = await usersService.create(testUserDto);
|
||||
console.log('✅ 用户创建成功:', {
|
||||
id: createdUser.id.toString(),
|
||||
username: createdUser.username,
|
||||
nickname: createdUser.nickname,
|
||||
email: createdUser.email
|
||||
});
|
||||
|
||||
console.log('\n🔍 测试查询用户...');
|
||||
const foundUser = await usersService.findOne(createdUser.id);
|
||||
console.log('✅ 用户查询成功:', foundUser.username);
|
||||
|
||||
console.log('\n📊 测试用户统计...');
|
||||
const userCount = await usersService.count();
|
||||
console.log('✅ 当前用户总数:', userCount);
|
||||
|
||||
console.log('\n🔍 测试根据用户名查询...');
|
||||
const userByUsername = await usersService.findByUsername(createdUser.username);
|
||||
console.log('✅ 根据用户名查询成功:', userByUsername?.nickname);
|
||||
|
||||
console.log('\n✏️ 测试更新用户...');
|
||||
const updatedUser = await usersService.update(createdUser.id, {
|
||||
nickname: '更新后的昵称'
|
||||
});
|
||||
console.log('✅ 用户更新成功:', updatedUser.nickname);
|
||||
|
||||
console.log('\n📋 测试查询所有用户...');
|
||||
const allUsers = await usersService.findAll(5); // 限制5个
|
||||
console.log('✅ 查询到用户数量:', allUsers.length);
|
||||
|
||||
console.log('\n🔍 测试搜索功能...');
|
||||
const searchResults = await usersService.search('测试');
|
||||
console.log('✅ 搜索结果数量:', searchResults.length);
|
||||
|
||||
console.log('\n🗑️ 测试删除用户...');
|
||||
const deleteResult = await usersService.remove(createdUser.id);
|
||||
console.log('✅ 用户删除成功:', deleteResult.message);
|
||||
|
||||
// 验证删除
|
||||
console.log('\n✅ 验证删除结果...');
|
||||
try {
|
||||
await usersService.findOne(createdUser.id);
|
||||
console.log('❌ 删除验证失败:用户仍然存在');
|
||||
} catch (error) {
|
||||
console.log('✅ 删除验证成功:用户已不存在');
|
||||
}
|
||||
|
||||
await app.close();
|
||||
console.log('\n🎉 所有功能测试通过!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testUsersFunctionality();
|
||||
Reference in New Issue
Block a user