import { Inject, Provide } from '@midwayjs/core'; import { parse } from 'csv-parse'; import * as fs from 'fs'; 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; @InjectEntityModel(Dict) dictModel: Repository; @InjectEntityModel(DictItem) dictItemModel: Repository; @InjectEntityModel(Stock) stockModel: Repository; @InjectEntityModel(StockPoint) stockPointModel: Repository; @InjectEntityModel(ProductStockComponent) productStockComponentModel: Repository; @InjectEntityModel(Category) categoryModel: Repository; @Inject() siteApiService: SiteApiService; // 获取所有分类 async getCategoriesAll(): Promise { return this.categoryModel.find({ order: { sort: 'ASC', }, }); } // 获取分类下的属性配置 async getCategoryAttributes(categoryId: number): Promise { 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): Promise { 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): Promise { 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 { const result = await this.categoryModel.delete(id); return result.affected > 0; } // 创建分类属性关联 async createCategoryAttribute(payload: { categoryId: number; dictId: number }): Promise { 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 { const result = await this.categoryModel.manager.delete(CategoryAttribute, id); return result.affected > 0; } // async findProductsByName(name: string): Promise { // 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 { 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 = {}; 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 { return this.productModel.findOne({ where: { sku, }, relations: ['category', 'attributes', 'attributes.dict'] }); } async getProductList(query: UnifiedSearchParamsDTO): Promise { 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 || query.search || ''; // 处理品牌过滤 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 }); } // 处理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 = :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 }); } // 处理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}%` }); } // 处理产品类型过滤 if (query.where?.type) { 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 }); } if (query.where?.maxPrice !== undefined) { 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 }); } if (query.where?.maxPromotionPrice !== undefined) { 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) }); } if (query.where?.createdAtEnd) { 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) }); } if (query.where?.updatedAtEnd) { qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) }); } // 处理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) { 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 }); } // 处理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') { // 如果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 getOrCreateAttribute( dictName: string, itemTitle: string, itemName?: string ): Promise { // 查找字典 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 { const { attributes, sku, categoryId, type } = createProductDTO; // 条件判断(校验属性输入) // 当产品类型为 'bundle' 时,attributes 可以为空 // 当产品类型为 'single' 时,attributes 必须提供且不能为空 if (type === 'single') { if (!Array.isArray(attributes) || attributes.length === 0) { throw new Error('单品类型的属性列表不能为空'); } } 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} 不存在`); } 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 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 { // 检查产品是否存在(包含属性关系) const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict', 'category'] }); if (!product) { throw new Error(`产品 ID ${id} 不存在`); } // 使用 merge 更新基础字段,排除特殊处理字段 const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO; this.productModel.merge(product, simpleFields); // 处理分类更新 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; } } // 处理 SKU 更新 if (updateProductDTO.sku !== undefined) { // 校验 SKU 唯一性(如变更) const newSku = updateProductDTO.sku; if (newSku && newSku !== product.sku) { const exist = await this.productModel.findOne({ where: { 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) { // 如果属性是分类,特殊处理 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) { // 当提供 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 { 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, price, promotionPrice, 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.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 { // 条件判断:确保产品存在 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 { // 条件判断:确保产品存在 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 { // 条件判断:确保产品存在 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 { // 确认产品是否存在 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 { // 检查产品是否存在 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 { 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 { 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 { // 查找 '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 { // 查找 '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 { 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 { // 检查品牌是否存在 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 { 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 { 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 { 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 { 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 { // 查找 '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 { // 查找 '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 { 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 { // 先查询(确保尺寸项存在) 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 记录转换为数据对象 transformCsvRecordToData(rec: any): CreateProductDTO & { sku: string } | null { // 必须包含 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 Object.keys(rec)) { 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 }); } } } // 处理分类字段 const category = val(rec.category); return { sku, name: val(rec.name), nameCn: val(rec.nameCn), description: val(rec.description), price: num(rec.price), promotionPrice: num(rec.promotionPrice), type: val(rec.type), siteSkus: rec.siteSkus ? String(rec.siteSkus) .split(/[;,]/) // 支持英文分号或英文逗号分隔 .map(s => s.trim()) .filter(Boolean) : undefined, category, // 添加分类字段 attributes: attributes.length > 0 ? attributes : undefined, } as any; } // 准备创建产品的 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[]){ 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 { // 查询所有产品及其属性(包含字典关系)、组成和分类 const products = await this.productModel.find({ relations: ['attributes', 'attributes.dict', 'components', 'category'], order: { id: 'ASC' }, }); // 1. 收集所有动态属性的 dictName const dictNames = new Set(); // 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(',')); } return rows.join('\n'); } // 从 CSV 导入产品;存在则更新,不存在则创建 async importProductsCSV(file: any): Promise { let 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('无效的文件输入'); } // 解析 CSV(使用 csv-parse/sync 按表头解析) let records: any[] = []; try { records = await new Promise((resolve, reject) => { parse(buffer, { columns: true, skip_empty_lines: true, trim: true, bom: true, }, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }) console.log('Parsed records count:', records.length); if (records.length > 0) { console.log('First record keys:', Object.keys(records[0])); } } catch (e: any) { throw new Error(`CSV 解析失败:${e?.message || e}`) } let created = 0; let updated = 0; const errors: BatchErrorItem[] = []; // 逐条处理记录 for (const rec of records) { try { const data = this.transformCsvRecordToData(rec); if (!data) { errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过'}); continue; } const { sku } = data; // 查找现有产品 const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] }); if (!exist) { // 创建新产品 const createDTO = this.prepareCreateProductDTO(data); await this.createProduct(createDTO); created += 1; } else { // 更新产品 const updateDTO = this.prepareUpdateProductDTO(data); await this.updateProduct(exist.id, updateDTO); 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 { 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 { 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 { 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 { 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 { // 获取本地产品信息 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 { 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 { 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>, 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 { 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 { try { return await this.categoryModel.findOne({ where: { name } }); } catch (error) { return null; } } /** * 将本地产品转换为统一产品格式 * @param localProduct 本地产品对象 * @returns 统一产品对象 */ private async mapLocalToUnifiedProduct(localProduct: Product,siteSku?: string): Promise> { 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; } }