feat(产品): 实现产品库存组成功能
添加产品库存组成相关实体、DTO和服务方法 - 新增ProductStockComponent实体表示库存组成关系 - 添加获取、设置和自动绑定库存组成的API接口 - 实现库存组成的CRUD操作逻辑
This commit is contained in:
parent
a7d5db33f3
commit
fdf2819b3b
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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) : [];
|
||||
|
|
@ -342,6 +350,68 @@ export class ProductService {
|
|||
const saved = await this.productModel.save(product);
|
||||
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> {
|
||||
// 确认产品是否存在
|
||||
|
|
|
|||
Loading…
Reference in New Issue