import { Inject, Provide } from '@midwayjs/core'; import { In, Like, Not, Repository } from 'typeorm'; import { Product } from '../entity/product.entity'; import { paginate } from '../utils/paginate.util'; import { PaginationParams } from '../interface'; import { CreateProductDTO, UpdateProductDTO, } from '../dto/product.dto'; import { BrandPaginatedResponse, FlavorsPaginatedResponse, ProductPaginatedResponse, StrengthPaginatedResponse, SizePaginatedResponse, } from '../dto/reponse.dto'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; import { Dict } from '../entity/dict.entity'; import { DictItem } from '../entity/dict_item.entity'; import { Context } from '@midwayjs/koa'; import { TemplateService } from './template.service'; import { StockService } from './stock.service'; import { Stock } from '../entity/stock.entity'; import { StockPoint } from '../entity/stock_point.entity'; import { ProductStockComponent } from '../entity/product_stock_component.entity'; import { Category } from '../entity/category.entity'; @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(WpProduct) wpProductModel: Repository; @InjectEntityModel(Variation) variationModel: Repository; @InjectEntityModel(Stock) stockModel: Repository; @InjectEntityModel(StockPoint) stockPointModel: Repository; @InjectEntityModel(ProductStockComponent) productStockComponentModel: Repository; @InjectEntityModel(Category) categoryModel: Repository; // 获取所有 WordPress 商品 async getWpProducts() { return this.wpProductModel.find(); } // 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`); } // 英文名关键词匹配 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( pagination: PaginationParams, name?: string, brandId?: number ): Promise { const qb = this.productModel .createQueryBuilder('product') .leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('attribute.dict', 'dict') .leftJoinAndSelect('product.category', 'category'); // 模糊搜索 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); } // 品牌过滤 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; }); } // 分页 qb.skip((pagination.current - 1) * pagination.pageSize).take( pagination.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, ...pagination, }; } 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 { name, description, attributes, sku, price, categoryId } = createProductDTO; // 条件判断(中文注释:校验属性输入) if (!Array.isArray(attributes) || attributes.length === 0) { // 如果提供了 categoryId 但没有 attributes,初始化为空数组 if (!attributes && categoryId) { // 继续执行,下面会处理 categoryId } else { 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 } }); if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`); } for (const attr of safeAttributes) { // 中文注释:如果属性是分类,特殊处理 if (attr.dictName === 'category') { if (attr.id) { categoryItem = await this.categoryModel.findOneBy({ id: attr.id }); } else if (attr.name) { categoryItem = await this.categoryModel.findOneBy({ name: attr.name }); } 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); } // 检查完全相同属性组合是否已存在(中文注释:避免重复) 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(); product.name = name; product.description = description; product.attributes = resolvedAttributes; if (categoryItem) { product.category = categoryItem; } // 条件判断(中文注释:设置商品类型,默认 simple) product.type = (createProductDTO.type as any) || 'single'; // 生成或设置 SKU(中文注释:基于属性字典项的 name 生成) if (sku) { product.sku = sku; } else { const attributeMap: Record = {}; for (const a of resolvedAttributes) { if (a?.dict?.name && a?.name) attributeMap[a.dict.name] = a.name; } product.sku = await this.templateService.render('product_sku', { brand: attributeMap['brand'] || '', flavor: attributeMap['flavor'] || '', strength: attributeMap['strength'] || '', humidity: attributeMap['humidity'] || '', }); } // 价格与促销价(中文注释:可选字段) if (price !== undefined) { product.price = Number(price); } const promotionPrice = (createProductDTO as any)?.promotionPrice; if (promotionPrice !== undefined) { product.promotionPrice = Number(promotionPrice); } return await this.productModel.save(product); } 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} 不存在`); } // 处理基础字段更新(若传入则更新) if (updateProductDTO.name !== undefined) { product.name = updateProductDTO.name; } if (updateProductDTO.description !== undefined) { product.description = updateProductDTO.description; } if (updateProductDTO.categoryId !== undefined) { if (updateProductDTO.categoryId) { const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } }); if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`); product.category = categoryItem; } else { // 如果传了 0 或 null,可以清除分类(根据需求) // product.category = null; } } if (updateProductDTO.price !== undefined) { product.price = Number(updateProductDTO.price); } if ((updateProductDTO as any).promotionPrice !== undefined) { product.promotionPrice = Number((updateProductDTO as any).promotionPrice); } 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); return saved; } // 中文注释:获取产品的库存组成列表(表关联版本) 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') { throw new Error('单品无需设置组成'); } 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 sku = product.sku; // 查询 wp_product 表中是否存在与该 SKU 关联的产品 const wpProduct = await this.wpProductModel .createQueryBuilder('wp_product') .where('JSON_CONTAINS(wp_product.constitution, :sku)', { sku: JSON.stringify({ sku: sku }), }) .getOne(); if (wpProduct) { throw new Error('无法删除,请先删除关联的WP产品'); } const variation = await this.variationModel .createQueryBuilder('variation') .where('JSON_CONTAINS(variation.constitution, :sku)', { sku: JSON.stringify({ sku: sku }), }) .getOne(); if (variation) { console.log(variation); throw new Error('无法删除,请先删除关联的WP变体'); } // 删除产品 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 } ): 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.dict = dict; return await this.dictItemModel.save(item); } // 通用属性:更新字典项 async updateAttribute( id: number, payload: { title?: string; name?: 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; 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 文本 async exportProductsCSV(): Promise { // 查询所有产品及其属性(中文注释:包含字典关系) const products = await this.productModel.find({ relations: ['attributes', 'attributes.dict'], order: { id: 'ASC' }, }); // 定义 CSV 表头(中文注释:与导入字段一致) const headers = [ 'sku', 'name', 'nameCn', 'price', 'promotionPrice', 'type', 'stock', 'brand', 'flavor', 'strength', 'size', 'description', ]; // 中文注释: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 rows: string[] = []; rows.push(headers.join(',')); for (const p of products) { // 中文注释:逐行输出产品数据 const row = [ esc(p.sku), esc(p.name), esc(p.nameCn), esc(p.price), esc(p.promotionPrice), esc(p.type), esc(p.stock), esc(pickAttr(p, 'brand')), esc(pickAttr(p, 'flavor')), esc(pickAttr(p, 'strength')), esc(pickAttr(p, 'size')), esc(p.description), ].join(','); rows.push(row); } return rows.join('\n'); } // 中文注释:从 CSV 导入产品;存在则更新,不存在则创建 async importProductsCSV(buffer: Buffer): Promise<{ created: number; updated: number; errors: string[] }> { // 解析 CSV(中文注释:使用 csv-parse/sync 按表头解析) const { parse } = await import('csv-parse/sync'); let records: any[] = []; try { records = parse(buffer, { columns: true, skip_empty_lines: true, trim: true, }); } catch (e: any) { return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] }; } let created = 0; let updated = 0; const errors: string[] = []; // 中文注释:逐条处理记录 for (const rec of records) { try { // 条件判断:必须包含 sku const sku: string = (rec.sku || '').trim(); if (!sku) { // 缺少 SKU 直接跳过 errors.push('缺少 SKU 的记录已跳过'); continue; } // 查找现有产品 const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] }); // 中文注释:准备基础字段 const base = { name: rec.name || '', nameCn: rec.nameCn || '', description: rec.description || '', price: rec.price ? Number(rec.price) : undefined, promotionPrice: rec.promotionPrice ? Number(rec.promotionPrice) : undefined, type: rec.type || '', stock: rec.stock ? Number(rec.stock) : undefined, sku, } as any; // 中文注释:解析属性字段(分号分隔多值) const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []); const brands = parseList(rec.brand); const flavors = parseList(rec.flavor); const strengths = parseList(rec.strength); const sizes = parseList(rec.size); // 中文注释:将属性解析为 DTO 输入 const attrDTOs: { dictName: string; title: string }[] = []; for (const b of brands) attrDTOs.push({ dictName: 'brand', title: b }); for (const f of flavors) attrDTOs.push({ dictName: 'flavor', title: f }); for (const s of strengths) attrDTOs.push({ dictName: 'strength', title: s }); for (const z of sizes) attrDTOs.push({ dictName: 'size', title: z }); if (!exist) { // 中文注释:创建新产品 const dto = { name: base.name, description: base.description, price: base.price, sku: base.sku, attributes: attrDTOs, } as any; const createdProduct = await this.createProduct(dto); // 条件判断:更新可选字段 const patch: any = {}; if (base.nameCn) patch.nameCn = base.nameCn; if (base.promotionPrice !== undefined) patch.promotionPrice = base.promotionPrice; if (base.type) patch.type = base.type; if (base.stock !== undefined) patch.stock = base.stock; if (Object.keys(patch).length > 0) await this.productModel.update(createdProduct.id, patch); created += 1; } else { // 中文注释:更新产品 const updateDTO: any = { name: base.name || exist.name, description: base.description || exist.description, price: base.price !== undefined ? base.price : exist.price, sku: base.sku, attributes: attrDTOs, }; // 条件判断:附加可选字段 if (base.nameCn) updateDTO.nameCn = base.nameCn; if (base.promotionPrice !== undefined) updateDTO.promotionPrice = base.promotionPrice; if (base.type) updateDTO.type = base.type; if (base.stock !== undefined) updateDTO.stock = base.stock; await this.updateProduct(exist.id, updateDTO); updated += 1; } } catch (e: any) { errors.push(e?.message || String(e)); } } return { created, updated, errors }; } }