《NestJS 实战:RBAC 系统管理模块开发 (四)》:用户绑定

引言

大家好,我是elk。在之前的文章中,我们已经完成了菜单管理和角色管理模块的开发。今天,我们将聚焦整个权限管理系统的核心------用户管理 。用户管理作为系统权限控制的最终载体,承担着连接角色、菜单和组织架构的重要功能。

项目结构与模块创建

模块快速生成

使用Nest CLI快速生成用户模块的基础结构:

bash 复制代码
# 创建标准CRUD模板
nest g res user

该命令会自动生成控制器、服务、DTO等文件,位于 src/module/system/user 目录下,形成如下结构:

text 复制代码
user/
├── dto/
│   ├── create-user.dto.ts
│   ├── update-user.dto.ts
│   └── list-user-dto.ts
├── entities/
│   └── user.entity.ts
├── user.controller.ts
└── user.service.ts

DTO设计与实体映射

DTO定义 (create-user.dto.ts):

typescript 复制代码
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';

export class CreateUserDto {
  @ApiProperty({ description: '用户ID', required: false })
  userId?: number;
  
  @ApiProperty({ description: '用户名', required: true })
  userName: string;
  
  @ApiProperty({ description: '密码', required: true })
  password: string;
  
  @ApiProperty({ description: '部门ID' })
  deptId?: string;
  
  // 其他字段...
  nickName?: string;
  phone?: string;
  email?: string;
  avatar?: string;
  sex?: number;
  status?: number;
  remark?: string;
}

实体映射 (user.entity.ts):

typescript 复制代码
import { sys_user as User } from '@prisma/client';

export class UserEntity implements User {
  user_id: number;
  user_name: string;
  password: string;
  dept_id: string;
  nick_name: string;
  phone: string;
  email: string;
  avatar: string;
  sex: number;
  status: number;
  remark: string;
  created_at: Date;
  updated_at: Date;
}

用户管理核心实现

接口设计全景

用户管理模块需要实现以下核心功能接口:

功能 接口类型 路径 参数 技术点
新增用户 POST /create CreateUserDto 事务处理、关联创建
用户列表 GET /list pageNum, pageSize 分页优化、关联查询、敏感字段过滤、数据转换
用户详情 GET /:id id 关联加载、按需过滤、数据格式化
修改用户 PUT / UpdateUserDto 复杂事务、关联更新策略、部分更新
删除用户 DELETE /:id id 级联删除、原子操作

控制器实现

typescript 复制代码
import {
  Controller,
  Get,
  Post,
  Body,
  Put,
  Param,
  Delete,
  Query,
} from '@nestjs/common';
import { UserService } from './user.service';
import {
  ApiTags,
  ApiOperation,
  ApiBody,
  ApiParam,
  ApiQuery,
} from '@nestjs/swagger';

@ApiTags('用户管理')
@Controller('/system/user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  // 新增用户
  @Post('create')
  @ApiOperation({ 
    summary: '用户管理-新增用户', 
    description: '创建新用户并分配角色/部门' 
  })
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }

  // 获取用户列表(分页)
  @Get('/list')
  @ApiOperation({
    summary: '用户管理-获取用户列表',
    description: '获取分页用户列表,包含关联角色和部门信息',
  })
  list(@Query() params: ListUserDto) {
    return this.userService.findAll(params);
  }
  
  // 其他接口实现...
}

服务层核心逻辑

用户创建(事务处理)

typescript 复制代码
async create(createUserDto: CreateUserDto) {
  // 创建用户主记录
  const user = await this.prisma.sys_user.create({
    data: {
      user_name: createUserDto.userName,
      password: createUserDto.password, // 实际项目需加密存储
      dept_id: createUserDto.deptId,
      // 其他字段...
    },
  });

  // 创建用户角色关联
  const rolePromise = this.prisma.sys_user_role.create({
    data: {
      user_id: user.user_id,
      role_id: createUserDto.roleIds,
    },
  });

  // 创建用户部门关联(多对多)
  const deptIds = createUserDto.deptId.split(',') || ['0'];
  const deptPromise = this.prisma.sys_user_dept.createMany({
    data: deptIds.map(deptId => ({
      user_id: user.user_id,
      dept_id: Number(deptId),
    })),
  });

  // 事务处理 - 确保所有操作原子性
  try {
    await this.prisma.$transaction([rolePromise, deptPromise]);
    return '用户创建成功';
  } catch (error) {
    throw new Error('用户创建失败: ' + error.message);
  }
}

