|
|
|
|
@ -276,9 +276,19 @@ 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}%` });
|
|
|
|
|
qb.andWhere('product.sku = :sku', { sku: query.where.sku });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理SKU列表过滤
|
|
|
|
|
@ -286,6 +296,16 @@ export class ProductService {
|
|
|
|
|
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理where对象中的sku过滤
|
|
|
|
|
if (query.where?.sku) {
|
|
|
|
|
qb.andWhere('product.sku = :whereSku', { whereSku: query.where.sku });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理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}%` });
|
|
|
|
|
@ -296,6 +316,11 @@ 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 });
|
|
|
|
|
@ -305,6 +330,15 @@ 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 });
|
|
|
|
|
@ -314,6 +348,15 @@ 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) });
|
|
|
|
|
@ -323,6 +366,15 @@ 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) });
|
|
|
|
|
@ -332,40 +384,14 @@ export class ProductService {
|
|
|
|
|
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理属性过滤
|
|
|
|
|
const attributeFilters = query.where?.attributes || {};
|
|
|
|
|
Object.entries(attributeFilters).forEach(([attributeName, value], index) => {
|
|
|
|
|
if (value === 'hasValue') {
|
|
|
|
|
// 如果值为'hasValue',则过滤出具有该属性的产品
|
|
|
|
|
qb.andWhere(qb => {
|
|
|
|
|
const subQuery = qb
|
|
|
|
|
.subQuery()
|
|
|
|
|
.select('product_attributes_dict_item.productId')
|
|
|
|
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
|
|
|
|
.innerJoin('dict_item', 'dict_item', 'product_attributes_dict_item.dictItemId = dict_item.id')
|
|
|
|
|
.innerJoin('dict', 'dict', 'dict_item.dictId = dict.id')
|
|
|
|
|
.where('dict.name = :attributeName', {
|
|
|
|
|
attributeName,
|
|
|
|
|
})
|
|
|
|
|
.getQuery();
|
|
|
|
|
return 'product.id IN ' + subQuery;
|
|
|
|
|
});
|
|
|
|
|
} else if (typeof value === 'number' || !isNaN(Number(value))) {
|
|
|
|
|
// 如果值是数字,则过滤出该属性等于该值的产品
|
|
|
|
|
const attributeValueId = Number(value);
|
|
|
|
|
qb.andWhere(qb => {
|
|
|
|
|
const subQuery = qb
|
|
|
|
|
.subQuery()
|
|
|
|
|
.select('product_attributes_dict_item.productId')
|
|
|
|
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
|
|
|
|
.where('product_attributes_dict_item.dictItemId = :attributeValueId', {
|
|
|
|
|
attributeValueId,
|
|
|
|
|
})
|
|
|
|
|
.getQuery();
|
|
|
|
|
return 'product.id IN ' + subQuery;
|
|
|
|
|
});
|
|
|
|
|
// 处理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) });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 品牌过滤(向后兼容)
|
|
|
|
|
if (brandId) {
|
|
|
|
|
@ -407,6 +433,16 @@ 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') {
|
|
|
|
|
@ -498,8 +534,9 @@ export class ProductService {
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
|
|
|
|
const { attributes, sku, categoryId, categoryName, type } = createProductDTO;
|
|
|
|
|
const { attributes, sku, categoryId, type } = createProductDTO;
|
|
|
|
|
|
|
|
|
|
// 条件判断(校验属性输入)
|
|
|
|
|
// 当产品类型为 'bundle' 时,attributes 可以为空
|
|
|
|
|
@ -510,38 +547,47 @@ 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
|
|
|
|
|
@ -617,46 +663,27 @@ export class ProductService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用 merge 更新基础字段,排除特殊处理字段
|
|
|
|
|
const { attributes, categoryId, categoryName, sku, components, ...simpleFields } = updateProductDTO;
|
|
|
|
|
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO;
|
|
|
|
|
this.productModel.merge(product, simpleFields);
|
|
|
|
|
// 解析属性输入(按 id 或 dictName 创建/关联字典项)
|
|
|
|
|
let categoryItem: Category | null = null;
|
|
|
|
|
// 如果提供了 categoryId,设置分类
|
|
|
|
|
if (categoryId) {
|
|
|
|
|
categoryItem = await this.categoryModel.findOne({
|
|
|
|
|
where: { id: categoryId },
|
|
|
|
|
relations: ['attributes', 'attributes.attributeDict']
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 处理分类更新
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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: { id: Not(id), sku: newSku } });
|
|
|
|
|
const exist = await this.productModel.findOne({ where: { sku: newSku } });
|
|
|
|
|
if (exist) {
|
|
|
|
|
throw new Error('SKU 已存在,请更换后重试');
|
|
|
|
|
}
|
|
|
|
|
@ -674,6 +701,14 @@ 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) {
|
|
|
|
|
@ -1384,8 +1419,7 @@ export class ProductService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将单条 CSV 记录转换为数据对象
|
|
|
|
|
mapTableRecordToProduct(rec: any): CreateProductDTO | UpdateProductDTO | null {
|
|
|
|
|
const keys = Object.keys(rec);
|
|
|
|
|
transformCsvRecordToData(rec: any): CreateProductDTO & { sku: string } | null {
|
|
|
|
|
// 必须包含 sku
|
|
|
|
|
const sku: string = (rec.sku || '').trim();
|
|
|
|
|
if (!sku) {
|
|
|
|
|
@ -1406,105 +1440,43 @@ 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 keys) {
|
|
|
|
|
for (const key of Object.keys(rec)) {
|
|
|
|
|
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 categoryName = val(rec.category);
|
|
|
|
|
const category = 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 ? parseList(rec.siteSkus) : undefined,
|
|
|
|
|
categoryName, // 添加分类字段
|
|
|
|
|
components,
|
|
|
|
|
siteSkus: rec.siteSkus
|
|
|
|
|
? String(rec.siteSkus)
|
|
|
|
|
.split(/[;,]/) // 支持英文分号或英文逗号分隔
|
|
|
|
|
.map(s => s.trim())
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
: undefined,
|
|
|
|
|
category, // 添加分类字段
|
|
|
|
|
|
|
|
|
|
attributes: attributes.length > 0 ? attributes : undefined,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
isMixedSku(sku: string){
|
|
|
|
|
const splitSKu = sku.split('-')
|
|
|
|
|
const last = splitSKu[splitSKu.length - 1]
|
|
|
|
|
const second = splitSKu[splitSKu.length - 2]
|
|
|
|
|
// 这里判断 second 是否是数字
|
|
|
|
|
return sku.includes('-MX-') || sku.includes('-Mixed-') || /^\d+$/.test(second) && /^\d+$/.test(last)
|
|
|
|
|
}
|
|
|
|
|
async getComponentDetailFromSiteSku(siteProduct: { sku: string, name: string }) {
|
|
|
|
|
if (!siteProduct.sku) {
|
|
|
|
|
throw new Error('siteSku 不能为空')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let product = await this.productModel.findOne({
|
|
|
|
|
where: { siteSkus: Like(`%${siteProduct.sku}%`) },
|
|
|
|
|
relations: ['components', 'attributes', 'attributes.dict'],
|
|
|
|
|
});
|
|
|
|
|
let quantity = 1;
|
|
|
|
|
// 这里处理一下特殊情况,就是无法直接通过 siteProduct.sku去获取, 但有一定规则转换成有的产品,就是 bundle 的部分
|
|
|
|
|
// 考察各个站点的 bundle 规则, 会发现
|
|
|
|
|
// wordpress:
|
|
|
|
|
// togovape YOONE Wintergreen 9MG (Moisture) - 10 cans TV-YOONE-NP-S-WG-9MG-0010
|
|
|
|
|
// togovape mixed 是这样的 TV-YOONE-NP-G-12MG-MX-0003 TV-ZEX-NP-Mixed-12MG-0001
|
|
|
|
|
//
|
|
|
|
|
// shopyy: shopyy 已经
|
|
|
|
|
// 只有 bundle 做这个处理
|
|
|
|
|
if (!product && !this.isMixedSku(siteProduct.sku)) {
|
|
|
|
|
const skuSplitArr = siteProduct.sku.split('-')
|
|
|
|
|
const quantityStr = skuSplitArr[skuSplitArr.length - 1]
|
|
|
|
|
const isBundleSku = quantityStr.startsWith('0')
|
|
|
|
|
if(!isBundleSku){
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
quantity = Number(quantityStr)
|
|
|
|
|
if(!isBundleSku){
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
// 更正为正确的站点 sku
|
|
|
|
|
const childSku = skuSplitArr.slice(0, skuSplitArr.length - 1).join('-')
|
|
|
|
|
// 重新获取匹配的商品
|
|
|
|
|
product = await this.productModel.findOne({
|
|
|
|
|
where: { siteSkus: Like(`%${childSku}%`) },
|
|
|
|
|
relations: ['components', 'attributes', 'attributes.dict'],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!product) {
|
|
|
|
|
throw new Error(`产品 ${siteProduct.sku} 不存在`);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
product,
|
|
|
|
|
quantity,
|
|
|
|
|
}
|
|
|
|
|
} as any;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 准备创建产品的 DTO, 处理类型转换和默认值
|
|
|
|
|
@ -1743,7 +1715,7 @@ export class ProductService {
|
|
|
|
|
// 逐条处理记录
|
|
|
|
|
for (const rec of records) {
|
|
|
|
|
try {
|
|
|
|
|
const data = this.mapTableRecordToProduct(rec);
|
|
|
|
|
const data = this.transformCsvRecordToData(rec);
|
|
|
|
|
if (!data) {
|
|
|
|
|
errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过' });
|
|
|
|
|
continue;
|
|
|
|
|
@ -1751,17 +1723,17 @@ export class ProductService {
|
|
|
|
|
const { sku } = data;
|
|
|
|
|
|
|
|
|
|
// 查找现有产品
|
|
|
|
|
const exist = await this.productModel.findOne({ where: { sku } });
|
|
|
|
|
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
|
|
|
|
|
|
|
|
|
|
if (!exist) {
|
|
|
|
|
// 创建新产品
|
|
|
|
|
// const createDTO = this.prepareCreateProductDTO(data);
|
|
|
|
|
await this.createProduct(data as CreateProductDTO)
|
|
|
|
|
const createDTO = this.prepareCreateProductDTO(data);
|
|
|
|
|
await this.createProduct(createDTO);
|
|
|
|
|
created += 1;
|
|
|
|
|
} else {
|
|
|
|
|
// 更新产品
|
|
|
|
|
// const updateDTO = this.prepareUpdateProductDTO(data);
|
|
|
|
|
await this.updateProduct(exist.id, data);
|
|
|
|
|
const updateDTO = this.prepareUpdateProductDTO(data);
|
|
|
|
|
await this.updateProduct(exist.id, updateDTO);
|
|
|
|
|
updated += 1;
|
|
|
|
|
}
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
@ -2104,111 +2076,4 @@ export class ProductService {
|
|
|
|
|
|
|
|
|
|
return unifiedProduct;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取所有产品,支持按品牌过滤
|
|
|
|
|
* @param brand 品牌名称
|
|
|
|
|
* @returns 所有符合条件的产品
|
|
|
|
|
*/
|
|
|
|
|
async getAllProducts(brand?: string): Promise<{ items: Product[], total: number }> {
|
|
|
|
|
const qb = this.productModel
|
|
|
|
|
.createQueryBuilder('product')
|
|
|
|
|
.leftJoinAndSelect('product.attributes', 'attribute')
|
|
|
|
|
.leftJoinAndSelect('attribute.dict', 'dict')
|
|
|
|
|
.leftJoinAndSelect('product.category', 'category');
|
|
|
|
|
|
|
|
|
|
// 按品牌过滤
|
|
|
|
|
if (brand) {
|
|
|
|
|
// 先获取品牌对应的字典项
|
|
|
|
|
const brandDict = await this.dictModel.findOne({ where: { name: 'brand' } });
|
|
|
|
|
if (brandDict) {
|
|
|
|
|
// 查找品牌名称对应的字典项(支持标题和名称匹配)
|
|
|
|
|
const brandItem = await this.dictItemModel.findOne({
|
|
|
|
|
where: [
|
|
|
|
|
{
|
|
|
|
|
title: brand,
|
|
|
|
|
dict: { id: brandDict.id }
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: brand,
|
|
|
|
|
dict: { id: brandDict.id }
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (brandItem) {
|
|
|
|
|
qb.andWhere(qb => {
|
|
|
|
|
const subQuery = qb
|
|
|
|
|
.subQuery()
|
|
|
|
|
.select('product_attributes_dict_item.productId')
|
|
|
|
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
|
|
|
|
.where('product_attributes_dict_item.dictItemId = :brandId', {
|
|
|
|
|
brandId: brandItem.id,
|
|
|
|
|
})
|
|
|
|
|
.getQuery();
|
|
|
|
|
return 'product.id IN ' + subQuery;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据类型填充组成信息
|
|
|
|
|
const items = await qb.getMany();
|
|
|
|
|
for (const product of items) {
|
|
|
|
|
if (product.type === 'single') {
|
|
|
|
|
// 单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成
|
|
|
|
|
const component = new ProductStockComponent();
|
|
|
|
|
component.productId = product.id;
|
|
|
|
|
component.sku = product.sku;
|
|
|
|
|
component.quantity = 1;
|
|
|
|
|
product.components = [component];
|
|
|
|
|
} else {
|
|
|
|
|
// 混装商品返回持久化的 SKU 组成
|
|
|
|
|
product.components = await this.productStockComponentModel.find({
|
|
|
|
|
where: { productId: product.id },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保属性按强度正确划分,只保留强度相关的属性
|
|
|
|
|
// 这里根据需求,如果需要可以进一步过滤或重组属性
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
items,
|
|
|
|
|
total: items.length
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取产品按属性值分组,支持按强度划分
|
|
|
|
|
* @param brand 品牌名称
|
|
|
|
|
* @returns 按属性值分组的产品
|
|
|
|
|
*/
|
|
|
|
|
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);
|
|
|
|
|
if (attribute) {
|
|
|
|
|
const attributeValue = attribute.title || attribute.name;
|
|
|
|
|
if (!groupedProducts[attributeValue]) {
|
|
|
|
|
groupedProducts[attributeValue] = [];
|
|
|
|
|
}
|
|
|
|
|
groupedProducts[attributeValue].push(product);
|
|
|
|
|
} else {
|
|
|
|
|
// 如果没有该属性,放入未分组
|
|
|
|
|
if (!groupedProducts['未分组']) {
|
|
|
|
|
groupedProducts['未分组'] = [];
|
|
|
|
|
}
|
|
|
|
|
groupedProducts['未分组'].push(product);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return groupedProducts;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|