feat: 合同删除附件 附件管理

This commit is contained in:
louis 2024-03-01 15:23:28 +08:00
parent 9c8d2e6ca3
commit 481dd8456e
11 changed files with 148 additions and 33 deletions

View File

@ -45,5 +45,9 @@ export enum ErrorEnum {
// OSS相关 // OSS相关
OSS_FILE_OR_DIR_EXIST = '1401:当前创建的文件或目录已存在', OSS_FILE_OR_DIR_EXIST = '1401:当前创建的文件或目录已存在',
OSS_NO_OPERATION_REQUIRED = '1402:无需操作', OSS_NO_OPERATION_REQUIRED = '1402:无需操作',
OSS_EXCEE_MAXIMUM_QUANTITY = '1403:已超出支持的最大处理数量' OSS_EXCEE_MAXIMUM_QUANTITY = '1403:已超出支持的最大处理数量',
// Storage相关
STORAGE_NOT_FOUND = '1404:文件不存在,请重试',
STORAGE_REFRENCE_EXISTS = '1405:文件存在关联,无法删除,请先找到该文件关联的业务解除关联。'
} }

View File

@ -66,4 +66,12 @@ export class ContractController {
async delete(@IdParam() id: number): Promise<void> { async delete(@IdParam() id: number): Promise<void> {
await this.contractService.delete(id); await this.contractService.delete(id);
} }
@Put('unlink-attachments/:id')
@ApiOperation({ summary: '附件解除关联' })
@Perm(permissions.UPDATE)
async unlinkAttachments(@IdParam() id: number, @Body() {fileIds}: ContractUpdateDto): Promise<void> {
await this.contractService.unlinkAttachments(id, fileIds);
}
} }

View File

