diff --git a/src/constants/error-code.constant.ts b/src/constants/error-code.constant.ts index 95af623..3b381c2 100644 --- a/src/constants/error-code.constant.ts +++ b/src/constants/error-code.constant.ts @@ -45,5 +45,9 @@ export enum ErrorEnum { // OSS相关 OSS_FILE_OR_DIR_EXIST = '1401:当前创建的文件或目录已存在', OSS_NO_OPERATION_REQUIRED = '1402:无需操作', - OSS_EXCEE_MAXIMUM_QUANTITY = '1403:已超出支持的最大处理数量' + OSS_EXCEE_MAXIMUM_QUANTITY = '1403:已超出支持的最大处理数量', + + // Storage相关 + STORAGE_NOT_FOUND = '1404:文件不存在,请重试', + STORAGE_REFRENCE_EXISTS = '1405:文件存在关联,无法删除,请先找到该文件关联的业务解除关联。' } diff --git a/src/modules/contract/contract.controller.ts b/src/modules/contract/contract.controller.ts index 5bdc005..a3e716f 100644 --- a/src/modules/contract/contract.controller.ts +++ b/src/modules/contract/contract.controller.ts @@ -66,4 +66,12 @@ export class ContractController { async delete(@IdParam() id: number): Promise { await this.contractService.delete(id); } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments(@IdParam() id: number, @Body() {fileIds}: ContractUpdateDto): Promise { + await this.contractService.unlinkAttachments(id, fileIds); + } + } diff --git a/src/modules/contract/contract.dto.ts b/src/modules/contract/contract.dto.ts index 057ea88..9b4ca8b 100644 --- a/src/modules/contract/contract.dto.ts +++ b/src/modules/contract/contract.dto.ts @@ -9,10 +9,10 @@ import { IsOptional, IsString, Matches, - MinLength, - + MinLength } from 'class-validator'; import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; export class ContractDto { @ApiProperty({ description: '合同编号' }) @@ -47,9 +47,16 @@ export class ContractDto { @ApiProperty({ description: '审核状态(字典)' }) @IsIn([0, 1, 2]) status: number; + + @ApiProperty({ description: '附件' }) + files: Storage[]; } -export class ContractUpdateDto extends PartialType(ContractDto) {} +export class ContractUpdateDto extends PartialType(ContractDto) { + @ApiProperty({ description: '附件' }) + @IsArray({}) + fileIds: number[]; +} export class ContractQueryDto extends IntersectionType( PagerDto, PartialType(ContractDto) diff --git a/src/modules/contract/contract.entity.ts b/src/modules/contract/contract.entity.ts index 979533d..afdb5cf 100644 --- a/src/modules/contract/contract.entity.ts +++ b/src/modules/contract/contract.entity.ts @@ -1,6 +1,7 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm'; import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '../tools/storage/storage.entity'; @Entity({ name: 'contract' }) export class ContractEntity extends CommonEntity { @@ -41,4 +42,12 @@ export class ContractEntity extends CommonEntity { @Column({ name: 'status', type: 'tinyint', default: 0, comment: '审核状态(字典)' }) @ApiProperty({ description: '审核状态:0待审核,1同意,2.不同意(字典)' }) status: number; + + @ManyToMany(() => Storage, storage => storage.contracts) + @JoinTable({ + name: 'contract_storage', + joinColumn: { name: 'contract_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; } diff --git a/src/modules/contract/contract.module.ts b/src/modules/contract/contract.module.ts index 46dfda5..d5eabe8 100644 --- a/src/modules/contract/contract.module.ts +++ b/src/modules/contract/contract.module.ts @@ -3,9 +3,10 @@ import { ContractController } from './contract.controller'; import { ContractService } from './contract.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ContractEntity } from './contract.entity'; +import { StorageModule } from '../tools/storage/storage.module'; @Module({ - imports: [TypeOrmModule.forFeature([ContractEntity])], + imports: [TypeOrmModule.forFeature([ContractEntity]), StorageModule], controllers: [ContractController], providers: [ContractService] }) diff --git a/src/modules/contract/contract.service.ts b/src/modules/contract/contract.service.ts index 6eafc10..b28eae9 100644 --- a/src/modules/contract/contract.service.ts +++ b/src/modules/contract/contract.service.ts @@ -1,17 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { ContractEntity } from './contract.entity'; -import { Like, Repository } from 'typeorm'; -import { ContractDto, ContractQueryDto } from './contract.dto'; +import { EntityManager, Like, Repository } from 'typeorm'; +import { ContractDto, ContractQueryDto, ContractUpdateDto } from './contract.dto'; import { Pagination } from '~/helper/paginate/pagination'; import { isNumber } from 'lodash'; import { paginate } from '~/helper/paginate'; +import { Storage } from '../tools/storage/storage.entity'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; @Injectable() export class ContractService { constructor( + @InjectEntityManager() private entityManager: EntityManager, @InjectRepository(ContractEntity) - private contractRepository: Repository + private contractRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository ) {} /** * 列举所有角色:除去超级管理员 @@ -24,12 +30,16 @@ export class ContractService { type, status }: ContractQueryDto): Promise> { - const queryBuilder = this.contractRepository.createQueryBuilder('contract').where({ - ...(contractNumber ? { contractNumber: Like(`%${contractNumber}%`) } : null), - ...(title ? { title: Like(`%${title}%`) } : null), - ...(isNumber(type) ? { type } : null), - ...(isNumber(status) ? { status } : null) - }); + const queryBuilder = this.contractRepository + .createQueryBuilder('contract') + .leftJoin('contract.files', 'files') + .addSelect(['files.id', 'files.path']) + .where({ + ...(contractNumber ? { contractNumber: Like(`%${contractNumber}%`) } : null), + ...(title ? { title: Like(`%${title}%`) } : null), + ...(isNumber(type) ? { type } : null), + ...(isNumber(status) ? { status } : null) + }); return paginate(queryBuilder, { page, @@ -47,8 +57,30 @@ export class ContractService { /** * 更新 */ - async update(id: number, dto: Partial): Promise { - await this.contractRepository.update(id, dto); + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(ContractEntity, id, { + ...data + }); + const contract = await this.contractRepository + .createQueryBuilder('contract') + .leftJoinAndSelect('contract.files', 'files') + .where('contract.id = :id', { id }) + .getOne(); + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ContractEntity, 'files') + .of(id) + .addAndRemove(fileIds, contract.files); + }); } /** @@ -70,4 +102,28 @@ export class ContractService { .getOne(); return info; } + + /** + * 解除附件关联 + * @param id 合同ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const contract = await this.contractRepository + .createQueryBuilder('contract') + .leftJoinAndSelect('contract.files', 'files') + .where('contract.id = :id', { id }) + .getOne(); + const linkedFiles = contract.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ContractEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, contract.files); + }); + } } diff --git a/src/modules/tools/storage/storage.dto.ts b/src/modules/tools/storage/storage.dto.ts index ed25a7e..1dfd010 100644 --- a/src/modules/tools/storage/storage.dto.ts +++ b/src/modules/tools/storage/storage.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsOptional, IsString } from 'class-validator'; import { PagerDto } from '~/common/dto/pager.dto'; @@ -32,6 +33,18 @@ export class StoragePageDto extends PagerDto { @IsString() @IsOptional() username: string; + + @ApiProperty({ description: '附件' }) + @IsOptional() + @Transform( + ({ value: val }) => { + return val ? val.split(',').map(item => Number(item)) : []; + }, + { + toClassOnly: true + } + ) + ids: number[]; } export class StorageCreateDto { diff --git a/src/modules/tools/storage/storage.entity.ts b/src/modules/tools/storage/storage.entity.ts index 7680f12..69253a5 100644 --- a/src/modules/tools/storage/storage.entity.ts +++ b/src/modules/tools/storage/storage.entity.ts @@ -1,7 +1,8 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity } from 'typeorm'; +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, ManyToMany, Relation } from 'typeorm'; import { CommonEntity } from '~/common/entity/common.entity'; +import { ContractEntity } from '~/modules/contract/contract.entity'; @Entity({ name: 'tool_storage' }) export class Storage extends CommonEntity { @@ -37,4 +38,8 @@ export class Storage extends CommonEntity { @Column({ nullable: true, name: 'user_id' }) @ApiProperty({ description: '用户ID' }) userId: number; + + @ApiHideProperty() + @ManyToMany(() => ContractEntity, contract => contract.files) + contracts: Relation; } diff --git a/src/modules/tools/storage/storage.service.ts b/src/modules/tools/storage/storage.service.ts index 5a8cbf3..414af8f 100644 --- a/src/modules/tools/storage/storage.service.ts +++ b/src/modules/tools/storage/storage.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Between, Like, Repository } from 'typeorm'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { Between, EntityManager, In, Like, Repository } from 'typeorm'; import { paginateRaw } from '~/helper/paginate'; import { PaginationTypeEnum } from '~/helper/paginate/interface'; @@ -11,10 +11,13 @@ import { deleteFile } from '~/utils'; import { StorageCreateDto, StoragePageDto } from './storage.dto'; import { StorageInfo } from './storage.modal'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; @Injectable() export class StorageService { constructor( + @InjectEntityManager() private entityManager: EntityManager, @InjectRepository(Storage) private storageRepository: Repository, @InjectRepository(UserEntity) @@ -32,11 +35,18 @@ export class StorageService { * 删除文件 */ async delete(fileIds: number[]): Promise { - const items = await this.storageRepository.findByIds(fileIds); - await this.storageRepository.delete(fileIds); - - items.forEach(el => { - deleteFile(el.path); + await this.entityManager.transaction(async manager => { + const items = await this.storageRepository.findBy({ id: In(fileIds) }); + try { + await manager.delete(Storage, fileIds); + items.forEach(el => { + deleteFile(el.path); + }); + } catch (e) { + if (e.code === 'ER_ROW_IS_REFERENCED_2') { + throw new BusinessException(ErrorEnum.STORAGE_REFRENCE_EXISTS); + } + } }); } @@ -48,7 +58,8 @@ export class StorageService { size, extName, time, - username + username, + ids }: StoragePageDto): Promise> { const queryBuilder = this.storageRepository .createQueryBuilder('storage') @@ -61,7 +72,8 @@ export class StorageService { ...(time && { createdAt: Between(time[0], time[1]) }), ...(username && { userId: await (await this.userRepository.findOneBy({ username })).id - }) + }), + ...(ids && { id: In(ids) }) }) .orderBy('storage.created_at', 'DESC'); diff --git a/src/modules/tools/upload/upload.controller.ts b/src/modules/tools/upload/upload.controller.ts index 7c44644..31756ab 100644 --- a/src/modules/tools/upload/upload.controller.ts +++ b/src/modules/tools/upload/upload.controller.ts @@ -38,10 +38,10 @@ export class UploadController { // console.log(part.file) try { - const path = await this.uploadService.saveFile(file, user.uid); + const savedFile = await this.uploadService.saveFile(file, user.uid); return { - filename: path + filename: savedFile }; } catch (error) { console.log(error); diff --git a/src/modules/tools/upload/upload.service.ts b/src/modules/tools/upload/upload.service.ts index 605f977..05b2a31 100644 --- a/src/modules/tools/upload/upload.service.ts +++ b/src/modules/tools/upload/upload.service.ts @@ -25,7 +25,7 @@ export class UploadService { /** * 保存文件上传记录 */ - async saveFile(file: MultipartFile, userId: number): Promise { + async saveFile(file: MultipartFile, userId: number): Promise<{ id: number; path: string }> { if (isNil(file)) throw new NotFoundException('Have not any file to upload!'); const fileName = file.filename; @@ -37,7 +37,7 @@ export class UploadService { saveLocalFile(await file.toBuffer(), name); - await this.storageRepository.save({ + const storage = await this.storageRepository.save({ name, fileName, extName, @@ -47,6 +47,6 @@ export class UploadService { userId }); - return path; + return { path, id: storage.id }; } }