2508 lines
83 KiB
TypeScript
2508 lines
83 KiB
TypeScript
import { Inject, Provide } from '@midwayjs/core';
|
||
import * as fs from 'fs';
|
||
import * as xlsx from 'xlsx';
|
||
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 {
|
||
BatchUpdateProductDTO,
|
||
CreateProductDTO,
|
||
ProductWhereFilter,
|
||
UpdateProductDTO
|
||
} from '../dto/product.dto';
|
||
import {
|
||
BrandPaginatedResponse,
|
||
FlavorsPaginatedResponse,
|
||
ProductPaginatedResponse,
|
||
SizePaginatedResponse,
|
||
StrengthPaginatedResponse,
|
||
} from '../dto/reponse.dto';
|
||
import { Dict } from '../entity/dict.entity';
|
||
import { DictItem } from '../entity/dict_item.entity';
|
||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||
import { Stock } from '../entity/stock.entity';
|
||
import { StockPoint } from '../entity/stock_point.entity';
|
||
import { StockService } from './stock.service';
|
||
import { TemplateService } from './template.service';
|
||
|
||
import { BatchErrorItem, BatchOperationResult, SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
||
import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto';
|
||
import { Category } from '../entity/category.entity';
|
||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||
import { SiteApiService } from './site-api.service';
|
||
|
||
@Provide()
|
||
export class ProductService {
|
||
@Inject()
|
||
ctx: Context;
|
||
|
||
@Inject()
|
||
templateService: TemplateService;
|
||
|
||
@Inject()
|
||
stockService: StockService;
|
||
|
||
@InjectEntityModel(Product)
|
||
productModel: Repository<Product>;
|
||
|
||
@InjectEntityModel(Dict)
|
||
dictModel: Repository<Dict>;
|
||
|
||
@InjectEntityModel(DictItem)
|
||
dictItemModel: Repository<DictItem>;
|
||
|
||
@InjectEntityModel(Stock)
|
||
stockModel: Repository<Stock>;
|
||
|
||
@InjectEntityModel(StockPoint)
|
||
stockPointModel: Repository<StockPoint>;
|
||
|
||
@InjectEntityModel(ProductStockComponent)
|
||
productStockComponentModel: Repository<ProductStockComponent>;
|
||
|
||
@InjectEntityModel(Category)
|
||
categoryModel: Repository<Category>;
|
||
|
||
@Inject()
|
||
siteApiService: SiteApiService;
|
||
|
||
// 获取所有分类
|
||
async getCategoriesAll(): Promise<Category[]> {
|
||
return this.categoryModel.find({
|
||
order: {
|
||
sort: 'ASC',
|
||
},
|
||
});
|
||
}
|
||
|
||
// 获取分类下的属性配置
|
||
async getCategoryAttributes(categoryId: number): Promise<any[]> {
|
||
const category = await this.categoryModel.findOne({
|
||
where: { id: categoryId },
|
||
relations: ['attributes', 'attributes.attributeDict', 'attributes.attributeDict.items'],
|
||
});
|
||
|
||
if (!category) {
|
||
return [];
|
||
}
|
||
|
||
// 格式化返回,匹配前端期望的数据结构
|
||
return category.attributes.map(attr => ({
|
||
id: attr.id,
|
||
dictId: attr.attributeDict.id,
|
||
name: attr.attributeDict.name,
|
||
title: attr.attributeDict.title,
|
||
items: attr.attributeDict.items, // 如果需要返回具体的选项
|
||
}));
|
||
}
|
||
|
||
// 创建分类
|
||
async createCategory(payload: Partial<Category>): Promise<Category> {
|
||
const exists = await this.categoryModel.findOne({ where: { name: payload.name } });
|
||
if (exists) {
|
||
throw new Error('分类已存在');
|
||
}
|
||
return this.categoryModel.save(payload);
|
||
}
|
||
|
||
// 更新分类
|
||
async updateCategory(id: number, payload: Partial<Category>): Promise<Category> {
|
||
const category = await this.categoryModel.findOne({ where: { id } });
|
||
if (!category) {
|
||
throw new Error('分类不存在');
|
||
}
|
||
await this.categoryModel.update(id, payload);
|
||
return this.categoryModel.findOne({ where: { id } });
|
||
}
|
||
|
||
// 删除分类
|
||
async deleteCategory(id: number): Promise<boolean> {
|
||
const result = await this.categoryModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
// 创建分类属性关联
|
||
async createCategoryAttribute(payload: { categoryId: number; dictId: number }): Promise<any> {
|
||
const category = await this.categoryModel.findOne({ where: { id: payload.categoryId } });
|
||
if (!category) {
|
||
throw new Error('分类不存在');
|
||
}
|
||
|
||
const dict = await this.dictModel.findOne({ where: { id: payload.dictId } });
|
||
if (!dict) {
|
||
throw new Error('字典不存在');
|
||
}
|
||
|
||
const existing = await this.categoryModel.manager.findOne(CategoryAttribute, {
|
||
where: {
|
||
category: { id: payload.categoryId },
|
||
attributeDict: { id: payload.dictId },
|
||
},
|
||
});
|
||
|
||
if (existing) {
|
||
throw new Error('该属性已关联到此分类');
|
||
}
|
||
|
||
const attr = this.categoryModel.manager.create(CategoryAttribute, {
|
||
category,
|
||
attributeDict: dict
|
||
});
|
||
return this.categoryModel.manager.save(attr);
|
||
}
|
||
|
||
// 删除分类属性关联
|
||
async deleteCategoryAttribute(id: number): Promise<boolean> {
|
||
const result = await this.categoryModel.manager.delete(CategoryAttribute, id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
|
||
// async findProductsByName(name: string): Promise<Product[]> {
|
||
// const where: any = {};
|
||
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||
// if (nameFilter.length > 0) {
|
||
// const nameConditions = nameFilter.map(word => Like(`%${word}%`));
|
||
// where.name = And(...nameConditions);
|
||
// }
|
||
// if(name){
|
||
// where.nameCn = Like(`%${name}%`)
|
||
// }
|
||
// where.sku = Not(IsNull());
|
||
// // 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
|
||
// return this.productModel.find({
|
||
// where,
|
||
// take: 50,
|
||
// });
|
||
// }
|
||
|
||
async findProductsByName(name: string): Promise<Product[]> {
|
||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||
const query = this.productModel.createQueryBuilder('product')
|
||
.leftJoinAndSelect('product.category', 'category');
|
||
|
||
// 保证 sku 不为空
|
||
query.where('product.sku IS NOT NULL');
|
||
|
||
if (nameFilter.length > 0 || name) {
|
||
const params: Record<string, string> = {};
|
||
const conditions: string[] = [];
|
||
|
||
// 英文名关键词全部匹配(AND)
|
||
if (nameFilter.length > 0) {
|
||
const nameConds = nameFilter.map((word, index) => {
|
||
const key = `name${index}`;
|
||
params[key] = `%${word}%`;
|
||
return `product.name LIKE :${key}`;
|
||
});
|
||
conditions.push(`(${nameConds.join(' AND ')})`);
|
||
}
|
||
|
||
// 中文名模糊匹配
|
||
if (name) {
|
||
params['nameCn'] = `%${name}%`;
|
||
conditions.push(`product.nameCn LIKE :nameCn`);
|
||
}
|
||
|
||
// 只有当 conditions 不为空时才添加 AND 条件
|
||
if (conditions.length > 0) {
|
||
// 英文名关键词匹配 OR 中文名匹配
|
||
query.andWhere(`(${conditions.join(' OR ')})`, params);
|
||
}
|
||
}
|
||
|
||
query.take(50);
|
||
|
||
return await query.getMany();
|
||
}
|
||
|
||
async findProductBySku(sku: string): Promise<Product> {
|
||
return this.productModel.findOne({
|
||
where: {
|
||
sku,
|
||
},
|
||
relations: ['category', 'attributes', 'attributes.dict']
|
||
});
|
||
}
|
||
|
||
async getProductList(query: UnifiedSearchParamsDTO<ProductWhereFilter>): Promise<ProductPaginatedResponse> {
|
||
const qb = this.productModel
|
||
.createQueryBuilder('product')
|
||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||
.leftJoinAndSelect('attribute.dict', 'dict')
|
||
.leftJoinAndSelect('product.category', 'category');
|
||
// 处理分页参数(支持新旧两种格式)
|
||
const page = query.page || 1;
|
||
const pageSize = query.per_page || 10;
|
||
|
||
// 处理搜索参数
|
||
const name = query.where?.name || '';
|
||
|
||
// 处理品牌过滤
|
||
const brandId = query.where?.brandId;
|
||
const brandIds = query.where?.brandIds;
|
||
|
||
// 处理分类过滤
|
||
const categoryId = query.where?.categoryId;
|
||
const categoryIds = query.where?.categoryIds;
|
||
|
||
// 处理排序参数
|
||
const orderBy = query.orderBy;
|
||
|
||
// 模糊搜索 name,支持多个关键词
|
||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||
if (nameFilter.length > 0) {
|
||
const nameConditions = nameFilter
|
||
.map((word, index) => `product.name LIKE :name${index}`)
|
||
.join(' AND ');
|
||
const nameParams = nameFilter.reduce(
|
||
(params, word, index) => ({ ...params, [`name${index}`]: `%${word}%` }),
|
||
{}
|
||
);
|
||
qb.where(`(${nameConditions})`, nameParams);
|
||
}
|
||
|
||
// 处理产品ID过滤
|
||
if (query.where?.id) {
|
||
qb.andWhere('product.id = :id', { id: query.where.id });
|
||
}
|
||
|
||
// 处理产品ID列表过滤
|
||
if (query.where?.ids && query.where.ids.length > 0) {
|
||
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
|
||
}
|
||
|
||
// 处理SKU过滤
|
||
if (query.where?.sku) {
|
||
qb.andWhere('product.sku LIKE :sku', { sku: `%${query.where.sku}%` });
|
||
}
|
||
|
||
// 处理SKU列表过滤
|
||
if (query.where?.skus && query.where.skus.length > 0) {
|
||
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
||
}
|
||
|
||
// 处理产品中文名称过滤
|
||
if (query.where?.nameCn) {
|
||
qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` });
|
||
}
|
||
|
||
// 处理产品类型过滤
|
||
if (query.where?.type) {
|
||
qb.andWhere('product.type = :type', { type: query.where.type });
|
||
}
|
||
|
||
// 处理价格范围过滤
|
||
if (query.where?.minPrice !== undefined) {
|
||
qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice });
|
||
}
|
||
|
||
if (query.where?.maxPrice !== undefined) {
|
||
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
|
||
}
|
||
|
||
// 处理促销价格范围过滤
|
||
if (query.where?.minPromotionPrice !== undefined) {
|
||
qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice });
|
||
}
|
||
|
||
if (query.where?.maxPromotionPrice !== undefined) {
|
||
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
|
||
}
|
||
|
||
// 处理创建时间范围过滤
|
||
if (query.where?.createdAtStart) {
|
||
qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) });
|
||
}
|
||
|
||
if (query.where?.createdAtEnd) {
|
||
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
|
||
}
|
||
|
||
// 处理更新时间范围过滤
|
||
if (query.where?.updatedAtStart) {
|
||
qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) });
|
||
}
|
||
|
||
if (query.where?.updatedAtEnd) {
|
||
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.dict_id = 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;
|
||
});
|
||
}
|
||
});
|
||
|
||
// 品牌过滤(向后兼容)
|
||
if (brandId) {
|
||
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,
|
||
})
|
||
.getQuery();
|
||
return 'product.id IN ' + subQuery;
|
||
});
|
||
}
|
||
|
||
// 处理品牌ID列表过滤
|
||
if (brandIds && brandIds.length > 0) {
|
||
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 IN (:...brandIds)', {
|
||
brandIds,
|
||
})
|
||
.getQuery();
|
||
return 'product.id IN ' + subQuery;
|
||
});
|
||
}
|
||
|
||
// 分类过滤(向后兼容)
|
||
if (categoryId) {
|
||
qb.andWhere('product.categoryId = :categoryId', { categoryId });
|
||
}
|
||
|
||
// 处理分类ID列表过滤
|
||
if (categoryIds && categoryIds.length > 0) {
|
||
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
|
||
}
|
||
|
||
// 处理排序(支持新旧两种格式)
|
||
if (orderBy) {
|
||
if (typeof orderBy === 'string') {
|
||
// 如果orderBy是字符串,尝试解析JSON
|
||
try {
|
||
const orderByObj = JSON.parse(orderBy);
|
||
Object.keys(orderByObj).forEach(key => {
|
||
const order = orderByObj[key].toUpperCase();
|
||
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||
if (allowedSortFields.includes(key)) {
|
||
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||
}
|
||
});
|
||
} catch (e) {
|
||
// 解析失败,使用默认排序
|
||
qb.orderBy('product.createdAt', 'DESC');
|
||
}
|
||
} else if (typeof orderBy === 'object') {
|
||
// 如果orderBy是对象,直接使用
|
||
Object.keys(orderBy).forEach(key => {
|
||
const order = orderBy[key].toUpperCase();
|
||
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||
if (allowedSortFields.includes(key)) {
|
||
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
qb.orderBy('product.createdAt', 'DESC');
|
||
}
|
||
|
||
// 分页
|
||
qb.skip((page - 1) * pageSize).take(pageSize);
|
||
|
||
const [items, total] = await qb.getManyAndCount();
|
||
|
||
// 根据类型填充组成信息
|
||
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,
|
||
current: page,
|
||
pageSize,
|
||
};
|
||
}
|
||
|
||
async getProductListGrouped(query: UnifiedSearchParamsDTO<ProductWhereFilter>): Promise<Record<string, Product[]>> {
|
||
// 创建查询构建器
|
||
const qb = this.productModel
|
||
.createQueryBuilder('product')
|
||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||
.leftJoinAndSelect('attribute.dict', 'dict')
|
||
.leftJoinAndSelect('product.category', 'category');
|
||
|
||
// 验证分组字段
|
||
const groupBy = query.groupBy;
|
||
if (!groupBy) {
|
||
throw new Error('分组字段不能为空');
|
||
}
|
||
|
||
// 处理搜索参数
|
||
const name = query.where?.name || '';
|
||
|
||
// 处理品牌过滤
|
||
const brandId = query.where?.brandId;
|
||
const brandIds = query.where?.brandIds;
|
||
|
||
// 处理分类过滤
|
||
const categoryId = query.where?.categoryId;
|
||
const categoryIds = query.where?.categoryIds;
|
||
|
||
// 处理排序参数
|
||
const orderBy = query.orderBy;
|
||
|
||
// 模糊搜索 name,支持多个关键词
|
||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||
if (nameFilter.length > 0) {
|
||
const nameConditions = nameFilter
|
||
.map((word, index) => `product.name LIKE :name${index}`)
|
||
.join(' AND ');
|
||
const nameParams = nameFilter.reduce(
|
||
(params, word, index) => ({ ...params, [`name${index}`]: `%${word}%` }),
|
||
{}
|
||
);
|
||
qb.where(`(${nameConditions})`, nameParams);
|
||
}
|
||
|
||
// 处理产品ID过滤
|
||
if (query.where?.id) {
|
||
qb.andWhere('product.id = :id', { id: query.where.id });
|
||
}
|
||
|
||
// 处理产品ID列表过滤
|
||
if (query.where?.ids && query.where.ids.length > 0) {
|
||
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
|
||
}
|
||
|
||
// 处理SKU过滤
|
||
if (query.where?.sku) {
|
||
qb.andWhere('product.sku LIKE :sku', { sku: `%${query.where.sku}%` });
|
||
}
|
||
|
||
// 处理SKU列表过滤
|
||
if (query.where?.skus && query.where.skus.length > 0) {
|
||
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
||
}
|
||
|
||
// 处理产品中文名称过滤
|
||
if (query.where?.nameCn) {
|
||
qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` });
|
||
}
|
||
|
||
// 处理产品类型过滤
|
||
if (query.where?.type) {
|
||
qb.andWhere('product.type = :type', { type: query.where.type });
|
||
}
|
||
|
||
// 处理价格范围过滤
|
||
if (query.where?.minPrice !== undefined) {
|
||
qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice });
|
||
}
|
||
|
||
if (query.where?.maxPrice !== undefined) {
|
||
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
|
||
}
|
||
|
||
// 处理促销价格范围过滤
|
||
if (query.where?.minPromotionPrice !== undefined) {
|
||
qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice });
|
||
}
|
||
|
||
if (query.where?.maxPromotionPrice !== undefined) {
|
||
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
|
||
}
|
||
|
||
// 处理创建时间范围过滤
|
||
if (query.where?.createdAtStart) {
|
||
qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) });
|
||
}
|
||
|
||
if (query.where?.createdAtEnd) {
|
||
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
|
||
}
|
||
|
||
// 处理更新时间范围过滤
|
||
if (query.where?.updatedAtStart) {
|
||
qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) });
|
||
}
|
||
|
||
if (query.where?.updatedAtEnd) {
|
||
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.dict_id = 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;
|
||
});
|
||
}
|
||
});
|
||
|
||
// 品牌过滤(向后兼容)
|
||
if (brandId) {
|
||
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,
|
||
})
|
||
.getQuery();
|
||
return 'product.id IN ' + subQuery;
|
||
});
|
||
}
|
||
|
||
// 处理品牌ID列表过滤
|
||
if (brandIds && brandIds.length > 0) {
|
||
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 IN (:...brandIds)', {
|
||
brandIds,
|
||
})
|
||
.getQuery();
|
||
return 'product.id IN ' + subQuery;
|
||
});
|
||
}
|
||
|
||
// 分类过滤(向后兼容)
|
||
if (categoryId) {
|
||
qb.andWhere('product.categoryId = :categoryId', { categoryId });
|
||
}
|
||
|
||
// 处理分类ID列表过滤
|
||
if (categoryIds && categoryIds.length > 0) {
|
||
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
|
||
}
|
||
|
||
// 处理排序(支持新旧两种格式)
|
||
if (orderBy) {
|
||
if (typeof orderBy === 'string') {
|
||
// 如果orderBy是字符串,尝试解析JSON
|
||
try {
|
||
const orderByObj = JSON.parse(orderBy);
|
||
Object.keys(orderByObj).forEach(key => {
|
||
const order = orderByObj[key].toUpperCase();
|
||
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||
if (allowedSortFields.includes(key)) {
|
||
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||
}
|
||
});
|
||
} catch (e) {
|
||
// 解析失败,使用默认排序
|
||
qb.orderBy('product.createdAt', 'DESC');
|
||
}
|
||
} else if (typeof orderBy === 'object') {
|
||
// 如果orderBy是对象,直接使用
|
||
Object.keys(orderBy).forEach(key => {
|
||
const order = orderBy[key].toUpperCase();
|
||
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||
if (allowedSortFields.includes(key)) {
|
||
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
qb.orderBy('product.createdAt', 'DESC');
|
||
}
|
||
|
||
// 执行查询
|
||
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 },
|
||
});
|
||
}
|
||
}
|
||
|
||
// 按指定字段分组
|
||
const groupedResult: Record<string, Product[]> = {};
|
||
|
||
// 检查是否按属性的字典名称分组
|
||
const isAttributeGrouping = await this.dictModel.findOne({ where: { name: groupBy } });
|
||
|
||
if (isAttributeGrouping) {
|
||
// 使用原生SQL查询获取每个产品对应的分组属性值
|
||
const attributeGroupQuery = `
|
||
SELECT product.id as productId, dict_item.id as attributeId, dict_item.name as attributeName, dict_item.title as attributeTitle
|
||
FROM product
|
||
INNER JOIN product_attributes_dict_item ON product.id = product_attributes_dict_item.productId
|
||
INNER JOIN dict_item ON product_attributes_dict_item.dictItemId = dict_item.id
|
||
INNER JOIN dict ON dict_item.dict_id = dict.id
|
||
WHERE dict.name = ?
|
||
`;
|
||
|
||
const attributeGroupResults = await this.productModel.query(attributeGroupQuery, [groupBy]);
|
||
|
||
// 创建产品ID到分组值的映射
|
||
const productGroupMap: Record<number, string> = {};
|
||
attributeGroupResults.forEach((result: any) => {
|
||
productGroupMap[result.productId] = result.attributeName;
|
||
});
|
||
|
||
items.forEach(product => {
|
||
// 获取分组值
|
||
const groupValue = productGroupMap[product.id] || 'unknown';
|
||
// 转换为字符串作为键
|
||
const groupKey = String(groupValue);
|
||
|
||
// 初始化分组
|
||
if (!groupedResult[groupKey]) {
|
||
groupedResult[groupKey] = [];
|
||
}
|
||
|
||
// 添加产品到分组
|
||
groupedResult[groupKey].push(product);
|
||
});
|
||
} else {
|
||
// 按产品自身字段分组
|
||
items.forEach(product => {
|
||
// 获取分组值
|
||
const groupValue = product[groupBy as keyof Product];
|
||
// 转换为字符串作为键
|
||
const groupKey = String(groupValue);
|
||
|
||
// 初始化分组
|
||
if (!groupedResult[groupKey]) {
|
||
groupedResult[groupKey] = [];
|
||
}
|
||
|
||
// 添加产品到分组
|
||
groupedResult[groupKey].push(product);
|
||
});
|
||
}
|
||
|
||
return groupedResult;
|
||
}
|
||
|
||
async getOrCreateAttribute(
|
||
dictName: string,
|
||
itemTitle: string,
|
||
itemName?: string
|
||
): Promise<DictItem> {
|
||
// 查找字典
|
||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||
if (!dict) {
|
||
throw new Error(`字典 '${dictName}' 不存在`);
|
||
}
|
||
|
||
const nameForLookup = itemName || itemTitle;
|
||
|
||
// 查找字典项
|
||
let item = await this.dictItemModel.findOne({
|
||
where: { name: nameForLookup, dict: { id: dict.id } },
|
||
});
|
||
|
||
// 如果字典项不存在,则创建
|
||
if (!item) {
|
||
item = new DictItem();
|
||
item.title = itemTitle;
|
||
item.name = nameForLookup;
|
||
item.dict = dict;
|
||
await this.dictItemModel.save(item);
|
||
}
|
||
|
||
return item;
|
||
}
|
||
|
||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||
const { attributes, sku, categoryId, categoryName, type } = createProductDTO;
|
||
|
||
// 条件判断(校验属性输入)
|
||
// 当产品类型为 'bundle' 时,attributes 可以为空
|
||
// 当产品类型为 'single' 时,attributes 必须提供且不能为空
|
||
if (type === 'single') {
|
||
if (!Array.isArray(attributes) || attributes.length === 0) {
|
||
throw new Error('单品类型的属性列表不能为空');
|
||
}
|
||
}
|
||
|
||
// 解析属性输入(按 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']
|
||
});
|
||
}
|
||
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) {
|
||
let item: DictItem | null = null;
|
||
if (attr.id) {
|
||
// 如果传入了 id,直接查找字典项并使用,不强制要求 dictName
|
||
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
|
||
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
|
||
} else {
|
||
// 当未提供 id 时,需要 dictName 与 title/name 信息创建或获取字典项
|
||
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
|
||
const titleOrName = attr.title || attr.name;
|
||
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
|
||
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
|
||
}
|
||
resolvedAttributes.push(item);
|
||
}
|
||
|
||
// 检查完全相同属性组合是否已存在(避免重复)
|
||
// 仅当产品类型为 '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, ...simpleFields } = createProductDTO;
|
||
this.productModel.merge(product, simpleFields);
|
||
|
||
product.attributes = resolvedAttributes;
|
||
if (categoryItem) {
|
||
product.category = categoryItem;
|
||
}
|
||
// 确保默认类型
|
||
if (!product.type) product.type = 'single';
|
||
|
||
// 生成或设置 SKU(基于属性字典项的 name 生成)
|
||
if (sku) {
|
||
product.sku = sku;
|
||
} else {
|
||
product.sku = await this.templateService.render('product.sku', { product });
|
||
}
|
||
|
||
const savedProduct = await this.productModel.save(product);
|
||
|
||
// 保存组件信息
|
||
if (createProductDTO.components && createProductDTO.components.length > 0) {
|
||
await this.setProductComponents(savedProduct.id, createProductDTO.components);
|
||
// 重新加载带组件的产品
|
||
return await this.productModel.findOne({ where: { id: savedProduct.id }, relations: ['attributes', 'attributes.dict', 'category', 'components', 'siteSkus'] });
|
||
}
|
||
|
||
return savedProduct;
|
||
}
|
||
|
||
async updateProduct(
|
||
id: number,
|
||
updateProductDTO: UpdateProductDTO
|
||
): Promise<Product> {
|
||
// 检查产品是否存在(包含属性关系)
|
||
const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict', 'category'] });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 使用 merge 更新基础字段,排除特殊处理字段
|
||
const { attributes, categoryId, categoryName, sku, 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 (!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 } });
|
||
if (exist) {
|
||
throw new Error('SKU 已存在,请更换后重试');
|
||
}
|
||
product.sku = newSku;
|
||
}
|
||
}
|
||
|
||
// 处理属性更新(若传入 attributes 则按字典名称替换对应项)
|
||
if (Array.isArray(updateProductDTO.attributes) && updateProductDTO.attributes.length > 0) {
|
||
const nextAttributes: DictItem[] = [...(product.attributes || [])];
|
||
|
||
const replaceAttr = (dictName: string, item: DictItem) => {
|
||
const idx = nextAttributes.findIndex(a => a.dict?.name === dictName);
|
||
if (idx >= 0) nextAttributes[idx] = item; else nextAttributes.push(item);
|
||
};
|
||
|
||
for (const attr of updateProductDTO.attributes) {
|
||
|
||
let item: DictItem | null = null;
|
||
if (attr.id) {
|
||
// 当提供 id 时直接查询字典项,不强制要求 dictName
|
||
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
|
||
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
|
||
} else {
|
||
// 未提供 id 则需要 dictName 与 title/name 信息
|
||
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
|
||
const titleOrName = attr.title || attr.name;
|
||
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
|
||
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
|
||
}
|
||
// 以传入的 dictName 或查询到的 item.dict.name 作为替换键
|
||
const dictKey = attr.dictName || item?.dict?.name;
|
||
if (!dictKey) throw new Error('无法确定字典名称用于替换属性');
|
||
replaceAttr(dictKey, item);
|
||
}
|
||
|
||
product.attributes = nextAttributes;
|
||
}
|
||
|
||
// 条件判断(更新商品类型,如传入)
|
||
if (updateProductDTO.type !== undefined) {
|
||
product.type = updateProductDTO.type as any;
|
||
}
|
||
|
||
// 保存更新后的产品
|
||
const saved = await this.productModel.save(product);
|
||
|
||
// 处理组件更新
|
||
if (updateProductDTO.components !== undefined) {
|
||
// 如果 components 为空数组,则删除所有组件? setProductComponents 会处理
|
||
await this.setProductComponents(saved.id, updateProductDTO.components);
|
||
}
|
||
|
||
return saved;
|
||
}
|
||
|
||
async batchUpdateProduct(
|
||
batchUpdateProductDTO: BatchUpdateProductDTO
|
||
): Promise<boolean> {
|
||
const { ids, ...updateData } = batchUpdateProductDTO;
|
||
if (!ids || ids.length === 0) {
|
||
throw new Error('未选择任何产品');
|
||
}
|
||
|
||
// 检查 updateData 中是否有复杂字段 (attributes, categoryId, type, sku)
|
||
// 如果包含复杂字段,需要复用 updateProduct 的逻辑
|
||
const hasComplexFields =
|
||
updateData.attributes !== undefined ||
|
||
updateData.categoryId !== undefined ||
|
||
updateData.type !== undefined ||
|
||
updateData.sku !== undefined;
|
||
|
||
if (hasComplexFields) {
|
||
// 循环调用 updateProduct
|
||
for (const id of ids) {
|
||
const updateDTO = new UpdateProductDTO();
|
||
// 复制属性
|
||
Object.assign(updateDTO, updateData);
|
||
await this.updateProduct(id, updateDTO);
|
||
}
|
||
} else {
|
||
// 简单字段,直接批量更新以提高性能
|
||
// UpdateProductDTO 里的简单字段: name, nameCn, description, shortDescription, price, promotionPrice, image, siteSkus
|
||
|
||
const simpleUpdate: any = {};
|
||
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
||
if (updateData.nameCn !== undefined) simpleUpdate.nameCn = updateData.nameCn;
|
||
if (updateData.description !== undefined) simpleUpdate.description = updateData.description;
|
||
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.image !== undefined) simpleUpdate.image = updateData.image;
|
||
if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus;
|
||
|
||
if (Object.keys(simpleUpdate).length > 0) {
|
||
await this.productModel.update({ id: In(ids) }, simpleUpdate);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
async batchDeleteProduct(ids: number[]): Promise<{ success: number; failed: number; errors: string[] }> {
|
||
if (!ids || ids.length === 0) {
|
||
throw new Error('未选择任何产品');
|
||
}
|
||
|
||
let success = 0;
|
||
let failed = 0;
|
||
const errors: string[] = [];
|
||
|
||
for (const id of ids) {
|
||
try {
|
||
await this.deleteProduct(id);
|
||
success++;
|
||
} catch (error) {
|
||
failed++;
|
||
errors.push(`ID ${id}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
return { success, failed, errors };
|
||
}
|
||
|
||
// 获取产品的库存组成列表(表关联版本)
|
||
async getProductComponents(productId: number): Promise<any[]> {
|
||
// 条件判断:确保产品存在
|
||
const product = await this.productModel.findOne({ where: { id: productId } });
|
||
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||
|
||
let components: ProductStockComponent[] = [];
|
||
// 条件判断(单品 simple 不持久化组成,按 SKU 动态返回单条组成)
|
||
if (product.type === 'single') {
|
||
const comp = new ProductStockComponent();
|
||
comp.productId = productId;
|
||
comp.sku = product.sku;
|
||
comp.quantity = 1;
|
||
components = [comp];
|
||
} else {
|
||
// 混装 bundle:返回已保存的 SKU 组成
|
||
components = await this.productStockComponentModel.find({ where: { productId } });
|
||
}
|
||
|
||
// 获取所有组件的 SKU 列表
|
||
const skus = components.map(c => c.sku);
|
||
if (skus.length === 0) {
|
||
return components;
|
||
}
|
||
|
||
// 查询这些 SKU 的库存信息
|
||
const stocks = await this.stockModel.find({
|
||
where: { sku: In(skus) },
|
||
});
|
||
|
||
// 获取所有相关的库存点 ID
|
||
const stockPointIds = [...new Set(stocks.map(s => s.stockPointId))];
|
||
const stockPoints = await this.stockPointModel.find({ where: { id: In(stockPointIds) } });
|
||
const stockPointMap = stockPoints.reduce((map, sp) => {
|
||
map[sp.id] = sp;
|
||
return map;
|
||
}, {});
|
||
|
||
// 将库存信息按 SKU 分组
|
||
const stockMap = stocks.reduce((map, stock) => {
|
||
if (!map[stock.sku]) {
|
||
map[stock.sku] = [];
|
||
}
|
||
const stockPoint = stockPointMap[stock.stockPointId];
|
||
if (stockPoint) {
|
||
map[stock.sku].push({
|
||
name: stockPoint.name,
|
||
quantity: stock.quantity,
|
||
});
|
||
}
|
||
return map;
|
||
}, {});
|
||
|
||
// 将库存信息附加到组件上
|
||
const componentsWithStock = components.map(comp => {
|
||
return {
|
||
...comp,
|
||
stock: stockMap[comp.sku] || [],
|
||
};
|
||
});
|
||
|
||
return componentsWithStock;
|
||
}
|
||
|
||
// 设置产品的库存组成(覆盖式,表关联版本)
|
||
async setProductComponents(
|
||
productId: number,
|
||
items: { sku: string; quantity: number }[]
|
||
): Promise<ProductStockComponent[]> {
|
||
// 条件判断:确保产品存在
|
||
const product = await this.productModel.findOne({ where: { id: productId } });
|
||
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||
// 条件判断(单品 simple 不允许手动设置组成)
|
||
if (product.type === 'single') {
|
||
// 单品类型,直接清空关联的组成(如果有)
|
||
await this.productStockComponentModel.delete({ productId });
|
||
return [];
|
||
}
|
||
|
||
const validItems = (items || [])
|
||
.filter(i => i && i.sku && i.quantity && i.quantity > 0)
|
||
.map(i => ({ sku: String(i.sku), quantity: Number(i.quantity) }));
|
||
|
||
// 删除旧的组成
|
||
await this.productStockComponentModel.delete({ productId });
|
||
|
||
// 插入新的组成
|
||
const created: ProductStockComponent[] = [];
|
||
for (const i of validItems) {
|
||
// 校验 SKU 格式,允许不存在库存但必须非空
|
||
if (!i.sku || i.sku.trim().length === 0) {
|
||
throw new Error('SKU 不能为空');
|
||
}
|
||
const comp = new ProductStockComponent();
|
||
comp.productId = productId;
|
||
comp.sku = i.sku;
|
||
comp.quantity = i.quantity;
|
||
created.push(await this.productStockComponentModel.save(comp));
|
||
}
|
||
return created;
|
||
}
|
||
|
||
// 根据 SKU 自动绑定产品的库存组成(匹配所有相同 SKU 的库存,默认数量 1)
|
||
async autoBindComponentsBySku(productId: number): Promise<ProductStockComponent[]> {
|
||
// 条件判断:确保产品存在
|
||
const product = await this.productModel.findOne({ where: { id: productId } });
|
||
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||
// 按 SKU 自动绑定
|
||
// 条件判断:simple 类型不持久化组成,直接返回单条基于 SKU 的组成
|
||
if (product.type === 'single') {
|
||
const comp = new ProductStockComponent();
|
||
comp.productId = productId;
|
||
comp.sku = product.sku;
|
||
comp.quantity = 1; // 默认数量 1
|
||
return [comp];
|
||
}
|
||
// bundle 类型:若不存在则持久化一条基于 SKU 的组成
|
||
const exist = await this.productStockComponentModel.findOne({ where: { productId, sku: product.sku } });
|
||
if (!exist) {
|
||
const comp = new ProductStockComponent();
|
||
comp.productId = productId;
|
||
comp.sku = product.sku;
|
||
comp.quantity = 1;
|
||
await this.productStockComponentModel.save(comp);
|
||
}
|
||
return await this.getProductComponents(productId);
|
||
}
|
||
|
||
|
||
// 重复定义的 getProductList 已合并到前面的实现(移除重复)
|
||
|
||
async updatenameCn(id: number, nameCn: string): Promise<Product> {
|
||
// 确认产品是否存在
|
||
const product = await this.productModel.findOneBy({ id });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${id} 不存在`);
|
||
}
|
||
// 更新产品
|
||
await this.productModel.update(id, { nameCn });
|
||
// 返回更新后的产品
|
||
return await this.productModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteProduct(id: number): Promise<boolean> {
|
||
// 检查产品是否存在
|
||
const product = await this.productModel.findOneBy({ id });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 不再阻塞于远端站点商品/变体的存在,删除仅按本地引用保护
|
||
|
||
// 删除产品
|
||
const result = await this.productModel.delete(id);
|
||
return result.affected > 0; // `affected` 表示删除的行数
|
||
}
|
||
|
||
|
||
async hasAttribute(
|
||
dictName: string,
|
||
title: string,
|
||
id?: number
|
||
): Promise<boolean> {
|
||
const dict = await this.dictModel.findOne({
|
||
where: { name: dictName },
|
||
});
|
||
if (!dict) {
|
||
return false;
|
||
}
|
||
const where: any = { title, dict: { id: dict.id } };
|
||
if (id) where.id = Not(id);
|
||
const count = await this.dictItemModel.count({
|
||
where,
|
||
});
|
||
return count > 0;
|
||
}
|
||
|
||
async hasProductsInAttribute(attributeId: number): Promise<boolean> {
|
||
const count = await this.productModel
|
||
.createQueryBuilder('product')
|
||
.innerJoin('product.attributes', 'attribute')
|
||
.where('attribute.id = :attributeId', { attributeId })
|
||
.getCount();
|
||
return count > 0;
|
||
}
|
||
|
||
async getBrandList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<BrandPaginatedResponse> {
|
||
// 查找 'brand' 字典
|
||
const brandDict = await this.dictModel.findOne({
|
||
where: { name: 'brand' },
|
||
});
|
||
|
||
// 如果字典不存在,则返回空
|
||
if (!brandDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
};
|
||
}
|
||
|
||
// 设置查询条件
|
||
const where: any = { dict: { id: brandDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
|
||
// 分页查询
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getBrandAll(): Promise<BrandPaginatedResponse> {
|
||
// 查找 'brand' 字典
|
||
const brandDict = await this.dictModel.findOne({
|
||
where: { name: 'brand' },
|
||
});
|
||
|
||
// 如果字典不存在,则返回空数组
|
||
if (!brandDict) {
|
||
return [];
|
||
}
|
||
|
||
// 返回所有品牌
|
||
return this.dictItemModel.find({ where: { dict: { id: brandDict.id } } });
|
||
}
|
||
|
||
async createBrand(createBrandDTO: any): Promise<DictItem> {
|
||
const { title, name } = createBrandDTO;
|
||
|
||
// 查找 'brand' 字典
|
||
const brandDict = await this.dictModel.findOne({
|
||
where: { name: 'brand' },
|
||
});
|
||
|
||
// 如果字典不存在,则抛出错误
|
||
if (!brandDict) {
|
||
throw new Error('品牌字典不存在');
|
||
}
|
||
|
||
// 创建新的品牌实例
|
||
const brand = new DictItem();
|
||
brand.title = title;
|
||
brand.name = name;
|
||
brand.dict = brandDict;
|
||
|
||
// 保存到数据库
|
||
return await this.dictItemModel.save(brand);
|
||
}
|
||
|
||
async updateBrand(id: number, updateBrand: any) {
|
||
// 确认品牌是否存在
|
||
const brand = await this.dictItemModel.findOneBy({ id });
|
||
if (!brand) {
|
||
throw new Error(`品牌 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 更新品牌
|
||
await this.dictItemModel.update(id, updateBrand);
|
||
|
||
// 返回更新后的品牌
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteBrand(id: number): Promise<boolean> {
|
||
// 检查品牌是否存在
|
||
const brand = await this.dictItemModel.findOneBy({ id });
|
||
if (!brand) {
|
||
throw new Error(`品牌 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 删除品牌
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0; // `affected` 表示删除的行数
|
||
}
|
||
|
||
|
||
|
||
|
||
async getFlavorsList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<FlavorsPaginatedResponse> {
|
||
const flavorsDict = await this.dictModel.findOne({
|
||
where: { name: 'flavor' },
|
||
});
|
||
if (!flavorsDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
};
|
||
}
|
||
const where: any = { dict: { id: flavorsDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getFlavorsAll(): Promise<FlavorsPaginatedResponse> {
|
||
const flavorsDict = await this.dictModel.findOne({
|
||
where: { name: 'flavor' },
|
||
});
|
||
if (!flavorsDict) {
|
||
return [];
|
||
}
|
||
return this.dictItemModel.find({ where: { dict: { id: flavorsDict.id } } });
|
||
}
|
||
|
||
async createFlavors(createFlavorsDTO: any): Promise<DictItem> {
|
||
const { title, name } = createFlavorsDTO;
|
||
const flavorsDict = await this.dictModel.findOne({
|
||
where: { name: 'flavor' },
|
||
});
|
||
if (!flavorsDict) {
|
||
throw new Error('口味字典不存在');
|
||
}
|
||
const flavors = new DictItem();
|
||
flavors.title = title;
|
||
flavors.name = name;
|
||
flavors.dict = flavorsDict;
|
||
return await this.dictItemModel.save(flavors);
|
||
}
|
||
|
||
async updateFlavors(id: number, updateFlavors: any) {
|
||
const flavors = await this.dictItemModel.findOneBy({ id });
|
||
if (!flavors) {
|
||
throw new Error(`口味 ID ${id} 不存在`);
|
||
}
|
||
await this.dictItemModel.update(id, updateFlavors);
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteFlavors(id: number): Promise<boolean> {
|
||
const flavors = await this.dictItemModel.findOneBy({ id });
|
||
if (!flavors) {
|
||
throw new Error(`口味 ID ${id} 不存在`);
|
||
}
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
// size 尺寸相关方法
|
||
async getSizeList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<SizePaginatedResponse> {
|
||
// 查找 'size' 字典(用于尺寸)
|
||
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||
// 条件判断(如果字典不存在则返回空分页)
|
||
if (!sizeDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
} as any;
|
||
}
|
||
// 构建 where 条件(按标题模糊搜索)
|
||
const where: any = { dict: { id: sizeDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
// 分页查询(复用通用分页工具)
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getSizeAll(): Promise<SizePaginatedResponse> {
|
||
// 查找 'size' 字典(获取所有尺寸项)
|
||
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||
// 条件判断(如果字典不存在返回空数组)
|
||
if (!sizeDict) {
|
||
return [] as any;
|
||
}
|
||
return this.dictItemModel.find({ where: { dict: { id: sizeDict.id } } }) as any;
|
||
}
|
||
|
||
async createSize(createSizeDTO: any): Promise<DictItem> {
|
||
const { title, name } = createSizeDTO;
|
||
// 获取 size 字典(用于挂载尺寸项)
|
||
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||
// 条件判断(尺寸字典不存在则抛错)
|
||
if (!sizeDict) {
|
||
throw new Error('尺寸字典不存在');
|
||
}
|
||
// 创建字典项(保存尺寸名称与唯一标识)
|
||
const size = new DictItem();
|
||
size.title = title;
|
||
size.name = name;
|
||
size.dict = sizeDict;
|
||
return await this.dictItemModel.save(size);
|
||
}
|
||
|
||
async updateSize(id: number, updateSize: any) {
|
||
// 先查询(确保尺寸项存在)
|
||
const size = await this.dictItemModel.findOneBy({ id });
|
||
// 条件判断(不存在则报错)
|
||
if (!size) {
|
||
throw new Error(`尺寸 ID ${id} 不存在`);
|
||
}
|
||
// 更新(写入变更字段)
|
||
await this.dictItemModel.update(id, updateSize);
|
||
// 返回最新(再次查询返回)
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteSize(id: number): Promise<boolean> {
|
||
// 先查询(确保尺寸项存在)
|
||
const size = await this.dictItemModel.findOneBy({ id });
|
||
// 条件判断(不存在则报错)
|
||
if (!size) {
|
||
throw new Error(`尺寸 ID ${id} 不存在`);
|
||
}
|
||
// 删除(执行删除并返回受影响行数是否>0)
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
|
||
async hasStrength(title: string, id?: string): Promise<boolean> {
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
return false;
|
||
}
|
||
const where: any = { title, dict: { id: strengthDict.id } };
|
||
if (id) where.id = Not(id);
|
||
const count = await this.dictItemModel.count({
|
||
where,
|
||
});
|
||
return count > 0;
|
||
}
|
||
async getStrengthList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<StrengthPaginatedResponse> {
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
};
|
||
}
|
||
const where: any = { dict: { id: strengthDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getStrengthAll(): Promise<StrengthPaginatedResponse> {
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
return [];
|
||
}
|
||
return this.dictItemModel.find({ where: { dict: { id: strengthDict.id } } });
|
||
}
|
||
|
||
async createStrength(createStrengthDTO: any): Promise<DictItem> {
|
||
const { title, name } = createStrengthDTO;
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
throw new Error('规格字典不存在');
|
||
}
|
||
const strength = new DictItem();
|
||
strength.title = title;
|
||
strength.name = name;
|
||
strength.dict = strengthDict;
|
||
return await this.dictItemModel.save(strength);
|
||
}
|
||
|
||
// 通用属性:分页获取指定字典的字典项
|
||
async getAttributeList(
|
||
dictName: string,
|
||
pagination: PaginationParams,
|
||
name?: string
|
||
): Promise<BrandPaginatedResponse> {
|
||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||
if (!dict) return { items: [], total: 0, ...pagination } as any;
|
||
const where: any = { dict: { id: dict.id } };
|
||
if (name) where.title = Like(`%${name}%`);
|
||
const [items, total] = await this.dictItemModel.findAndCount({
|
||
where,
|
||
skip: (pagination.current - 1) * pagination.pageSize,
|
||
take: pagination.pageSize,
|
||
order: { sort: 'ASC', id: 'DESC' },
|
||
relations: ['dict'],
|
||
});
|
||
return { items, total, ...pagination } as any;
|
||
}
|
||
|
||
// 通用属性:获取指定字典的全部字典项
|
||
async getAttributeAll(dictName: string): Promise<DictItem[]> {
|
||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||
if (!dict) return [];
|
||
return this.dictItemModel.find({
|
||
where: { dict: { id: dict.id } },
|
||
order: { sort: 'ASC', id: 'DESC' },
|
||
relations: ['dict'],
|
||
});
|
||
}
|
||
|
||
// 通用属性:创建字典项
|
||
async createAttribute(
|
||
dictName: string,
|
||
payload: { title: string; name: string; image?: string; shortName?: string }
|
||
): Promise<DictItem> {
|
||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
|
||
const exists = await this.dictItemModel.findOne({
|
||
where: { name: payload.name, dict: { id: dict.id } },
|
||
relations: ['dict'],
|
||
});
|
||
if (exists) throw new Error('字典项已存在');
|
||
const item = new DictItem();
|
||
item.title = payload.title;
|
||
item.name = payload.name;
|
||
item.image = payload.image;
|
||
item.shortName = payload.shortName;
|
||
item.dict = dict;
|
||
return await this.dictItemModel.save(item);
|
||
}
|
||
|
||
// 通用属性:更新字典项
|
||
async updateAttribute(
|
||
id: number,
|
||
payload: { title?: string; name?: string; image?: string; shortName?: string }
|
||
): Promise<DictItem> {
|
||
const item = await this.dictItemModel.findOne({ where: { id } });
|
||
if (!item) throw new Error('字典项不存在');
|
||
if (payload.title !== undefined) item.title = payload.title;
|
||
if (payload.name !== undefined) item.name = payload.name;
|
||
if (payload.image !== undefined) item.image = payload.image;
|
||
if (payload.shortName !== undefined) item.shortName = payload.shortName;
|
||
return await this.dictItemModel.save(item);
|
||
}
|
||
|
||
// 通用属性:删除字典项(若存在产品关联则禁止删除)
|
||
async deleteAttribute(id: number): Promise<void> {
|
||
const hasProducts = await this.hasProductsInAttribute(id);
|
||
if (hasProducts) throw new Error('当前字典项存在关联产品,无法删除');
|
||
await this.dictItemModel.delete({ id });
|
||
}
|
||
|
||
async updateStrength(id: number, updateStrength: any) {
|
||
const strength = await this.dictItemModel.findOneBy({ id });
|
||
if (!strength) {
|
||
throw new Error(`规格 ID ${id} 不存在`);
|
||
}
|
||
await this.dictItemModel.update(id, updateStrength);
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteStrength(id: number): Promise<boolean> {
|
||
const strength = await this.dictItemModel.findOneBy({ id });
|
||
if (!strength) {
|
||
throw new Error(`规格 ID ${id} 不存在`);
|
||
}
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
async batchSetSku(skus: { productId: number; sku: string }[]) {
|
||
// 提取所有 sku
|
||
const skuList = skus.map(item => item.sku);
|
||
|
||
// 检查是否存在重复 sku
|
||
const existingProducts = await this.productModel.find({
|
||
where: { sku: In(skuList) },
|
||
});
|
||
|
||
if (existingProducts.length > 0) {
|
||
const existingSkus = existingProducts.map(product => product.sku);
|
||
throw new Error(`以下 SKU 已存在: ${existingSkus.join(', ')}`);
|
||
}
|
||
|
||
// 遍历检查产品 ID 是否存在,并更新 sku
|
||
for (const { productId, sku } of skus) {
|
||
const product = await this.productModel.findOne({
|
||
where: { id: productId },
|
||
});
|
||
if (!product) {
|
||
throw new Error(`产品 ID '${productId}' 不存在`);
|
||
}
|
||
|
||
product.sku = sku;
|
||
await this.productModel.save(product);
|
||
}
|
||
|
||
return `成功更新 ${skus.length} 个 sku`;
|
||
}
|
||
|
||
// 将单条 CSV 记录转换为数据对象
|
||
mapTableRecordToProduct(rec: any): CreateProductDTO | UpdateProductDTO | null {
|
||
const keys = Object.keys(rec);
|
||
// 必须包含 sku
|
||
const sku: string = (rec.sku || '').trim();
|
||
if (!sku) {
|
||
return null;
|
||
}
|
||
|
||
// 辅助函数:处理空字符串为 undefined
|
||
const val = (v: any) => {
|
||
if (v === undefined || v === null) return undefined;
|
||
const s = String(v).trim();
|
||
return s === '' ? undefined : s;
|
||
};
|
||
|
||
// 辅助函数:处理数字
|
||
const num = (v: any) => {
|
||
const s = val(v);
|
||
return s ? Number(s) : undefined;
|
||
};
|
||
|
||
// 解析属性字段(分号分隔多值)
|
||
const parseList = (v: string) => (v && String(v).split(/[;,]/).map(s => s.trim()).filter(Boolean));
|
||
|
||
// 将属性解析为 DTO 输入
|
||
const attributes: any[] = [];
|
||
|
||
// 处理动态属性字段 (attribute_*)
|
||
for (const key of keys) {
|
||
if (key.startsWith('attribute_')) {
|
||
const dictName = key.replace('attribute_', '');
|
||
if (dictName) {
|
||
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);
|
||
|
||
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,
|
||
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,
|
||
}
|
||
}
|
||
|
||
// 准备创建产品的 DTO, 处理类型转换和默认值
|
||
prepareCreateProductDTO(data: any): CreateProductDTO {
|
||
const dto = new CreateProductDTO();
|
||
// 基础字段赋值
|
||
dto.name = data.name;
|
||
dto.nameCn = data.nameCn;
|
||
dto.description = data.description;
|
||
dto.sku = data.sku;
|
||
if (data.siteSkus) dto.siteSkus = data.siteSkus;
|
||
|
||
// 数值类型转换
|
||
if (data.price !== undefined) dto.price = Number(data.price);
|
||
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
|
||
|
||
// 处理分类字段
|
||
if (data.categoryId !== undefined) {
|
||
dto.categoryId = Number(data.categoryId);
|
||
} else if (data.category) {
|
||
// 如果是字符串,需要后续在createProduct中处理
|
||
dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }];
|
||
}
|
||
|
||
// 默认值和特殊处理
|
||
dto.attributes = Array.isArray(data.attributes) ? data.attributes : [];
|
||
|
||
// 如果有组件信息,透传
|
||
dto.type = data.type || 'single';
|
||
|
||
return dto;
|
||
}
|
||
|
||
// 准备更新产品的 DTO, 处理类型转换
|
||
prepareUpdateProductDTO(data: any): UpdateProductDTO {
|
||
const dto = new UpdateProductDTO();
|
||
|
||
if (data.name !== undefined) dto.name = data.name;
|
||
if (data.nameCn !== undefined) dto.nameCn = data.nameCn;
|
||
if (data.description !== undefined) dto.description = data.description;
|
||
if (data.sku !== undefined) dto.sku = data.sku;
|
||
if (data.siteSkus !== undefined) dto.siteSkus = data.siteSkus;
|
||
|
||
if (data.price !== undefined) dto.price = Number(data.price);
|
||
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
|
||
|
||
// 处理分类字段
|
||
if (data.categoryId !== undefined) {
|
||
dto.categoryId = Number(data.categoryId);
|
||
} else if (data.category) {
|
||
// 如果是字符串,需要后续在updateProduct中处理
|
||
dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }];
|
||
}
|
||
|
||
if (data.type !== undefined) dto.type = data.type;
|
||
if (data.attributes !== undefined) dto.attributes = data.attributes;
|
||
if (data.components !== undefined) dto.components = data.components;
|
||
|
||
return dto;
|
||
}
|
||
getAttributesObject(attributes: DictItem[]) {
|
||
if (!attributes) return {}
|
||
const obj: any = {}
|
||
attributes.forEach(attr => {
|
||
obj[attr.dict.name] = attr
|
||
})
|
||
return obj
|
||
}
|
||
// 将单个产品转换为 CSV 行数组
|
||
transformProductToCsvRow(
|
||
p: Product,
|
||
sortedDictNames: string[],
|
||
maxComponentCount: number
|
||
): string[] {
|
||
// CSV 字段转义,处理逗号与双引号
|
||
const esc = (v: any) => {
|
||
const s = v === undefined || v === null ? '' : String(v);
|
||
const needsQuote = /[",\n]/.test(s);
|
||
const escaped = s.replace(/"/g, '""');
|
||
return needsQuote ? `"${escaped}"` : escaped;
|
||
};
|
||
|
||
// 将属性列表转为字典名到显示值的映射
|
||
const pickAttr = (prod: Product, key: string) => {
|
||
const list = (prod.attributes || []).filter(a => a?.dict?.name === key);
|
||
if (list.length === 0) return '';
|
||
// 多个值使用分号分隔
|
||
return list.map(a => a.title || a.name).join(';');
|
||
};
|
||
|
||
// 基础数据
|
||
const rowData = [
|
||
esc(p.sku),
|
||
esc(p.siteSkus ? p.siteSkus.join(',') : ''),
|
||
esc(p.name),
|
||
esc(p.nameCn),
|
||
esc(p.price),
|
||
esc(p.promotionPrice),
|
||
esc(p.type),
|
||
esc(p.description),
|
||
esc(p.category ? p.category.name || p.category.title : ''), // 添加分类字段
|
||
];
|
||
|
||
// 属性数据
|
||
for (const dictName of sortedDictNames) {
|
||
rowData.push(esc(pickAttr(p, dictName)));
|
||
}
|
||
|
||
// 组件数据
|
||
const components = p.components || [];
|
||
for (let i = 0; i < maxComponentCount; i++) {
|
||
const comp = components[i];
|
||
if (comp) {
|
||
rowData.push(esc(comp.sku));
|
||
rowData.push(esc(comp.quantity));
|
||
} else {
|
||
rowData.push('');
|
||
rowData.push('');
|
||
}
|
||
}
|
||
|
||
return rowData;
|
||
}
|
||
|
||
// 导出所有产品为 CSV 文本
|
||
async exportProductsCSV(): Promise<string> {
|
||
// 查询所有产品及其属性(包含字典关系)、组成和分类
|
||
const products = await this.productModel.find({
|
||
relations: ['attributes', 'attributes.dict', 'components', 'category'],
|
||
order: { id: 'ASC' },
|
||
});
|
||
|
||
// 1. 收集所有动态属性的 dictName
|
||
const dictNames = new Set<string>();
|
||
// 2. 收集最大的组件数量
|
||
let maxComponentCount = 0;
|
||
|
||
for (const p of products) {
|
||
if (p.attributes) {
|
||
for (const attr of p.attributes) {
|
||
if (attr.dict && attr.dict.name) {
|
||
dictNames.add(attr.dict.name);
|
||
}
|
||
}
|
||
}
|
||
if (p.components) {
|
||
if (p.components.length > maxComponentCount) {
|
||
maxComponentCount = p.components.length;
|
||
}
|
||
}
|
||
}
|
||
|
||
const sortedDictNames = Array.from(dictNames).sort();
|
||
|
||
// 定义 CSV 表头(与导入字段一致)
|
||
const baseHeaders = [
|
||
'sku',
|
||
'siteSkus',
|
||
'name',
|
||
'nameCn',
|
||
'price',
|
||
'promotionPrice',
|
||
'type',
|
||
'description',
|
||
'category',
|
||
];
|
||
|
||
// 动态属性表头
|
||
const attributeHeaders = sortedDictNames.map(name => `attribute_${name}`);
|
||
|
||
// 动态组件表头
|
||
const componentHeaders = [];
|
||
for (let i = 1; i <= maxComponentCount; i++) {
|
||
componentHeaders.push(`component_${i}_sku`);
|
||
componentHeaders.push(`component_${i}_quantity`);
|
||
}
|
||
|
||
const allHeaders = [...baseHeaders, ...attributeHeaders, ...componentHeaders];
|
||
|
||
const rows: string[] = [];
|
||
rows.push(allHeaders.join(','));
|
||
|
||
for (const p of products) {
|
||
const rowData = this.transformProductToCsvRow(p, sortedDictNames, maxComponentCount);
|
||
rows.push(rowData.join(','));
|
||
}
|
||
|
||
// 添加UTF-8 BOM以确保中文在Excel中正确显示
|
||
return '\ufeff' + rows.join('\n');
|
||
|
||
}
|
||
async getRecordsFromTable(file: any) {
|
||
// 解析文件(使用 xlsx 包自动识别文件类型并解析)
|
||
try {
|
||
let buffer: Buffer;
|
||
|
||
// 处理文件输入,获取 buffer
|
||
if (Buffer.isBuffer(file)) {
|
||
buffer = file;
|
||
}
|
||
else if (file?.data) {
|
||
if (typeof file.data === 'string') {
|
||
buffer = fs.readFileSync(file.data);
|
||
} else {
|
||
buffer = file.data;
|
||
}
|
||
} else {
|
||
throw new Error('无效的文件输入');
|
||
}
|
||
|
||
let records: any[] = []
|
||
// xlsx 包会自动根据文件内容识别文件类型(CSV 或 XLSX)
|
||
// 添加codepage: 65001以确保正确处理UTF-8编码的中文
|
||
const workbook = xlsx.read(buffer, { type: 'buffer', codepage: 65001 });
|
||
// 获取第一个工作表
|
||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||
// 将工作表转换为 JSON 数组
|
||
records = xlsx.utils.sheet_to_json(worksheet);
|
||
|
||
console.log('Parsed records count:', records.length);
|
||
if (records.length > 0) {
|
||
console.log('First record keys:', Object.keys(records[0]));
|
||
}
|
||
return records;
|
||
} catch (e: any) {
|
||
throw new Error(`文件解析失败:${e?.message || e}`);
|
||
}
|
||
}
|
||
|
||
// 从 CSV 导入产品;存在则更新,不存在则创建
|
||
async importProductsFromTable(file: any): Promise<BatchOperationResult> {
|
||
let created = 0;
|
||
let updated = 0;
|
||
const errors: BatchErrorItem[] = [];
|
||
const records = await this.getRecordsFromTable(file);
|
||
// 逐条处理记录
|
||
for (const rec of records) {
|
||
try {
|
||
const data = this.mapTableRecordToProduct(rec);
|
||
if (!data) {
|
||
errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过' });
|
||
continue;
|
||
}
|
||
const { sku } = data;
|
||
|
||
// 查找现有产品
|
||
const exist = await this.productModel.findOne({ where: { sku } });
|
||
|
||
if (!exist) {
|
||
// 创建新产品
|
||
// const createDTO = this.prepareCreateProductDTO(data);
|
||
await this.createProduct(data as CreateProductDTO)
|
||
created += 1;
|
||
} else {
|
||
// 更新产品
|
||
// const updateDTO = this.prepareUpdateProductDTO(data);
|
||
await this.updateProduct(exist.id, data);
|
||
updated += 1;
|
||
}
|
||
} catch (e: any) {
|
||
errors.push({ identifier: '' + rec.sku, error: `产品${rec?.sku}导入失败:${e?.message || String(e)}` });
|
||
}
|
||
}
|
||
|
||
return { total: records.length, processed: records.length - errors.length, created, updated, errors };
|
||
}
|
||
|
||
// 将库存记录的 sku 添加到产品单品中
|
||
async syncStockToProduct(): Promise<{ added: number; errors: string[] }> {
|
||
// 1. 获取所有库存记录的 SKU (去重)
|
||
const stockSkus = await this.stockModel
|
||
.createQueryBuilder('stock')
|
||
.select('DISTINCT(stock.sku)', 'sku')
|
||
.getRawMany();
|
||
|
||
const skus = stockSkus.map(s => s.sku).filter(Boolean);
|
||
let added = 0;
|
||
const errors: string[] = [];
|
||
|
||
// 2. 遍历 SKU,检查并添加
|
||
for (const sku of skus) {
|
||
try {
|
||
const exist = await this.productModel.findOne({ where: { sku } });
|
||
if (!exist) {
|
||
const product = new Product();
|
||
product.sku = sku;
|
||
product.name = sku; // 默认使用 SKU 作为名称
|
||
product.type = 'single';
|
||
product.price = 0;
|
||
product.promotionPrice = 0;
|
||
await this.productModel.save(product);
|
||
added++;
|
||
}
|
||
} catch (error) {
|
||
errors.push(`SKU ${sku} 添加失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
return { added, errors };
|
||
}
|
||
|
||
|
||
// 根据ID获取产品详情(包含站点SKU)
|
||
async getProductById(id: number): Promise<Product> {
|
||
const product = await this.productModel.findOne({
|
||
where: { id },
|
||
relations: ['category', 'attributes', 'attributes.dict', 'components']
|
||
});
|
||
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 根据类型填充组成信息
|
||
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 product;
|
||
}
|
||
|
||
// 根据站点SKU查询产品
|
||
async findProductBySiteSku(siteSku: string): Promise<Product> {
|
||
const product = await this.productModel.findOne({
|
||
where: { siteSkus: Like(`%${siteSku}%`) },
|
||
relations: ['category', 'attributes', 'attributes.dict', 'components']
|
||
});
|
||
|
||
if (!product) {
|
||
throw new Error(`站点SKU ${siteSku} 不存在`);
|
||
}
|
||
|
||
// 获取完整的产品信息,包含所有关联数据
|
||
return this.getProductById(product.id);
|
||
}
|
||
|
||
// 获取产品的站点SKU列表
|
||
async getProductSiteSkus(productId: number): Promise<string[]> {
|
||
const product = await this.productModel.findOne({ where: { id: productId } });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${productId} 不存在`);
|
||
}
|
||
return product.siteSkus || [];
|
||
}
|
||
|
||
// 绑定产品的站点SKU列表
|
||
async bindSiteSkus(productId: number, siteSkus: string[]): Promise<string[]> {
|
||
const product = await this.productModel.findOne({ where: { id: productId } });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${productId} 不存在`);
|
||
}
|
||
const normalizedSiteSkus = (siteSkus || [])
|
||
.map(c => String(c).trim())
|
||
.filter(c => c.length > 0);
|
||
product.siteSkus = normalizedSiteSkus;
|
||
await this.productModel.save(product);
|
||
return product.siteSkus || [];
|
||
}
|
||
|
||
/**
|
||
* 将本地产品同步到站点
|
||
* @param productId 本地产品ID
|
||
* @param siteId 站点ID
|
||
* @returns 同步结果
|
||
*/
|
||
async syncToSite(params: SyncProductToSiteDTO): Promise<any> {
|
||
// 获取本地产品信息
|
||
const localProduct = await this.getProductById(params.productId);
|
||
if (!localProduct) {
|
||
throw new Error(`本地产品 ID ${params.productId} 不存在`);
|
||
}
|
||
|
||
// 将本地产品转换为站点API所需格式
|
||
const unifiedProduct = await this.mapLocalToUnifiedProduct(localProduct, params.siteSku);
|
||
|
||
// 调用站点API的upsertProduct方法
|
||
try {
|
||
const result = await this.siteApiService.upsertProduct(params.siteId, unifiedProduct);
|
||
// 绑定站点SKU
|
||
await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]);
|
||
return result;
|
||
} catch (error) {
|
||
throw new Error(`同步产品到站点失败: ${error?.response?.data?.message ?? error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量将本地产品同步到站点
|
||
* @param siteId 站点ID
|
||
* @param data 产品站点SKU列表
|
||
* @returns 批量同步结果
|
||
*/
|
||
async batchSyncToSite(siteId: number, data: ProductSiteSkuDTO[]): Promise<SyncOperationResultDTO> {
|
||
const results: SyncOperationResultDTO = {
|
||
total: data.length,
|
||
processed: 0,
|
||
synced: 0,
|
||
errors: []
|
||
};
|
||
|
||
for (const item of data) {
|
||
try {
|
||
// 先同步产品到站点
|
||
await this.syncToSite({
|
||
productId: item.productId,
|
||
siteId,
|
||
siteSku: item.siteSku
|
||
});
|
||
|
||
results.synced++;
|
||
results.processed++;
|
||
} catch (error) {
|
||
results.processed++;
|
||
results.errors.push({
|
||
identifier: String(item.productId),
|
||
error: `产品ID ${item.productId} 同步失败: ${error.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 从站点同步产品到本地
|
||
* @param siteId 站点ID
|
||
* @param siteProductId 站点产品ID
|
||
* @returns 同步后的本地产品
|
||
*/
|
||
async syncProductFromSite(siteId: number, siteProductId: string | number, sku: string): Promise<any> {
|
||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||
const siteProduct = await adapter.getProduct({ id: siteProductId });
|
||
// 从站点获取产品信息
|
||
if (!siteProduct) {
|
||
throw new Error(`站点产品 ID ${siteProductId} 不存在`);
|
||
}
|
||
// 将站点产品转换为本地产品格式
|
||
const productData = await this.mapUnifiedToLocalProduct(siteProduct);
|
||
return await this.upsertProduct({ sku }, productData);
|
||
}
|
||
async upsertProduct(where: Partial<Pick<Product, 'id' | 'sku'>>, productData: any) {
|
||
const existingProduct = await this.productModel.findOne({ where: where });
|
||
if (existingProduct) {
|
||
// 更新现有产品
|
||
const updateData: UpdateProductDTO = productData;
|
||
return await this.updateProduct(existingProduct.id, updateData);
|
||
} else {
|
||
// 创建新产品
|
||
const createData: CreateProductDTO = productData;
|
||
return await this.createProduct(createData);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量从站点同步产品到本地
|
||
* @param siteId 站点ID
|
||
* @param siteProductIds 站点产品ID数组
|
||
* @returns 批量同步结果
|
||
*/
|
||
async batchSyncFromSite(siteId: number, data: Array<{ siteProductId: string, sku: string }>): Promise<{ synced: number, errors: string[] }> {
|
||
const results = {
|
||
synced: 0,
|
||
errors: []
|
||
};
|
||
|
||
for (const item of data) {
|
||
try {
|
||
await this.syncProductFromSite(siteId, item.siteProductId, item.sku);
|
||
results.synced++;
|
||
} catch (error) {
|
||
results.errors.push(`站点产品ID ${item.siteProductId} 同步失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 将站点产品转换为本地产品格式
|
||
* @param siteProduct 站点产品对象
|
||
* @returns 本地产品数据
|
||
*/
|
||
private async mapUnifiedToLocalProduct(siteProduct: any): Promise<CreateProductDTO> {
|
||
const productData: any = {
|
||
sku: siteProduct.sku,
|
||
name: siteProduct.name,
|
||
nameCn: siteProduct.name,
|
||
price: siteProduct.price ? parseFloat(siteProduct.price) : 0,
|
||
promotionPrice: siteProduct.sale_price ? parseFloat(siteProduct.sale_price) : 0,
|
||
description: siteProduct.description || '',
|
||
images: [],
|
||
attributes: [],
|
||
categoryId: null
|
||
};
|
||
|
||
// 处理图片
|
||
if (siteProduct.images && Array.isArray(siteProduct.images)) {
|
||
productData.images = siteProduct.images.map((img: any) => ({
|
||
url: img.src || img.url,
|
||
name: img.name || img.alt || '',
|
||
alt: img.alt || ''
|
||
}));
|
||
}
|
||
|
||
// 处理分类
|
||
if (siteProduct.categories && Array.isArray(siteProduct.categories) && siteProduct.categories.length > 0) {
|
||
// 尝试通过分类名称匹配本地分类
|
||
const categoryName = siteProduct.categories[0].name;
|
||
const category = await this.findCategoryByName(categoryName);
|
||
if (category) {
|
||
productData.categoryId = category.id;
|
||
}
|
||
}
|
||
|
||
// 处理属性
|
||
if (siteProduct.attributes && Array.isArray(siteProduct.attributes)) {
|
||
productData.attributes = siteProduct.attributes.map((attr: any) => ({
|
||
name: attr.name,
|
||
value: attr.options && attr.options.length > 0 ? attr.options[0] : ''
|
||
}));
|
||
}
|
||
|
||
return productData;
|
||
}
|
||
|
||
/**
|
||
* 根据分类名称查找分类
|
||
* @param name 分类名称
|
||
* @returns 分类对象
|
||
*/
|
||
private async findCategoryByName(name: string): Promise<Category | null> {
|
||
try {
|
||
return await this.categoryModel.findOne({
|
||
where: { name }
|
||
});
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将本地产品转换为统一产品格式
|
||
* @param localProduct 本地产品对象
|
||
* @returns 统一产品对象
|
||
*/
|
||
private async mapLocalToUnifiedProduct(localProduct: Product, siteSku?: string): Promise<Partial<UnifiedProductDTO>> {
|
||
const tags = localProduct.attributes?.map(a => ({ name: a.name })) || [];
|
||
// 将本地产品数据转换为UnifiedProductDTO格式
|
||
const unifiedProduct: any = {
|
||
id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在,使用现有ID
|
||
name: localProduct.name,
|
||
type: localProduct.type === 'single' ? 'simple' : 'bundle', // 默认类型,可以根据实际需要调整
|
||
status: 'publish', // 默认状态,可以根据实际需要调整
|
||
sku: siteSku || await this.templateService.render('site.product.sku', { product: localProduct, sku: localProduct.sku }),
|
||
regular_price: String(localProduct.price || 0),
|
||
sale_price: String(localProduct.promotionPrice || localProduct.price || 0),
|
||
price: String(localProduct.price || 0),
|
||
// TODO 库存暂时无法同步
|
||
// stock_status: localProduct.components && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock',
|
||
// stock_quantity: localProduct.stockQuantity || 0,
|
||
// images: localProduct.images ? localProduct.images.map(img => ({
|
||
// id: img.id,
|
||
// src: img.url,
|
||
// name: img.name || '',
|
||
// alt: img.alt || ''
|
||
// })) : [],
|
||
tags,
|
||
categories: localProduct.category ? [{
|
||
id: localProduct.category.id,
|
||
name: localProduct.category.name
|
||
}] : [],
|
||
attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({
|
||
id: attr.dict.id,
|
||
name: attr.dict.name,
|
||
position: attr.dict.sort || 0,
|
||
visible: true,
|
||
variation: false,
|
||
options: [attr.name]
|
||
})) : [],
|
||
variations: [],
|
||
date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(),
|
||
date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(),
|
||
raw: {
|
||
...localProduct
|
||
}
|
||
};
|
||
|
||
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;
|
||
}
|
||
}
|