NestJS实战-后端开发-用户及权限模块

569 阅读10分钟

NestJS实战-后端开发-用户及权限模块

本文介绍 NestJS 实战的用户及权限模块:用户表和权限角色表的构建、权限路由守卫、Roles装饰器、用户接口CRUD操作、服务中如何操作数据库

供自己以后查漏补缺,也欢迎同道朋友交流学习。

引言

项目的后端全局配置安装,上一章已经介绍过了,本章主要介绍下最基础的模块:用户管理、权限管理。

账号管理

我们先做最基础的账号功能,有了账号就可以进行登录了。

用户表 user

我们先进行用户账号表结构设计:

  • id (主键)
  • account (账号)
  • username (用户名)
  • passwordHash (密码哈希)
  • roleId (权限id 外键 关联role表)
  • roleType (权限类型 外键 关联role表)
  • roleWeight (权限重量 外键 关联role表)
  • roleName (权限名称 外键 关联role表)
  • createdBy (创建人id)
  • createdByAccount (创建人账号)
  • createdTime (创建时间)
  • updatedBy (更新人Id)
  • updatedByAccount (更新人账号)
  • updatedTime (更新时间)
  • isDeleted (是否删除 0未删除 1已删除)

用户模块开发

使用快捷键新建 user 模块:

nest g res user

# 默认选择 REST API

根据用户表结构配置 user.entity.ts

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
import { InternalServerErrorException } from '@nestjs/common';

@Entity()
export class User {
  // 主键 唯一且自增长
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 50, unique: true })
  account: string;

  @Column({ type: 'varchar', length: 50 })
  username: string;

  @Column()
  passwordHash: string; // 存储密码哈希

  @Column({ default: 4 }) // 默认访客
  roleId: number; // 外键,关联到角色表的id

  @Column({ default: 'viewer' }) // 默认viewer
  roleType: string; // 外键,关联到角色表的type

  @Column({ default: 3 }) // 默认权重 3
  roleWeight: number; // 外键,关联到角色表的weight

  @Column({ default: '访客' }) // 默认访客
  roleName: string; // 外键,关联到角色表的weight

  @Column({ default: 1 })
  createdBy: number; // 创建人id

  @Column()
  createdByAccount: string;

  @CreateDateColumn()
  createdTime: Date;

  @Column({ default: 1 })
  updatedBy: number; // 更新人Id

  @Column()
  updatedByAccount: string;

  @UpdateDateColumn()
  updatedTime: Date;

  @Column({ default: 0 })
  isDeleted: number; // 是否删除,0表示未删除,1表示已删除

  // 可以添加一个方法来设置密码哈希
  async setPasswordHash(password: string): Promise<string> {
    try {
      const salt = await bcrypt.genSalt(10);
      return await bcrypt.hash(password, salt);
    } catch (error) {
      throw new InternalServerErrorException('设置密码失败');
    }
  }

  // 可以添加一个方法来验证密码
  async validatePassword(password: string): Promise<boolean> {
    return bcrypt.compare(password, this.passwordHash);
  }
}

从上面代码中可以看到,我们设置密码需要安装 bcrypt 来进行设置密码哈希

npm install bcrypt

同时还要关联角色权限表,角色权限表我不想扩展了,就写死4个角色来管理账号,我们的设计如下:

  • 系统管理员systemAdmin 控制系统的一切
  • 管理员admin 除了不能删除修改 systemAdmin,目前其他操作都可以
  • 用户user 不能对账号管理进行增删改操作,其他都可以
  • 访客viewer 不能访问账号管理页面,其他页面也仅可以查看,不能增删改

权限角色表 role

表结构如下

  • id (主键)
  • type (类型)
  • name (名称)
  • weight (权重)

role.entity.ts的代码如下:

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  type: string;

  @Column()
  name: string;

  @Column()
  weight: number;
}

现在我们使用 SQL 语言进行 4 个角色的插入以供后面使用:

INSERT INTO role (type, name, weight)
VALUES
('systemAdmin', '系统管理员', 0),
('admin', '管理员', 1),
('user', '用户', 2),
('viewer', '访客', 3);

权限角色守卫 RolesGuard

src/user 目录下新建一个角色守卫roles.guard.ts去监听路由对用户的角色进行判断他有没有操作权限:

// src/user/roles.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Logger,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Repository } from 'typeorm';
import { Role } from './entities/role.entity';
import { InjectRepository } from '@nestjs/typeorm';

@Injectable()
export class RolesGuard implements CanActivate {
  private logger = new Logger('RolesGuard');

  constructor(
    private reflector: Reflector,
    @InjectRepository(Role) private readonly roleRepository: Repository<Role>,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const requiredRoles = this.reflector.get<string[]>(
      'roles',
      context.getHandler(),
    );

    this.logger.log('@@@@ 路由需要的权限', requiredRoles);

    if (!requiredRoles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    // 查询用户的权限
    if (user.roleId) {
      const roles = await this.roleRepository.findOneBy({ id: user.roleId });
      this.logger.log('@@@@ 用户权限', roles);
      if (requiredRoles.includes(roles.type)) {
        return true;
      } else {
        throw new ForbiddenException('用户没有权限进行操作');
        return false;
      }
    } else {
      this.logger.log('@@@@ 没有用户roleId', user);
      throw new ForbiddenException('没有用户roleId');
      return false;
    }
  }
}

Roles装饰器

再新建一个装饰器配合传递权限数据:

// src/user/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

使用方法如下:

// 其他代码...
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';

@Controller('user')
@UseGuards(RolesGuard)
@ApiTags('用户及角色管理')
export class UserController {
  // 其他代码...

  @Post()
  @Roles('systemAdmin', 'admin')
  create(@Body() createUserDto: CreateUserDto, @Req() req) {
    // 这里就表示只允许系统管理员和管理员新建账户
    // 其他代码...
  }

  // 其他代码...
}

定义用户数据传输对象

新建用户数据传输对象用于控制器中进行类型定义、格式校验,实现如下功能:

  • 安装 class-validatorclass-transformer 用于后续格式校验
  • 提供用户的增删改查相关数据传输对象
创建用户对象 CreateUserDto

create-user.dto.ts 代码如下:

import { ApiProperty } from '@nestjs/swagger';
import {
  IsNotEmpty,
  IsString,
  IsEmail,
  IsNumber,
  IsOptional,
  Matches,
} from 'class-validator';

export class CreateUserDto {
  @IsEmail({}, { message: '账号必须是邮箱格式' })
  @IsString({ message: '账号必须是字符串' })
  @IsNotEmpty({ message: '账号不能为空' })
  @ApiProperty({
    description: '账号(邮箱格式)',
    example: 'niunai@niunai.com',
  })
  account: string;

  @IsString({ message: '用户名必须是字符串' })
  @IsNotEmpty({ message: '用户名不能为空' })
  @ApiProperty({ description: '用户名', example: '牛奶' })
  username: string;

  @Matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/, {
    message: '请输入8-16位数字+字母的密码',
  })
  @IsString({ message: '密码必须是字符串' })
  @IsNotEmpty({ message: '密码不能为空' })
  @ApiProperty({ description: '密码', example: 'admin123' })
  password: string;

  @ApiProperty({
    description: '角色id',
    example: 4,
    required: false,
  })
  @IsNumber()
  @IsOptional()
  roleId?: number = 4;
}
查询用户对象 ListUserDto

list-user.dto.ts 代码如下:

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional } from 'class-validator';

export class ListUserDto {
  @ApiProperty({ description: '角色id', example: 4 })
  @IsOptional()
  roleId?: null | number = 4;

  @ApiProperty({
    description: '账号(邮箱格式)',
    example: 'niunai@niunai.com',
  })
  @IsOptional()
  account?: null | string;

  @ApiProperty({ description: '用户名', example: '牛奶' })
  @IsOptional()
  username?: null | string;

  @ApiProperty({ description: '账号id', example: 1 })
  @IsOptional()
  id?: null | number;

