feat(api): 添加用户角色管理接口

- GET /users/:id/roles 获取用户详情(包含角色)
- PATCH /users/:id/roles 分配角色给用户
- 新增 AssignRolesDto、UserRoleResponseDto、UserWithRolesResponseDto

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
charilezhou
2026-01-17 21:37:01 +08:00
parent f4c42cee27
commit fe4ea4121c
3 changed files with 152 additions and 4 deletions

View File

@@ -1,6 +1,12 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import type { UpdateUserDto as IUpdateUserDto, UserResponse } from '@seclusion/shared';
import { IsString, IsOptional } from 'class-validator';
import type {
AssignRolesDto as IAssignRolesDto,
UpdateUserDto as IUpdateUserDto,
UserResponse,
UserRoleResponse,
UserWithRolesResponse,
} from '@seclusion/shared';
import { IsString, IsOptional, IsArray } from 'class-validator';
import { createPaginatedResponseDto } from '@/common/crud/dto/paginated-response.dto';
@@ -11,6 +17,30 @@ export class UpdateUserDto implements IUpdateUserDto {
name?: string;
}
/** 分配角色请求 DTO */
export class AssignRolesDto implements IAssignRolesDto {
@ApiProperty({
example: ['role_id_1', 'role_id_2'],
description: '角色 ID 列表',
type: [String],
})
@IsArray()
@IsString({ each: true })
roleIds: string[];
}
/** 用户角色响应 DTO */
export class UserRoleResponseDto implements UserRoleResponse {
@ApiProperty({ example: 'clxxx123', description: '角色 ID' })
id: string;
@ApiProperty({ example: 'user', description: '角色编码' })
code: string;
@ApiProperty({ example: '普通用户', description: '角色名称' })
name: string;
}
/** 用户响应 DTO */
export class UserResponseDto implements UserResponse {
@ApiProperty({ example: 'clxxx123', description: '用户 ID' })
@@ -32,5 +62,29 @@ export class UserResponseDto implements UserResponse {
deletedAt?: string | null;
}
/** 用户详情响应(包含角色) */
export class UserWithRolesResponseDto implements UserWithRolesResponse {
@ApiProperty({ example: 'clxxx123', description: '用户 ID' })
id: string;
@ApiProperty({ example: 'user@example.com', description: '用户邮箱' })
email: string;
@ApiProperty({ example: '张三', description: '用户名称', nullable: true })
name: string | null;
@ApiProperty({ example: false, description: '是否为超级管理员' })
isSuperAdmin: boolean;
@ApiProperty({ type: [UserRoleResponseDto], description: '用户角色列表' })
roles: UserRoleResponseDto[];
@ApiProperty({ example: '2026-01-16T10:00:00.000Z', description: '创建时间' })
createdAt: string;
@ApiProperty({ example: '2026-01-16T10:00:00.000Z', description: '更新时间' })
updatedAt: string;
}
/** 分页用户响应 DTO */
export class PaginatedUserResponseDto extends createPaginatedResponseDto(UserResponseDto) {}

View File

@@ -1,7 +1,13 @@
import { Controller, Get, Patch, Delete, Param, Body, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiOkResponse } from '@nestjs/swagger';
import { UpdateUserDto, UserResponseDto, PaginatedUserResponseDto } from './dto/user.dto';
import {
AssignRolesDto,
UpdateUserDto,
UserResponseDto,
UserWithRolesResponseDto,
PaginatedUserResponseDto,
} from './dto/user.dto';
import { UserService } from './user.service';
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
@@ -28,6 +34,13 @@ export class UserController {
return this.userService.findDeleted(query);
}
@Get(':id/roles')
@ApiOperation({ summary: '获取用户详情(包含角色)' })
@ApiOkResponse({ type: UserWithRolesResponseDto, description: '用户详情及角色' })
findByIdWithRoles(@Param('id') id: string) {
return this.userService.findByIdWithRoles(id);
}
@Get(':id')
@ApiOperation({ summary: '根据 ID 获取用户' })
@ApiOkResponse({ type: UserResponseDto, description: '用户详情' })
@@ -42,6 +55,13 @@ export class UserController {
return this.userService.update(id, dto);
}
@Patch(':id/roles')
@ApiOperation({ summary: '分配角色给用户' })
@ApiOkResponse({ type: UserWithRolesResponseDto, description: '分配角色后的用户信息' })
assignRoles(@Param('id') id: string, @Body() dto: AssignRolesDto) {
return this.userService.assignRoles(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: '删除用户' })
@ApiOkResponse({ type: UserResponseDto, description: '被删除的用户信息' })

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma, User } from '@prisma/client';
import type { AssignRolesDto, UserWithRolesResponse } from '@seclusion/shared';
import { UpdateUserDto } from './dto/user.dto';
@@ -44,4 +45,77 @@ export class UserService extends CrudService<
protected getDeletedNotFoundMessage(): string {
return '已删除的用户不存在';
}
/**
* 获取用户详情(包含角色信息)
*/
async findByIdWithRoles(id: string): Promise<UserWithRolesResponse> {
const user = await this.prisma.user.findUnique({
where: { id },
include: {
roles: {
include: {
role: {
select: {
id: true,
code: true,
name: true,
},
},
},
},
},
});
if (!user) {
throw new NotFoundException('用户不存在');
}
return {
id: user.id,
email: user.email,
name: user.name,
isSuperAdmin: user.isSuperAdmin,
roles: user.roles.map((ur) => ur.role),
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
};
}
/**
* 分配角色给用户
* 会先删除用户现有的所有角色,再分配新角色
*/
async assignRoles(userId: string, dto: AssignRolesDto): Promise<UserWithRolesResponse> {
// 检查用户是否存在
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new NotFoundException('用户不存在');
}
// 使用事务:先删除所有现有角色,再添加新角色
await this.prisma.client.$transaction(async (tx) => {
// 删除现有角色关联
await tx.userRole.deleteMany({
where: { userId },
});
// 添加新角色关联
if (dto.roleIds.length > 0) {
await tx.userRole.createMany({
data: dto.roleIds.map((roleId) => ({
userId,
roleId,
})),
skipDuplicates: true,
});
}
});
// 返回更新后的用户信息
return this.findByIdWithRoles(userId);
}
}