feat(产品): 实现产品库存组成功能
添加产品库存组成相关实体、DTO和服务方法 - 新增ProductStockComponent实体表示库存组成关系 - 添加获取、设置和自动绑定库存组成的API接口 - 实现库存组成的CRUD操作逻辑
This commit is contained in:
parent
a7d5db33f3
commit
fdf2819b3b
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from '@midwayjs/core';
|
} from '@midwayjs/core';
|
||||||
import { ProductService } from '../service/product.service';
|
import { ProductService } from '../service/product.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
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 { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
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()
|
@ApiOkResponse()
|
||||||
|
|
|
||||||
|
|
@ -92,9 +92,9 @@ export class QueryProductDTO {
|
||||||
|
|
||||||
// 属性输入项(中文注释:用于在创建/更新产品时传递字典项信息)
|
// 属性输入项(中文注释:用于在创建/更新产品时传递字典项信息)
|
||||||
export class AttributeInputDTO {
|
export class AttributeInputDTO {
|
||||||
@ApiProperty({ description: '字典名称', example: 'brand' })
|
@ApiProperty({ description: '字典名称', example: 'brand', required: false})
|
||||||
@Rule(RuleType.string().required())
|
@Rule(RuleType.string())
|
||||||
dictName: string;
|
dictName?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '字典项 ID', required: false })
|
@ApiProperty({ description: '字典项 ID', required: false })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
|
|
@ -271,3 +271,21 @@ export class BatchSetSkuDTO {
|
||||||
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
|
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
|
||||||
skus: 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,
|
Entity,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
|
OneToMany,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { DictItem } from './dict_item.entity';
|
import { DictItem } from './dict_item.entity';
|
||||||
|
import { ProductStockComponent } from './product_stock_component.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Product {
|
export class Product {
|
||||||
|
|
@ -34,6 +36,7 @@ export class Product {
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
nameCn: string;
|
nameCn: string;
|
||||||
|
|
||||||
|
|
||||||
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
@ -46,7 +49,10 @@ export class Product {
|
||||||
@ApiProperty({ description: '价格', example: 99.99 })
|
@ApiProperty({ description: '价格', example: 99.99 })
|
||||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
price: number;
|
price: number;
|
||||||
|
// 类型 主要用来区分混装和单品 单品死
|
||||||
|
@ApiProperty({ description: '类型' })
|
||||||
|
@Column()
|
||||||
|
type: string;
|
||||||
// 促销价格
|
// 促销价格
|
||||||
@ApiProperty({ description: '促销价格', example: 99.99 })
|
@ApiProperty({ description: '促销价格', example: 99.99 })
|
||||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
|
|
@ -62,6 +68,11 @@ export class Product {
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
attributes: DictItem[];
|
attributes: DictItem[];
|
||||||
|
|
||||||
|
// 中文注释:产品的库存组成,一对多关系(使用独立表)
|
||||||
|
@ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true })
|
||||||
|
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
||||||
|
components: ProductStockComponent[];
|
||||||
|
|
||||||
// 来源
|
// 来源
|
||||||
@ApiProperty({ description: '来源', example: '1' })
|
@ApiProperty({ description: '来源', example: '1' })
|
||||||
@Column({ default: 0 })
|
@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 { Context } from '@midwayjs/koa';
|
||||||
import { TemplateService } from './template.service';
|
import { TemplateService } from './template.service';
|
||||||
import { StockService } from './stock.service';
|
import { StockService } from './stock.service';
|
||||||
|
import { Stock } from '../entity/stock.entity';
|
||||||
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class ProductService {
|
export class ProductService {
|
||||||
|
|
@ -57,6 +59,12 @@ export class ProductService {
|
||||||
@InjectEntityModel(Variation)
|
@InjectEntityModel(Variation)
|
||||||
variationModel: Repository<Variation>;
|
variationModel: Repository<Variation>;
|
||||||
|
|
||||||
|
@InjectEntityModel(Stock)
|
||||||
|
stockModel: Repository<Stock>;
|
||||||
|
|
||||||
|
@InjectEntityModel(ProductStockComponent)
|
||||||
|
productStockComponentModel: Repository<ProductStockComponent>;
|
||||||
|
|
||||||
// async findProductsByName(name: string): Promise<Product[]> {
|
// async findProductsByName(name: string): Promise<Product[]> {
|
||||||
// const where: any = {};
|
// const where: any = {};
|
||||||
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
|
|
@ -343,6 +351,68 @@ export class ProductService {
|
||||||
return saved;
|
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> {
|
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
|
||||||
// 确认产品是否存在
|
// 确认产品是否存在
|
||||||
const product = await this.productModel.findOneBy({ id });
|
const product = await this.productModel.findOneBy({ id });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue