feat: 合同删除附件 附件管理
This commit is contained in:
parent
9c8d2e6ca3
commit
481dd8456e
|
@ -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:文件存在关联,无法删除,请先找到该文件关联的业务解除关联。'
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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[]>;
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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[]>;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue