From 47b7015c16a4c5a836bfaae0b8961f88277a6b5b Mon Sep 17 00:00:00 2001 From: louis <869322496@qq.com> Date: Mon, 4 Mar 2024 17:31:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8E=9F=E6=9D=90=E6=96=99=E5=87=BA?= =?UTF-8?q?=E5=85=A5=E5=BA=93=E8=AE=B0=E5=BD=95=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 5 +- src/constants/enum/index.ts | 6 + .../in_out/materials_in_out.controller.ts | 70 +++++++++ .../in_out/materials_in_out.dto.ts | 31 ++++ .../in_out/materials_in_out.entity.ts | 105 ++++++++++++++ .../in_out/materials_in_out.service.ts | 134 ++++++++++++++++++ .../materials_inventory.controller.ts | 10 -- .../materials_inventory.dto.ts | 12 +- .../materials_inventory.entity.ts | 8 -- .../materials_inventory.service.ts | 71 +--------- src/modules/product/product.controller.ts | 79 +++++++++++ src/modules/product/product.dto.ts | 39 +++++ src/modules/product/product.entity.ts | 29 ++++ src/modules/product/product.module.ts | 14 ++ src/modules/product/product.service.ts | 127 +++++++++++++++++ src/modules/tools/storage/storage.entity.ts | 10 +- 16 files changed, 652 insertions(+), 98 deletions(-) create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.controller.ts create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.dto.ts create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.entity.ts create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.service.ts create mode 100644 src/modules/product/product.controller.ts create mode 100644 src/modules/product/product.dto.ts create mode 100644 src/modules/product/product.entity.ts create mode 100644 src/modules/product/product.module.ts create mode 100644 src/modules/product/product.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 27710c1..93b7423 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -30,6 +30,7 @@ import { VehicleUsageController } from './modules/vehicle-usage/vehicle-usage.co 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'; +import { ProductModule } from './modules/product/product.module'; @Module({ imports: [ @@ -64,7 +65,9 @@ import { CompanyModule } from './modules/company/company.module'; MaterialsInventoryModule, - CompanyModule + CompanyModule, + + ProductModule ], providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter }, diff --git a/src/constants/enum/index.ts b/src/constants/enum/index.ts index 75f926d..0fd9687 100644 --- a/src/constants/enum/index.ts +++ b/src/constants/enum/index.ts @@ -12,3 +12,9 @@ export enum BusinessModuleEnum { MATERIALS_INVENTORY = 2, COMPANY = 3 } + +// 原材料出库或者入库 +export enum MaterialsInOrOutEnum { + In, + Out +} diff --git a/src/modules/materials_inventory/in_out/materials_in_out.controller.ts b/src/modules/materials_inventory/in_out/materials_in_out.controller.ts new file mode 100644 index 0000000..b7144e0 --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.controller.ts @@ -0,0 +1,70 @@ +import { Body, Controller, Delete, Get, Post, Put, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { MaterialsInOutService } from './materials_in_out.service'; +import { MaterialsInOutEntity } from './materials_in_out.entity'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { definePermission, Perm } from '~/modules/auth/decorators/permission.decorator'; +import { MaterialsInOutQueryDto, MaterialsInOutDto, MaterialsInOutUpdateDto } from './materials_in_out.dto'; + +export const permissions = definePermission('materials_inventory:history_in_out', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Materials In Out History - 原材料出入库管理') +@ApiSecurityAuth() +@Controller('materials-in-out') +export class MaterialsInOutController { + constructor(private miService: MaterialsInOutService) {} + @Get() + @ApiOperation({ summary: '获取原材料盘点列表' }) + @ApiResult({ type: [MaterialsInOutEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: MaterialsInOutQueryDto) { + return this.miService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取原材料盘点信息' }) + @ApiResult({ type: MaterialsInOutDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.miService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增原材料盘点' }) + @Perm(permissions.CREATE) + async create(@Body() dto: MaterialsInOutDto): Promise { + await this.miService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新原材料盘点' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: MaterialsInOutUpdateDto): Promise { + await this.miService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除原材料盘点' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.miService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: MaterialsInOutUpdateDto + ): Promise { + await this.miService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/materials_inventory/in_out/materials_in_out.dto.ts b/src/modules/materials_inventory/in_out/materials_in_out.dto.ts new file mode 100644 index 0000000..9971663 --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.dto.ts @@ -0,0 +1,31 @@ +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 '~/modules/tools/storage/storage.entity'; + +export class MaterialsInOutDto { + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class MaterialsInOutUpdateDto extends PartialType(MaterialsInOutDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} +export class MaterialsInOutQueryDto extends IntersectionType( + PagerDto, + PartialType(MaterialsInOutDto) +) {} diff --git a/src/modules/materials_inventory/in_out/materials_in_out.entity.ts b/src/modules/materials_inventory/in_out/materials_in_out.entity.ts new file mode 100644 index 0000000..2cd9370 --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.entity.ts @@ -0,0 +1,105 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { MaterialsInOrOutEnum } from '~/constants/enum'; +import { Storage } from '~/modules/tools/storage/storage.entity'; + +@Entity({ name: 'materials_in_out' }) +export class MaterialsInOutEntity extends CommonEntity { + @Column({ + name: 'product', + type: 'int', + comment: '产品名称' + }) + @ApiProperty({ description: '产品名称' }) + product: number; + + @Column({ + name: 'unit', + type: 'int', + comment: '单位(字典)' + }) + @ApiProperty({ description: '单位(字典)' }) + unit: number; + + @Column({ + name: 'inOrOut', + type: 'tinyint', + comment: '入库或出库' + }) + @ApiProperty({ description: '入库或出库 0:入库 1:出库' }) + inOrOut: MaterialsInOrOutEnum; + + @Column({ + name: 'inventory_time', + type: 'date', + nullable: true, + comment: '时间' + }) + @ApiProperty({ description: '时间' }) + time: Date; + + @Column({ + name: 'quantity', + type: 'int', + default: 0, + comment: '数量' + }) + @ApiProperty({ description: '数量' }) + quantity: number; + + @Column({ + name: 'unit_price', + type: 'decimal', + precision: 10, + default: 0, + scale: 2, + comment: '单价' + }) + @ApiProperty({ description: '入库单价' }) + unitPrice: number; + + @Column({ + name: 'amount', + type: 'decimal', + precision: 10, + default: 0, + scale: 2, + comment: '金额' + }) + @ApiProperty({ description: '金额' }) + amount: number; + + @Column({ name: 'agent', type: 'varchar', length: 50, comment: '经办人', nullable: true }) + @ApiProperty({ description: '经办人' }) + agent: string; + + @Column({ + name: 'issuance_number', + type: 'varchar', + length: 100, + comment: '领料单号' + }) + @ApiProperty({ description: '领料单号' }) + issuanceNumber: string; + + @Column({ name: 'remark', type: 'varchar', length: 255, comment: '备注', nullable: true }) + @ApiProperty({ description: '备注' }) + remark: string; + + @Column({ name: 'project', type: 'varchar', length: 255, comment: '项目', nullable: false }) + @ApiProperty({ description: '项目' }) + project: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @ManyToMany(() => Storage, storage => storage.materialsInOut) + @JoinTable({ + name: 'materials_in_out_storage', + joinColumn: { name: 'materials_in_out_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/materials_inventory/in_out/materials_in_out.service.ts b/src/modules/materials_inventory/in_out/materials_in_out.service.ts new file mode 100644 index 0000000..dc72e58 --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.service.ts @@ -0,0 +1,134 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; + +import { EntityManager, Repository } from 'typeorm'; +import { Pagination } from '~/helper/paginate/pagination'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { MaterialsInOutQueryDto, MaterialsInOutDto, MaterialsInOutUpdateDto } from './materials_in_out.dto'; +import { MaterialsInOutEntity } from './materials_in_out.entity'; + +@Injectable() +export class MaterialsInOutService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(MaterialsInOutEntity) + private materialsInventoryRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + /** + * 查询所有出入库记录 + */ + async findAll({ + page, + pageSize + // materialsInventoryNumber, + // title, + // type, + // status + }: MaterialsInOutQueryDto): Promise> { + const queryBuilder = this.materialsInventoryRepository + .createQueryBuilder('materialsInventory') + .leftJoin('materialsInventory.files', 'files') + .addSelect(['files.id', 'files.path']) + // .where({ + // ...(materialsInventoryNumber ? { materialsInventoryNumber: Like(`%${materialsInventoryNumber}%`) } : null), + // ...(title ? { title: Like(`%${title}%`) } : null), + // ...(isNumber(type) ? { type } : null), + // ...(isNumber(status) ? { status } : null) + // }) + .andWhere('materialsInventory.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: MaterialsInOutDto): Promise { + await this.materialsInventoryRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(MaterialsInOutEntity, id, { + ...data + }); + const materialsInventory = await this.materialsInventoryRepository + .createQueryBuilder('materialsInventory') + .leftJoinAndSelect('materialsInventory.files', 'files') + .where('materialsInventory.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(MaterialsInOutEntity, 'files') + .of(id) + .addAndRemove(fileIds, materialsInventory.files); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 合同比较重要,做逻辑删除 + await this.materialsInventoryRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个合同信息 + */ + async info(id: number) { + const info = await this.materialsInventoryRepository + .createQueryBuilder('materialsInventory') + .where({ + id + }) + .andWhere('materialsInventory.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 合同ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const materialsInventory = await this.materialsInventoryRepository + .createQueryBuilder('materialsInventory') + .leftJoinAndSelect('materialsInventory.files', 'files') + .where('materialsInventory.id = :id', { id }) + .getOne(); + const linkedFiles = materialsInventory.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(MaterialsInOutEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, materialsInventory.files); + }); + } +} diff --git a/src/modules/materials_inventory/materials_inventory.controller.ts b/src/modules/materials_inventory/materials_inventory.controller.ts index f9c0cdc..18a1f27 100644 --- a/src/modules/materials_inventory/materials_inventory.controller.ts +++ b/src/modules/materials_inventory/materials_inventory.controller.ts @@ -57,14 +57,4 @@ export class MaterialsInventoryController { async delete(@IdParam() id: number): Promise { await this.miService.delete(id); } - - @Put('unlink-attachments/:id') - @ApiOperation({ summary: '附件解除关联' }) - @Perm(permissions.UPDATE) - async unlinkAttachments( - @IdParam() id: number, - @Body() { fileIds }: ContractUpdateDto - ): Promise { - await this.miService.unlinkAttachments(id, fileIds); - } } diff --git a/src/modules/materials_inventory/materials_inventory.dto.ts b/src/modules/materials_inventory/materials_inventory.dto.ts index 85c839f..bee1740 100644 --- a/src/modules/materials_inventory/materials_inventory.dto.ts +++ b/src/modules/materials_inventory/materials_inventory.dto.ts @@ -14,17 +14,9 @@ import { import { PagerDto } from '~/common/dto/pager.dto'; import { Storage } from '../tools/storage/storage.entity'; -export class MaterialsInventoryDto { - @ApiProperty({ description: '附件' }) - files: Storage[]; -} +export class MaterialsInventoryDto {} -export class MaterialsInventoryUpdateDto extends PartialType(MaterialsInventoryDto) { - @ApiProperty({ description: '附件' }) - @IsOptional() - @IsArray() - fileIds: number[]; -} +export class MaterialsInventoryUpdateDto extends PartialType(MaterialsInventoryDto) {} export class MaterialsInventoryQueryDto extends IntersectionType( PagerDto, PartialType(MaterialsInventoryDto) diff --git a/src/modules/materials_inventory/materials_inventory.entity.ts b/src/modules/materials_inventory/materials_inventory.entity.ts index c0432f4..e80b830 100644 --- a/src/modules/materials_inventory/materials_inventory.entity.ts +++ b/src/modules/materials_inventory/materials_inventory.entity.ts @@ -191,12 +191,4 @@ export class MaterialsInventoryEntity extends CommonEntity { @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) @ApiProperty({ description: '删除状态:0未删除,1已删除' }) isDelete: number; - - @ManyToMany(() => Storage, storage => storage.materialsInventories) - @JoinTable({ - name: 'materials_inventory_storage', - joinColumn: { name: 'materials_inventory_id', referencedColumnName: 'id' }, - inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } - }) - files: Relation; } diff --git a/src/modules/materials_inventory/materials_inventory.service.ts b/src/modules/materials_inventory/materials_inventory.service.ts index 00a7b7e..927337b 100644 --- a/src/modules/materials_inventory/materials_inventory.service.ts +++ b/src/modules/materials_inventory/materials_inventory.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { MaterialsInventoryEntity } from './materials_inventory.entity'; -import { Storage } from '../tools/storage/storage.entity'; import { EntityManager, Repository } from 'typeorm'; import { MaterialsInventoryDto, @@ -9,8 +8,7 @@ import { MaterialsInventoryUpdateDto } from './materials_inventory.dto'; import { Pagination } from '~/helper/paginate/pagination'; -import { BusinessException } from '~/common/exceptions/biz.exception'; -import { ErrorEnum } from '~/constants/error-code.constant'; + import { paginate } from '~/helper/paginate'; @Injectable() @@ -18,31 +16,17 @@ export class MaterialsInventoryService { constructor( @InjectEntityManager() private entityManager: EntityManager, @InjectRepository(MaterialsInventoryEntity) - private materialsInventoryRepository: Repository, - @InjectRepository(Storage) - private storageRepository: Repository + private materialsInventoryRepository: Repository ) {} /** - * 列举所有角色:除去超级管理员 + * 查询所有盘点信息 */ async findAll({ page, pageSize - // materialsInventoryNumber, - // title, - // type, - // status }: MaterialsInventoryQueryDto): Promise> { const queryBuilder = this.materialsInventoryRepository .createQueryBuilder('materialsInventory') - .leftJoin('materialsInventory.files', 'files') - .addSelect(['files.id', 'files.path']) - // .where({ - // ...(materialsInventoryNumber ? { materialsInventoryNumber: Like(`%${materialsInventoryNumber}%`) } : null), - // ...(title ? { title: Like(`%${title}%`) } : null), - // ...(isNumber(type) ? { type } : null), - // ...(isNumber(status) ? { status } : null) - // }) .andWhere('materialsInventory.isDelete = 0'); return paginate(queryBuilder, { @@ -61,34 +45,11 @@ export class MaterialsInventoryService { /** * 更新 */ - async update( - id: number, - { fileIds, ...data }: Partial - ): Promise { + async update(id: number, data: Partial): Promise { await this.entityManager.transaction(async manager => { await manager.update(MaterialsInventoryEntity, id, { ...data }); - const materialsInventory = await this.materialsInventoryRepository - .createQueryBuilder('materialsInventory') - .leftJoinAndSelect('materialsInventory.files', 'files') - .where('materialsInventory.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(MaterialsInventoryEntity, 'files') - .of(id) - .addAndRemove(fileIds, materialsInventory.files); - } }); } @@ -113,28 +74,4 @@ export class MaterialsInventoryService { .getOne(); return info; } - - /** - * 解除附件关联 - * @param id 合同ID - * @param fileIds 附件ID - */ - async unlinkAttachments(id: number, fileIds: number[]) { - await this.entityManager.transaction(async manager => { - const materialsInventory = await this.materialsInventoryRepository - .createQueryBuilder('materialsInventory') - .leftJoinAndSelect('materialsInventory.files', 'files') - .where('materialsInventory.id = :id', { id }) - .getOne(); - const linkedFiles = materialsInventory.files - .map(item => item.id) - .filter(item => !fileIds.includes(item)); - // 附件要批量更新 - await manager - .createQueryBuilder() - .relation(MaterialsInventoryEntity, 'files') - .of(id) - .addAndRemove(linkedFiles, materialsInventory.files); - }); - } } diff --git a/src/modules/product/product.controller.ts b/src/modules/product/product.controller.ts new file mode 100644 index 0000000..f1c28c2 --- /dev/null +++ b/src/modules/product/product.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 { ProductService } from './product.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ProductEntity } from './product.entity'; +import { ProductDto, ProductQueryDto, ProductUpdateDto } from './product.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +export const permissions = definePermission('app:product', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Product - 产品') +@ApiSecurityAuth() +@Controller('product') +export class ProductController { + constructor(private productService: ProductService) {} + + @Get() + @ApiOperation({ summary: '获取产品列表' }) + @ApiResult({ type: [ProductEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: ProductQueryDto) { + return this.productService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取产品信息' }) + @ApiResult({ type: ProductDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.productService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增产品' }) + @Perm(permissions.CREATE) + async create(@Body() dto: ProductDto): Promise { + await this.productService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新产品' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: ProductUpdateDto): Promise { + await this.productService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除产品' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.productService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: ProductUpdateDto + ): Promise { + await this.productService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/product/product.dto.ts b/src/modules/product/product.dto.ts new file mode 100644 index 0000000..60414bd --- /dev/null +++ b/src/modules/product/product.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 { ProductEntity } from './product.entity'; + +export class ProductDto { + @ApiProperty({ description: '产品名称' }) + @IsUnique(ProductEntity, { message: '已存在同名产品' }) + @IsString() + name: string; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class ProductUpdateDto extends PartialType(ProductDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ProductQueryDto extends IntersectionType( + PagerDto, + PartialType(ProductDto) +) {} diff --git a/src/modules/product/product.entity.ts b/src/modules/product/product.entity.ts new file mode 100644 index 0000000..c68bace --- /dev/null +++ b/src/modules/product/product.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: 'product' }) +export class ProductEntity 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.products) + @JoinTable({ + name: 'product_storage', + joinColumn: { name: 'product_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/product/product.module.ts b/src/modules/product/product.module.ts new file mode 100644 index 0000000..75ea48f --- /dev/null +++ b/src/modules/product/product.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ProductController } from './product.controller'; +import { ProductService } from './product.service'; +import { ProductEntity } from './product.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '../tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([ProductEntity]), StorageModule, DatabaseModule], + controllers: [ProductController], + providers: [ProductService] +}) +export class ProductModule {} diff --git a/src/modules/product/product.service.ts b/src/modules/product/product.service.ts new file mode 100644 index 0000000..df0589f --- /dev/null +++ b/src/modules/product/product.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { ProductEntity } from './product.entity'; +import { EntityManager, Like, Repository } from 'typeorm'; +import { ProductDto, ProductQueryDto, ProductUpdateDto } from './product.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 ProductService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(ProductEntity) + private productRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 查询所有产品 + */ + async findAll({ + page, + pageSize, + ...fields + }: ProductQueryDto): Promise> { + const queryBuilder = this.productRepository + .createQueryBuilder('product') + .leftJoin('product.files', 'files') + .addSelect(['files.id', 'files.path']) + .where(fieldSearch(fields)) + .andWhere('product.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: ProductDto): Promise { + await this.productRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(ProductEntity, id, { + ...data + }); + const product = await this.productRepository + .createQueryBuilder('product') + .leftJoinAndSelect('product.files', 'files') + .where('product.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(ProductEntity, 'files') + .of(id) + .addAndRemove(fileIds, product.files); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 合同比较重要,做逻辑删除 + await this.productRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个合同信息 + */ + async info(id: number) { + const info = await this.productRepository + .createQueryBuilder('product') + .where({ + id + }) + .andWhere('product.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 合同ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const product = await this.productRepository + .createQueryBuilder('product') + .leftJoinAndSelect('product.files', 'files') + .where('product.id = :id', { id }) + .getOne(); + const linkedFiles = product.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ProductEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, product.files); + }); + } +} diff --git a/src/modules/tools/storage/storage.entity.ts b/src/modules/tools/storage/storage.entity.ts index d84c97e..c1a59b7 100644 --- a/src/modules/tools/storage/storage.entity.ts +++ b/src/modules/tools/storage/storage.entity.ts @@ -4,7 +4,9 @@ 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 { MaterialsInOutEntity } from '~/modules/materials_inventory/in_out/materials_in_out.entity'; import { MaterialsInventoryEntity } from '~/modules/materials_inventory/materials_inventory.entity'; +import { ProductEntity } from '~/modules/product/product.entity'; @Entity({ name: 'tool_storage' }) export class Storage extends CommonEntity { @@ -50,6 +52,10 @@ export class Storage extends CommonEntity { companys: Relation; @ApiHideProperty() - @ManyToMany(() => MaterialsInventoryEntity, materialsInventories => materialsInventories.files) - materialsInventories: Relation; + @ManyToMany(() => MaterialsInOutEntity, materialsInOut => materialsInOut.files) + materialsInOut: Relation; + + @ApiHideProperty() + @ManyToMany(() => ProductEntity, product => product.files) + products: Relation; }