  @ApiProperty({ description: '页码', example: 1 })
  @IsNotEmpty({ message: 'pageNum不能为空' })
  pageNum: number = 1;

  @ApiProperty({ description: '每页查询数量', example: 10 })
  @IsNotEmpty({ message: 'pageSize不能为空' })
  pageSize: number = 10;
}
更新用户对象 UpdateUserDto

update-user.dto.ts 代码如下:

import { OmitType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends OmitType(CreateUserDto, [
  'account', // account不允许修改
  'password', // password不允许修改
] as const) {}
重置密码对象 ResetPasswordDto

reset-password.dto.ts 代码如下:

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, Matches } from 'class-validator';

export class ResetPasswordDto {
  @IsNotEmpty({ message: 'id不能为空' })
  @ApiProperty({ description: '账号id', example: 5 })
  id: number;

  @Matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/, {
    message: '请输入8-16位数字+字母的密码',
  })
  @IsNotEmpty({ message: '新密码不能为空' })
  @ApiProperty({ description: '新密码', example: 'admin123' })
  newPassword: string;

  @IsNotEmpty({ message: '确认密码不能为空' })
  @ApiProperty({ description: '确认密码', example: 'admin123' })
  confirmPassword: string;
}
更新密码 UpdatePasswordDto
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, Matches } from 'class-validator';

export class UpdatePasswordDto {
  @Matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/, {
    message: '请输入8-16位数字+字母的密码',
  })
  @IsNotEmpty({ message: '旧密码不能为空' })
  @ApiProperty({ description: '旧密码', example: 'admin123' })
  oldPassword: string;

  @Matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/, {
    message: '请输入8-16位数字+字母的密码',
  })
  @IsNotEmpty({ message: '新密码不能为空' })
  @ApiProperty({ description: '新密码', example: 'admin123' })
  newPassword: string;

  @IsNotEmpty({ message: '确认密码不能为空' })
  @ApiProperty({ description: '确认密码', example: 'admin123' })
  confirmPassword: string;
}

用户CRUD相关操作 UserController

在控制器中编写用户增删改查相关操作,这个也是给前端的接口定义,我们主要实现如下几个接口:

接口请求定义描述备注
/user/roleListGET查询角色列表获取所有角色列表,用来做角色权限管理写死的4个角色,后期不会增改
/userPost创建用户创建用户account是唯一值,不可修改
/userGet查询用户列表分页查询用户列表根据query、pageSize、PageNum分页查询用户列表
/user/{id}Get查询用户根据id查询用户-
/user/{id}Patch更新用户根据id更新用户account不能修改
/user/{id}Delete删除用户根据id删除用户-
/userDelete删除用户根据ids删除用户-
/user/userSearchExportPost用户表查询导出用户表查询导出根据查询条件导出excel
/user/userSelectExportPost用户表勾选导出用户表勾选导出根据勾选的ids导出excel
/user/resetPasswordPost用户重置密码用户重置密码重置密码
/user/userUpdatePasswordPost用户修改密码用户修改密码用户修改密码

