diff --git a/.env b/.env index bb13c7b..fe20704 100644 --- a/.env +++ b/.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 # ======================================================================================== # 数据库配置 diff --git a/README.md b/README.md index 182c257..d4ccfef 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ - .dockerignore 配置哪些文件应该被忽略掉 - .gitea/workflows/depoy.yaml 流水线任务的配置文件,语法上与 Github Actions 一致 +## 笔记 + +- createUserDto与User分开 +- 涉及关系时,先用 service 查出有效关系,避免存储不存在的关联ID + ## 最后 如果你在使用过程中遇到问题,欢迎在 Issue 中提问。 \ No newline at end of file diff --git a/content/database/db.sqlite b/content/database/db.sqlite index 74d8176..c1a7ca0 100644 Binary files a/content/database/db.sqlite and b/content/database/db.sqlite differ diff --git a/content/upload/2023/10/1698212617266.png b/content/upload/2023/10/1698212617266.png new file mode 100644 index 0000000..36f4b24 Binary files /dev/null and b/content/upload/2023/10/1698212617266.png differ diff --git a/content/upload/2023/10/1698213842784.png b/content/upload/2023/10/1698213842784.png new file mode 100644 index 0000000..36f4b24 Binary files /dev/null and b/content/upload/2023/10/1698213842784.png differ diff --git a/content/upload/2023/10/1698213851915.png b/content/upload/2023/10/1698213851915.png new file mode 100644 index 0000000..36f4b24 Binary files /dev/null and b/content/upload/2023/10/1698213851915.png differ diff --git a/content/upload/2023/10/1698214169067.png b/content/upload/2023/10/1698214169067.png new file mode 100644 index 0000000..36f4b24 Binary files /dev/null and b/content/upload/2023/10/1698214169067.png differ diff --git a/content/upload/2023/10/1698214323331.jpg b/content/upload/2023/10/1698214323331.jpg new file mode 100644 index 0000000..1d74004 Binary files /dev/null and b/content/upload/2023/10/1698214323331.jpg differ diff --git a/content/upload/2023/10/1698214507527.jpg b/content/upload/2023/10/1698214507527.jpg new file mode 100644 index 0000000..1d74004 Binary files /dev/null and b/content/upload/2023/10/1698214507527.jpg differ diff --git a/content/upload/2023/10/1698214986603.jpg b/content/upload/2023/10/1698214986603.jpg new file mode 100644 index 0000000..eb432e4 Binary files /dev/null and b/content/upload/2023/10/1698214986603.jpg differ diff --git a/content/upload/2023/10/1698215028794.jpg b/content/upload/2023/10/1698215028794.jpg new file mode 100644 index 0000000..eb432e4 Binary files /dev/null and b/content/upload/2023/10/1698215028794.jpg differ diff --git a/content/upload/2023/10/1698215128518.jpg b/content/upload/2023/10/1698215128518.jpg new file mode 100644 index 0000000..1d74004 Binary files /dev/null and b/content/upload/2023/10/1698215128518.jpg differ diff --git a/content/upload/2023/10/1698215769517.jpg b/content/upload/2023/10/1698215769517.jpg new file mode 100644 index 0000000..1d74004 Binary files /dev/null and b/content/upload/2023/10/1698215769517.jpg differ diff --git a/content/upload/2023/10/1698216163562.jpg b/content/upload/2023/10/1698216163562.jpg new file mode 100644 index 0000000..eb432e4 Binary files /dev/null and b/content/upload/2023/10/1698216163562.jpg differ diff --git a/src/app.module.ts b/src/app.module.ts index 6827351..5735723 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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 {} diff --git a/src/common/logger/logger.interceptor.ts b/src/common/logger/logger.interceptor.ts index 7d46b2b..e98a662 100644 --- a/src/common/logger/logger.interceptor.ts +++ b/src/common/logger/logger.interceptor.ts @@ -10,12 +10,11 @@ export class LoggerInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable | Promise> { const { method, url } = context.switchToHttp().getRequest(); 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 })); } } diff --git a/src/common/response/pagination.dto.ts b/src/common/response/pagination.dto.ts index 2e7a5be..7d8e38b 100644 --- a/src/common/response/pagination.dto.ts +++ b/src/common/response/pagination.dto.ts @@ -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; } diff --git a/src/common/validation/validation.error.ts b/src/common/validation/validation.error.ts index 11f2340..0064e4d 100644 --- a/src/common/validation/validation.error.ts +++ b/src/common/validation/validation.error.ts @@ -1,5 +1,5 @@ export class ValidationError extends Error { - constructor(public messages: string[]) { - super('参数错误'); + constructor(message: string) { + super(message); } } diff --git a/src/common/validation/validation.filter.ts b/src/common/validation/validation.filter.ts index 0fac536..9d9137f 100644 --- a/src/common/validation/validation.filter.ts +++ b/src/common/validation/validation.filter.ts @@ -10,7 +10,6 @@ export class ValidationExecptionFilter implements ExceptionFilter { const response = ctx.getResponse(); 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 }); } } diff --git a/src/common/validation/validation.pipe.ts b/src/common/validation/validation.pipe.ts index a5e87e7..30c04c4 100644 --- a/src/common/validation/validation.pipe.ts +++ b/src/common/validation/validation.pipe.ts @@ -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); }, }); }; diff --git a/src/content/post/entities/post.entity.ts b/src/content/post/entities/post.entity.ts index 7734f86..5d63fd7 100644 --- a/src/content/post/entities/post.entity.ts +++ b/src/content/post/entities/post.entity.ts @@ -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() diff --git a/src/database/entities/base.ts b/src/database/entities/base.ts index f632dd1..4acbbd1 100644 --- a/src/database/entities/base.ts +++ b/src/database/entities/base.ts @@ -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; } diff --git a/src/storage/upload/upload.service.ts b/src/storage/upload/upload.service.ts index 72fb5de..6876041 100644 --- a/src/storage/upload/upload.service.ts +++ b/src/storage/upload/upload.service.ts @@ -29,7 +29,7 @@ export class UploadService extends BaseService { extension: extname(file.originalname), }); await this.uploadRepository.save(upload); - return upload.id; + return upload; } findAll() { diff --git a/src/modules/auth/auth.controller.ts b/src/system/auth/auth.controller.ts similarity index 100% rename from src/modules/auth/auth.controller.ts rename to src/system/auth/auth.controller.ts diff --git a/src/modules/auth/auth.module.ts b/src/system/auth/auth.module.ts similarity index 100% rename from src/modules/auth/auth.module.ts rename to src/system/auth/auth.module.ts diff --git a/src/modules/auth/auth.service.ts b/src/system/auth/auth.service.ts similarity index 87% rename from src/modules/auth/auth.service.ts rename to src/system/auth/auth.service.ts index bc40524..eac3707 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/system/auth/auth.service.ts @@ -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) { diff --git a/src/modules/auth/dto/auth-user.dto.ts b/src/system/auth/dto/auth-user.dto.ts similarity index 100% rename from src/modules/auth/dto/auth-user.dto.ts rename to src/system/auth/dto/auth-user.dto.ts diff --git a/src/modules/auth/index.ts b/src/system/auth/index.ts similarity index 100% rename from src/modules/auth/index.ts rename to src/system/auth/index.ts diff --git a/src/modules/auth/jwt/index.ts b/src/system/auth/jwt/index.ts similarity index 100% rename from src/modules/auth/jwt/index.ts rename to src/system/auth/jwt/index.ts diff --git a/src/modules/auth/jwt/jwt-decorator.ts b/src/system/auth/jwt/jwt-decorator.ts similarity index 100% rename from src/modules/auth/jwt/jwt-decorator.ts rename to src/system/auth/jwt/jwt-decorator.ts diff --git a/src/modules/auth/jwt/jwt-guard.ts b/src/system/auth/jwt/jwt-guard.ts similarity index 100% rename from src/modules/auth/jwt/jwt-guard.ts rename to src/system/auth/jwt/jwt-guard.ts diff --git a/src/modules/auth/jwt/jwt-module.ts b/src/system/auth/jwt/jwt-module.ts similarity index 100% rename from src/modules/auth/jwt/jwt-module.ts rename to src/system/auth/jwt/jwt-module.ts diff --git a/src/modules/auth/vo/logined-user.vo.ts b/src/system/auth/vo/logined-user.vo.ts similarity index 91% rename from src/modules/auth/vo/logined-user.vo.ts rename to src/system/auth/vo/logined-user.vo.ts index fea86ae..b168eb4 100644 --- a/src/modules/auth/vo/logined-user.vo.ts +++ b/src/system/auth/vo/logined-user.vo.ts @@ -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) { diff --git a/src/system/menu/dto/create-menu.dto.ts b/src/system/menu/dto/create-menu.dto.ts new file mode 100644 index 0000000..f6aa48d --- /dev/null +++ b/src/system/menu/dto/create-menu.dto.ts @@ -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; +} diff --git a/src/system/menu/dto/find-menu.dto.ts b/src/system/menu/dto/find-menu.dto.ts new file mode 100644 index 0000000..02fff29 --- /dev/null +++ b/src/system/menu/dto/find-menu.dto.ts @@ -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; +} diff --git a/src/system/menu/dto/update-menu.dto.ts b/src/system/menu/dto/update-menu.dto.ts new file mode 100644 index 0000000..ec2def7 --- /dev/null +++ b/src/system/menu/dto/update-menu.dto.ts @@ -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) {} diff --git a/src/system/menu/entities/menu.entity.ts b/src/system/menu/entities/menu.entity.ts new file mode 100644 index 0000000..e8923d2 --- /dev/null +++ b/src/system/menu/entities/menu.entity.ts @@ -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[]; +} diff --git a/src/system/menu/index.ts b/src/system/menu/index.ts new file mode 100644 index 0000000..5036e79 --- /dev/null +++ b/src/system/menu/index.ts @@ -0,0 +1,4 @@ +export * from './entities/menu.entity'; +export * from './menu.controller'; +export * from './menu.module'; +export * from './menu.service'; diff --git a/src/system/menu/menu.controller.ts b/src/system/menu/menu.controller.ts new file mode 100644 index 0000000..65d46af --- /dev/null +++ b/src/system/menu/menu.controller.ts @@ -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 { + 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); + } +} diff --git a/src/system/menu/menu.module.ts b/src/system/menu/menu.module.ts new file mode 100644 index 0000000..dfd29c0 --- /dev/null +++ b/src/system/menu/menu.module.ts @@ -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 {} diff --git a/src/system/menu/menu.service.ts b/src/system/menu/menu.service.ts new file mode 100644 index 0000000..a89f686 --- /dev/null +++ b/src/system/menu/menu.service.ts @@ -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) { + 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) { + 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); + } +} diff --git a/src/modules/permission/dto/create-permission.dto.ts b/src/system/permission/dto/create-permission.dto.ts similarity index 100% rename from src/modules/permission/dto/create-permission.dto.ts rename to src/system/permission/dto/create-permission.dto.ts diff --git a/src/modules/permission/dto/update-permission.dto.ts b/src/system/permission/dto/update-permission.dto.ts similarity index 100% rename from src/modules/permission/dto/update-permission.dto.ts rename to src/system/permission/dto/update-permission.dto.ts diff --git a/src/modules/permission/entities/permission.entity.ts b/src/system/permission/entities/permission.entity.ts similarity index 93% rename from src/modules/permission/entities/permission.entity.ts rename to src/system/permission/entities/permission.entity.ts index 158247d..60a4656 100644 --- a/src/modules/permission/entities/permission.entity.ts +++ b/src/system/permission/entities/permission.entity.ts @@ -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 { diff --git a/src/modules/permission/index.ts b/src/system/permission/index.ts similarity index 100% rename from src/modules/permission/index.ts rename to src/system/permission/index.ts diff --git a/src/modules/permission/permission.controller.ts b/src/system/permission/permission.controller.ts similarity index 100% rename from src/modules/permission/permission.controller.ts rename to src/system/permission/permission.controller.ts diff --git a/src/modules/permission/permission.decorator.ts b/src/system/permission/permission.decorator.ts similarity index 100% rename from src/modules/permission/permission.decorator.ts rename to src/system/permission/permission.decorator.ts diff --git a/src/modules/permission/permission.guard.ts b/src/system/permission/permission.guard.ts similarity index 96% rename from src/modules/permission/permission.guard.ts rename to src/system/permission/permission.guard.ts index 8afb82c..856a214 100644 --- a/src/modules/permission/permission.guard.ts +++ b/src/system/permission/permission.guard.ts @@ -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 { diff --git a/src/modules/permission/permission.module.ts b/src/system/permission/permission.module.ts similarity index 100% rename from src/modules/permission/permission.module.ts rename to src/system/permission/permission.module.ts diff --git a/src/modules/permission/permission.service.ts b/src/system/permission/permission.service.ts similarity index 100% rename from src/modules/permission/permission.service.ts rename to src/system/permission/permission.service.ts diff --git a/src/modules/role/dto/create-role.dto.ts b/src/system/role/dto/create-role.dto.ts similarity index 78% rename from src/modules/role/dto/create-role.dto.ts rename to src/system/role/dto/create-role.dto.ts index f35e81e..6b70ccf 100644 --- a/src/modules/role/dto/create-role.dto.ts +++ b/src/system/role/dto/create-role.dto.ts @@ -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 { diff --git a/src/system/role/dto/find-role.dto.ts b/src/system/role/dto/find-role.dto.ts new file mode 100644 index 0000000..b29cd22 --- /dev/null +++ b/src/system/role/dto/find-role.dto.ts @@ -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; +} diff --git a/src/modules/role/dto/update-role.dto.ts b/src/system/role/dto/update-role.dto.ts similarity index 100% rename from src/modules/role/dto/update-role.dto.ts rename to src/system/role/dto/update-role.dto.ts diff --git a/src/modules/role/entities/role.entity.ts b/src/system/role/entities/role.entity.ts similarity index 68% rename from src/modules/role/entities/role.entity.ts rename to src/system/role/entities/role.entity.ts index 19b86ad..ea0fa67 100644 --- a/src/modules/role/entities/role.entity.ts +++ b/src/system/role/entities/role.entity.ts @@ -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[]; } diff --git a/src/modules/role/index.ts b/src/system/role/index.ts similarity index 100% rename from src/modules/role/index.ts rename to src/system/role/index.ts diff --git a/src/modules/role/role.controller.ts b/src/system/role/role.controller.ts similarity index 100% rename from src/modules/role/role.controller.ts rename to src/system/role/role.controller.ts diff --git a/src/modules/role/role.module.ts b/src/system/role/role.module.ts similarity index 93% rename from src/modules/role/role.module.ts rename to src/system/role/role.module.ts index b7b90a1..92e06f2 100644 --- a/src/modules/role/role.module.ts +++ b/src/system/role/role.module.ts @@ -8,5 +8,6 @@ import { RoleService } from './role.service'; imports: [TypeOrmModule.forFeature([Role])], controllers: [RoleController], providers: [RoleService], + exports: [RoleService], }) export class RoleModule {} diff --git a/src/modules/role/role.service.ts b/src/system/role/role.service.ts similarity index 86% rename from src/modules/role/role.service.ts rename to src/system/role/role.service.ts index a140a74..cbd39ab 100644 --- a/src/modules/role/role.service.ts +++ b/src/system/role/role.service.ts @@ -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`; } diff --git a/src/modules/user/dto/create-user.dto.ts b/src/system/user/dto/create-user.dto.ts similarity index 72% rename from src/modules/user/dto/create-user.dto.ts rename to src/system/user/dto/create-user.dto.ts index fc6f7d3..ed8eb9c 100644 --- a/src/modules/user/dto/create-user.dto.ts +++ b/src/system/user/dto/create-user.dto.ts @@ -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 }) diff --git a/src/modules/user/dto/find-user.dto.ts b/src/system/user/dto/find-user.dto.ts similarity index 100% rename from src/modules/user/dto/find-user.dto.ts rename to src/system/user/dto/find-user.dto.ts diff --git a/src/modules/user/dto/update-user.dto.ts b/src/system/user/dto/update-user.dto.ts similarity index 84% rename from src/modules/user/dto/update-user.dto.ts rename to src/system/user/dto/update-user.dto.ts index d21d4e4..76b2756 100644 --- a/src/modules/user/dto/update-user.dto.ts +++ b/src/system/user/dto/update-user.dto.ts @@ -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; } diff --git a/src/modules/user/entities/user.entity.ts b/src/system/user/entities/user.entity.ts similarity index 92% rename from src/modules/user/entities/user.entity.ts rename to src/system/user/entities/user.entity.ts index 9e4c116..16292ce 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/system/user/entities/user.entity.ts @@ -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' diff --git a/src/modules/user/index.ts b/src/system/user/index.ts similarity index 100% rename from src/modules/user/index.ts rename to src/system/user/index.ts diff --git a/src/modules/user/user.controller.ts b/src/system/user/user.controller.ts similarity index 74% rename from src/modules/user/user.controller.ts rename to src/system/user/user.controller.ts index 3a989f6..af87326 100644 --- a/src/modules/user/user.controller.ts +++ b/src/system/user/user.controller.ts @@ -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 { 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); } diff --git a/src/modules/user/user.module.ts b/src/system/user/user.module.ts similarity index 78% rename from src/modules/user/user.module.ts rename to src/system/user/user.module.ts index 79b9575..41ae2d4 100644 --- a/src/modules/user/user.module.ts +++ b/src/system/user/user.module.ts @@ -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], diff --git a/src/modules/user/user.service.ts b/src/system/user/user.service.ts similarity index 89% rename from src/modules/user/user.service.ts rename to src/system/user/user.service.ts index bd34241..292d81b 100644 --- a/src/modules/user/user.service.ts +++ b/src/system/user/user.service.ts @@ -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) { + constructor(@InjectRepository(User) private userRepository: Repository, 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 } }); } diff --git a/src/utils/list-tree.ts b/src/utils/list-tree.ts new file mode 100644 index 0000000..a271608 --- /dev/null +++ b/src/utils/list-tree.ts @@ -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; + }); +}