forked from yoone/API
fix(product): 修复产品创建和更新时的属性校验问题
调整产品DTO中attributes字段的校验规则,使其在type为'single'时必填,为'bundle'时可选 移除不必要的siteSkus处理逻辑,简化产品创建和更新流程
This commit is contained in:
parent
58ae594d5e
commit
338625c3d2
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// 如果有值就赋值
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue