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 { CreateBrandDTO, CreateFlavorsDTO, CreateProductDTO, CreateStrengthDTO, CreateSizeDTO, UpdateBrandDTO, UpdateFlavorsDTO, UpdateProductDTO, UpdateStrengthDTO, UpdateSizeDTO, } 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'; @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; // 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'); // 保证 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, }, }); } async getProductList( pagination: PaginationParams, name?: string, brandId?: number ): Promise { const qb = this.productModel .createQueryBuilder('product') .leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('attribute.dict', 'dict'); // 模糊搜索 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(); 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}' 不存在`); } // 查找字典项 let item = await this.dictItemModel.findOne({ where: { title: itemTitle, dict: { id: dict.id } }, }); // 如果字典项不存在,则创建 if (!item) { item = new DictItem(); item.title = itemTitle; item.name = itemName || itemTitle; // 如果没有提供 name,则使用 title item.dict = dict; await this.dictItemModel.save(item); } return item; } async createProduct(createProductDTO: CreateProductDTO): Promise { const { name, description, attributes, sku, price } = createProductDTO; // 条件判断(中文注释:校验属性输入) if (!Array.isArray(attributes) || attributes.length === 0) { throw new Error('属性列表不能为空'); } // 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项) const resolvedAttributes: DictItem[] = []; for (const attr of 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); } 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; // 生成或设置 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'] }); 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.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) { 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; } // 保存更新后的产品 const saved = await this.productModel.save(product); return saved; } async updateProductNameCn(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 productSku = product.sku; // 查询 wp_product 表中是否存在与该 SKU 关联的产品 const wpProduct = await this.wpProductModel .createQueryBuilder('wp_product') .where('JSON_CONTAINS(wp_product.constitution, :sku)', { sku: JSON.stringify({ sku: productSku }), }) .getOne(); if (wpProduct) { throw new Error('无法删除,请先删除关联的WP产品'); } const variation = await this.variationModel .createQueryBuilder('variation') .where('JSON_CONTAINS(variation.constitution, :sku)', { sku: JSON.stringify({ sku: productSku }), }) .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: CreateBrandDTO): 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: UpdateBrandDTO) { // 确认品牌是否存在 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: CreateFlavorsDTO): 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: UpdateFlavorsDTO) { 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: CreateSizeDTO): 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: UpdateSizeDTO) { // 先查询(中文注释:确保尺寸项存在) 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: CreateStrengthDTO): 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: UpdateStrengthDTO) { 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`; } }