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