forked from yoone/API
1
0
Fork 0

fix(product): 修复产品创建和更新时的属性校验问题

调整产品DTO中attributes字段的校验规则,使其在type为'single'时必填,为'bundle'时可选
移除不必要的siteSkus处理逻辑,简化产品创建和更新流程
This commit is contained in:
tikkhun 2026-01-04 20:05:37 +08:00
parent 58ae594d5e
commit 338625c3d2
5 changed files with 87 additions and 44 deletions

View File

@ -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 })

View File

@ -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;
}

View File

@ -102,7 +102,7 @@ export class OrderService {
async syncOrders(siteId: number, params: Record<string, any> = {}): Promise<SyncOperationResult> {
// 调用 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;
// 如果有值就赋值

View File

@ -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<Product> {
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<string> {
// 查询所有产品及其属性(包含字典关系)和组成
const products = await this.productModel.find({
relations: ['attributes', 'attributes.dict', 'components', 'siteSkus'],
relations: ['attributes', 'attributes.dict', 'components'],
order: { id: 'ASC' },
});

View File

@ -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 };