用户管理
生成user模块
nest res g user
然后我们在 src/user/entities 目录,新建 3 个实体 User、Role、Permission。
用户表:
| 字段名 | 数据描述 | 描述 |
|---|---|---|
| id | INT | 用户ID |
| username | VARCHAR(50) | 用户名 |
| password | VARCHAR(50) | 密码 |
| nick_name | VARCHAR(50) | 昵称 |
| VARCHAR(50) | 邮箱 | |
| head_pic | VARCHAR(100) | 头像 |
| phone_number | VARCHAR(20) | 手机号 |
| is_frozen | BOOLEAN | 是否被冻结 |
| is_admin | BOOLEAN | 是否是管理员 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
| ip | VARCHA(50) | ip地址 |
角色表 roles
| 字段名 | 数据描述 | 描述 |
|---|---|---|
| id | INT | ID |
| name | VARCHAR(20) | 角色名 |
角色表 permissions
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | INT | ID |
| code | VARCHAR(20) | 权限代码 |
| description | VARCHAR(100) | 权限描述 |
用户-角色中间表 user_roles
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | INT | ID |
| user_id | INT | 用户 ID |
| role_id | INT | 角色 ID |
角色-权限中间表 role_permissions
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | INT | ID |
| role_id | INT | 角色 ID |
| permission_id | INT | 权限 ID |
代码实现;
import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Role } from "./role.entity";
@Entity({
name: 'users'
})
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 20,
comment: '用户ip',
nullable: true
})
ip: string;
@Column({
length: 50,
comment: '用户名'
})
username: string;
@Column({
length: 50,
comment: '盐'
})
salt: string;
@Column({
length: 60,
comment: '密码'
})
password: string;
@Column({
name: 'nick_name',
length: 50,
comment: '昵称'
})
nickName: string;
@Column({
comment: '邮箱',
length: 50
})
email: string;
@Column({
comment: '头像',
length: 100,
nullable: true
})
headPic: string;
@Column({
comment: '手机号',
length: 20,
nullable: true
})
phoneNumber: string;
@Column({
comment: '是否冻结',
default: false
})
isFrozen: boolean;
@Column({
comment: '是否是管理员',
default: false
})
isAdmin: boolean;
@CreateDateColumn()
createTime: Date;
@UpdateDateColumn()
updateTime: Date;
@ManyToMany(() => Role)
@JoinTable({
name: 'user_roles'
})
roles: Role[]
}
roles:
import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Permission } from "./permission.entity";
@Entity({
name: 'roles'
})
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 20,
comment: '角色名'
})
name: string;
@ManyToMany(() => Permission)
@JoinTable({
name: 'role_permissions'
})
permissions: Permission[]
}
permission:
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
@Entity({
name: 'permissions'
})
export class Permission {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 20,
comment: '权限代码'
})
code: string;
@Column({
length: 100,
comment: '权限描述'
})
description: string;
}
注册接口
在 base.controller 增加一个 post 接口:
@Post('register')
async create(@Body() user: RegisterUserDto, @Request() headers) {
console.log(registerUser);
return "success"
}
dto 是封装 body 里的请求参数的,根据前端界面上要填的信息,创建 RegisterUserDto:
export class RegisterUserDto {
username: string;
nickName: string;
password: string;
email: string;
captcha: string;
}
然后加一下 ValidationPipe,来对请求体做校验。
安装用到的包:
npm install --save class-validator class-transformer
全局启用 ValidationPipe:
然后加一下校验规则:
import { IsEmail, IsNotEmpty, MinLength } from "class-validator";
export class RegisterUserDto {
@IsNotEmpty({
message: "用户名不能为空"
})
username: string;
@IsNotEmpty({
message: '昵称不能为空'
})
nickName: string;
@IsNotEmpty({
message: '密码不能为空'
})
@MinLength(6, {
message: '密码不能少于 6 位'
})
password: string;
@IsNotEmpty({
message: '邮箱不能为空'
})
@IsEmail({}, {
message: '不是合法的邮箱格式'
})
email: string;
@IsNotEmpty({
message: '验证码不能为空'
})
captcha: string;
}
实现验证码发送
@Get('register-captcha')
async captcha(@Query('address') address: string) {
const code = Math.random().toString().slice(2,8);
await this.redisService.set(`captcha_${address}`, code, 5 * 60);
await this.emailService.sendMail({
to: address,
subject: '注册验证码',
html: `<p>你的注册验证码是 ${code}</p>`
});
return '发送成功';
}
然后实现注册的逻辑。
在 userService 里添加 register 方法:
创建 logger 对象,注入 Repository。
这里注入 Repository 需要在 UserModule 里引入下 TypeOrm.forFeature
private logger = new Logger();
@InjectRepository(User)
private userRepository: Repository<User>;
@InjectRepository(Role)
private roleRepository: Repository<Role>;
@InjectRepository(Permission)
private permissionRepository: Repository<Permission>;
@Inject(RedisService)
private redisService: RedisService;
@Inject(EmailService)
private emailService: EmailService;
这里密码使用了bcryptjs加密方法
登录
在 UserController 添加两个接口:
@Post('login')
async userLogin(@Body() loginUser: LoginUserDto) {
console.log(loginUser);
return 'success';
}
@Post('admin/login')
async adminLogin(@Body() loginUser: LoginUserDto) {
console.log(loginUser);
return 'success';
}
添加 src/user/dto/login-user.dto.ts:
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty } from "class-validator";
export class LoginUserDto {
@IsNotEmpty({
message: "用户名不能为空"
})
@ApiProperty()
username: string;
@IsNotEmpty({
message: '密码不能为空'
})
@ApiProperty()
password: string;
}
然后在 UserService 实现 login 方法:
async login(loginUserDto: LoginUserDto, isAdmin: boolean) {
const user = await this.userRepository.findOne({
where: {
username: loginUserDto.username,
isAdmin
},
relations: ['roles', 'roles.permissions']
});
if(!user) {
throw new HttpException('用户名或密码错误', HttpStatus.BAD_REQUEST);
}
if(user.isFrozen) {
throw new HttpException('用户已被冻结,请联系管理员', HttpStatus.BAD_REQUEST);
}
const isPasswordValid = await compare(loginUserDto.password, user.password);
if(!isPasswordValid) {
throw new HttpException('用户名或密码错误', HttpStatus.BAD_REQUEST);
}
const VO = new LoginUserVo();
VO.userInfo = {
id: user.id,
username: user.username,
nickName: user.nickName,
email: user.email,
phoneNumber: user.phoneNumber,
headPic: user.headPic,
createTime: user.createTime.getTime(),
isFrozen: user.isFrozen,
isAdmin: user.isAdmin,
roles: user.roles.map(item => item.name),
permissions: user.roles.reduce((arr, item) => {
item.permissions.forEach(permission => {
if(arr.indexOf(permission) === -1) {
arr.push(permission);
}
})
return arr;
}, [])
}
return VO;
}
我们创建个 vo (view object)对象来封装返回的数据。
dto 是接收参数的,vo 是封装返回的数据的,entity 是和数据库表对应的。
创建 src/user/vo/login-user.vo.ts
interface UserInfo {
id: number;
username: string;
nickName: string;
email: string;
headPic: string;
phoneNumber: string;
isFrozen: boolean;
isAdmin: boolean;
createTime: number;
roles: string[];
permissions: string[]
}
export class LoginUserVo {
userInfo: UserInfo;
accessToken: string;
refreshToken: string;
}
在BaseController里返回 vo:
@Post('login')
async userLogin(@Body() loginUser: LoginUserDto) {
const vo = await this.userService.login(loginUser, false);
return vo;
}
@Post('admin/login')
async adminLogin(@Body() loginUser: LoginUserDto) {
const vo = await this.userService.login(loginUser, true);
return vo;
}
然后引入 jwt 模块:
npm install --save @nestjs/jwt
这里的密钥放到 .env 里配置:
然后登录认证通过之后返回 access_token 和 refresh_token:
@Post('login')
async userLogin(@Body() loginUser: LoginUserDto) {
const vo = await this.userService.login(loginUser, false);
vo.accessToken = this.jwtService.sign({
userId: vo.userInfo.id,
username: vo.userInfo.username,
roles: vo.userInfo.roles,
permissions: vo.userInfo.permissions
}, {
expiresIn: this.configService.get('jwt_access_token_expires_time') || '30m'
});
vo.refreshToken = this.jwtService.sign({
userId: vo.userInfo.id
}, {
expiresIn: this.configService.get('jwt_refresh_token_expres_time') || '7d'
});
return vo;
}
另外一个接口也一样
然后再增加一个 refresh_token 的接口用来刷新 token:
@Get('refresh')
async refresh(@Query('refreshToken') refreshToken: string) {
try {
const data = this.jwtService.verify(refreshToken);
const user = await this.userService.findUserById(data.userId, false);
const access_token = this.jwtService.sign(
{
userId: user.id,
username: user.username,
roles: user.roles,
permissions: user.permissions,
},
{
expiresIn:
this.configService.get('jwt_access_token_expires_time') || '30m',
},
);
const refresh_token = this.jwtService.sign(
{
userId: user.id,
},
{
expiresIn:
this.configService.get('jwt_refresh_token_expres_time') || '7d',
},
);
const vo = new RefreshTokenVo();
vo.accessToken = access_token;
vo.refreshToken = refresh_token;
return vo;
} catch (e) {
throw new UnauthorizedException('token 已失效,请重新登录');
}
}
在 UserService 实现这个 findUserById 方法:
async findUserById(userId: number, isAdmin: boolean) {
const user = await this.userRepository.findOne({
where: {
id: userId,
isAdmin
},
relations: [ 'roles', 'roles.permissions']
});
return {
id: user.id,
username: user.username,
isAdmin: user.isAdmin,
roles: user.roles.map(item => item.name),
permissions: user.roles.reduce((arr, item) => {
item.permissions.forEach(permission => {
if(arr.indexOf(permission) === -1) {
arr.push(permission);
}
})
return arr;
}, [])
}
}
同样的方式再实现一个后台管理的 refresh 接口
然后我们加上 LoginGuard 和 PermissionGuard 来做鉴权:
nest g guard login --flat --no-spec
nest g guard permission --flat --no-spec
LoginGuard 的实现代码如下:
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { UnLoginException } from './unlogin.filter';
import { Permission } from './user/entities/permission.entity';
interface JwtUserData {
userId: number;
username: string;
roles: string[];
permissions: Permission[]
}
declare module 'express' {
interface Request {
user: JwtUserData
}
}
@Injectable()
export class LoginGuard implements CanActivate {
@Inject()
private reflector: Reflector;
@Inject(JwtService)
private jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
const requireLogin = this.reflector.getAllAndOverride('require-login', [
context.getClass(),
context.getHandler()
]);
if(!requireLogin) {
return true;
}
const authorization = request.headers.authorization;
if(!authorization) {
throw new UnLoginException();
}
try{
const token = authorization.split(' ')[1];
const data = this.jwtService.verify<JwtUserData>(token);
request.user = {
userId: data.userId,
username: data.username,
roles: data.roles,
permissions: data.permissions
}
return true;
}catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}
}
用 reflector 从目标 controller 和 handler 上拿到 require-login 的 metadata。
如果没有 metadata,就是不需要登录,返回 true 放行。
否则从 authorization 的 header 取出 jwt 来,把用户信息设置到 request,然后放行。
如果 jwt 无效,返回 401 响应,提示 token 失效,请重新登录。
然后全局启用这个 Guard,在 AppModule 里添加这个 provider:
然后我们继续实现 PermissionGuard:
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
@Injectable()
export class PermissionGuard implements CanActivate {
@Inject(Reflector)
private reflector: Reflector;
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
if(!request.user) {
return true;
}
const permissions = request.user.permissions;
const requiredPermissions = this.reflector.getAllAndOverride<string[]>('require-permission', [
context.getClass(),
context.getHandler()
])
if(!requiredPermissions) {
return true;
}
for(let i = 0; i < requiredPermissions.length; i++) {
const curPermission = requiredPermissions[i];
const found = permissions.find(item => item.code === curPermission);
if(!found) {
throw new UnauthorizedException('您没有访问该接口的权限');
}
}
return true;
}
}
同样是用 reflector 取出 handler 或者 controller 上的 require-permission 的 metadata。
如果没有,就是不需要权限,直接放行,返回 true。
对于需要的每个权限,检查下用户是否拥有,没有的话就返回 401,提示没权限。
否则就放行,返回 true。
同样是全局启用这个 PermissionGuard
这样,接口鉴权就完成了
最后我们把这两个 @SetMetadata 封装成自定义装饰器
新建 src/custom.decorator.ts
// 自定义装饰器
import { SetMetadata } from "@nestjs/common";
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from "express";
export const RequireLogin = () => SetMetadata('require-login', true);
export const RequirePermission = (...permissions: string[]) => SetMetadata('require-permission', permissions);
export const UserInfo = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<Request>();
if(!request.user) {
return null;
}
return data ? request.user[data] : request.user;
},
)
UserInfo 装饰器是用来取 user 信息传入 handler 的。
传入属性名的时候,返回对应的属性值,否则返回全部的 user 信息。
响应内容的拦截器和自定义Exception Filter
nest g interceptor format-response --flat
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Response } from 'express';
import { map, Observable } from 'rxjs';
// 响应内容拦截器
@Injectable()
export class FormatResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const response = context.switchToHttp().getResponse<Response>();
return next.handle().pipe(map((data) => {
return {
code: response.statusCode,
message: 'success',
data
}
}));
}
}
全局启用它:
然后再加一个接口访问记录的 interceptor:记录下访问的 ip、user agent、请求的 controller、method,接口耗时、响应内容,当前登录用户等信息。
nest g interceptor invoke-record --flat
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Response } from 'express';
import { Request } from 'express';
import { Observable, tap } from 'rxjs';
// 接口访问记录
@Injectable()
export class InvokeRecordInterceptor implements NestInterceptor {
private readonly logger = new Logger(InvokeRecordInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const userAgent = request.headers['user-agent'];
const { ip, method, path } = request;
this.logger.debug(
`${method} ${path} ${ip} ${userAgent}: ${
context.getClass().name
} ${
context.getHandler().name
} invoked...`,
);
this.logger.debug(`user: ${request.user?.userId}, ${request.user?.username}`);
const now = Date.now();
return next.handle().pipe(
tap((res) => {
this.logger.debug(
`${method} ${path} ${ip} ${userAgent}: ${response.statusCode}: ${Date.now() - now}ms`,
);
this.logger.debug(`Response: ${JSON.stringify(res)}`);
}),
);
}
}
全局启用
自定义 Exception Filter
我们新建个 exception filter:
nest g filter unlogin --flat
@Catch 的参数可以指定具体 catch 的异常类型:
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
// 错误统一处理
export class UnLoginException{
message: string;
constructor(message?){
this.message = message;
}
}
@Catch(UnLoginException)
export class UnloginFilter implements ExceptionFilter {
catch(exception: UnLoginException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
response.json({
code: HttpStatus.UNAUTHORIZED,
message: 'fail',
data: exception.message || '用户未登录'
}).end();
}
}
我们自定义了 UnLoginException 的异常,在 @Catch 指定捕获这个异常,返回对应的响应。
全局启用
然后把 LoginGuard 里的异常改成 UnLoginException
修改密码
管理员和用户修改密码的页面是一样的,我们就用一个接口就好了。
@Post(['update_password', 'admin/update_password'])
@RequireLogin()
async updatePassword(@UserInfo('userId') userId: number, @Body() passwordDto: UpdateUserPasswordDto) {
console.log(passwordDto);
return 'success';
}
这个接口同样是需要登录的,所以加上 @RequireLogin 的装饰器。
用 @UserInfo 从 request.user 取 userId,其余的通过 dto 传。
创建 src/user/dto/update-user-password.dto.ts
import { IsEmail, IsNotEmpty, MinLength } from "class-validator";
export class UpdateUserPasswordDto {
@IsNotEmpty({
message: '密码不能为空'
})
@MinLength(6, {
message: '密码不能少于 6 位'
})
password: string;
@IsNotEmpty({
message: '邮箱不能为空'
})
@IsEmail({}, {
message: '不是合法的邮箱格式'
})
email: string;
@IsNotEmpty({
message: '验证码不能为空'
})
captcha: string;
}
需要传的是邮箱、密码、验证码。
确认密码在前端和密码对比就行,不需要传到后端。
在 UserController 里调用 UserService 的方法:
@Post(['update_password', 'admin/update_password'])
@RequireLogin()
async updatePassword(@UserInfo('userId') userId: number, @Body() passwordDto: UpdateUserPasswordDto) {
return await this.userService.updatePassword(userId, passwordDto);
}
UserService 实现具体的逻辑:
async updatePassword(userId: number, passwordDto: UpdateUserPasswordDto) {
const captcha = await this.redisService.get(`update_password_captcha_${passwordDto.email}`);
if(!captcha) {
throw new HttpException('验证码已失效', HttpStatus.BAD_REQUEST);
}
if(passwordDto.captcha !== captcha) {
throw new HttpException('验证码不正确', HttpStatus.BAD_REQUEST);
}
const foundUser = await this.userRepository.findOneBy({
id: userId
});
foundUser.password = hashSync(passwordDto.password, foundUser.salt);
try {
await this.userRepository.save(foundUser);
return '密码修改成功';
} catch(e) {
this.logger.error(e, UserService);
return '密码修改失败';
}
}
先查询 redis 中有相对应的验证码,检查通过之后根据 id 查询用户信息,修改密码之后 save。
然后再加上这个发送邮箱验证码的接口:
@RequireLogin()
@Get('update_password/captcha')
async updatePasswordCaptcha(@Query('address') address: string) {
const code = Math.random().toString().slice(2, 8);
await this.redisService.set(
`update_password_captcha_${address}`,
code,
10 * 60,
);
await this.emailService.sendMail({
to: address,
subject: '更改密码验证码',
html: `<p>你的更改密码验证码是 ${code}</p>`,
});
return '发送成功';
}
修改个人信息
对应 /user/udpate 和 /user/admin/update 接口。
回显数据的接口就用 /user/info 这个。
实现流程和修改密码的差不多:
@Post(['update', 'admin/update'])
@RequireLogin()
async update(@UserInfo('userId') userId: number, @Body() updateUserDto: UpdateUserDto) {
return await this.userService.update(userId, updateUserDto);
}
在 UserController 定义两个 post 接口。
创建 src/user/dto/udpate-user.dto.ts
import { IsEmail, IsNotEmpty } from "class-validator";
export class UpdateUserDto {
headPic: string;
nickName: string;
@IsNotEmpty({
message: '邮箱不能为空'
})
@IsEmail({}, {
message: '不是合法的邮箱格式'
})
email: string;
@IsNotEmpty({
message: '验证码不能为空'
})
captcha: string;
}
对应的 UserService 里的逻辑和修改密码的差不多:
async update(userId: number, updateUserDto: UpdateUserDto) {
const captcha = await this.redisService.get(`update_user_captcha_${updateUserDto.email}`);
if(!captcha) {
throw new HttpException('验证码已失效', HttpStatus.BAD_REQUEST);
}
if(updateUserDto.captcha !== captcha) {
throw new HttpException('验证码不正确', HttpStatus.BAD_REQUEST);
}
const foundUser = await this.userRepository.findOneBy({
id: userId
});
if(updateUserDto.nickName) {
foundUser.nickName = updateUserDto.nickName;
}
if(updateUserDto.headPic) {
foundUser.headPic = updateUserDto.headPic;
}
try {
await this.userRepository.save(foundUser);
return '用户信息修改成功';
} catch(e) {
this.logger.error(e, UserService);
return '用户信息修改成功';
}
}
只不过现在是传了的属性才会修改,没传的不修改。
然后还要加一个发验证码的接口,这个和别的发验证码的逻辑一样:
@Get('update/captcha')
async updateCaptcha(@Query('address') address: string) {
const code = Math.random().toString().slice(2,8);
await this.redisService.set(`update_user_captcha_${address}`, code, 10 * 60);
await this.emailService.sendMail({
to: address,
subject: '更改用户信息验证码',
html: `<p>你的验证码是 ${code}</p>`
});
return '发送成功';
}
用户列表和分页查询
我们实现冻结用户的接口,冻结的用户不能登录。
@Get('freeze')
async freeze(@Query('id') userId: number) {
await this.userService.freezeUserById(userId,true);
return 'success';
}
在 UserService 定义这个 freezeUserById 方法:
async freezeUserById(id: number, isFreez: boolean) {
const user = await this.userRepository.findOneBy({
id
});
user.isFrozen = isFreez;
await this.userRepository.save(user);
}
解冻用户
@Get('unfreeze')
@RequireLogin()
@RequirePermission('admin')
async unfreeze(@Query('id') userId: number) {
await this.userService.freezeUserById(userId, false);
return 'success';
}
然后实现 /user/list 用户列表接口。
@Get('list')
async list(@Query('pageNo', ParseIntPipe) pageNo: number, @Query('pageSize', ParseIntPipe) pageSize: number) {
return await this.userService.findUsersByPage(pageNo, pageSize);
}
这个接口支持分页查询,传入 pageNo、pageSize,返回对应页的数据。
我们在 UserService 里实现下:
async findUsersByPage(pageNo: number, pageSize: number) {
const skipCount = (pageNo - 1) * pageSize;
const [users, totalCount] = await this.userRepository.findAndCount({
skip: skipCount,
take: pageSize
});
return {
users,
totalCount
}
}
没传 pageNo 的时候会返回 400 的错误。
这个报错信息不够友好,我们改一下:
@Get('list')
async list(
@Query('pageNo', new ParseIntPipe({
exceptionFactory() {
throw new BadRequestException('pageNo 应该传数字');
}
})) pageNo: number,
@Query('pageSize', new ParseIntPipe({
exceptionFactory() {
throw new BadRequestException('pageSize 应该传数字');
}
})) pageSize: number
) {
return await this.userService.findUsersByPage(pageNo, pageSize);
}
我们重构下代码:
把这段代码抽离到 src/utils.ts 里:
export function generateParseIntPipe(name) {
return new ParseIntPipe({
exceptionFactory() {
throw new BadRequestException(name + ' 应该传数字');
}
})
}
那 controller 的代码就可以简化成这样了:
@Get('list')
async list(
@Query('pageNo', generateParseIntPipe('pageNo')) pageNo: number,
@Query('pageSize', generateParseIntPipe('pageSize')) pageSize: number,
) {
return await this.userService.findUsersByPage(pageNo, pageSize);
}
那如果没有传 pageNo 和 pageSize 的时候要设置个默认值呢?
可以使用 DefaultValuePipe:
@Query('pageNo', new DefaultValuePipe(1), generateParseIntPipe('pageNo'))
pageNo: number,
@Query(
'pageSize',
new DefaultValuePipe(2),
generateParseIntPipe('pageSize'),
)
没有传 pageNo 的时候设置为 1,没有传 pageSize 的时候设置为 2。
一条是分页查询,指定了 limit 2 offset 2,这个和 limit 2, 2 是一样的
一条是 count 统计,统计了用户的总条数。
这里返回的信息同样也需要做一些修改。
我们指定下 select 的字段:
select: ['id', 'username', 'nickName', 'email', 'phoneNumber', 'isFrozen', 'headPic', 'createTime'],
用户列表的需求除了分页外,还需要支持根据 username、nickName、email 的搜索:
@Get('list')
async list(
@Query('pageNo', new DefaultValuePipe(1), generateParseIntPipe('pageNo')) pageNo: number,
@Query('pageSize', new DefaultValuePipe(2), generateParseIntPipe('pageSize')) pageSize: number,
@Query('username') username: string,
@Query('nickName') nickName: string,
@Query('email') email: string
) {
return await this.userService.findUsers(username, nickName, email, pageNo, pageSize);
}
在 UserService 添加 findUsers 方法:
async findUsersByPage(username: string, nickName: string, email: string, pageNo: number, pageSize: number) {
const skipCount = (pageNo - 1) * pageSize;
const condition: Record<string, any> = {};
if(username) {
condition.username = Like(`%${username}%`);
}
if(nickName) {
condition.nickName = Like(`%${nickName}%`);
}
if(email) {
condition.email = Like(`%${email}%`);
}
const [users, totalCount] = await this.userRepository.findAndCount({
select: ['id', 'username', 'nickName', 'email', 'phoneNumber', 'isFrozen', 'headPic', 'createTime'],
skip: skipCount,
take: pageSize,
where: condition
});
return {
users,
totalCount
}
}
和之前的区别就是多了个 where 条件。
根据 username、nickName、email 搜索的时候,使用模糊查询。