From 338625c3d202773626ad5a577ff496fe6bc3324a Mon Sep 17 00:00:00 2001 From: tikkhun Date: Sun, 4 Jan 2026 20:05:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(product):=20=E4=BF=AE=E5=A4=8D=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=88=9B=E5=BB=BA=E5=92=8C=E6=9B=B4=E6=96=B0=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E5=B1=9E=E6=80=A7=E6=A0=A1=E9=AA=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整产品DTO中attributes字段的校验规则,使其在type为'single'时必填,为'bundle'时可选 移除不必要的siteSkus处理逻辑,简化产品创建和更新流程 --- src/dto/product.dto.ts | 14 ++++++-- src/dto/site.dto.ts | 47 +++++++++++++++++++++++++- src/service/order.service.ts | 4 +-- src/service/product.service.ts | 60 ++++++++++++++-------------------- src/service/site.service.ts | 6 ++-- 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index a24e7e0..1feccfe 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -64,9 +64,17 @@ export class CreateProductDTO { siteSkus?: string[]; // 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等) - @ApiProperty({ description: '属性列表', type: 'array' }) - @Rule(RuleType.array().required()) - attributes: AttributeInputDTO[]; + // 当 type 为 'single' 时必填,当 type 为 'bundle' 时可选 + @ApiProperty({ description: '属性列表', type: 'array', required: false }) + @Rule( + RuleType.array() + .when('type', { + is: 'single', + then: RuleType.array().required(), + otherwise: RuleType.array().optional() + }) + ) + attributes?: AttributeInputDTO[]; // 商品价格 @ApiProperty({ description: '价格', example: 99.99, required: false }) diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index b45a3e3..27d8bde 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -36,22 +36,39 @@ export class SiteConfig { } export class CreateSiteDTO { + @ApiProperty({ description: '站点 API URL', required: false }) @Rule(RuleType.string().optional()) apiUrl?: string; + + @ApiProperty({ description: '站点网站 URL', required: false }) @Rule(RuleType.string().optional()) websiteUrl?: string; + + @ApiProperty({ description: '站点 REST Key', required: false }) @Rule(RuleType.string().optional()) consumerKey?: string; + + @ApiProperty({ description: '站点 REST 秘钥', required: false }) @Rule(RuleType.string().optional()) consumerSecret?: string; + + @ApiProperty({ description: '访问令牌', required: false }) @Rule(RuleType.string().optional()) token?: string; + + @ApiProperty({ description: '站点名称' }) @Rule(RuleType.string()) name: string; + + @ApiProperty({ description: '站点描述', required: false }) @Rule(RuleType.string().allow('').optional()) description?: string; + + @ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false }) @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) type?: string; + + @ApiProperty({ description: 'SKU 前缀', required: false }) @Rule(RuleType.string().optional()) skuPrefix?: string; @@ -67,22 +84,39 @@ export class CreateSiteDTO { } export class UpdateSiteDTO { + @ApiProperty({ description: '站点 API URL', required: false }) @Rule(RuleType.string().optional()) apiUrl?: string; + + @ApiProperty({ description: '站点 REST Key', required: false }) @Rule(RuleType.string().optional()) consumerKey?: string; + + @ApiProperty({ description: '站点 REST 秘钥', required: false }) @Rule(RuleType.string().optional()) consumerSecret?: string; + + @ApiProperty({ description: '访问令牌', required: false }) @Rule(RuleType.string().optional()) token?: string; + + @ApiProperty({ description: '站点名称', required: false }) @Rule(RuleType.string().optional()) name?: string; + + @ApiProperty({ description: '站点描述', required: false }) @Rule(RuleType.string().allow('').optional()) description?: string; + + @ApiProperty({ description: '是否禁用', required: false }) @Rule(RuleType.boolean().optional()) isDisabled?: boolean; + + @ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false }) @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) type?: string; + + @ApiProperty({ description: 'SKU 前缀', required: false }) @Rule(RuleType.string().optional()) skuPrefix?: string; @@ -95,25 +129,36 @@ export class UpdateSiteDTO { @ApiProperty({ description: '绑定仓库ID列表' }) @Rule(RuleType.array().items(RuleType.number()).optional()) stockPointIds?: number[]; - @ApiProperty({ description: '站点网站URL' }) + + @ApiProperty({ description: '站点网站URL', required: false }) @Rule(RuleType.string().optional()) websiteUrl?: string; } export class QuerySiteDTO { + @ApiProperty({ description: '当前页码', required: false }) @Rule(RuleType.number().optional()) current?: number; + + @ApiProperty({ description: '每页数量', required: false }) @Rule(RuleType.number().optional()) pageSize?: number; + + @ApiProperty({ description: '搜索关键词', required: false }) @Rule(RuleType.string().optional()) keyword?: string; + + @ApiProperty({ description: '是否禁用', required: false }) @Rule(RuleType.boolean().optional()) isDisabled?: boolean; + + @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) @Rule(RuleType.string().optional()) ids?: string; } export class DisableSiteDTO { + @ApiProperty({ description: '是否禁用' }) @Rule(RuleType.boolean()) disabled: boolean; } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index d822c20..775b341 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -102,7 +102,7 @@ export class OrderService { async syncOrders(siteId: number, params: Record = {}): Promise { // 调用 WooCommerce API 获取订单 - const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); + const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); const syncResult: SyncOperationResult = { total: result.length, @@ -202,7 +202,7 @@ export class OrderService { // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换 async autoUpdateOrderStatus(siteId: number, order: any) { - console.log('更新订单状态', order) + console.log('更新订单状态', order.status, '=>', this.orderAutoNextStatusMap[order.status]) // 其他状态保持不变 const originStatus = order.status; // 如果有值就赋值 diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 6a8d7dd..1a06051 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -5,7 +5,6 @@ import { In, Like, Not, Repository } from 'typeorm'; import { Product } from '../entity/product.entity'; import { PaginationParams } from '../interface'; import { paginate } from '../utils/paginate.util'; - import { Context } from '@midwayjs/koa'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { @@ -538,15 +537,14 @@ export class ProductService { async createProduct(createProductDTO: CreateProductDTO): Promise { - const { attributes, sku, categoryId } = createProductDTO; + const { attributes, sku, categoryId, type } = createProductDTO; // 条件判断(校验属性输入) - if (!Array.isArray(attributes) || attributes.length === 0) { - // 如果提供了 categoryId 但没有 attributes,初始化为空数组 - if (!attributes && categoryId) { - // 继续执行,下面会处理 categoryId - } else { - throw new Error('属性列表不能为空'); + // 当产品类型为 'bundle' 时,attributes 可以为空 + // 当产品类型为 'single' 时,attributes 必须提供且不能为空 + if (type === 'single') { + if (!Array.isArray(attributes) || attributes.length === 0) { + throw new Error('单品类型的属性列表不能为空'); } } @@ -607,23 +605,26 @@ export class ProductService { } // 检查完全相同属性组合是否已存在(避免重复) - const qb = this.productModel.createQueryBuilder('product'); - resolvedAttributes.forEach((attr, index) => { - qb.innerJoin( - 'product.attributes', - `attr${index}`, - `attr${index}.id = :attrId${index}`, - { [`attrId${index}`]: attr.id } - ); - }); - const isExist = await qb.getOne(); - if (isExist) throw new Error('相同产品属性的产品已存在'); + // 仅当产品类型为 'single' 且有属性时才检查重复 + if (type === 'single' && resolvedAttributes.length > 0) { + const qb = this.productModel.createQueryBuilder('product'); + resolvedAttributes.forEach((attr, index) => { + qb.innerJoin( + 'product.attributes', + `attr${index}`, + `attr${index}.id = :attrId${index}`, + { [`attrId${index}`]: attr.id } + ); + }); + const isExist = await qb.getOne(); + if (isExist) throw new Error('相同产品属性的产品已存在'); + } // 创建新产品实例(绑定属性与基础字段) const product = new Product(); // 使用 merge 填充基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = createProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = createProductDTO; this.productModel.merge(product, simpleFields); product.attributes = resolvedAttributes; @@ -642,13 +643,6 @@ export class ProductService { const savedProduct = await this.productModel.save(product); - // 设置站点 SKU 列表(确保始终设置,即使为空数组) - if (createProductDTO.siteSkus !== undefined) { - product.siteSkus = createProductDTO.siteSkus; - // 重新保存产品以更新 siteSkus - await this.productModel.save(product); - } - // 保存组件信息 if (createProductDTO.components && createProductDTO.components.length > 0) { await this.setProductComponents(savedProduct.id, createProductDTO.components); @@ -670,7 +664,7 @@ export class ProductService { } // 使用 merge 更新基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = updateProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO; this.productModel.merge(product, simpleFields); // 处理分类更新 @@ -685,11 +679,6 @@ export class ProductService { } } - // 处理站点 SKU 更新 - if (updateProductDTO.siteSkus !== undefined) { - product.siteSkus = updateProductDTO.siteSkus; - } - // 处理 SKU 更新 if (updateProductDTO.sku !== undefined) { // 校验 SKU 唯一性(如变更) @@ -786,7 +775,7 @@ export class ProductService { } } else { // 简单字段,直接批量更新以提高性能 - // UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice + // UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice, siteSkus const simpleUpdate: any = {}; if (updateData.name !== undefined) simpleUpdate.name = updateData.name; @@ -795,6 +784,7 @@ export class ProductService { if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription; if (updateData.price !== undefined) simpleUpdate.price = updateData.price; if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice; + if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus; if (Object.keys(simpleUpdate).length > 0) { await this.productModel.update({ id: In(ids) }, simpleUpdate); @@ -1588,7 +1578,7 @@ export class ProductService { async exportProductsCSV(): Promise { // 查询所有产品及其属性(包含字典关系)和组成 const products = await this.productModel.find({ - relations: ['attributes', 'attributes.dict', 'components', 'siteSkus'], + relations: ['attributes', 'attributes.dict', 'components'], order: { id: 'ASC' }, }); diff --git a/src/service/site.service.ts b/src/service/site.service.ts index a8b8fe4..58664d7 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -21,9 +21,9 @@ export class SiteService { async create(data: CreateSiteDTO) { // 从 DTO 中分离出区域代码和其他站点数据 - const { areas: areaCodes, stockPointIds, websiteUrl, ...restData } = data; + const { areas: areaCodes, stockPointIds, ...restData } = data; const newSite = new Site(); - Object.assign(newSite, restData, { websiteUrl }); + Object.assign(newSite, restData); // 如果传入了区域代码,则查询并关联 Area 实体 if (areaCodes && areaCodes.length > 0) { @@ -163,7 +163,7 @@ export class SiteService { const data = includeSecret ? items : items.map((item: any) => { - const { consumerKey, consumerSecret, ...rest } = item; + const { consumerKey, consumerSecret, token, ...rest } = item; return rest; }); return { items: data, total, current, pageSize };