diff --git a/.gitea/stack.yaml b/.gitea/stack.yaml index 5e2ce65..6e541d2 100644 --- a/.gitea/stack.yaml +++ b/.gitea/stack.yaml @@ -2,7 +2,7 @@ version: '3' services: server: - image: git.dev.juetan.cn/juetan/server:latest + image: git.app.juetan.cn/appnify/server:latest networks: - public deploy: @@ -12,14 +12,15 @@ services: constraints: [node.role == manager] labels: - traefik.enable=true - - traefik.http.routers.nest.rule=Host(`nest.dev.juetan.cn`) && PathPrefix(`/api`, `/upload`) - - traefik.http.routers.nest.entrypoints=websecure - - traefik.http.routers.nest.tls=true - - traefik.http.routers.nest.tls.certresolver=acmer - - traefik.http.services.nest1.loadbalancer.server.port=3030 + - traefik.http.routers.aserver.rule=Host(`appnify.app.juetan.cn`) && PathPrefix(`/api`, `/upload`) + - traefik.http.routers.aserver.entrypoints=websecure + - traefik.http.routers.aserver.tls=true + - traefik.http.routers.aserver.tls.certresolver=acmer + - traefik.http.routers.aserver.middlewares=tohttps@docker + - traefik.http.services.aserver1.loadbalancer.server.port=3030 web: - image: git.dev.juetan.cn/juetan/web:latest + image: git.app.juetan.cn/appnify/web:latest networks: - public deploy: @@ -29,11 +30,12 @@ services: constraints: [node.role == manager] labels: - traefik.enable=true - - traefik.http.routers.vue.rule=Host(`nest.dev.juetan.cn`) - - traefik.http.routers.vue.entrypoints=websecure - - traefik.http.routers.vue.tls=true - - traefik.http.routers.vue.tls.certresolver=acmer - - traefik.http.services.vue1.loadbalancer.server.port=80 + - traefik.http.routers.aweb.rule=Host(`appnify.app.juetan.cn`) + - traefik.http.routers.aweb.entrypoints=websecure + - traefik.http.routers.aweb.tls=true + - traefik.http.routers.aweb.tls.certresolver=acmer + - traefik.http.routers.aweb.middlewares=tohttps@docker + - traefik.http.services.aweb1.loadbalancer.server.port=80 networks: public: diff --git a/content/data/db.sqlite b/content/data/db.sqlite index c7765b0..6625cd6 100644 Binary files a/content/data/db.sqlite and b/content/data/db.sqlite differ diff --git a/src/app.module.ts b/src/app.module.ts index 57b5995..a992fd8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,7 +8,7 @@ import { SerializationModule } from '@/middlewares/serialization'; import { ValidationModule } from '@/middlewares/validation'; import { LoggerModule } from '@/monitor/logger'; import { CacheModule } from '@/storage/cache'; -import { UploadModule } from '@/storage/file'; +import { FileModule } from '@/storage/file'; import { AuthModule } from '@/system/auth'; import { RoleModule } from '@/system/role'; import { UserModule } from '@/system/user'; @@ -16,6 +16,7 @@ import { ScanModule } from '@/utils/scan.module'; import { Module } from '@nestjs/common'; import { MenuModule } from './system/menu'; import { DictModule, DictTypeModule } from './system/dict'; +import { FileCategoryModule } from './storage/fileCategory'; @Module({ imports: [ @@ -80,7 +81,11 @@ import { DictModule, DictTypeModule } from './system/dict'; /** * 上传模块 */ - UploadModule, + FileModule, + /** + * 文件分类 + */ + FileCategoryModule, /** * 文章模块 */ diff --git a/src/common/base/base.service.ts b/src/common/base/base.service.ts index e6d8aed..7f673bd 100644 --- a/src/common/base/base.service.ts +++ b/src/common/base/base.service.ts @@ -1,5 +1,14 @@ import { ConfigService } from '@/config'; +import { BaseEntity } from '@/database'; import { Inject } from '@nestjs/common'; +import { Between, FindManyOptions } from 'typeorm'; + +interface Params { + page: number; + size: number; + startDateTime: string; + endDateTime: string; +} /** * 服务基类 @@ -42,4 +51,19 @@ export class BaseService { }), }; } + + mergeCommonParams(params: Params): FindManyOptions { + const { page, size, startDateTime, endDateTime } = params; + const skip = (page - 1) * size; + const take = size === 0 ? this.config.defaultPageSize : size; + const fromDate = new Date(startDateTime); + const endDate = new Date(endDateTime); + return { + skip, + take, + where: { + createdAt: Between(fromDate, endDate), + }, + }; + } } diff --git a/src/storage/file/dto/find-file.dto.ts b/src/storage/file/dto/find-file.dto.ts index e69de29..b3e021a 100644 --- a/src/storage/file/dto/find-file.dto.ts +++ b/src/storage/file/dto/find-file.dto.ts @@ -0,0 +1,23 @@ +import { PaginationDto } from '@/middlewares/response'; +import { IntersectionType } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsInt, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class FindFileDto extends IntersectionType(PaginationDto) { + /** + * 文件名称 + * @example '风景' + */ + @IsOptional() + @IsString() + name?: string; + + /** + * 分类ID + * @example 1 + */ + @IsOptional() + @IsNumber() + @Transform(({ value }) => Number(value)) + categoryId?: number; +} diff --git a/src/storage/file/dto/update-file.dto.ts b/src/storage/file/dto/update-file.dto.ts index 8e2f809..c1979e7 100644 --- a/src/storage/file/dto/update-file.dto.ts +++ b/src/storage/file/dto/update-file.dto.ts @@ -1,12 +1,13 @@ -import { IsOptional, IsString } from 'class-validator'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; export class UpdateFileDto { /** * 文件名 * @example "头像.jpg" */ + @IsOptional() @IsString() - name: string; + name?: string; /** * 描述 @@ -15,4 +16,12 @@ export class UpdateFileDto { @IsOptional() @IsString() description?: string; + + /** + * 分类ID + * @example 1 + */ + @IsOptional() + @IsNumber() + categoryId?: number; } diff --git a/src/storage/file/entities/file.entity.ts b/src/storage/file/entities/file.entity.ts index f170eb7..232dd43 100644 --- a/src/storage/file/entities/file.entity.ts +++ b/src/storage/file/entities/file.entity.ts @@ -1,5 +1,7 @@ import { BaseEntity } from '@/database'; -import { Column, Entity } from 'typeorm'; +import { FileCategory } from '@/storage/fileCategory'; +import { ApiHideProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; @Entity({ orderBy: { id: 'DESC' } }) export class File extends BaseEntity { @@ -33,7 +35,7 @@ export class File extends BaseEntity { /** * 文件路径 - * @example "/upload/2021/10/01/xxx.jpg" + * @example "/upload/2021-10-01/xxx.jpg" */ @Column({ comment: '文件路径' }) path: string; @@ -51,4 +53,19 @@ export class File extends BaseEntity { */ @Column({ comment: '文件后缀' }) extension: string; + + /** + * 分类 + */ + @ApiHideProperty() + @ManyToOne(() => FileCategory, (category) => category.files) + @JoinColumn() + category: FileCategory; + + /** + * 分类ID + * @example 0 + */ + @Column({ comment: '分类ID', nullable: true }) + categoryId: number; } diff --git a/src/storage/file/file.controller.ts b/src/storage/file/file.controller.ts index 29508a9..fb479a6 100644 --- a/src/storage/file/file.controller.ts +++ b/src/storage/file/file.controller.ts @@ -8,6 +8,7 @@ import { Param, Patch, Post, + Query, Req, UploadedFile, UseInterceptors, @@ -17,12 +18,13 @@ import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; import { CreateFileDto } from './dto/create-file.dto'; import { UpdateFileDto } from './dto/update-file.dto'; -import { UploadService } from './file.service'; +import { FileService } from './file.service'; +import { FindFileDto } from './dto/find-file.dto'; @ApiTags('file') @Controller('file') -export class UploadController { - constructor(private readonly uploadService: UploadService) {} +export class FileController { + constructor(private readonly fileService: FileService) {} @Post() @UseInterceptors(FileInterceptor('file')) @@ -30,37 +32,43 @@ export class UploadController { @ApiBody({ description: '要上传的文件', type: CreateFileDto }) @ApiOperation({ description: '上传文件', operationId: 'addFile' }) create(@UploadedFile() file: Express.Multer.File, @Req() req: Request, @Ip() ip: string) { - return this.uploadService.create(file); + return this.fileService.create(file); } @Get() @Respond(RespondType.PAGINATION) @ApiOperation({ description: '批量查询', operationId: 'getFiles' }) - findAll() { - return this.uploadService.findAll(); + findMany(@Query() findFileDto: FindFileDto) { + return this.fileService.findMany(findFileDto); } @Get(':id') @ApiOperation({ description: '查询', operationId: 'getFile' }) findOne(@Param('id') id: number) { - return this.uploadService.findOne(+id); + return this.fileService.findOne(+id); } @Get('hash/:hash') @ApiOperation({ description: '根据哈希查询', operationId: 'getFileByHash' }) getByHash(@Param('hash') hash: string) { - return this.uploadService.getByHash(hash); + return this.fileService.getByHash(hash); } @Patch(':id') @ApiOperation({ description: '更新', operationId: 'setFile' }) update(@Param('id') id: number, @Body() updateFileDto: UpdateFileDto) { - return this.uploadService.update(id, updateFileDto); + return this.fileService.update(id, updateFileDto); } @Delete(':id') @ApiOperation({ description: '删除', operationId: 'delFile' }) remove(@Param('id') id: number) { - return this.uploadService.remove(+id); + return this.fileService.remove(+id); + } + + @Delete() + @ApiOperation({ description: '批量删除文件', operationId: 'delFiles' }) + removeMany(@Body() ids: number[]) { + return this.fileService.removeMany(ids); } } diff --git a/src/storage/file/file.module.ts b/src/storage/file/file.module.ts index b99df31..bd39540 100644 --- a/src/storage/file/file.module.ts +++ b/src/storage/file/file.module.ts @@ -1,14 +1,15 @@ import { ConfigService } from '@/config'; -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { MulterModule } from '@nestjs/platform-express'; import { TypeOrmModule } from '@nestjs/typeorm'; import { existsSync, mkdirSync } from 'fs'; import { diskStorage } from 'multer'; import { extname, join } from 'path'; import { File } from './entities/file.entity'; -import { UploadController } from './file.controller'; -import { UploadService } from './file.service'; +import { FileController } from './file.controller'; +import { FileService } from './file.service'; import { dayjs } from '@/libraries'; +import { FileCategoryModule } from '../fileCategory'; const MulteredModule = MulterModule.registerAsync({ useFactory: (config: ConfigService) => { @@ -31,8 +32,8 @@ const MulteredModule = MulterModule.registerAsync({ }); @Module({ - imports: [TypeOrmModule.forFeature([File]), MulteredModule], - controllers: [UploadController], - providers: [UploadService], + imports: [TypeOrmModule.forFeature([File]), MulteredModule, forwardRef(() => FileCategoryModule)], + controllers: [FileController], + providers: [FileService], }) -export class UploadModule {} +export class FileModule {} diff --git a/src/storage/file/file.service.ts b/src/storage/file/file.service.ts index 058a650..b31a942 100644 --- a/src/storage/file/file.service.ts +++ b/src/storage/file/file.service.ts @@ -2,13 +2,18 @@ import { BaseService } from '@/common/base'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { extname, relative, sep } from 'path'; -import { Repository } from 'typeorm'; +import { FindOptionsWhere, Like, Repository } from 'typeorm'; import { UpdateFileDto } from './dto/update-file.dto'; import { File } from './entities/file.entity'; +import { FindFileDto } from './dto/find-file.dto'; +import { FileCategoryService } from '../fileCategory'; @Injectable() -export class UploadService extends BaseService { - constructor(@InjectRepository(File) private readonly repository: Repository) { +export class FileService extends BaseService { + constructor( + @InjectRepository(File) private readonly repository: Repository, + private fileCategoryService: FileCategoryService, + ) { super(); } @@ -36,8 +41,17 @@ export class UploadService extends BaseService { return file.id; } - findAll() { - return this.repository.findAndCount(); + findMany(findFileDto: FindFileDto) { + const { page, size, name, categoryId } = findFileDto; + const { skip, take } = this.formatPagination(page, size, true); + const where: FindOptionsWhere = {}; + if (name) { + where.name = Like(`%${name}%`); + } + if (categoryId) { + where.categoryId = categoryId; + } + return this.repository.findAndCount({ skip, take, where }); } findOne(id: number) { @@ -48,10 +62,16 @@ export class UploadService extends BaseService { * 更新文件信息 * @param id 文件ID * @param updateFileDto 更新信息 - * @returns + * @returns */ - update(id: number, updateFileDto: UpdateFileDto) { - return this.repository.update(id, updateFileDto); + async update(id: number, updateFileDto: UpdateFileDto) { + const { categoryId, ...rest } = updateFileDto; + let category; + if (categoryId) { + category = await this.fileCategoryService.findOne(categoryId); + } + console.log(category); + return this.repository.update(id, { ...rest, category }); } /** @@ -66,4 +86,13 @@ export class UploadService extends BaseService { remove(id: number) { return this.repository.softDelete(id); } + + /** + * 批量删除文件 + * @param ids ID数组 + * @returns + */ + removeMany(ids: number[]) { + return this.repository.softDelete(ids); + } } diff --git a/src/storage/fileCategory/dto/create-fileCategory.dto.ts b/src/storage/fileCategory/dto/create-fileCategory.dto.ts new file mode 100644 index 0000000..84a16fa --- /dev/null +++ b/src/storage/fileCategory/dto/create-fileCategory.dto.ts @@ -0,0 +1,33 @@ +import { IsInt, IsOptional, IsString } from 'class-validator'; + +export class CreateFileCategoryDto { + /** + * 分类名称 + * @example '风景' + */ + @IsString() + name: string; + + /** + * 分类编码 + * @example 'view' + */ + @IsString() + code: string; + + /** + * 分类描述 + * @example '这是一段很长的描述' + */ + @IsOptional() + @IsString() + description?: string; + + /** + * 父级ID + * @example 0 + */ + @IsOptional() + @IsInt() + parentId?: number; +} diff --git a/src/storage/fileCategory/dto/find-fileCategory.dto.ts b/src/storage/fileCategory/dto/find-fileCategory.dto.ts new file mode 100644 index 0000000..5d7f515 --- /dev/null +++ b/src/storage/fileCategory/dto/find-fileCategory.dto.ts @@ -0,0 +1,13 @@ +import { PaginationDto } from '@/middlewares/response'; +import { IntersectionType } from '@nestjs/swagger'; +import { IsInt, IsOptional, IsString } from 'class-validator'; + +export class FindFileCategoryDto extends IntersectionType(PaginationDto) { + /** + * 分类名称 + * @example '风景' + */ + @IsOptional() + @IsString() + name?: string; +} diff --git a/src/storage/fileCategory/dto/update-fileCategory.dto.ts b/src/storage/fileCategory/dto/update-fileCategory.dto.ts new file mode 100644 index 0000000..bfba271 --- /dev/null +++ b/src/storage/fileCategory/dto/update-fileCategory.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateFileCategoryDto } from './create-fileCategory.dto'; + +export class UpdateFileCategoryDto extends PartialType(CreateFileCategoryDto) {} diff --git a/src/storage/fileCategory/entities/fileCategory.entity.ts b/src/storage/fileCategory/entities/fileCategory.entity.ts new file mode 100644 index 0000000..43c3e74 --- /dev/null +++ b/src/storage/fileCategory/entities/fileCategory.entity.ts @@ -0,0 +1,56 @@ +import { BaseEntity } from '@/database'; +import { File } from '@/storage/file'; +import { ApiHideProperty } from '@nestjs/swagger'; +import { Column, Entity, OneToMany, Tree, TreeChildren, TreeParent } from 'typeorm'; + +@Tree('materialized-path') +@Entity({ orderBy: { id: 'DESC' } }) +export class FileCategory extends BaseEntity { + /** + * 分类名称 + * @example '风景' + */ + @Column({ comment: '分类描述' }) + name: string; + + /** + * 分类编码 + * @example 'view' + */ + @Column({ comment: '分类编码' }) + code: string; + + /** + * 分类描述 + * @example '这是一段很长的描述' + */ + @Column({ comment: '分类描述', nullable: true }) + description?: string; + + /** + * 文件列表 + */ + @ApiHideProperty() + @OneToMany(() => File, file => file.category) + files: File[]; + + /** + * 父级分类 + */ + @ApiHideProperty() + @TreeParent() + parent?: FileCategory; + + /** + * 父级ID + */ + @Column({ comment: '父级ID', nullable: true }) + parentId?: number; + + /** + * 子项数组 + */ + @ApiHideProperty() + @TreeChildren() + childrent: FileCategory[]; +} diff --git a/src/storage/fileCategory/fileCategory.controller.ts b/src/storage/fileCategory/fileCategory.controller.ts new file mode 100644 index 0000000..3fa494d --- /dev/null +++ b/src/storage/fileCategory/fileCategory.controller.ts @@ -0,0 +1,49 @@ +import { BaseController } from '@/common/base'; +import { Respond, RespondType } from '@/middlewares/response'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { CreateFileCategoryDto } from './dto/create-fileCategory.dto'; +import { FindFileCategoryDto } from './dto/find-fileCategory.dto'; +import { UpdateFileCategoryDto } from './dto/update-fileCategory.dto'; +import { FileCategory } from './entities/fileCategory.entity'; +import { FileCategoryService } from './fileCategory.service'; + +@ApiTags('fileCategory') +@Controller('fileCategorys') +export class FileCategoryController extends BaseController { + constructor(private fileCategoryService: FileCategoryService) { + super(); + } + + @Post() + @ApiOperation({ description: '新增文件分类', operationId: 'addFileCategory' }) + addFileCategory(@Body() createFileCategoryDto: CreateFileCategoryDto) { + return this.fileCategoryService.create(createFileCategoryDto); + } + + @Get() + @Respond(RespondType.PAGINATION) + @ApiOkResponse({ isArray: true, type: FileCategory }) + @ApiOperation({ description: '查询文件分类', operationId: 'getFileCategorys' }) + getFileCategorys(@Query() query: FindFileCategoryDto) { + return this.fileCategoryService.findMany(query); + } + + @Get(':id') + @ApiOperation({ description: '获取文件分类', operationId: 'getFileCategory' }) + getFileCategory(@Param('id') id: number): Promise { + return this.fileCategoryService.findOne(id); + } + + @Patch(':id') + @ApiOperation({ description: '更新文件分类', operationId: 'setFileCategory' }) + updateFileCategory(@Param('id') id: number, @Body() updateFileCategoryDto: UpdateFileCategoryDto) { + return this.fileCategoryService.update(+id, updateFileCategoryDto); + } + + @Delete(':id') + @ApiOperation({ description: '删除文件分类', operationId: 'delFileCategory' }) + delFileCategory(@Param('id') id: number) { + return this.fileCategoryService.remove(+id); + } +} diff --git a/src/storage/fileCategory/fileCategory.module.ts b/src/storage/fileCategory/fileCategory.module.ts new file mode 100644 index 0000000..c614b1f --- /dev/null +++ b/src/storage/fileCategory/fileCategory.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FileCategory } from './entities/fileCategory.entity'; +import { FileCategoryController } from './fileCategory.controller'; +import { FileCategoryService } from './fileCategory.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([FileCategory])], + controllers: [FileCategoryController], + providers: [FileCategoryService], + exports: [FileCategoryService], +}) +export class FileCategoryModule {} diff --git a/src/storage/fileCategory/fileCategory.service.ts b/src/storage/fileCategory/fileCategory.service.ts new file mode 100644 index 0000000..707ae66 --- /dev/null +++ b/src/storage/fileCategory/fileCategory.service.ts @@ -0,0 +1,55 @@ +import { BaseService } from '@/common/base'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FindOptionsWhere, Like, Repository } from 'typeorm'; +import { CreateFileCategoryDto } from './dto/create-fileCategory.dto'; +import { FindFileCategoryDto } from './dto/find-fileCategory.dto'; +import { UpdateFileCategoryDto } from './dto/update-fileCategory.dto'; +import { FileCategory } from './entities/fileCategory.entity'; + +@Injectable() +export class FileCategoryService extends BaseService { + constructor(@InjectRepository(FileCategory) private fileCategoryRepository: Repository) { + super(); + } + + /** + * 新增文件分类 + */ + async create(createFileCategoryDto: CreateFileCategoryDto) { + const fileCategory = this.fileCategoryRepository.create(createFileCategoryDto); + await this.fileCategoryRepository.save(fileCategory); + return fileCategory.id; + } + + /** + * 条件/分页查询 + */ + async findMany(findFileCategorydto: FindFileCategoryDto) { + const { page, size } = findFileCategorydto; + const { skip, take } = this.formatPagination(page, size, true); + return this.fileCategoryRepository.findAndCount({ skip, take }); + } + + /** + * 根据ID查询 + */ + findOne(idOrOptions: number | Partial) { + const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any); + return this.fileCategoryRepository.findOne({ where }); + } + + /** + * 根据ID更新 + */ + update(id: number, updateFileCategoryDto: UpdateFileCategoryDto) { + return this.fileCategoryRepository.update(id, updateFileCategoryDto); + } + + /** + * 根据ID删除(软删除) + */ + remove(id: number) { + return this.fileCategoryRepository.softDelete(id); + } +} diff --git a/src/storage/fileCategory/index.ts b/src/storage/fileCategory/index.ts new file mode 100644 index 0000000..7a89442 --- /dev/null +++ b/src/storage/fileCategory/index.ts @@ -0,0 +1,4 @@ +export * from './entities/fileCategory.entity'; +export * from './fileCategory.controller'; +export * from './fileCategory.module'; +export * from './fileCategory.service';