diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 8a1ffc2..e16fa81 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -37,6 +37,7 @@ import { DictItem } from '../entity/dict_item.entity'; import { Template } from '../entity/template.entity'; import { Area } from '../entity/area.entity'; import { ProductStockComponent } from '../entity/product_stock_component.entity'; +import { ProductSiteSku } from '../entity/product_site_sku.entity'; import { CategoryAttribute } from '../entity/category_attribute.entity'; import { Category } from '../entity/category.entity'; import DictSeeder from '../db/seeds/dict.seeder'; @@ -51,6 +52,7 @@ export default { entities: [ Product, ProductStockComponent, + ProductSiteSku, WpProduct, Variation, User, diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index d7514c9..167641f 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -46,6 +46,10 @@ export class CreateProductDTO { @Rule(RuleType.string()) description: string; + @ApiProperty({ example: '产品简短描述', description: '产品简短描述' }) + @Rule(RuleType.string().optional()) + shortDescription?: string; + @ApiProperty({ description: '产品 SKU', required: false }) @Rule(RuleType.string()) sku?: string; @@ -54,6 +58,10 @@ export class CreateProductDTO { @Rule(RuleType.number()) categoryId?: number; + @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) + @Rule(RuleType.array().items(RuleType.string()).optional()) + siteSkus?: string[]; + // 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等) @ApiProperty({ description: '属性列表', type: 'array' }) @Rule(RuleType.array().required()) @@ -110,6 +118,10 @@ export class UpdateProductDTO { @Rule(RuleType.string()) description?: string; + @ApiProperty({ example: '产品简短描述', description: '产品简短描述' }) + @Rule(RuleType.string().optional()) + shortDescription?: string; + @ApiProperty({ description: '产品 SKU', required: false }) @Rule(RuleType.string()) sku?: string; @@ -118,6 +130,10 @@ export class UpdateProductDTO { @Rule(RuleType.number()) categoryId?: number; + @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) + @Rule(RuleType.array().items(RuleType.string()).optional()) + siteSkus?: string[]; + // 商品价格 @ApiProperty({ description: '价格', example: 99.99, required: false }) @Rule(RuleType.number()) @@ -179,6 +195,10 @@ export class BatchUpdateProductDTO { @Rule(RuleType.string().optional()) description?: string; + @ApiProperty({ example: '产品简短描述', description: '产品简短描述', required: false }) + @Rule(RuleType.string().optional()) + shortDescription?: string; + @ApiProperty({ description: '产品 SKU', required: false }) @Rule(RuleType.string().optional()) sku?: string; @@ -187,6 +207,10 @@ export class BatchUpdateProductDTO { @Rule(RuleType.number().optional()) categoryId?: number; + @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) + @Rule(RuleType.array().items(RuleType.string()).optional()) + siteSkus?: string[]; + @ApiProperty({ description: '价格', example: 99.99, required: false }) @Rule(RuleType.number().optional()) price?: number; diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index f3702ca..9cb7e79 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -13,6 +13,7 @@ import { import { ApiProperty } from '@midwayjs/swagger'; import { DictItem } from './dict_item.entity'; import { ProductStockComponent } from './product_stock_component.entity'; +import { ProductSiteSku } from './product_site_sku.entity'; import { Category } from './category.entity'; @Entity() @@ -49,6 +50,10 @@ export class Product { @Column({ nullable: true }) description?: string; + @ApiProperty({ example: '产品简短描述', description: '产品简短描述' }) + @Column({ nullable: true }) + shortDescription?: string; + @ApiProperty({ description: 'sku'}) @Column({ unique: true }) sku: string; @@ -82,6 +87,10 @@ export class Product { @OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true }) components: ProductStockComponent[]; + @ApiProperty({ description: '站点 SKU 列表', type: ProductSiteSku, isArray: true }) + @OneToMany(() => ProductSiteSku, (siteSku) => siteSku.product, { cascade: true }) + siteSkus: ProductSiteSku[]; + // 来源 @ApiProperty({ description: '来源', example: '1' }) @Column({ default: 0 }) diff --git a/src/entity/product_site_sku.entity.ts b/src/entity/product_site_sku.entity.ts new file mode 100644 index 0000000..f454d60 --- /dev/null +++ b/src/entity/product_site_sku.entity.ts @@ -0,0 +1,36 @@ +import { + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Entity, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { ApiProperty } from '@midwayjs/swagger'; +import { Product } from './product.entity'; + +@Entity('product_site_sku') +export class ProductSiteSku { + @PrimaryGeneratedColumn() + id: number; + + @ApiProperty({ description: '站点 SKU' }) + @Column({ length: 100, comment: '站点 SKU' }) + code: string; + + @ManyToOne(() => Product, product => product.siteSkus, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'productId' }) + product: Product; + + @Column() + productId: number; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 36741d3..d1733ee 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -29,6 +29,7 @@ import { StockService } from './stock.service'; import { Stock } from '../entity/stock.entity'; import { StockPoint } from '../entity/stock_point.entity'; import { ProductStockComponent } from '../entity/product_stock_component.entity'; +import { ProductSiteSku } from '../entity/product_site_sku.entity'; import { Category } from '../entity/category.entity'; import { CategoryAttribute } from '../entity/category_attribute.entity'; @@ -67,6 +68,9 @@ export class ProductService { @InjectEntityModel(ProductStockComponent) productStockComponentModel: Repository; + @InjectEntityModel(ProductSiteSku) + productSiteSkuModel: Repository; + @InjectEntityModel(Category) categoryModel: Repository; @@ -242,7 +246,8 @@ export class ProductService { .createQueryBuilder('product') .leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('attribute.dict', 'dict') - .leftJoinAndSelect('product.category', 'category'); + .leftJoinAndSelect('product.category', 'category') + .leftJoinAndSelect('product.siteSkus', 'siteSku'); // 模糊搜索 name,支持多个关键词 const nameFilter = name ? name.split(' ').filter(Boolean) : []; @@ -421,7 +426,7 @@ export class ProductService { const product = new Product(); // 使用 merge 填充基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = createProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = createProductDTO; this.productModel.merge(product, simpleFields); product.attributes = resolvedAttributes; @@ -449,11 +454,22 @@ export class ProductService { const savedProduct = await this.productModel.save(product); + // 保存站点 SKU 列表 + if (createProductDTO.siteSkus && createProductDTO.siteSkus.length > 0) { + const siteSkus = createProductDTO.siteSkus.map(code => { + const s = new ProductSiteSku(); + s.code = code; + s.product = savedProduct; + return s; + }); + await this.productSiteSkuModel.save(siteSkus); + } + // 保存组件信息 if (createProductDTO.components && createProductDTO.components.length > 0) { await this.setProductComponents(savedProduct.id, createProductDTO.components); // 重新加载带组件的产品 - return await this.productModel.findOne({ where: { id: savedProduct.id }, relations: ['attributes', 'attributes.dict', 'category', 'components'] }); + return await this.productModel.findOne({ where: { id: savedProduct.id }, relations: ['attributes', 'attributes.dict', 'category', 'components', 'siteSkus'] }); } return savedProduct; @@ -470,7 +486,7 @@ export class ProductService { } // 使用 merge 更新基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = updateProductDTO; this.productModel.merge(product, simpleFields); // 处理分类更新 @@ -485,6 +501,23 @@ export class ProductService { } } + // 处理站点 SKU 更新 + if (updateProductDTO.siteSkus !== undefined) { + // 删除旧的 siteSkus + await this.productSiteSkuModel.delete({ productId: id }); + + // 如果有新的 siteSkus,则保存 + if (updateProductDTO.siteSkus.length > 0) { + const siteSkus = updateProductDTO.siteSkus.map(code => { + const s = new ProductSiteSku(); + s.code = code; + s.productId = id; + return s; + }); + await this.productSiteSkuModel.save(siteSkus); + } + } + // 处理 SKU 更新 if (updateProductDTO.sku !== undefined) { // 校验 SKU 唯一性(如变更) @@ -587,6 +620,7 @@ export class ProductService { if (updateData.name !== undefined) simpleUpdate.name = updateData.name; if (updateData.nameCn !== undefined) simpleUpdate.nameCn = updateData.nameCn; if (updateData.description !== undefined) simpleUpdate.description = updateData.description; + if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription; if (updateData.price !== undefined) simpleUpdate.price = updateData.price; if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice; @@ -672,7 +706,9 @@ export class ProductService { if (!product) throw new Error(`产品 ID ${productId} 不存在`); // 条件判断(单品 simple 不允许手动设置组成) if (product.type === 'single') { - throw new Error('单品无需设置组成'); + // 单品类型,直接清空关联的组成(如果有) + await this.productStockComponentModel.delete({ productId }); + return []; } const validItems = (items || []) @@ -1285,6 +1321,7 @@ export class ProductService { price: num(rec.price), promotionPrice: num(rec.promotionPrice), type: val(rec.type), + siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined, attributes: attributes.length > 0 ? attributes : undefined, components: components.length > 0 ? components : undefined, @@ -1299,6 +1336,7 @@ export class ProductService { dto.nameCn = data.nameCn; dto.description = data.description; dto.sku = data.sku; + if (data.siteSkus) dto.siteSkus = data.siteSkus; // 数值类型转换 if (data.price !== undefined) dto.price = Number(data.price); @@ -1325,6 +1363,7 @@ export class ProductService { if (data.nameCn !== undefined) dto.nameCn = data.nameCn; if (data.description !== undefined) dto.description = data.description; if (data.sku !== undefined) dto.sku = data.sku; + if (data.siteSkus !== undefined) dto.siteSkus = data.siteSkus; if (data.price !== undefined) dto.price = Number(data.price); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); @@ -1363,6 +1402,7 @@ export class ProductService { // 基础数据 const rowData = [ esc(p.sku), + esc(p.siteSkus ? p.siteSkus.map(s => s.code).join(',') : ''), esc(p.name), esc(p.nameCn), esc(p.price), @@ -1397,7 +1437,7 @@ export class ProductService { async exportProductsCSV(): Promise { // 查询所有产品及其属性(包含字典关系)和组成 const products = await this.productModel.find({ - relations: ['attributes', 'attributes.dict', 'components'], + relations: ['attributes', 'attributes.dict', 'components', 'siteSkus'], order: { id: 'ASC' }, }); @@ -1426,6 +1466,7 @@ export class ProductService { // 定义 CSV 表头(与导入字段一致) const baseHeaders = [ 'sku', + 'siteSkus', 'name', 'nameCn', 'price',