diff --git a/src/app.module.ts b/src/app.module.ts index de220bc..27710c1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,6 +29,7 @@ import { VehicleUsageModule } from './modules/vehicle-usage/vehicle-usage.module import { VehicleUsageController } from './modules/vehicle-usage/vehicle-usage.controller'; import { VehicleUsageService } from './modules/vehicle-usage/vehicle-usage.service'; import { MaterialsInventoryModule } from './modules/materials_inventory/materials_inventory.module'; +import { CompanyModule } from './modules/company/company.module'; @Module({ imports: [ @@ -61,7 +62,9 @@ import { MaterialsInventoryModule } from './modules/materials_inventory/material VehicleUsageModule, - MaterialsInventoryModule + MaterialsInventoryModule, + + CompanyModule ], providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter }, diff --git a/src/constants/enum/index.ts b/src/constants/enum/index.ts index 5444016..75f926d 100644 --- a/src/constants/enum/index.ts +++ b/src/constants/enum/index.ts @@ -1,6 +1,14 @@ +// 字典项status export enum DictTypeStatusEnum { /** 启用 */ ENABLE = 1, /** 禁用 */ DISABLE = 0 } + +// 业务模块枚举 +export enum BusinessModuleEnum { + CONTRACT = 1, + MATERIALS_INVENTORY = 2, + COMPANY = 3 +} diff --git a/src/modules/company/company.controller.ts b/src/modules/company/company.controller.ts new file mode 100644 index 0000000..0333d14 --- /dev/null +++ b/src/modules/company/company.controller.ts @@ -0,0 +1,79 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { CompanyService } from './company.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { CompanyEntity } from './company.entity'; +import { CompanyDto, CompanyQueryDto, CompanyUpdateDto } from './company.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +export const permissions = definePermission('app:company', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Company - 公司') +@ApiSecurityAuth() +@Controller('company') +export class CompanyController { + constructor(private companyService: CompanyService) {} + + @Get() + @ApiOperation({ summary: '获取公司列表' }) + @ApiResult({ type: [CompanyEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: CompanyQueryDto) { + return this.companyService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取公司信息' }) + @ApiResult({ type: CompanyDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.companyService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增公司' }) + @Perm(permissions.CREATE) + async create(@Body() dto: CompanyDto): Promise { + await this.companyService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新公司' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: CompanyUpdateDto): Promise { + await this.companyService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除公司' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.companyService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: CompanyUpdateDto + ): Promise { + await this.companyService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/company/company.dto.ts b/src/modules/company/company.dto.ts new file mode 100644 index 0000000..d388b4d --- /dev/null +++ b/src/modules/company/company.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { + IsArray, + IsDate, + IsDateString, + IsIn, + IsInt, + IsNumber, + IsOptional, + IsString, + Matches, + MinLength +} from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { CompanyEntity } from './company.entity'; + +export class CompanyDto { + @ApiProperty({ description: '公司名称' }) + @IsUnique(CompanyEntity, { message: '已存在同名公司' }) + @IsString() + name: string; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class CompanyUpdateDto extends PartialType(CompanyDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class CompanyQueryDto extends IntersectionType( + PagerDto, + PartialType(CompanyDto) +) {} diff --git a/src/modules/company/company.entity.ts b/src/modules/company/company.entity.ts new file mode 100644 index 0000000..38d7428 --- /dev/null +++ b/src/modules/company/company.entity.ts @@ -0,0 +1,29 @@ +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: 'company' }) +export class CompanyEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '公司名称' + }) + @ApiProperty({ description: '公司名称' }) + name: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @ManyToMany(() => Storage, storage => storage.companys) + @JoinTable({ + name: 'company_storage', + joinColumn: { name: 'company_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/company/company.module.ts b/src/modules/company/company.module.ts new file mode 100644 index 0000000..5fba5b1 --- /dev/null +++ b/src/modules/company/company.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { CompanyController } from './company.controller'; +import { CompanyService } from './company.service'; +import { CompanyEntity } from './company.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '../tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([CompanyEntity]), StorageModule, DatabaseModule], + controllers: [CompanyController], + providers: [CompanyService] +}) +export class CompanyModule {} diff --git a/src/modules/company/company.service.ts b/src/modules/company/company.service.ts new file mode 100644 index 0000000..c0f2465 --- /dev/null +++ b/src/modules/company/company.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { CompanyEntity } from './company.entity'; +import { EntityManager, Like, Repository } from 'typeorm'; +import { CompanyDto, CompanyQueryDto, CompanyUpdateDto } from './company.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +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'; +import { fieldSearch } from '~/shared/database/field-search'; + +@Injectable() +export class CompanyService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(CompanyEntity) + private companyRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 查询所有公司 + */ + async findAll({ + page, + pageSize, + ...fields + }: CompanyQueryDto): Promise> { + const queryBuilder = this.companyRepository + .createQueryBuilder('company') + .leftJoin('company.files', 'files') + .addSelect(['files.id', 'files.path']) + .where(fieldSearch(fields)) + .andWhere('company.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: CompanyDto): Promise { + await this.companyRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(CompanyEntity, id, { + ...data + }); + const company = await this.companyRepository + .createQueryBuilder('company') + .leftJoinAndSelect('company.files', 'files') + .where('company.id = :id', { id }) + .getOne(); + if (fileIds?.length) { + 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(CompanyEntity, 'files') + .of(id) + .addAndRemove(fileIds, company.files); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 合同比较重要,做逻辑删除 + await this.companyRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个合同信息 + */ + async info(id: number) { + const info = await this.companyRepository + .createQueryBuilder('company') + .where({ + id + }) + .andWhere('company.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 合同ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const company = await this.companyRepository + .createQueryBuilder('company') + .leftJoinAndSelect('company.files', 'files') + .where('company.id = :id', { id }) + .getOne(); + const linkedFiles = company.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(CompanyEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, company.files); + }); + } +} diff --git a/src/modules/contract/contract.service.ts b/src/modules/contract/contract.service.ts index 6ff5b66..030ea1a 100644 --- a/src/modules/contract/contract.service.ts +++ b/src/modules/contract/contract.service.ts @@ -9,6 +9,7 @@ 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'; +import { fieldSearch } from '~/shared/database/field-search'; @Injectable() export class ContractService { @@ -19,27 +20,20 @@ export class ContractService { @InjectRepository(Storage) private storageRepository: Repository ) {} + /** - * 列举所有角色:除去超级管理员 + * 查找所有合同 */ async findAll({ page, pageSize, - contractNumber, - title, - type, - status + ...fields }: ContractQueryDto): Promise> { 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) - }) + .where(fieldSearch(fields)) .andWhere('contract.isDelete = 0'); return paginate(queryBuilder, { diff --git a/src/modules/materials_inventory/materials_inventory.entity.ts b/src/modules/materials_inventory/materials_inventory.entity.ts index 039574a..c0432f4 100644 --- a/src/modules/materials_inventory/materials_inventory.entity.ts +++ b/src/modules/materials_inventory/materials_inventory.entity.ts @@ -192,7 +192,7 @@ export class MaterialsInventoryEntity extends CommonEntity { @ApiProperty({ description: '删除状态:0未删除,1已删除' }) isDelete: number; - @ManyToMany(() => Storage, storage => storage.contracts) + @ManyToMany(() => Storage, storage => storage.materialsInventories) @JoinTable({ name: 'materials_inventory_storage', joinColumn: { name: 'materials_inventory_id', referencedColumnName: 'id' }, diff --git a/src/modules/tools/storage/storage.entity.ts b/src/modules/tools/storage/storage.entity.ts index 69253a5..d84c97e 100644 --- a/src/modules/tools/storage/storage.entity.ts +++ b/src/modules/tools/storage/storage.entity.ts @@ -2,7 +2,9 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { Column, Entity, ManyToMany, Relation } from 'typeorm'; import { CommonEntity } from '~/common/entity/common.entity'; +import { CompanyEntity } from '~/modules/company/company.entity'; import { ContractEntity } from '~/modules/contract/contract.entity'; +import { MaterialsInventoryEntity } from '~/modules/materials_inventory/materials_inventory.entity'; @Entity({ name: 'tool_storage' }) export class Storage extends CommonEntity { @@ -42,4 +44,12 @@ export class Storage extends CommonEntity { @ApiHideProperty() @ManyToMany(() => ContractEntity, contract => contract.files) contracts: Relation; + + @ApiHideProperty() + @ManyToMany(() => CompanyEntity, company => company.files) + companys: Relation; + + @ApiHideProperty() + @ManyToMany(() => MaterialsInventoryEntity, materialsInventories => materialsInventories.files) + materialsInventories: Relation; } diff --git a/src/shared/database/field-search/index.ts b/src/shared/database/field-search/index.ts new file mode 100644 index 0000000..f44e3d6 --- /dev/null +++ b/src/shared/database/field-search/index.ts @@ -0,0 +1,24 @@ +import { isNumber } from 'lodash'; +import { Like, ObjectLiteral, ObjectType } from 'typeorm'; +export const fieldSearch = (entity: Partial): ObjectLiteral => { + let result = {}; + for (let key in entity) { + if (entity.hasOwnProperty(key)) { + switch (typeof entity[key]) { + case 'number': + result = { ...result, ...(isNumber(entity[key]) ? { [key]: entity[key] } : null) }; + break; + case 'string': + result = { ...result, ...(entity[key] ? { [key]: Like(`%${entity[key]}%`) } : null) }; + break; + case 'boolean': + result = { ...result, ...(entity[key] === true ? { [key]: 1 } : { [key]: 0 }) }; + break; + default: + result = { ...result, ...(entity[key] ? { [key]: entity[key] } : null) }; + break; + } + } + } + return result; +};