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_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:文件存在关联,无法删除,请先找到该文件关联的业务解除关联。'
}

View File

@ -66,4 +66,12 @@ export class ContractController {
async delete(@IdParam() id: number): Promise<void> {
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,
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<ContractDto>,
PartialType(ContractDto)

View File

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

View File

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

View File

@ -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<ContractEntity>
private contractRepository: Repository<ContractEntity>,
@InjectRepository(Storage)
private storageRepository: Repository<Storage>
) {}
/**
*
@ -24,12 +30,16 @@ export class ContractService {
type,
status
}: ContractQueryDto): Promise<Pagination<ContractEntity>> {
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<ContractEntity>(queryBuilder, {
page,
@ -47,8 +57,30 @@ export class ContractService {
/**
*
*/
async update(id: number, dto: Partial<ContractDto>): Promise<void> {
await this.contractRepository.update(id, dto);
async update(id: number, { fileIds, ...data }: Partial<ContractUpdateDto>): Promise<void> {
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);
});
}
}

View File

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

View File

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

View File

@ -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<Storage>,
@InjectRepository(UserEntity)
@ -32,11 +35,18 @@ export class StorageService {
*
*/
async delete(fileIds: number[]): Promise<void> {
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<Pagination<StorageInfo>> {
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');

View File

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

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!');
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 };
}
}