feat(产品): 实现产品库存组成功能

添加产品库存组成相关实体、DTO和服务方法
- 新增ProductStockComponent实体表示库存组成关系
- 添加获取、设置和自动绑定库存组成的API接口
- 实现库存组成的CRUD操作逻辑
This commit is contained in:
tikkhun 2025-11-28 18:40:11 +08:00
parent a7d5db33f3
commit fdf2819b3b
5 changed files with 180 additions and 5 deletions

View File

@ -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()

View File

@ -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[];
}

View File

@ -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 })

View File

@ -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;
}

View File

@ -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<Variation>;
@InjectEntityModel(Stock)
stockModel: Repository<Stock>;
@InjectEntityModel(ProductStockComponent)
productStockComponentModel: Repository<ProductStockComponent>;
// async findProductsByName(name: string): Promise<Product[]> {
// const where: any = {};
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
@ -343,6 +351,68 @@ export class ProductService {
return saved;
}
// 中文注释:获取产品的库存组成列表(表关联版本)
async getProductComponents(productId: number): Promise<ProductStockComponent[]> {
// 条件判断:确保产品存在
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<ProductStockComponent[]> {
// 条件判断:确保产品存在
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<ProductStockComponent[]> {
// 条件判断:确保产品存在
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<Product> {
// 确认产品是否存在
const product = await this.productModel.findOneBy({ id });