user.controller.ts 代码如下:

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  Query,
  Res,
  BadRequestException,
  HttpStatus,
  Logger,
  Req,
  UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import {
  ApiBody,
  ApiOperation,
  ApiParam,
  ApiQuery,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { ListUserDto } from './dto/list-user.dto';
import { ExcelService } from 'src/common/excel/excel.service';
import { ResetPasswordDto } from './dto/reset-password.dto';
import { MetricsService } from 'src/common/metrics/metrics.service';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
import { UpdatePasswordDto } from './dto/update-password.dto';

@Controller('user')
@UseGuards(RolesGuard)
@ApiTags('用户及角色管理')
export class UserController {
  private logger = new Logger('UserController');
  constructor(
    private readonly userService: UserService,
    private readonly excelService: ExcelService,
    private readonly metricsService: MetricsService,
  ) {}

  @Get('roleList')
  @ApiOperation({
    summary: '查询角色列表',
    description: '获取所有角色列表,用来做角色权限管理',
  })
  roleList() {
    return this.userService.roleList();
  }

  @Post()
  @Roles('systemAdmin', 'admin')
  @ApiOperation({
    summary: '创建用户',
    description: '创建用户',
  })
  @ApiBody({ type: CreateUserDto })
  @ApiResponse({ status: 200, description: '创建成功' })
  create(@Body() createUserDto: CreateUserDto, @Req() req) {
    // 服务监控代码-与业务无关
    this.metricsService.incrementRequestCount('Post', '/user');
    return this.userService.create(createUserDto, req);
  }

  @Get()
  @ApiOperation({
    summary: '查询用户列表',
    description: '查询用户列表',
  })
  @ApiQuery({ name: 'query', type: ListUserDto })
  @ApiResponse({ status: 200, description: '查询用户成功' })
  findAll(@Query() query: ListUserDto) {
    return this.userService.findAllByPage(query);
  }

  @Get(':id')
  @ApiOperation({
    summary: '查询用户',
    description: '根据id查询用户',
  })
  @ApiParam({ name: 'id', description: '账号id' })
  @ApiResponse({ status: 200, description: '查询用户成功' })
  findOne(@Param('id') id: string) {
    return this.userService.findOne(+id);
  }

  @Patch(':id')
  @Roles('systemAdmin', 'admin')
  @ApiOperation({
    summary: '更新用户',
    description: '根据id更新用户',
  })
  @ApiParam({ name: 'id', description: '账号id' })
  @ApiBody({ description: '账号信息', type: UpdateUserDto })
  @ApiResponse({ status: 200, description: '更新成功' })
  update(
    @Param('id') id: string,
    @Body() updateUserDto: UpdateUserDto,
    @Req() req,
  ) {
    return this.userService.update(+id, updateUserDto, req);
  }

  @Delete(':id')
  @Roles('systemAdmin', 'admin')
  @ApiOperation({
    summary: '删除用户',
    description: '根据id删除用户',
  })
  @ApiParam({ name: 'id', description: '账号id' })
  @ApiResponse({ status: 200, description: '删除成功' })
  remove(@Param('id') id: string, @Req() req) {
    return this.userService.softDeleteUser(+id, req);
  }

  @Delete()
  @Roles('systemAdmin', 'admin')
  @ApiOperation({
    summary: '批量删除用户',
    description: '根据ids删除用户',
  })
  @ApiBody({ description: '账号id列表' })
  @ApiResponse({ status: 200, description: '批量删除用户成功' })
  removeUsers(@Body('ids') ids: number[], @Req() req) {
    this.logger.log('@@@ ids', ids);
    return this.userService.softDeleteUsers(ids, req);
  }

  @Post('/userSearchExport')
  @Roles('systemAdmin', 'admin', 'user')
  @ApiOperation({
    summary: '用户表查询导出',
    description: '用户表查询导出',
  })
  @ApiBody({ type: ListUserDto })
  @ApiResponse({ status: 200, description: '用户表查询导出成功' })
  async userSearchExport(@Body() body: ListUserDto, @Res() res): Promise<void> {
    const userList = await this.userService.findAll(body);

    if (!userList?.length) {
      throw new BadRequestException('导出数据为空');
    }

    try {
      // 导出为 Excel 文件
      const filename = encodeURIComponent('用户表.xlsx');
      const buffer = this.excelService.exportAsExcelFile(userList);

      this.logger.log('@@@ buffer', buffer);
      this.logger.log('@@@ filename', filename);

      // 设置响应头
      res.setHeader(
        'Content-Type',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      );
      res.setHeader('Content-Disposition', `attachment; filename=${filename}`);

      // 发送文件
      res.status(HttpStatus.OK).end(buffer);
    } catch (error) {
      this.logger.error('@@@ 用户表查询导出接口调用失败:', error);
      throw new BadRequestException('用户表查询导出接口调用失败');
    }
  }

  @Post('/userSelectExport')
  @Roles('systemAdmin', 'admin', 'user')
  @ApiOperation({
    summary: '用户表勾选导出',
    description: '用户表勾选导出',
  })
  @ApiBody({ description: '账号id列表' })
  @ApiResponse({ status: 200, description: '用户表勾选导出成功' })
  async userSelectExport(
    @Body('ids') ids: number[],
    @Res() res,
  ): Promise<void> {
    const userList = await this.userService.findByIds(ids);

    if (!userList?.length) {
      throw new BadRequestException('导出数据为空');
    }

    try {
      // 导出为 Excel 文件
      const filename = encodeURIComponent('用户勾选表.xlsx');
      const buffer = this.excelService.exportAsExcelFile(userList);

      this.logger.log('@@@ buffer', buffer);
      this.logger.log('@@@ filename', filename);

      // 设置响应头
      res.setHeader(
        'Content-Type',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      );
      res.setHeader('Content-Disposition', `attachment; filename=${filename}`);

      // 发送文件
      res.status(HttpStatus.OK).end(buffer);
    } catch (error) {
      this.logger.error('@@@ 用户表勾选导出接口调用失败:', error);
      throw new BadRequestException('用户表勾选导出接口调用失败');
    }
  }

  @Post('/resetPassword')
  @Roles('systemAdmin', 'admin')
  @ApiOperation({
    summary: '用户重置密码',
    description: '用户重置密码',
  })
  @ApiBody({ type: ResetPasswordDto })
  @ApiResponse({ status: 200, description: '用户重置密码成功' })
  async resetPassword(@Body() body: ResetPasswordDto, @Req() req) {
    return this.userService.resetPassword(body, req);
  }

  @Post('/userUpdatePassword')
  @ApiOperation({
    summary: '用户修改密码',
    description: '用户修改密码',
  })
  @ApiBody({ type: UpdatePasswordDto })
  @ApiResponse({ status: 200, description: '用户修改密码成功' })
  async userUpdatePassword(@Body() body: UpdatePasswordDto, @Req() req) {
    return this.userService.userUpdatePassword(body, req);
  }
}

用户服务 UserService

数据库的操作主要是在 Service 里做,UserService 主要是给 UserController 提供服务,也可以提供给其他模块调用:

import {
  BadRequestException,
  Injectable,
  InternalServerErrorException,
  Logger,
  Req,
} from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Like, Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { ListUserDto } from './dto/list-user.dto';
import { ResetPasswordDto } from './dto/reset-password.dto';
import {
  removeUnnecessaryData,
  setCreatedUser,
  setDeletedUser,
  setUpdatedUser,
} from 'src/utils';
import { UpdatePasswordDto } from './dto/update-password.dto';

@Injectable()
export class UserService {
  private logger = new Logger('UserService');
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    @InjectRepository(Role) private readonly roleRepository: Repository<Role>,
  ) {}

  // 获取用户权限列表
  roleList() {
    return this.roleRepository.find();
  }

  async findRoleById(id: number): Promise<Role> {
    if (!id) {
      throw new BadRequestException('id必填');
    }

    return await this.roleRepository.findOne({
      where: { id },
    });
  }

  async create(createUserDto: CreateUserDto, @Req() req) {
    this.logger.log('@@@@ 创建用户参数:', createUserDto);
    // 检查roleId是否存在
    const role = await this.findRoleById(createUserDto.roleId || 4);
    if (!role) {
      this.logger.warn(`id为${createUserDto.roleId}的角色没有找到`);
      throw new BadRequestException(
        `id为${createUserDto.roleId}的角色没有找到`,
      );
    }

    // 检查account是否已存在
    const account = await this.userRepository.findOne({
      where: { account: createUserDto.account },
    });

    if (account) {
      this.logger.warn(`账号${createUserDto.account}已存在,不能重复创建`);
      throw new BadRequestException(
        `账号${createUserDto.account}已存在,不能重复创建`,
      );
    }

    // 创建用户实体
    const user = this.userRepository.create(createUserDto);
    user.roleType = role.type;
    user.roleWeight = role.weight;
    user.roleName = role.name;

    // 设置密码哈希
    const passwordHash: string = await user.setPasswordHash(
      createUserDto.password,
    );
    user.passwordHash = passwordHash;

    const createdUser = setCreatedUser(req, user);

    this.logger.log('@@@@ 创建用户实体 createdUser', createdUser);

    try {
      return await this.userRepository.save(createdUser);
    } catch (error) {
      this.logger.error('@@@@ 新建账号失败:', error);
      throw new InternalServerErrorException('新建账号失败');
    }
  }

  async findAll(query: ListUserDto) {
    try {
      const queryParams = {
        account: query?.account || null,
        username: query.username ? Like(`%${query.username}%`) : null,
        roleId: query?.roleId || null,
        isDeleted: 0,
      };
      const data = await this.userRepository.find({
        where: queryParams,
      });

      return removeUnnecessaryData(data);
    } catch (error) {
      this.logger.error('@@@@ 账号列表查询失败:', error);
      throw new InternalServerErrorException('账号列表查询失败');
    }
  }

  async findAllByPage(query: ListUserDto) {
    try {
      const queryParams = {
        id: query?.id || null,
        account: query?.account ? Like(`%${query.account}%`) : null,
        username: query.username ? Like(`%${query.username}%`) : null,
        roleId: query?.roleId || null,
        isDeleted: 0,
      };
      const [data, total] = await this.userRepository.findAndCount({
        where: queryParams,
        order: {
          id: 'DESC',
        },
        skip: (query.pageNum - 1) * query.pageSize,
        take: query.pageSize,
      });

      return {
        list: removeUnnecessaryData(data),
        pageNum: Number(query.pageNum),
        pageSize: Number(query.pageSize),
        total,
      };
    } catch (error) {
      this.logger.error('@@@@ 账号列表查询失败:', error);
      throw new InternalServerErrorException('账号列表查询失败');
    }
  }

  async findOne(id: number) {
    if (!id) {
      throw new BadRequestException('id必填');
    }
    try {
      const data = await this.userRepository.findOne({
        where: { id, isDeleted: 0 },
      });
      delete data.passwordHash;
      delete data.isDeleted;
      return data;
    } catch (error) {
      this.logger.error('@@@@ 账号查询失败:', error);
      throw new InternalServerErrorException('账号查询失败');
    }
  }

  async findByIds(ids: number[]): Promise<User[]> {
    try {
      const data = await this.userRepository.find({
        where: { id: In(ids), isDeleted: 0 },
      });
      return removeUnnecessaryData(data);
    } catch (error) {
      this.logger.error('@@@@ 账号批量查询失败:', error);
      throw new InternalServerErrorException('账号批量查询失败');
    }
  }

  // 更新用户
  async update(id: number, updateUserDto: UpdateUserDto, @Req() req) {
    if (!id) {
      throw new BadRequestException('id必填');
    }

    const user = await this.userRepository.findOneBy({
      id,
    });

    if (req.user.roleWeight >= user.roleWeight && req.user.roleWeight !== 0) {
      throw new BadRequestException(
        `当前角色权限过低,无法对${user.account}进行该操作`,
      );
    }

    const role = await this.roleRepository.findOneBy({
      id: updateUserDto.roleId,
    });
    user.roleType = role.type;
    user.roleWeight = role.weight;
    user.roleName = role.name;

    try {
      // 设置更新用户信息
      const updatedUser = setUpdatedUser(req, user);

      this.logger.log('@@@ 更新用户 updatedUser', updatedUser);

      return await this.userRepository.update(id, updatedUser);
    } catch (error) {
      this.logger.error('@@@@ 更新账号失败:', error);
      throw new InternalServerErrorException('更新账号失败');
    }
  }

  // 重置登录密码
  async resetPassword(body: ResetPasswordDto, @Req() req) {
    const user = await this.userRepository.findOneBy({
      id: body.id,
    });

    if (req.user.roleWeight >= user.roleWeight && req.user.roleWeight !== 0) {
      throw new BadRequestException(
        `当前角色权限过低,无法对${user.account}进行该操作`,
      );
    }

    if (body.confirmPassword !== body.newPassword) {
      throw new BadRequestException('新密码与确认密码不一致');
    }

    try {
      // 设置密码哈希
      const passwordHash: string = await user.setPasswordHash(body.newPassword);
      user.passwordHash = passwordHash;
      // 设置更新用户信息
      const updatedUser = setUpdatedUser(req, user);

      this.logger.log('@@@ 重置登录密码 updatedUser', updatedUser);

      return await this.userRepository.update(body.id, updatedUser);
    } catch (error) {
      this.logger.error('@@@@ 更新密码失败:', error);
      throw new InternalServerErrorException('更新密码失败');
    }
  }

  // 更新密码
  async userUpdatePassword(body: UpdatePasswordDto, @Req() req) {
    // 旧密码和新密码比较
    if (body.oldPassword === body.newPassword) {
      throw new BadRequestException('旧密码不能和新密码相同');
    }

    // 新密码和确认密码比较
    if (body.confirmPassword !== body.newPassword) {
      throw new BadRequestException('新密码与确认密码不一致');
    }

    // 调用数据库验证用户旧密码
    const user: User = await this.userRepository.findOneBy({
      id: req.user.userId,
    });
    const isPassword = await user.validatePassword(body.oldPassword);
    if (!isPassword) {
      throw new BadRequestException('旧密码错误,请确认后重新输入');
    }

    try {
      this.logger.log('@@@@ 修改账户密码 body', body);
      // 修改账户密码
      const passwordHash: string = await user.setPasswordHash(body.newPassword);
      user.passwordHash = passwordHash;
      // 设置更新用户信息
      const updatedUser = setUpdatedUser(req, user);

      return await this.userRepository.update(user.id, updatedUser);
    } catch (error) {
      this.logger.error('@@@@ 修改账户密码失败:', error);
      throw new InternalServerErrorException('修改账户密码失败');
    }
  }

  // 软删除单个用户
  async softDeleteUser(id: number, @Req() req) {
    if (!id) {
      throw new BadRequestException('id必填');
    }

    const user = await this.userRepository.findOneBy({
      id,
    });

    if (req.user.roleWeight >= user.roleWeight && req.user.roleWeight !== 0) {
      throw new BadRequestException(
        `当前角色权限过低,无法对${user.account}进行该操作`,
      );
    }

    try {
      const removedUser = setDeletedUser(req, user);

      this.logger.log('@@@ 软删除单个用户 removedUser', removedUser);

      return await this.userRepository.update(id, removedUser);
    } catch (error) {
      this.logger.error('@@@@ 删除账号失败:', error);
      throw new InternalServerErrorException('删除账号失败');
    }
  }

  // 软删除批量用户
  async softDeleteUsers(ids: number[], @Req() req) {
    for (const id of ids) {
      const user = await this.userRepository.findOneBy({
        id,
      });

      if (req.user.roleWeight >= user.roleWeight && req.user.roleWeight !== 0) {
        throw new BadRequestException(
          `当前角色权限过低,无法对${user.account}进行该操作`,
        );
        break;
      }
    }

    try {
      const user = new User();
      const removedUser = setDeletedUser(req, user);

      this.logger.log('@@@ 软删除批量用户 removedUser', removedUser);

      return await this.userRepository.update(ids, removedUser);
    } catch (error) {
      this.logger.error('@@@@ 批量删除账号失败:', error);
      throw new InternalServerErrorException('批量删除账号失败');
    }
  }
}

用户模型 UserModule

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { ExcelModule } from 'src/common/excel/excel.module';
import { MetricsService } from 'src/common/metrics/metrics.service';

@Module({
  imports: [TypeOrmModule.forFeature([User, Role]), ExcelModule],
  controllers: [UserController],
  providers: [UserService, MetricsService],
  exports: [UserService], // 导出UserService以便其他模块可以使用
})
export class UserModule {}

用户管理功能接口完成

目前完成了11个用户相关的接口

user-module

实战合集地址

仓库地址