From d39341d683bc2d68be900d843412280800e5dbd6 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Fri, 16 Jan 2026 15:02:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BA=A7=E5=93=81=E6=9C=8D=E5=8A=A1):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=86=E7=B1=BB=E5=90=8D=E7=A7=B0=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BA=A7=E5=93=81=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加分类名称字段支持,允许通过分类名称创建和更新产品 移除重复的查询条件逻辑,简化产品服务查询方法 重构产品导入功能,改进CSV记录到产品对象的映射 --- src/dto/product.dto.ts | 8 + src/entity/product.entity.ts | 4 + src/service/product.service.ts | 238 +++++++++++----------------- src/service/site-product.service.ts | 0 4 files changed, 103 insertions(+), 147 deletions(-) create mode 100644 src/service/site-product.service.ts diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index 6ef0be8..d1c52e7 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -59,6 +59,10 @@ export class CreateProductDTO { @Rule(RuleType.number()) categoryId?: number; + @ApiProperty({ description: '分类名称', required: false }) + @Rule(RuleType.string().optional()) + categoryName?: string; + @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) @Rule(RuleType.array().items(RuleType.string()).optional()) siteSkus?: string[]; @@ -142,6 +146,10 @@ export class UpdateProductDTO { @Rule(RuleType.number()) categoryId?: number; + @ApiProperty({ description: '分类名称', required: false }) + @Rule(RuleType.string().optional()) + categoryName?: string; + @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) @Rule(RuleType.array().items(RuleType.string()).optional()) siteSkus?: string[]; diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index eec0e20..11cf00d 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -73,6 +73,10 @@ export class Product { @JoinColumn({ name: 'categoryId' }) category: Category; + @ApiProperty({ description: '分类 ID', nullable: true, example: 1 }) + @Column({ nullable: true }) + categoryId?: number; + @ManyToMany(() => DictItem, dictItem => dictItem.products, { cascade: true, }) diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 170ddeb..35b923d 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -276,16 +276,6 @@ export class ProductService { qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids }); } - // 处理where对象中的id过滤 - if (query.where?.id) { - qb.andWhere('product.id = :whereId', { whereId: query.where.id }); - } - - // 处理where对象中的ids过滤 - if (query.where?.ids && query.where.ids.length > 0) { - qb.andWhere('product.id IN (:...whereIds)', { whereIds: query.where.ids }); - } - // 处理SKU过滤 if (query.where?.sku) { qb.andWhere('product.sku LIKE :sku', { sku: `%${query.where.sku}%` }); @@ -296,12 +286,6 @@ export class ProductService { qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus }); } - - // 处理where对象中的skus过滤 - if (query.where?.skus && query.where.skus.length > 0) { - qb.andWhere('product.sku IN (:...whereSkus)', { whereSkus: query.where.skus }); - } - // 处理产品中文名称过滤 if (query.where?.nameCn) { qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` }); @@ -312,11 +296,6 @@ export class ProductService { qb.andWhere('product.type = :type', { type: query.where.type }); } - // 处理where对象中的type过滤 - if (query.where?.type) { - qb.andWhere('product.type = :whereType', { whereType: query.where.type }); - } - // 处理价格范围过滤 if (query.where?.minPrice !== undefined) { qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice }); @@ -326,15 +305,6 @@ export class ProductService { qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice }); } - // 处理where对象中的价格范围过滤 - if (query.where?.minPrice !== undefined) { - qb.andWhere('product.price >= :whereMinPrice', { whereMinPrice: query.where.minPrice }); - } - - if (query.where?.maxPrice !== undefined) { - qb.andWhere('product.price <= :whereMaxPrice', { whereMaxPrice: query.where.maxPrice }); - } - // 处理促销价格范围过滤 if (query.where?.minPromotionPrice !== undefined) { qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice }); @@ -344,15 +314,6 @@ export class ProductService { qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice }); } - // 处理where对象中的促销价格范围过滤 - if (query.where?.minPromotionPrice !== undefined) { - qb.andWhere('product.promotionPrice >= :whereMinPromotionPrice', { whereMinPromotionPrice: query.where.minPromotionPrice }); - } - - if (query.where?.maxPromotionPrice !== undefined) { - qb.andWhere('product.promotionPrice <= :whereMaxPromotionPrice', { whereMaxPromotionPrice: query.where.maxPromotionPrice }); - } - // 处理创建时间范围过滤 if (query.where?.createdAtStart) { qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) }); @@ -362,15 +323,6 @@ export class ProductService { qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) }); } - // 处理where对象中的创建时间范围过滤 - if (query.where?.createdAtStart) { - qb.andWhere('product.createdAt >= :whereCreatedAtStart', { whereCreatedAtStart: new Date(query.where.createdAtStart) }); - } - - if (query.where?.createdAtEnd) { - qb.andWhere('product.createdAt <= :whereCreatedAtEnd', { whereCreatedAtEnd: new Date(query.where.createdAtEnd) }); - } - // 处理更新时间范围过滤 if (query.where?.updatedAtStart) { qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) }); @@ -380,15 +332,6 @@ export class ProductService { qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) }); } - // 处理where对象中的更新时间范围过滤 - if (query.where?.updatedAtStart) { - qb.andWhere('product.updatedAt >= :whereUpdatedAtStart', { whereUpdatedAtStart: new Date(query.where.updatedAtStart) }); - } - - if (query.where?.updatedAtEnd) { - qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) }); - } - // 处理属性过滤 const attributeFilters = query.where?.attributes || {}; Object.entries(attributeFilters).forEach(([attributeName, value], index) => { @@ -464,16 +407,6 @@ export class ProductService { qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds }); } - // 处理where对象中的分类ID过滤 - if (query.where?.categoryId) { - qb.andWhere('product.categoryId = :whereCategoryId', { whereCategoryId: query.where.categoryId }); - } - - // 处理where对象中的分类ID列表过滤 - if (query.where?.categoryIds && query.where.categoryIds.length > 0) { - qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds }); - } - // 处理排序(支持新旧两种格式) if (orderBy) { if (typeof orderBy === 'string') { @@ -565,9 +498,8 @@ export class ProductService { return item; } - async createProduct(createProductDTO: CreateProductDTO): Promise { - const { attributes, sku, categoryId, type } = createProductDTO; + const { attributes, sku, categoryId, categoryName, type } = createProductDTO; // 条件判断(校验属性输入) // 当产品类型为 'bundle' 时,attributes 可以为空 @@ -578,47 +510,38 @@ export class ProductService { } } - const safeAttributes = attributes || []; - // 解析属性输入(按 id 或 dictName 创建/关联字典项) - const resolvedAttributes: DictItem[] = []; let categoryItem: Category | null = null; - // 如果提供了 categoryId,设置分类 if (categoryId) { categoryItem = await this.categoryModel.findOne({ where: { id: categoryId }, relations: ['attributes', 'attributes.attributeDict'] }); - if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`); } + if (!categoryItem && categoryName) { + categoryItem = await this.categoryModel.findOne({ + where: { name: categoryName }, + relations: ['attributes', 'attributes.attributeDict'] + }); + } + if (!categoryItem && categoryName) { + const category = new Category(); + category.name = categoryName || ''; + category.title = categoryName || ''; + const savedCategory = await this.categoryModel.save(category); + categoryItem = await this.categoryModel.findOne({ + where: { id: savedCategory.id }, + relations: ['attributes', 'attributes.attributeDict'] + }); + if (!categoryItem) throw new Error(`分类名称 ${categoryName} 不存在`); + } + // 创造一定要有商品分类 + if (!categoryItem) throw new Error('必须提供分类 ID 或分类名称'); + const resolvedAttributes: DictItem[] = []; + const safeAttributes = attributes || []; for (const attr of safeAttributes) { - // 如果属性是分类,特殊处理 - if (attr.dictName === 'category') { - if (attr.id) { - categoryItem = await this.categoryModel.findOne({ - where: { id: attr.id }, - relations: ['attributes', 'attributes.attributeDict'] - }); - } else if (attr.name) { - categoryItem = await this.categoryModel.findOne({ - where: { name: attr.name }, - relations: ['attributes', 'attributes.attributeDict'] - }); - } else if (attr.title) { - // 尝试用 title 匹配 name 或 title - categoryItem = await this.categoryModel.findOne({ - where: [ - { name: attr.title }, - { title: attr.title } - ], - relations: ['attributes', 'attributes.attributeDict'] - }); - } - continue; - } - let item: DictItem | null = null; if (attr.id) { // 如果传入了 id,直接查找字典项并使用,不强制要求 dictName @@ -694,27 +617,46 @@ export class ProductService { } // 使用 merge 更新基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO; + const { attributes, categoryId, categoryName, sku, components, ...simpleFields } = updateProductDTO; this.productModel.merge(product, simpleFields); - - // 处理分类更新 - if (updateProductDTO.categoryId !== undefined) { - if (updateProductDTO.categoryId) { - const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } }); - if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`); - product.category = categoryItem; - } else { - // 如果传了 0 或 null,可以清除分类(根据需求) - // product.category = null; - } + // 解析属性输入(按 id 或 dictName 创建/关联字典项) + let categoryItem: Category | null = null; + // 如果提供了 categoryId,设置分类 + if (categoryId) { + categoryItem = await this.categoryModel.findOne({ + where: { id: categoryId }, + relations: ['attributes', 'attributes.attributeDict'] + }); } - + if (!categoryItem && categoryName) { + categoryItem = await this.categoryModel.findOne({ + where: { name: categoryName }, + relations: ['attributes', 'attributes.attributeDict'] + }); + } + function nameToTitle(name: string) { + return name.replace('-',' '); + } + if (!categoryItem && categoryName) { + const category = new Category(); + category.name = categoryName || ''; + category.title = nameToTitle(categoryName || ''); + const savedCategory = await this.categoryModel.save(category); + categoryItem = await this.categoryModel.findOne({ + where: { id: savedCategory.id }, + relations: ['attributes', 'attributes.attributeDict'] + }); + if (!categoryItem) throw new Error(`分类名称 ${categoryName} 不存在`); + } + // 创造一定要有商品分类 + if (!categoryItem) throw new Error('必须提供分类 ID 或分类名称'); + product.categoryId = categoryItem.id; // 处理 SKU 更新 if (updateProductDTO.sku !== undefined) { // 校验 SKU 唯一性(如变更) const newSku = updateProductDTO.sku; if (newSku && newSku !== product.sku) { - const exist = await this.productModel.findOne({ where: { sku: newSku } }); + const exist = await this.productModel.findOne({ where: { id: Not(id), sku: newSku } }); if (exist) { throw new Error('SKU 已存在,请更换后重试'); } @@ -732,14 +674,6 @@ export class ProductService { }; for (const attr of updateProductDTO.attributes) { - // 如果属性是分类,特殊处理 - if (attr.dictName === 'category') { - if (attr.id) { - const categoryItem = await this.categoryModel.findOneBy({ id: attr.id }); - if (categoryItem) product.category = categoryItem; - } - continue; - } let item: DictItem | null = null; if (attr.id) { @@ -1450,7 +1384,8 @@ export class ProductService { } // 将单条 CSV 记录转换为数据对象 - transformCsvRecordToData(rec: any): CreateProductDTO & { sku: string } | null { + mapTableRecordToProduct(rec: any): CreateProductDTO | UpdateProductDTO | null { + const keys = Object.keys(rec); // 必须包含 sku const sku: string = (rec.sku || '').trim(); if (!sku) { @@ -1471,43 +1406,52 @@ export class ProductService { }; // 解析属性字段(分号分隔多值) - const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []); + const parseList = (v: string) => (v && String(v).split(/[;,]/).map(s => s.trim()).filter(Boolean)); // 将属性解析为 DTO 输入 const attributes: any[] = []; // 处理动态属性字段 (attribute_*) - for (const key of Object.keys(rec)) { + for (const key of keys) { if (key.startsWith('attribute_')) { const dictName = key.replace('attribute_', ''); if (dictName) { - const list = parseList(rec[key]); + const list = parseList(rec[key]) || []; for (const item of list) attributes.push({ dictName, title: item }); } } } - + // 目前的 components 由 component_{index}_sku和component_{index}_quantity组成 + const component_sku_keys = keys.filter(key => key.startsWith('component_') && key.endsWith('_sku')); + const components = []; + for (const key of component_sku_keys) { + const index = key.replace('component_', '').replace('_sku', ''); + if (index) { + const sku = val(rec[`component_${index}_sku`]); + const quantity = num(rec[`component_${index}_quantity`]); + if (sku && quantity) { + components.push({ sku, quantity }); + } + } + } // 处理分类字段 - const category = val(rec.category); + const categoryName = val(rec.category); return { sku, name: val(rec.name), nameCn: val(rec.nameCn), + image: val(rec.image), description: val(rec.description), + shortDescription: val(rec.shortDescription), 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, - category, // 添加分类字段 - + siteSkus: rec.siteSkus ? parseList(rec.siteSkus) : undefined, + categoryName, // 添加分类字段 + components, attributes: attributes.length > 0 ? attributes : undefined, - } as any; + } } // 准备创建产品的 DTO, 处理类型转换和默认值 @@ -1746,7 +1690,7 @@ export class ProductService { // 逐条处理记录 for (const rec of records) { try { - const data = this.transformCsvRecordToData(rec); + const data = this.mapTableRecordToProduct(rec); if (!data) { errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过' }); continue; @@ -1754,17 +1698,17 @@ export class ProductService { const { sku } = data; // 查找现有产品 - const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] }); + const exist = await this.productModel.findOne({ where: { sku } }); if (!exist) { // 创建新产品 - const createDTO = this.prepareCreateProductDTO(data); - await this.createProduct(createDTO); + // const createDTO = this.prepareCreateProductDTO(data); + await this.createProduct(data as CreateProductDTO) created += 1; } else { // 更新产品 - const updateDTO = this.prepareUpdateProductDTO(data); - await this.updateProduct(exist.id, updateDTO); + // const updateDTO = this.prepareUpdateProductDTO(data); + await this.updateProduct(exist.id, data); updated += 1; } } catch (e: any) { @@ -2138,7 +2082,7 @@ export class ProductService { } ] }); - + if (brandItem) { qb.andWhere(qb => { const subQuery = qb @@ -2171,7 +2115,7 @@ export class ProductService { where: { productId: product.id }, }); } - + // 确保属性按强度正确划分,只保留强度相关的属性 // 这里根据需求,如果需要可以进一步过滤或重组属性 } @@ -2190,10 +2134,10 @@ export class ProductService { async getProductsGroupedByAttribute(brand?: string, attributeName: string = 'strength'): Promise<{ [key: string]: Product[] }> { // 首先获取所有产品 const { items } = await this.getAllProducts(brand); - + // 按指定属性分组 const groupedProducts: { [key: string]: Product[] } = {}; - + items.forEach(product => { // 获取产品的指定属性值 const attribute = product.attributes.find(attr => attr.dict.name === attributeName); @@ -2211,7 +2155,7 @@ export class ProductService { groupedProducts['未分组'].push(product); } }); - + return groupedProducts; } } diff --git a/src/service/site-product.service.ts b/src/service/site-product.service.ts new file mode 100644 index 0000000..e69de29