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