feat: 添加菜单管理
2
.env
|
|
@ -12,7 +12,7 @@ SERVER_HOST = 0.0.0.0
|
|||
# 服务域名
|
||||
SERVER_URL = http://127.0.0.1
|
||||
# 接口地址
|
||||
SERVER_OPENAPI_URL = /api
|
||||
SERVER_OPENAPI_URL = /api/openapi
|
||||
|
||||
# ========================================================================================
|
||||
# 数据库配置
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@
|
|||
- .dockerignore 配置哪些文件应该被忽略掉
|
||||
- .gitea/workflows/depoy.yaml 流水线任务的配置文件,语法上与 Github Actions 一致
|
||||
|
||||
## 笔记
|
||||
|
||||
- createUserDto与User分开
|
||||
- 涉及关系时,先用 service 查出有效关系,避免存储不存在的关联ID
|
||||
|
||||
## 最后
|
||||
|
||||
如果你在使用过程中遇到问题,欢迎在 Issue 中提问。
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
|
@ -1,20 +1,21 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PostModule } from '@/content/post';
|
||||
import { RoleModule } from '@/modules/role';
|
||||
import { RoleModule } from '@/system/role';
|
||||
import { UploadModule } from '@/storage/upload';
|
||||
import { PermissionModule } from '@/modules/permission';
|
||||
import { PermissionModule } from '@/system/permission';
|
||||
import { ConfigModule } from '@/config';
|
||||
import { LoggerModule } from '@/common/logger';
|
||||
import { ServeStaticModule } from '@/common/static';
|
||||
import { DatabaseModule } from '@/database';
|
||||
import { ValidationModule } from '@/common/validation';
|
||||
import { AuthModule } from '@/modules/auth';
|
||||
import { UserModule } from '@/modules/user';
|
||||
import { AuthModule } from '@/system/auth';
|
||||
import { UserModule } from '@/system/user';
|
||||
import { ResponseModule } from '@/common/response';
|
||||
import { SerializationModule } from '@/common/serialization';
|
||||
import { CacheModule } from '@/storage/cache';
|
||||
import { ScanModule } from '@/utils/scan.module';
|
||||
import { ContentModule } from '@/content/content.module';
|
||||
import { MenuModule } from './system/menu';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -63,7 +64,6 @@ import { ContentModule } from '@/content/content.module';
|
|||
*/
|
||||
DatabaseModule,
|
||||
|
||||
|
||||
/**
|
||||
* 用户模块
|
||||
*/
|
||||
|
|
@ -81,7 +81,6 @@ import { ContentModule } from '@/content/content.module';
|
|||
*/
|
||||
PermissionModule,
|
||||
|
||||
|
||||
/**
|
||||
* 上传模块
|
||||
*/
|
||||
|
|
@ -90,7 +89,8 @@ import { ContentModule } from '@/content/content.module';
|
|||
* 文章模块
|
||||
*/
|
||||
PostModule,
|
||||
ContentModule
|
||||
ContentModule,
|
||||
MenuModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ export class LoggerInterceptor implements NestInterceptor {
|
|||
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
|
||||
const { method, url } = context.switchToHttp().getRequest<Request>();
|
||||
const now = Date.now();
|
||||
return next.handle().pipe(
|
||||
tap(() => {
|
||||
const ms = Date.now() - now;
|
||||
const scope = [context.getClass().name, context.getHandler().name].join('.');
|
||||
this.logger.log(`${method} ${url}(${ms} ms) +1`, scope);
|
||||
}),
|
||||
);
|
||||
const handle = () => {
|
||||
const ms = Date.now() - now;
|
||||
const scope = [context.getClass().name, context.getHandler().name].join('.');
|
||||
this.logger.log(`${method} ${url}(${ms} ms) +1`, scope);
|
||||
};
|
||||
return next.handle().pipe(tap({ next: handle, error: handle }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,4 +39,12 @@ export class PaginationDto {
|
|||
@Min(0)
|
||||
@Transform(({ value }) => Number(value))
|
||||
size?: number;
|
||||
|
||||
/**
|
||||
* 创建起始事件
|
||||
* @example '2020-02-02 02:02:02'
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
createdFrom?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export class ValidationError extends Error {
|
||||
constructor(public messages: string[]) {
|
||||
super('参数错误');
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export class ValidationExecptionFilter implements ExceptionFilter {
|
|||
const response = ctx.getResponse<Response>();
|
||||
const code = ResponseCode.PARAM_ERROR;
|
||||
const message = exception.message;
|
||||
const data = exception.messages;
|
||||
response.status(HttpStatus.BAD_REQUEST).json({ code, message, data });
|
||||
response.status(HttpStatus.BAD_REQUEST).json({ code, message, data: undefined });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ export const validationPipeFactory = () => {
|
|||
transform: true,
|
||||
whitelist: true,
|
||||
exceptionFactory: (errors) => {
|
||||
const messages: string[] = [];
|
||||
let message = '参数错误';
|
||||
for (const error of errors) {
|
||||
const { property, constraints } = error;
|
||||
for (const [key, val] of Object.entries(constraints)) {
|
||||
messages.push(map[key] ? `参数(${property})${map[key]}` : val);
|
||||
message = map[key] ? `参数(${property})${map[key]}` : val;
|
||||
}
|
||||
}
|
||||
return new ValidationError(messages);
|
||||
return new ValidationError(message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseEntity } from '@/database';
|
||||
import { User } from 'src/modules/user';
|
||||
import { User } from '@/system/user';
|
||||
import { Column, Entity, ManyToMany } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ApiHideProperty } from '@nestjs/swagger';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ export class BaseEntity {
|
|||
* @example "2022-01-03 12:12:12"
|
||||
*/
|
||||
@Exclude()
|
||||
@ApiHideProperty()
|
||||
@DeleteDateColumn({ comment: '删除时间', select: false })
|
||||
deleteddAt: Date;
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ export class BaseEntity {
|
|||
* @example 1
|
||||
*/
|
||||
@Exclude()
|
||||
@ApiHideProperty()
|
||||
@Column({ comment: '删除人', nullable: true, select: false })
|
||||
deletedBy: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export class UploadService extends BaseService {
|
|||
extension: extname(file.originalname),
|
||||
});
|
||||
await this.uploadRepository.save(upload);
|
||||
return upload.id;
|
||||
return upload;
|
||||
}
|
||||
|
||||
findAll() {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ export class AuthService {
|
|||
constructor(private userService: UserService, private jwtService: JwtService) {}
|
||||
|
||||
async signIn(authUserDto: AuthUserDto) {
|
||||
const user = await this.userService.findByUsername(authUserDto.username);
|
||||
const user = await this.userService.findOne({ username: authUserDto.username });
|
||||
if (!user) {
|
||||
console.log(user, authUserDto);
|
||||
throw new UnauthorizedException('用户名不存在');
|
||||
}
|
||||
if (user.password !== authUserDto.password) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { User } from '@/modules/user';
|
||||
import { User } from '@/system/user';
|
||||
import { OmitType } from '@nestjs/swagger';
|
||||
|
||||
export class LoginedUserVo extends OmitType(User, ['password', 'id'] as const) {
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { IsInt, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateMenuDto {
|
||||
/**
|
||||
* 父级ID
|
||||
* @example 0
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
parentId: number;
|
||||
|
||||
/**
|
||||
* 菜单名称
|
||||
* @example '首页'
|
||||
*/
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 标识
|
||||
* @example 'home'
|
||||
*/
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
/**
|
||||
* 访问路径
|
||||
* @example '/home'
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 图标类名
|
||||
* @example 'icon-park-outline-home'
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
* @example 1
|
||||
* @description 1目录 2页面 3按钮
|
||||
*/
|
||||
@IsInt()
|
||||
type: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { PaginationDto } from '@/common/response';
|
||||
import { IntersectionType } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class FindMenuDto extends IntersectionType(PaginationDto) {
|
||||
/**
|
||||
* 是否以树结构返回
|
||||
* @example false
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => Boolean(value))
|
||||
tree?: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { CreateMenuDto } from './create-menu.dto';
|
||||
|
||||
export class UpdateMenuDto extends PartialType(CreateMenuDto) {}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { BaseEntity } from '@/database';
|
||||
import { Role } from '@/system/role';
|
||||
import { ApiHideProperty } from '@nestjs/swagger';
|
||||
import { Column, Entity, JoinColumn, ManyToMany, Tree, TreeChildren, TreeParent } from 'typeorm';
|
||||
|
||||
@Entity({ orderBy: { id: 'DESC' } })
|
||||
@Tree('materialized-path')
|
||||
export class Menu extends BaseEntity {
|
||||
/**
|
||||
* 菜单名称
|
||||
* @example '首页'
|
||||
*/
|
||||
@Column({ comment: '菜单名称' })
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 标识
|
||||
* @example 'home'
|
||||
*/
|
||||
@Column({ comment: '标识' })
|
||||
code: string;
|
||||
|
||||
/**
|
||||
* 访问路径
|
||||
* @example '/home'
|
||||
*/
|
||||
@Column({ comment: '访问路径', nullable: true })
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 图标类名
|
||||
* @example 'icon-park-outline-home'
|
||||
*/
|
||||
@Column({ comment: '图标类名', nullable: true })
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
* @example 1
|
||||
* @description 1目录 2页面 3按钮
|
||||
*/
|
||||
@Column({ comment: '类型(1: 目录, 2: 页面, 3: 按钮)' })
|
||||
type: number;
|
||||
|
||||
@ApiHideProperty()
|
||||
@TreeParent()
|
||||
parent: Menu;
|
||||
|
||||
@Column({ comment: '父级ID', nullable: true, default: 0 })
|
||||
parentId: number;
|
||||
|
||||
@ApiHideProperty()
|
||||
@TreeChildren()
|
||||
children: Menu[];
|
||||
|
||||
@ApiHideProperty()
|
||||
@ManyToMany(() => Role, (role) => role.menus)
|
||||
roles: Role[];
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './entities/menu.entity';
|
||||
export * from './menu.controller';
|
||||
export * from './menu.module';
|
||||
export * from './menu.service';
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { BaseController } from '@/common/base';
|
||||
import { Respond, RespondType } from '@/common/response';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, ParseIntPipe } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateMenuDto } from './dto/create-menu.dto';
|
||||
import { FindMenuDto } from './dto/find-menu.dto';
|
||||
import { UpdateMenuDto } from './dto/update-menu.dto';
|
||||
import { Menu } from './entities/menu.entity';
|
||||
import { MenuService } from './menu.service';
|
||||
|
||||
@ApiTags('menu')
|
||||
@Controller('menus')
|
||||
export class MenuController extends BaseController {
|
||||
constructor(private menuService: MenuService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ description: '新增菜单', operationId: 'addMenu' })
|
||||
addMenu(@Body() createMenuDto: CreateMenuDto) {
|
||||
return this.menuService.create(createMenuDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Respond(RespondType.PAGINATION)
|
||||
@ApiOkResponse({ isArray: true, type: Menu })
|
||||
@ApiOperation({ description: '查询菜单', operationId: 'getMenus' })
|
||||
getMenus(@Query() query: FindMenuDto) {
|
||||
return this.menuService.findMany(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: '获取菜单', operationId: 'getMenu' })
|
||||
getMenu(@Param('id') id: number): Promise<Menu> {
|
||||
return this.menuService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ description: '更新菜单', operationId: 'setMenu' })
|
||||
updateMenu(@Param('id') id: number, @Body() updateMenuDto: UpdateMenuDto) {
|
||||
return this.menuService.update(+id, updateMenuDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: '删除菜单', operationId: 'delMenu' })
|
||||
delMenu(id: number) {
|
||||
return this.menuService.remove(+id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Menu } from './entities/menu.entity';
|
||||
import { MenuController } from './menu.controller';
|
||||
import { MenuService } from './menu.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Menu])],
|
||||
controllers: [MenuController],
|
||||
providers: [MenuService],
|
||||
exports: [MenuService],
|
||||
})
|
||||
export class MenuModule {}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { BaseService } from '@/common/base';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Like, Repository } from 'typeorm';
|
||||
import { CreateMenuDto } from './dto/create-menu.dto';
|
||||
import { FindMenuDto } from './dto/find-menu.dto';
|
||||
import { UpdateMenuDto } from './dto/update-menu.dto';
|
||||
import { Menu } from './entities/menu.entity';
|
||||
import { listToTree } from '@/utils/list-tree';
|
||||
|
||||
@Injectable()
|
||||
export class MenuService extends BaseService {
|
||||
constructor(@InjectRepository(Menu) private menuRepository: Repository<Menu>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增菜单
|
||||
*/
|
||||
async create(createMenuDto: CreateMenuDto) {
|
||||
const menu = this.menuRepository.create(createMenuDto);
|
||||
const { parentId } = createMenuDto;
|
||||
if (parentId) {
|
||||
const parent = await this.menuRepository.findOne({ where: { id: parentId } });
|
||||
menu.parent = parent;
|
||||
}
|
||||
await this.menuRepository.save(menu);
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件/分页查询
|
||||
*/
|
||||
async findMany(findMenudto: FindMenuDto) {
|
||||
const { page, size, tree } = findMenudto;
|
||||
const { skip, take } = this.formatPagination(page, size, true);
|
||||
let [data, total] = await this.menuRepository.findAndCount({ skip, take });
|
||||
if (tree) {
|
||||
data = listToTree(data);
|
||||
}
|
||||
return [data, total];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
findOne(idOrOptions: number | Partial<Menu>) {
|
||||
const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any);
|
||||
return this.menuRepository.findOne({ where });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID更新
|
||||
*/
|
||||
async update(id: number, updateMenuDto: UpdateMenuDto) {
|
||||
const { parentId } = updateMenuDto;
|
||||
if (parentId !== undefined) {
|
||||
const parent = await this.menuRepository.findOne({ where: { id: parentId } });
|
||||
updateMenuDto.parentId = parent?.id;
|
||||
}
|
||||
return this.menuRepository.update(id, updateMenuDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID删除(软删除)
|
||||
*/
|
||||
remove(id: number) {
|
||||
return this.menuRepository.softDelete(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseEntity } from '@/database';
|
||||
import { Role } from '@/modules/role/entities/role.entity';
|
||||
import { Role } from '@/system/role/entities/role.entity';
|
||||
import { Column, Entity, ManyToMany } from 'typeorm';
|
||||
|
||||
enum PermissionType {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException, forwardRef } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PERMISSION_KEY } from './permission.decorator';
|
||||
import { UserService } from '@/modules/user';
|
||||
import { UserService } from '@/system/user';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionGuard implements CanActivate {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Permission } from '@/modules/permission/entities/permission.entity';
|
||||
import { Permission } from '@/system/permission/entities/permission.entity';
|
||||
import { IsInt, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateRoleDto {
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { PaginationDto } from '@/common/response';
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
|
||||
export class FindMenuDto extends PaginationDto {
|
||||
/**
|
||||
* 是否以树结构返回
|
||||
* @example false
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
tree?: boolean;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { BaseEntity } from '@/database';
|
||||
import { Permission } from '@/modules/permission/entities/permission.entity';
|
||||
import { User } from 'src/modules/user';
|
||||
import { Menu } from '@/system/menu';
|
||||
import { Permission } from '@/system/permission/entities/permission.entity';
|
||||
import { User } from '@/system/user';
|
||||
import { ApiHideProperty } from '@nestjs/swagger';
|
||||
import { Column, Entity, JoinTable, ManyToMany, RelationId } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
|
|
@ -26,25 +28,20 @@ export class Role extends BaseEntity {
|
|||
@Column({ comment: '角色描述', nullable: true })
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* 角色权限
|
||||
* @example [1, 2, 3]
|
||||
*/
|
||||
@ApiHideProperty()
|
||||
@ManyToMany(() => User, (user) => user.roles)
|
||||
user: User;
|
||||
|
||||
@ApiHideProperty()
|
||||
@JoinTable()
|
||||
@ManyToMany(() => Permission, (permission) => permission.roles)
|
||||
permissions: Permission[];
|
||||
|
||||
/**
|
||||
* 角色权限ID
|
||||
* @example [1, 2, 3]
|
||||
*/
|
||||
@ApiHideProperty()
|
||||
@RelationId('permissions')
|
||||
permissionIds: number[];
|
||||
|
||||
/**
|
||||
* 角色用户
|
||||
* @example [1, 2, 3]
|
||||
*/
|
||||
@ManyToMany(() => User, (user) => user.roles)
|
||||
user: User;
|
||||
@ApiHideProperty()
|
||||
@ManyToMany(() => Menu, (menu) => menu.roles)
|
||||
menus: Menu[];
|
||||
}
|
||||
|
|
@ -8,5 +8,6 @@ import { RoleService } from './role.service';
|
|||
imports: [TypeOrmModule.forFeature([Role])],
|
||||
controllers: [RoleController],
|
||||
providers: [RoleService],
|
||||
exports: [RoleService],
|
||||
})
|
||||
export class RoleModule {}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||
import { Role } from './entities/role.entity';
|
||||
|
|
@ -39,6 +39,15 @@ export class RoleService {
|
|||
return this.roleRepository.update(id, updateRoleDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID数组查找角色
|
||||
* @param ids ID数组
|
||||
* @returns
|
||||
*/
|
||||
findByIds(ids: number[]) {
|
||||
return this.roleRepository.find({ where: { id: In(ids) } });
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} role`;
|
||||
}
|
||||
|
|
@ -7,12 +7,7 @@ export class CreateUserDto {
|
|||
*/
|
||||
@IsString()
|
||||
username: string;
|
||||
/**
|
||||
* 用户昵称
|
||||
* @example '绝弹'
|
||||
*/
|
||||
@IsString()
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 用户密码
|
||||
* @example 'password'
|
||||
|
|
@ -20,16 +15,33 @@ export class CreateUserDto {
|
|||
@IsOptional()
|
||||
@IsString()
|
||||
password?: string;
|
||||
|
||||
/**
|
||||
* 头像ID
|
||||
* 用户昵称
|
||||
* @example '绝弹'
|
||||
*/
|
||||
@IsString()
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 用户描述
|
||||
* @example '这个人很懒,什么也没有留下!'
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
* @example 1
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatarId?: number;
|
||||
avatar?: string;
|
||||
|
||||
/**
|
||||
* 角色ID列表
|
||||
* @example [1, 2, 3]
|
||||
* @example [1]
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsInt({ each: true })
|
||||
|
|
@ -3,6 +3,10 @@ import { CreateUserDto } from './create-user.dto';
|
|||
import { IsNumber } from 'class-validator';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {
|
||||
/**
|
||||
* 用户ID
|
||||
* @example 1
|
||||
*/
|
||||
@IsNumber()
|
||||
id: number;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { BaseEntity } from '@/database';
|
||||
import { Post } from '@/content/post';
|
||||
import { Role } from '@/modules/role';
|
||||
import { Role } from '@/system/role';
|
||||
import { ApiHideProperty } from '@nestjs/swagger';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { Column, Entity, JoinTable, ManyToMany, RelationId } from 'typeorm';
|
||||
|
|
@ -14,6 +14,14 @@ export class User extends BaseEntity {
|
|||
@Column({ length: 48 })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 用户密码
|
||||
* @example 'password'
|
||||
*/
|
||||
@Exclude()
|
||||
@Column({ length: 64 })
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
* @example '绝弹'
|
||||
|
|
@ -29,20 +37,12 @@ export class User extends BaseEntity {
|
|||
description: string;
|
||||
|
||||
/**
|
||||
* 用户头像(URL)
|
||||
* @example './assets/222421415123.png '
|
||||
* 用户头像
|
||||
* @example '/upload/assets/222421415123.png '
|
||||
*/
|
||||
@Column({ nullable: true })
|
||||
avatar: string;
|
||||
|
||||
/**
|
||||
* 用户密码
|
||||
* @example 'password'
|
||||
*/
|
||||
@Exclude()
|
||||
@Column({ length: 64 })
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
* @example 'example@mail.com'
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { BaseController } from '@/common/base';
|
||||
import { Respond, RespondType } from '@/common/response';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { FindUserDto } from './dto/find-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
|
@ -15,44 +15,34 @@ export class UserController extends BaseController {
|
|||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
*/
|
||||
@Post()
|
||||
@ApiOperation({ description: '添加用户', operationId: 'addUser' })
|
||||
addUser(@Body() createUserDto: CreateUserDto) {
|
||||
return this.userService.create(createUserDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页/条件查询用户
|
||||
*/
|
||||
@Get()
|
||||
@Respond(RespondType.PAGINATION)
|
||||
@ApiOkResponse({ type: User, isArray: true })
|
||||
@ApiOperation({ description: '分页获取用户', operationId: 'getUsers' })
|
||||
async getUsers(@Query() query: FindUserDto) {
|
||||
return this.userService.findMany(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
*/
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: '获取用户', operationId: 'getUser' })
|
||||
getUser(@Param('id') id: number): Promise<User> {
|
||||
return this.userService.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID更新用户
|
||||
*/
|
||||
@Patch(':id')
|
||||
@ApiOperation({ description: '更新用户', operationId: 'setUser' })
|
||||
updateUser(@Param('id') id: number, @Body() updateUserDto: UpdateUserDto) {
|
||||
return this.userService.update(+id, updateUserDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID删除用户
|
||||
*/
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: '删除用户', operationId: 'delUser' })
|
||||
delUser(@Param('id') id: number) {
|
||||
return this.userService.remove(+id);
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
import { User } from './entities/user.entity';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
import { RoleModule } from '../role';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
imports: [TypeOrmModule.forFeature([User]), RoleModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService],
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { BaseService } from '@/common/base';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Like, Repository } from 'typeorm';
|
||||
import { Like, Repository } from 'typeorm';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { FindUserDto } from './dto/find-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { User } from './entities/user.entity';
|
||||
import { RoleService } from '../role';
|
||||
|
||||
@Injectable()
|
||||
export class UserService extends BaseService {
|
||||
constructor(@InjectRepository(User) private userRepository: Repository<User>) {
|
||||
constructor(@InjectRepository(User) private userRepository: Repository<User>, private roleService: RoleService) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
|
@ -18,10 +19,7 @@ export class UserService extends BaseService {
|
|||
*/
|
||||
async create(createUserDto: CreateUserDto) {
|
||||
const user = this.userRepository.create(createUserDto);
|
||||
if (createUserDto.roleIds) {
|
||||
user.roles = createUserDto.roleIds.map((id) => ({ id })) as any;
|
||||
delete createUserDto.roleIds;
|
||||
}
|
||||
user.roles = await this.roleService.findByIds(user.roleIds ?? []);
|
||||
await this.userRepository.save(user);
|
||||
return user.id;
|
||||
}
|
||||
|
|
@ -31,7 +29,7 @@ export class UserService extends BaseService {
|
|||
*/
|
||||
async findMany(findUserdto: FindUserDto) {
|
||||
const { nickname: _nickname } = findUserdto;
|
||||
const nickname = _nickname && Like(`%${_nickname}%`);
|
||||
const nickname = _nickname ? Like(`%${_nickname}%`) : undefined;
|
||||
const { skip, take } = this.paginizate(findUserdto, { full: true });
|
||||
return this.userRepository.findAndCount({ skip, take, where: { nickname } });
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 列表转树结构
|
||||
* @param list 列表
|
||||
* @returns
|
||||
*/
|
||||
export function listToTree(list: any[]) {
|
||||
const listMap = new Map();
|
||||
for (const item of list) {
|
||||
listMap.set(item.id, item);
|
||||
}
|
||||
return list.filter((item) => {
|
||||
const parent = listMap.get(item.parentId);
|
||||
if (parent) {
|
||||
parent.children = parent.children ?? [];
|
||||
parent.children.push(item);
|
||||
}
|
||||
return !item.parentId;
|
||||
});
|
||||
}
|
||||