@ -9,10 +9,10 @@ import {
IsOptional, IsOptional,
IsString, IsString,
Matches, Matches,
MinLength, MinLength
} from 'class-validator'; } from 'class-validator';
import { PagerDto } from '~/common/dto/pager.dto'; import { PagerDto } from '~/common/dto/pager.dto';
import { Storage } from '../tools/storage/storage.entity';
export class ContractDto { export class ContractDto {
@ApiProperty({ description: '合同编号' }) @ApiProperty({ description: '合同编号' })
@ -47,9 +47,16 @@ export class ContractDto {
@ApiProperty({ description: '审核状态(字典)' }) @ApiProperty({ description: '审核状态(字典)' })
@IsIn([0, 1, 2]) @IsIn([0, 1, 2])
status: number; 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( export class ContractQueryDto extends IntersectionType(
PagerDto<ContractDto>, PagerDto<ContractDto>,
PartialType(ContractDto) PartialType(ContractDto)

View File

@ -1,6 +1,7 @@
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm'; import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm';
import { CommonEntity } from '~/common/entity/common.entity'; import { CommonEntity } from '~/common/entity/common.entity';
import { Storage } from '../tools/storage/storage.entity';
@Entity({ name: 'contract' }) @Entity({ name: 'contract' })
export class ContractEntity extends CommonEntity { export class ContractEntity extends CommonEntity {
@ -41,4 +42,12 @@ export class ContractEntity extends CommonEntity {
@Column({ name: 'status', type: 'tinyint', default: 0, comment: '审核状态(字典)' }) @Column({ name: 'status', type: 'tinyint', default: 0, comment: '审核状态(字典)' })
@ApiProperty({ description: '审核状态0待审核1同意2.不同意(字典)' }) @ApiProperty({ description: '审核状态0待审核1同意2.不同意(字典)' })
status: number; 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<Storage[]>;
} }

View File

@ -3,9 +3,10 @@ import { ContractController } from './contract.controller';
import { ContractService } from './contract.service'; import { ContractService } from './contract.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ContractEntity } from './contract.entity'; import { ContractEntity } from './contract.entity';
import { StorageModule } from '../tools/storage/storage.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([ContractEntity])], imports: [TypeOrmModule.forFeature([ContractEntity]), StorageModule],
controllers: [ContractController], controllers: [ContractController],
providers: [ContractService] providers: [ContractService]
}) })

View File

@ -1,17 +1,23 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
import { ContractEntity } from './contract.entity'; import { ContractEntity } from './contract.entity';
import { Like, Repository } from 'typeorm'; import { EntityManager, Like, Repository } from 'typeorm';
import { ContractDto, ContractQueryDto } from './contract.dto'; import { ContractDto, ContractQueryDto, ContractUpdateDto } from './contract.dto';
import { Pagination } from '~/helper/paginate/pagination'; import { Pagination } from '~/helper/paginate/pagination';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { paginate } from '~/helper/paginate'; 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() @Injectable()
export class ContractService { export class ContractService {
constructor( constructor(
@InjectEntityManager() private entityManager: EntityManager,
@InjectRepository(ContractEntity) @InjectRepository(ContractEntity)
private contractRepository: Repository<ContractEntity> private contractRepository: Repository<ContractEntity>,
@InjectRepository(Storage)
private storageRepository: Repository<Storage>
) {} ) {}
/** /**
* *
@ -24,12 +30,16 @@ export class ContractService {
type, type,
status status
}: ContractQueryDto): Promise<Pagination<ContractEntity>> { }: ContractQueryDto): Promise<Pagination<ContractEntity>> {
const queryBuilder = this.contractRepository.createQueryBuilder('contract').where({ const queryBuilder = this.contractRepository
...(contractNumber ? { contractNumber: Like(`%${contractNumber}%`) } : null), .createQueryBuilder('contract')
...(title ? { title: Like(`%${title}%`) } : null), .leftJoin('contract.files', 'files')
...(isNumber(type) ? { type } : null), .addSelect(['files.id', 'files.path'])
...(isNumber(status) ? { status } : null) .where({
}); ...(contractNumber ? { contractNumber: Like(`%${contractNumber}%`) } : null),
...(title ? { title: Like(`%${title}%`) } : null),
...(isNumber(type) ? { type } : null),
...(isNumber(status) ? { status } : null)
});
return paginate<ContractEntity>(queryBuilder, { return paginate<ContractEntity>(queryBuilder, {
page, page,
@ -47,8 +57,30 @@ export class ContractService {
/** /**
* *
*/ */
async update(id: number, dto: Partial<ContractDto>): Promise<void> { async update(id: number, { fileIds, ...data }: Partial<ContractUpdateDto>): Promise<void> {
await this.contractRepository.update(id, dto); 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(); .getOne();
return info; 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);
});
}
} }

View File

@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsOptional, IsString } from 'class-validator'; import { ArrayNotEmpty, IsArray, IsOptional, IsString } from 'class-validator';
import { PagerDto } from '~/common/dto/pager.dto'; import { PagerDto } from '~/common/dto/pager.dto';
@ -32,6 +33,18 @@ export class StoragePageDto extends PagerDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
username: string; username: string;
@ApiProperty({ description: '附件' })
@IsOptional()
@Transform(
({ value: val }) => {
return val ? val.split(',').map(item => Number(item)) : [];
},
{
toClassOnly: true
}
)
ids: number[];
} }
export class StorageCreateDto { export class StorageCreateDto {

View File

@ -1,7 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
import { Column, Entity } from 'typeorm'; import { Column, Entity, ManyToMany, Relation } from 'typeorm';
import { CommonEntity } from '~/common/entity/common.entity'; import { CommonEntity } from '~/common/entity/common.entity';
import { ContractEntity } from '~/modules/contract/contract.entity';
@Entity({ name: 'tool_storage' }) @Entity({ name: 'tool_storage' })
export class Storage extends CommonEntity { export class Storage extends CommonEntity {
@ -37,4 +38,8 @@ export class Storage extends CommonEntity {
@Column({ nullable: true, name: 'user_id' }) @Column({ nullable: true, name: 'user_id' })
@ApiProperty({ description: '用户ID' }) @ApiProperty({ description: '用户ID' })
userId: number; userId: number;
@ApiHideProperty()
@ManyToMany(() => ContractEntity, contract => contract.files)
contracts: Relation<ContractEntity[]>;
} }

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
import { Between, Like, Repository } from 'typeorm'; import { Between, EntityManager, In, Like, Repository } from 'typeorm';
import { paginateRaw } from '~/helper/paginate'; import { paginateRaw } from '~/helper/paginate';
import { PaginationTypeEnum } from '~/helper/paginate/interface'; import { PaginationTypeEnum } from '~/helper/paginate/interface';
@ -11,10 +11,13 @@ import { deleteFile } from '~/utils';
import { StorageCreateDto, StoragePageDto } from './storage.dto'; import { StorageCreateDto, StoragePageDto } from './storage.dto';
import { StorageInfo } from './storage.modal'; import { StorageInfo } from './storage.modal';
import { BusinessException } from '~/common/exceptions/biz.exception';
import { ErrorEnum } from '~/constants/error-code.constant';
@Injectable() @Injectable()
export class StorageService { export class StorageService {
constructor( constructor(
@InjectEntityManager() private entityManager: EntityManager,
@InjectRepository(Storage) @InjectRepository(Storage)
private storageRepository: Repository<Storage>, private storageRepository: Repository<Storage>,
@InjectRepository(UserEntity) @InjectRepository(UserEntity)
@ -32,11 +35,18 @@ export class StorageService {
* *
*/ */
async delete(fileIds: number[]): Promise<void> { async delete(fileIds: number[]): Promise<void> {
const items = await this.storageRepository.findByIds(fileIds); await this.entityManager.transaction(async manager => {
await this.storageRepository.delete(fileIds); const items = await this.storageRepository.findBy({ id: In(fileIds) });
try {
items.forEach(el => { await manager.delete(Storage, fileIds);
deleteFile(el.path); 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, size,
extName, extName,
time, time,
username username,
ids
}: StoragePageDto): Promise<Pagination<StorageInfo>> { }: StoragePageDto): Promise<Pagination<StorageInfo>> {
const queryBuilder = this.storageRepository const queryBuilder = this.storageRepository
.createQueryBuilder('storage') .createQueryBuilder('storage')
@ -61,7 +72,8 @@ export class StorageService {
...(time && { createdAt: Between(time[0], time[1]) }), ...(time && { createdAt: Between(time[0], time[1]) }),
...(username && { ...(username && {
userId: await (await this.userRepository.findOneBy({ username })).id userId: await (await this.userRepository.findOneBy({ username })).id
}) }),
...(ids && { id: In(ids) })
}) })
.orderBy('storage.created_at', 'DESC'); .orderBy('storage.created_at', 'DESC');

View File

@ -38,10 +38,10 @@ export class UploadController {
// console.log(part.file) // console.log(part.file)
try { try {
const path = await this.uploadService.saveFile(file, user.uid); const savedFile = await this.uploadService.saveFile(file, user.uid);
return { return {
filename: path filename: savedFile
}; };
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -25,7 +25,7 @@ export class UploadService {
/** /**
* *
*/ */
async saveFile(file: MultipartFile, userId: number): Promise<string> { async saveFile(file: MultipartFile, userId: number): Promise<{ id: number; path: string }> {
if (isNil(file)) throw new NotFoundException('Have not any file to upload!'); if (isNil(file)) throw new NotFoundException('Have not any file to upload!');
const fileName = file.filename; const fileName = file.filename;
@ -37,7 +37,7 @@ export class UploadService {
saveLocalFile(await file.toBuffer(), name); saveLocalFile(await file.toBuffer(), name);
await this.storageRepository.save({ const storage = await this.storageRepository.save({
name, name,
fileName, fileName,
extName, extName,
@ -47,6 +47,6 @@ export class UploadService {
userId userId
}); });
return path; return { path, id: storage.id };
} }
} }