上一篇文章中我们已经实现了前端部分的个人中心页面以及相关接口的处理。对于前端来说功能实现很简单,无非就是调用接口渲染页面。其中接口的主要逻辑处理其实在后端。本篇文章就来介绍一下如何使用 NestJS 来实现密码修改、头像上传、个人信息查询、个人信息修改接口。
个人信息查询
我们从最简单的接口开始,实现个人信息查询接口。查询接口的实现很简单,我们只需要在user.controller.ts中添加一个getUserInfo方法,从Request中获取当前用户id,然后在user.service.ts中实现这个方法根据id查询用户表即可。
//user.controller.ts
//获取用户信息
@Get('/profile')
@ApiOperation({ summary: '获取用户信息' })
async getUserInfo(@Req() req: Request) {
return await this.userService.getUserInfo(req);
}
//user.service.ts
//个人信息
async getUserInfo(req) {
try {
const user = await this.userRepository.findOne({
where: {
id: req.user.sub
}
})
user.avatar = fileconfig.fileSaveUrl + user.avatar
return user
} catch (error) {
throw new ApiException('查询个人信息失败', ApiErrorCode.FAIL)
}
}
个人信息修改
除了查询他的个人信息,我们还需要让用户能修改自己的信息,我们只需要在user.controller.ts中添加一个updateUserInfo方法,从Request中获取当前用户id,然后在user.service.ts中实现这个方法根据id及前端传来的需要修改的信息更新用户表即可。
//user.controller.ts
//修改个人信息
@Put('/updateUserInfo')
@ApiOperation({ summary: '修改个人信息' })
async updateUserInfo(@Req() req: Request, @Body() updateUserDto: UpdateUserDto) {
return await this.userService.updateUserInfo(req, updateUserDto);
}
//user.service.ts
//修改个人信息
async updateUserInfo(req, updateUserDto: UpdateUserDto) {
const id = req.user.sub
try {
const newUser = new User();
//newUser.password = updateUserDto.password;
newUser.nickname = updateUserDto.nickname;
newUser.email = updateUserDto.email;
newUser.telephone = updateUserDto.telephone;
newUser.id = id;
await this.userRepository.save(newUser);
return '修改成功';
} catch (error) {
throw new ApiException('修改失败', ApiErrorCode.FAIL);
}
}
密码修改
然后我们看一下密码修改的接口实现。同样的,我们需要在user.controller.ts中添加一个updatePassword方法,从Request中获取当前用户id,然后在user.service.ts中根据前端传来的原密码及新密码进行一些逻辑的处理即可。
//user.controller.ts
//修改密码
@Put('/updatePassword')
@ApiOperation({ summary: '修改密码' })
async updatePassword(@Req() req: Request & { user: any }, @Body() updateUserDto: UpdateUserPasswordDto) {
return await this.userService.updatePassword(req, updateUserDto);
}
其中的UpdateUserPasswordDto定义前端需要传递的参数
export class UpdateUserPasswordDto {
@IsNotEmpty({
message: "原密码不能为空",
})
@ApiProperty({
example: "123456",
description: "原密码",
})
oldPassword: string;
@IsNotEmpty({
message: "新密码不能为空",
})
@ApiProperty({
example: "1234567",
description: "新密码",
})
newPassword: string;
}
然后在user.service.ts中实现对应逻辑。
- 第一步 根据
id查询用户表,获取到用户加密后的密码 - 第二步 将前端传来的原密码进行加密,然后与用户加密后的密码进行比较,如果不一致则抛出异常
- 第三步 将前端传来的新密码进行加密,然后更新用户表
代码如下
//修改密码
async updatePassword(req: Request & { user: any }, updatePasswordDto: UpdateUserPasswordDto) {
const { oldPassword, newPassword } = updatePasswordDto;
const id = req.user.sub;
try {
const user = await this.userRepository.findOne({
where: {
id
}
})
//判断输入的原密码是否正确
if (user.password !== encry(oldPassword, user.salt)) {
throw Error('原密码错误');
}
//加密新密码并更新密码
user.password = encry(newPassword, user.salt);
await this.userRepository.update(id, { password: user.password });
return '修改成功';
} catch (error) {
throw new ApiException(error.message, ApiErrorCode.FAIL);
}
}
这样就完成了密码修改的接口实现。这里需要注意的是我们数据库保存的密码都是加密后的,我们并不知道用户的真实密码。这也是为什么很多系统忘记密码要重置而不是告诉你原密码,因为他也不知道你的原密码是什么
头像上传
头像上传接口的实现相对于前几个接口要复杂一些,因为涉及到了文件的保存及相关的一些处理。处理前端传来的文件我们需要使用到FileInterceptor拦截器,它接收两个参数,第一个参数是前端传递的文件的字段名,第二个参数是配置对象,我们需要用到配置对象中的storage参数来指定文件的保存路径,以及文件名的生成规则。
import { Multer, diskStorage } from 'multer';
import { FileInterceptor } from'@nestjs/platform-express';
import fileconfig from 'src/config/file';
import { checkDirExists, deleteOldFile } from 'src/utils/fileUtils';
//user.controller.ts
//头像上传
@Post('/uploadAvatar')
@ApiOperation({ summary: '头像上传' })
@ApiConsumes('multipart/form-data')
@UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: async (req, _file, cb) => {
try {
//保存文件地址,这里我们将文件保存到以用户id命名的目录下
const saveDirectory = path.join(process.cwd(), fileconfig.saveDirectory, String(req.user.sub));
// 检查目录是否存在,如果不存在则创建
checkDirExists(saveDirectory);
//将文件保存地址保存到req中,可以在controller中使用
req.saveDirectory = saveDirectory;
cb(null, saveDirectory)
} catch (error) {
cb(new ApiException('文件目录处理失败', ApiErrorCode.COMMON_CODE), null)
}
},
filename: (req, file, cb) => {
//获取文件的扩展名
const ext = path.extname(file.originalname);
if (!ext.match(/\.(jpg|jpeg|png|gif)$/i)) {
cb(new ApiException('请上传图片类型文件', ApiErrorCode.COMMON_CODE), null)
return
}
if (file.size > 500 * 1024) {
cb(new ApiException('图片大小不能超过500k', ApiErrorCode.COMMON_CODE), null)
return
}
// 生成唯一文件名
const uniqueSuffix = Date.now() + '_' + Math.round(Math.random() * 1E9);
const filename = `${fileconfig.avatarPrefix}_${uniqueSuffix}${ext}`;
//将文件名保存到req中,可以在controller中使用
req.filename = filename;
cb(null, filename);
},
}),
}))
async uploadAvatar(@Req() req: Request & { filename: string, user: any }, @Body() _body: UploadFileDto) {
/**
* 此时最新头像图片已经保存,需要删除旧头像图片
*
* 头像存放目录 newAvatarDir
*/
const newAvatarDir = path.join(process.cwd(), fileconfig.saveDirectory, String(req.user.sub))
//查找当前用户目录下所有头像,如果不是最新头像文件,则删除
deleteOldFile(newAvatarDir, req.filename, fileconfig.avatarPrefix)
return await this.userService.uploadAvatar(`${fileconfig.saveDirectory}${req.user.sub}/${req.filename}`, req);
}
其中的fileconfig定义了文件保存的域名地址、目录及头像名前缀如下,这里的fileSaveUrl根据自己的服务器进行修改,当然如果你不想保存到自己的服务器,也可以使用其他的云存储服务。
export default {
// 上传文件保存地址
fileSaveUrl: "http://172.16.10.157:3000/",
// 上传文件保存目录
saveDirectory: "static/upload/",
// 头像图片名前缀
avatarPrefix: "fs_avatar",
};
checkDirExists是一个检查目录是否存在的方法,如果不存在则创建。deleteOldFile是一个删除旧文件的方法,它接收三个参数,第一个参数是文件目录,第二个参数是最新上传的文件名,第三个参数是文件类型前缀。它会查找当前目录下所有以当前前缀开头的文件,如果不是最新上传的文件,则删除。
import { existsSync, mkdirSync, unlinkSync, readdirSync } from "fs";
import { ApiErrorCode } from "src/common/enums/api-error-code.enum";
import { ApiException } from "src/common/filter/http-exception/api.exception";
//判断文件目录是否存在,不存在则创建
export const checkDirExists = (dir: string) => {
try {
if (!existsSync(dir)) mkdirSync(dir);
} catch (error) {
throw new ApiException("创建目录失败", ApiErrorCode.FILE_ERROR);
}
};
/**
*
* @param dir 文件目录
* @param newFileName 最新上传文件名
* @param typePreFix 文件类型前缀 如:fs_avatar表示头像
*/
export const deleteOldFile = (
dir: string,
newFileName: string,
typePreFix?: string
) => {
if (!newFileName)
throw new ApiException("文件操作有误,请重新上传", ApiErrorCode.FILE_ERROR);
try {
const fileList = readdirSync(dir);
fileList.forEach((fileName: string) => {
if (typePreFix) {
//如果以当前前缀开头,且不是最新上传的文件,则删除
if (fileName.startsWith(typePreFix) && fileName !== newFileName) {
unlinkSync(`${dir}/${fileName}`);
}
} else {
//如果没有传前缀类型,则不是最新上传的文件就删除
if (fileName !== newFileName) {
unlinkSync(`${dir}/${fileName}`);
}
}
});
} catch (error) {
throw new ApiException("删除旧文件失败", ApiErrorCode.FILE_ERROR);
}
};
最后调用user.service.ts中的uploadAvatar方法将路径保存到数据库中。
//头像上传
async uploadAvatar(path: string, req) {
const { user } = req
try {
await this.userRepository.update({ id: user.sub }, { avatar: path })
return '上传成功';
} catch (error) {
throw new ApiException('上传失败', ApiErrorCode.FAIL)
}
}
注意这里我们只保存文件目录地址而不包含baseUrl(这里指的就是我们配置的fileSaveUrl),因为你的主机地址可以会更改。因此前端查询个人信息的头像地址时需要拼接上baseUrl。如果不想让前端处理,可以在查询个人信息的时候做个简单处理,拼接上baseUrl返回给前端即可。
//个人信息
async getUserInfo(req) {
try {
const user = await this.userRepository.findOne({
where: {
id: req.user.sub
}
})
user.avatar = fileconfig.fileSaveUrl + user.avatar
return user
} catch (error) {
throw new ApiException('查询个人信息失败', ApiErrorCode.FAIL)
}
}
此时前端调用个人信息接口就可以获取到完整的头像地址了。