用户列表查询(关联数据处理)

typescript 复制代码
async findAll({ pageNum, pageSize }: ListUserDto) {
  const users = await this.prisma.sys_user.findMany({
    skip: (pageNum - 1) * pageSize,
    take: Number(pageSize),
    omit: { password: true }, // 关键:排除密码字段
    include: {
      roles: {
        select: { role: { select: { role_id: true, role_name: true } } }
      },
      depts: {
        select: { dept: { select: { dept_id: true, dept_name: true } } }
      },
    },
  });

  // 数据结构转换
  return users.map(user => ({
    ...user,
    roles: user.roles.map(r => r.role),
    depts: user.depts.map(d => d.dept)
  }));
}

用户更新(复杂事务处理)

typescript 复制代码
async update(updateUserDto: UpdateUserDto) {
  // 更新用户角色关联
  const rolePromise = this.prisma.sys_user_role.updateMany({
    where: { user_id: updateUserDto.userId },
    data: { role_id: updateUserDto.roleIds },
  });

  // 更新用户部门关联(先删后增)
  const deptIds = updateUserDto.deptId.split(',');
  
  const deleteDeptPromise = this.prisma.sys_user_dept.deleteMany({
    where: { user_id: updateUserDto.userId },
  });
  
  const createDeptPromise = this.prisma.sys_user_dept.createMany({
    data: deptIds.map(deptId => ({
      user_id: updateUserDto.userId,
      dept_id: Number(deptId),
    })),
  });

  // 更新用户基本信息
  const userPromise = this.prisma.sys_user.update({
    where: { user_id: updateUserDto.userId },
    data: { ...updateUserDto, updated_at: new Date() },
  });

  // 执行事务
  try {
    await this.prisma.$transaction([
      rolePromise, 
      deleteDeptPromise,
      createDeptPromise,
      userPromise
    ]);
    return '用户更新成功';
  } catch (error) {
    throw new Error('用户更新失败: ' + error.message);
  }
}

开发经验与最佳实践

事务处理的正确姿势

在用户管理模块中,多个表(用户表、角色关联表、部门关联表)需要保持数据一致性。使用Prisma的事务处理($transaction)是关键:

typescript 复制代码
// 正确的事务处理方式
const transaction = this.prisma.$transaction([
  operation1,
  operation2,
  operation3
]);

// 错误示例(会导致事务失效)
const transaction = this.prisma.$transaction([
  await operation1,  // 错误:不能使用await
  operation2,
  operation3
]);

经验总结

  1. 事务内操作不能单独使用await
  2. 将需要事务保证的操作放入数组传递
  3. 事务失败时进行回滚

关联数据处理技巧

处理多对多关系时,Prisma的include功能非常强大:

typescript 复制代码
// 包含关联的角色和部门信息
include: {
  roles: {
    select: { role: true } // 获取关联的完整角色对象
  },
  depts: {
    select: { dept: true } // 获取关联的完整部门对象
  }
}

数据结构优化技巧

typescript 复制代码
// 转换前
{
  roles: [{ role: { id: 1, name: 'admin' } }],
  depts: [{ dept: { id: 2, name: '研发部' } }]
}

// 转换后(更简洁的结构)
{
  roles: [{ id: 1, name: 'admin' }],
  depts: [{ id: 2, name: '研发部' }]
}

安全注意事项

密码安全

bash 复制代码
    - 永远不要在响应中包含密码字段
    - 使用`omit: { password: true }`排除密码
    - 存储时使用bcrypt等算法加密

权限控制

less 复制代码
``` typescript
// 在控制器中添加权限守卫
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions(PermissionContant.USER_MANAGE)
@Post('create')
createUser(@Body() dto: CreateUserDto) {
  // ...
}
```

输入验证

