diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index c8bcaf2..96e1617 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -11,7 +11,7 @@ import { } from '@midwayjs/core'; import { ProductService } from '../service/product.service'; import { errorResponse, successResponse } from '../utils/response.util'; -import { CreateProductDTO, QueryProductDTO, UpdateProductDTO } from '../dto/product.dto'; +import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto'; import { ApiOkResponse } from '@midwayjs/swagger'; import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto'; @@ -116,6 +116,42 @@ export class ProductController { } } + // 中文注释:获取产品的库存组成 + @ApiOkResponse() + @Get('/:id/components') + async getProductComponents(@Param('id') id: number) { + try { + const data = await this.productService.getProductComponents(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + // 中文注释:设置产品的库存组成(覆盖式) + @ApiOkResponse() + @Post('/:id/components') + async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) { + try { + const data = await this.productService.setProductComponents(id, body?.items || []); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + // 中文注释:根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存) + @ApiOkResponse() + @Post('/:id/components/auto') + async autoBindComponents(@Param('id') id: number) { + try { + const data = await this.productService.autoBindComponentsBySku(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + // 通用属性接口:分页列表 @ApiOkResponse() diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index cc95206..6f0fa36 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -92,9 +92,9 @@ export class QueryProductDTO { // 属性输入项(中文注释:用于在创建/更新产品时传递字典项信息) export class AttributeInputDTO { - @ApiProperty({ description: '字典名称', example: 'brand' }) - @Rule(RuleType.string().required()) - dictName: string; + @ApiProperty({ description: '字典名称', example: 'brand', required: false}) + @Rule(RuleType.string()) + dictName?: string; @ApiProperty({ description: '字典项 ID', required: false }) @Rule(RuleType.number()) @@ -271,3 +271,21 @@ export class BatchSetSkuDTO { @ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] }) skus: SkuItemDTO[]; } + +// 中文注释:产品库存组成项输入 +export class ProductComponentItemDTO { + @ApiProperty({ description: '库存记录ID' }) + @Rule(RuleType.number().required()) + stockId: number; + + @ApiProperty({ description: '组成数量', example: 1 }) + @Rule(RuleType.number().min(1).default(1)) + quantity: number; +} + +// 中文注释:设置产品库存组成输入 +export class SetProductComponentsDTO { + @ApiProperty({ description: '组成项列表', type: [ProductComponentItemDTO] }) + @Rule(RuleType.array().items(RuleType.object())) + items: ProductComponentItemDTO[]; +} diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index a718cf5..4776bd5 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -6,9 +6,11 @@ import { Entity, ManyToMany, JoinTable, + OneToMany, } from 'typeorm'; import { ApiProperty } from '@midwayjs/swagger'; import { DictItem } from './dict_item.entity'; +import { ProductStockComponent } from './product_stock_component.entity'; @Entity() export class Product { @@ -34,6 +36,7 @@ export class Product { @Column({ default: '' }) nameCn: string; + @ApiProperty({ example: '产品描述', description: '产品描述' }) @Column({ nullable: true }) description?: string; @@ -46,7 +49,10 @@ export class Product { @ApiProperty({ description: '价格', example: 99.99 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) price: number; - + // 类型 主要用来区分混装和单品 单品死 + @ApiProperty({ description: '类型' }) + @Column() + type: string; // 促销价格 @ApiProperty({ description: '促销价格', example: 99.99 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @@ -62,6 +68,11 @@ export class Product { @JoinTable() attributes: DictItem[]; + // 中文注释:产品的库存组成,一对多关系(使用独立表) + @ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true }) + @OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true }) + components: ProductStockComponent[]; + // 来源 @ApiProperty({ description: '来源', example: '1' }) @Column({ default: 0 }) diff --git a/src/entity/product_stock_component.entity.ts b/src/entity/product_stock_component.entity.ts new file mode 100644 index 0000000..10a12df --- /dev/null +++ b/src/entity/product_stock_component.entity.ts @@ -0,0 +1,40 @@ +import { ApiProperty } from '@midwayjs/swagger'; +import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { Product } from './product.entity'; +import { Stock } from './stock.entity'; + +@Entity('product_stock_component') +export class ProductStockComponent { + @ApiProperty({ type: Number }) + @PrimaryGeneratedColumn() + id: number; + + @ApiProperty({ type: Number }) + @Column() + productId: number; + + @ApiProperty({ type: Number }) + @Column() + stockId: number; + + @ApiProperty({ type: Number, description: '组成数量' }) + @Column({ type: 'int', default: 1 }) + quantity: number; + + // 中文注释:多对一,组件隶属于一个产品 + @ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' }) + product: Product; + + // 中文注释:多对一,组件引用一个库存记录 + @ManyToOne(() => Stock, { eager: true, onDelete: 'CASCADE' }) + stock: Stock; + + @ApiProperty({ description: '创建时间' }) + @CreateDateColumn() + createdAt: Date; + + @ApiProperty({ description: '更新时间' }) + @UpdateDateColumn() + updatedAt: Date; +} + diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 71bad04..f357188 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -30,6 +30,8 @@ import { DictItem } from '../entity/dict_item.entity'; import { Context } from '@midwayjs/koa'; import { TemplateService } from './template.service'; import { StockService } from './stock.service'; +import { Stock } from '../entity/stock.entity'; +import { ProductStockComponent } from '../entity/product_stock_component.entity'; @Provide() export class ProductService { @@ -57,6 +59,12 @@ export class ProductService { @InjectEntityModel(Variation) variationModel: Repository; + @InjectEntityModel(Stock) + stockModel: Repository; + + @InjectEntityModel(ProductStockComponent) + productStockComponentModel: Repository; + // async findProductsByName(name: string): Promise { // const where: any = {}; // const nameFilter = name ? name.split(' ').filter(Boolean) : []; @@ -342,6 +350,68 @@ export class ProductService { const saved = await this.productModel.save(product); return saved; } + + // 中文注释:获取产品的库存组成列表(表关联版本) + async getProductComponents(productId: number): Promise { + // 条件判断:确保产品存在 + const product = await this.productModel.findOne({ where: { id: productId } }); + if (!product) throw new Error(`产品 ID ${productId} 不存在`); + return await this.productStockComponentModel.find({ where: { productId } }); + } + + // 中文注释:设置产品的库存组成(覆盖式,表关联版本) + async setProductComponents( + productId: number, + items: { stockId: number; quantity: number }[] + ): Promise { + // 条件判断:确保产品存在 + const product = await this.productModel.findOne({ where: { id: productId } }); + if (!product) throw new Error(`产品 ID ${productId} 不存在`); + + const validItems = (items || []) + .filter(i => i && i.stockId && i.quantity && i.quantity > 0) + .map(i => ({ stockId: Number(i.stockId), quantity: Number(i.quantity) })); + + // 删除旧的组成 + await this.productStockComponentModel.delete({ productId }); + + // 插入新的组成 + const created: ProductStockComponent[] = []; + for (const i of validItems) { + const stock = await this.stockModel.findOne({ where: { id: i.stockId } }); + if (!stock) throw new Error(`库存 ID ${i.stockId} 不存在`); + const comp = new ProductStockComponent(); + comp.productId = productId; + comp.stockId = i.stockId; + comp.quantity = i.quantity; + comp.stock = stock; + created.push(await this.productStockComponentModel.save(comp)); + } + return created; + } + + // 中文注释:根据 SKU 自动绑定产品的库存组成(匹配所有相同 SKU 的库存,默认数量 1) + async autoBindComponentsBySku(productId: number): Promise { + // 条件判断:确保产品存在 + const product = await this.productModel.findOne({ where: { id: productId } }); + if (!product) throw new Error(`产品 ID ${productId} 不存在`); + + const stocks = await this.stockModel.find({ where: { productSku: product.sku } }); + if (stocks.length === 0) return []; + + for (const stock of stocks) { + // 条件判断:若已存在相同 stockId 的组成则跳过 + const exist = await this.productStockComponentModel.findOne({ where: { productId, stockId: stock.id } }); + if (exist) continue; + const comp = new ProductStockComponent(); + comp.productId = productId; + comp.stockId = stock.id; + comp.quantity = 1; // 默认数量 1 + comp.stock = stock; + await this.productStockComponentModel.save(comp); + } + return await this.getProductComponents(productId); + } async updateProductNameCn(id: number, nameCn: string): Promise { // 确认产品是否存在