《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进行数据验证和转换
  • 敏感字段(如密码)特殊处理
  • 事务处理保证数据一致性
  • 合理的关联数据加载策略
  • 分页查询的性能优化
相关推荐
定栓4 分钟前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
金銀銅鐵7 分钟前
[Java] 用 GUI 展示 ArrayList 扩容的过程
java·后端·ai编程
小熊在忙fpga13 分钟前
N32G430芯片的flash表示
前端·javascript·html
Data analyse45619 分钟前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
todoitbo25 分钟前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
会灭火的程序员30 分钟前
SuperMap AgentX Server智能体接入WEB应用技术文档
前端·supermap·agentx·agentx server
CRMEB35 分钟前
商品 SKU 设计,从简单规格到多规格踩过什么
java·前端·数据库·开源
霸道流氓气质36 分钟前
基于 Spring @Profile 的 MQ 消费者按需开关设计
java·后端·spring
Chloe.Zz36 分钟前
一篇入门Agent
开发语言·前端·javascript
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 表格标签
前端·html·html5