typescrit 复制代码
// 使用class-validator增强DTO验证
    import { IsEmail, IsStrongPassword } from 'class-validator';

    export class CreateUserDto {
      @IsEmail()
      email: string;
      
      @IsStrongPassword({
        minLength: 8,
        minLowercase: 1,
        minUppercase: 1,
        minNumbers: 1
      })
      password: string;
    }

性能优化点

分页查询优化

typescript 复制代码
// 使用cursor-based分页提升性能
async findAll(cursor: number, limit: number) {
  return this.prisma.sys_user.findMany({
    take: limit,
    skip: cursor ? 1 : 0,
    cursor: cursor ? { user_id: cursor } : undefined,
  });
}

批量操作优化

typescript 复制代码
// 使用createMany优化批量插入
await this.prisma.sys_user_dept.createMany({
  data: deptIds.map(deptId => ({
    user_id: userId,
    dept_id: Number(deptId)
  })),
});

Redis缓存应用

typescript 复制代码
// 用户详情加入缓存
async findOne(id: number) {
  const cacheKey = `user:${id}`;
  const cached = await this.redis.get(cacheKey);

  if (cached) return JSON.parse(cached);

  const user = await this.prisma.sys_user.findUnique({ 
    where: { user_id: id } 
  });

  await this.redis.set(cacheKey, JSON.stringify(user), 'EX', 300);
  return user;
}

踩坑与解决方案

多对多关系更新陷阱

问题场景

更新用户部门关系时,需要先删除旧关系再创建新关系

解决方案

typescript 复制代码
// 1. 删除所有旧关联
const deleteOp = this.prisma.sys_user_dept.deleteMany({
  where: { user_id: userId }
});

// 2. 创建所有新关联
const createOp = this.prisma.sys_user_dept.createMany({
  data: newDeptIds.map(deptId => ({
    user_id: userId,
    dept_id: deptId
  }))
});

// 3. 在事务中执行
await this.prisma.$transaction([deleteOp, createOp]);

密码字段处理

问题场景

更新用户信息时,密码字段需要特殊处理

解决方案

typescript 复制代码
async updateUser(dto: UpdateUserDto) {
  const data: any = { ...dto };
  
  // 如果密码字段存在且不为空,则处理
  if (dto.password) {
    data.password = await bcrypt.hash(dto.password, 10);
  } else {
    delete data.password; // 不更新密码
  }
  
  return this.prisma.sys_user.update({
    where: { user_id: dto.userId },
    data
  });
}

数据一致性挑战

问题场景

删除用户时需要同步删除所有关联数据

解决方案

typescript 复制代码
async remove(id: number) {
  return this.prisma.$transaction([
    // 1. 删除用户角色关联
    this.prisma.sys_user_role.deleteMany({ where: { user_id: id } }),
    
    // 2. 删除用户部门关联
    this.prisma.sys_user_dept.deleteMany({ where: { user_id: id } }),
    
    // 3. 删除用户本身
    this.prisma.sys_user.delete({ where: { user_id: id } })
  ]);
}

总结与展望

通过本文,我们系统性地实现了用户管理模块的核心功能,涵盖了:

  1. 模块化结构设计与代码组织
  2. RESTful接口规范实现
  3. 复杂关联数据处理技巧
  4. 数据库事务的合理应用
  5. 安全性与性能优化实践

值得注意的最佳实践

  • 使用DTO进行数据验证和转换
  • 敏感字段(如密码)特殊处理
  • 事务处理保证数据一致性
  • 合理的关联数据加载策略
  • 分页查询的性能优化
相关推荐
前端工作日常2 小时前
我理解的`npm pack` 和 `npm install <local-path>`
前端
码小凡2 小时前
优雅!用了这两款插件,我成了整个公司代码写得最规范的码农
java·后端
李剑一2 小时前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华2 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言2 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
奇舞精选2 小时前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
Danny_FD3 小时前
React中可有可无的优化-对象类型的使用
前端·javascript
用户757582318553 小时前
混合应用开发:企业降本增效之道——面向2025年移动应用开发趋势的实践路径
前端
星星电灯猴3 小时前
Charles抓包工具深度解析:如何高效调试HTTPHTTPS请求与API接口
后端
isfox3 小时前
Hadoop 版本进化论:从 1.0 到 2.0,架构革命全解析
大数据·后端