feat: 添加文件分类模块
自动部署 / build (push) Failing after 2s Details

master
luoer 2023-11-02 17:38:29 +08:00
parent e6d2adef44
commit 26013a9f92
18 changed files with 388 additions and 43 deletions

View File

@ -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:

Binary file not shown.

View File

@ -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,
/**
*
*/

View File

@ -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<BaseEntity> {
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),
},
};
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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<File>) {
export class FileService extends BaseService {
constructor(
@InjectRepository(File) private readonly repository: Repository<File>,
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<File> = {};
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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateFileCategoryDto } from './create-fileCategory.dto';
export class UpdateFileCategoryDto extends PartialType(CreateFileCategoryDto) {}

View File

@ -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[];
}

View File

@ -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<FileCategory> {
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);
}
}

View File

@ -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 {}

View File

@ -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<FileCategory>) {
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<FileCategory>) {
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);
}
}

View File

@ -0,0 +1,4 @@
export * from './entities/fileCategory.entity';
export * from './fileCategory.controller';
export * from './fileCategory.module';
export * from './fileCategory.service';