import { Config, Inject, Provide } from '@midwayjs/core'; import { WPService } from './wp.service'; import { WpSite } from '../interface'; import { WpProduct } from '../entity/wp_product.entity'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { And, Like, Not, Repository } from 'typeorm'; import { Variation } from '../entity/variation.entity'; import { QueryWpProductDTO, UpdateVariationDTO, UpdateWpProductDTO, } from '../dto/wp_product.dto'; import { Product } from '../entity/product.entty'; @Provide() export class WpProductService { @Config('wpSite') sites: WpSite[]; @Inject() private readonly wpApiService: WPService; @InjectEntityModel(WpProduct) wpProductModel: Repository; @InjectEntityModel(Variation) variationModel: Repository; geSite(id: string): WpSite { let idx = this.sites.findIndex(item => item.id === id); return this.sites[idx]; } async syncAllSites() { for (const site of this.sites) { const products = await this.wpApiService.getProducts(site); for (const product of products) { const variations = product.type === 'variable' ? await this.wpApiService.getVariations(site, product.id) : []; await this.syncProductAndVariations(site.id, product, variations); } } } async syncSite(siteId: string) { const site = this.geSite(siteId); const products = await this.wpApiService.getProducts(site); for (const product of products) { const variations = product.type === 'variable' ? await this.wpApiService.getVariations(site, product.id) : []; await this.syncProductAndVariations(site.id, product, variations); } } async findProduct( siteId: string, externalProductId: string ): Promise { return await this.wpProductModel.findOne({ where: { siteId, externalProductId }, }); } async findVariation( siteId: string, externalProductId: string, externalVariationId: string ): Promise { return await this.variationModel.findOne({ where: { siteId, externalProductId, externalVariationId }, }); } async updateWpProduct( siteId: string, productId: string, product: UpdateWpProductDTO ) { let existingProduct = await this.findProduct(siteId, productId); if (existingProduct) { existingProduct.name = product.name; existingProduct.sku = product.sku; product.regular_price && (existingProduct.regular_price = product.regular_price); product.sale_price && (existingProduct.sale_price = product.sale_price); await this.wpProductModel.save(existingProduct); } } async updateWpProductVaritation( siteId: string, productId: string, variationId: string, variation: UpdateVariationDTO ) { const existingVariation = await this.findVariation( siteId, productId, variationId ); if (existingVariation) { existingVariation.name = variation.name; existingVariation.sku = variation.sku; variation.regular_price && (existingVariation.regular_price = variation.regular_price); variation.sale_price && (existingVariation.sale_price = variation.sale_price); await this.variationModel.save(existingVariation); } } async syncProductAndVariations( siteId: string, product: WpProduct, variations: Variation[] ) { // 1. 处理产品同步 let existingProduct = await this.findProduct(siteId, String(product.id)); if (existingProduct) { existingProduct.name = product.name; existingProduct.status = product.status; existingProduct.type = product.type; existingProduct.sku = product.sku; product.regular_price && (existingProduct.regular_price = product.regular_price); product.sale_price && (existingProduct.sale_price = product.sale_price); existingProduct.on_sale = product.on_sale; existingProduct.metadata = product.metadata; await this.wpProductModel.save(existingProduct); } else { existingProduct = this.wpProductModel.create({ siteId, externalProductId: String(product.id), sku: product.sku, status: product.status, name: product.name, type: product.type, ...(product.regular_price ? { regular_price: product.regular_price } : {}), ...(product.sale_price ? { sale_price: product.sale_price } : {}), on_sale: product.on_sale, metadata: product.metadata, }); await this.wpProductModel.save(existingProduct); } // 2. 处理变体同步 if (product.type === 'variable') { const currentVariations = await this.variationModel.find({ where: { siteId, externalProductId: String(product.id) }, }); const syncedVariationIds = new Set(variations.map(v => String(v.id))); const variationsToDelete = currentVariations.filter( dbVariation => !syncedVariationIds.has(String(dbVariation.externalVariationId)) ); if (variationsToDelete.length > 0) { const idsToDelete = variationsToDelete.map(v => v.id); await this.variationModel.delete(idsToDelete); } for (const variation of variations) { const existingVariation = await this.findVariation( siteId, String(product.id), String(variation.id) ); if (existingVariation) { existingVariation.name = variation.name; existingVariation.attributes = variation.attributes; variation.regular_price && (existingVariation.regular_price = variation.regular_price); variation.sale_price && (existingVariation.sale_price = variation.sale_price); existingVariation.on_sale = variation.on_sale; await this.variationModel.save(existingVariation); } else { const newVariation = this.variationModel.create({ siteId, externalProductId: String(product.id), externalVariationId: String(variation.id), productId: existingProduct.id, sku: variation.sku, name: variation.name, ...(variation.regular_price ? { regular_price: variation.regular_price } : {}), ...(variation.sale_price ? { sale_price: variation.sale_price } : {}), on_sale: variation.on_sale, attributes: variation.attributes, }); await this.variationModel.save(newVariation); } } } else { // 清理之前的变体 await this.variationModel.delete({ siteId, externalProductId: String(product.id), }); } } async syncVariation(siteId: string, productId: string, variation: Variation) { let existingProduct = await this.findProduct(siteId, String(productId)); if (!existingProduct) return; const existingVariation = await this.findVariation( siteId, String(productId), String(variation.id) ); if (existingVariation) { existingVariation.name = variation.name; existingVariation.attributes = variation.attributes; variation.regular_price && (existingVariation.regular_price = variation.regular_price); variation.sale_price && (existingVariation.sale_price = variation.sale_price); existingVariation.on_sale = variation.on_sale; await this.variationModel.save(existingVariation); } else { const newVariation = this.variationModel.create({ siteId, externalProductId: String(productId), externalVariationId: String(variation.id), productId: existingProduct.id, sku: variation.sku, name: variation.name, ...(variation.regular_price ? { regular_price: variation.regular_price } : {}), ...(variation.sale_price ? { sale_price: variation.sale_price } : {}), on_sale: variation.on_sale, attributes: variation.attributes, }); await this.variationModel.save(newVariation); } } async getProductList(param: QueryWpProductDTO) { const { current = 1, pageSize = 10, name, siteId, status } = param; // 第一步:先查询分页的产品 const where: any = {}; if (siteId) { where.siteId = siteId; } const nameFilter = name ? name.split(' ').filter(Boolean) : []; if (nameFilter.length > 0) { const nameConditions = nameFilter.map(word => Like(`%${word}%`)); where.name = And(...nameConditions); } if (status) { where.status = status; } const products = await this.wpProductModel.find({ where, skip: (current - 1) * pageSize, take: pageSize, }); const total = await this.wpProductModel.count({ where, }); if (products.length === 0) { return { items: [], total, current, pageSize, }; } const variationQuery = this.wpProductModel .createQueryBuilder('wp_product') .leftJoin(Variation, 'variation', 'variation.productId = wp_product.id') .leftJoin( Product, 'product', 'JSON_UNQUOTE(JSON_EXTRACT(wp_product.constitution, "$.sku")) = product.sku' ) .leftJoin( Product, 'variation_product', 'JSON_UNQUOTE(JSON_EXTRACT(variation.constitution, "$.sku")) = variation_product.sku' ) .select([ 'wp_product.*', 'variation.id as variation_id', 'variation.siteId as variation_siteId', 'variation.externalProductId as variation_externalProductId', 'variation.externalVariationId as variation_externalVariationId', 'variation.productId as variation_productId', 'variation.sku as variation_sku', 'variation.name as variation_name', 'variation.regular_price as variation_regular_price', 'variation.sale_price as variation_sale_price', 'variation.on_sale as variation_on_sale', 'variation.constitution as variation_constitution', 'product.name as product_name', // 关联查询返回 product.name 'variation_product.name as variation_product_name', // 关联查询返回 variation 的产品 name ]) .where('wp_product.id IN (:...ids)', { ids: products.map(product => product.id), }); const rawResult = await variationQuery.getRawMany(); // 数据转换 const items = rawResult.reduce((acc, row) => { let product = acc.find(p => p.id === row.id); if (!product) { product = { ...Object.keys(row) .filter(key => !key.startsWith('variation_')) .reduce((obj, key) => { obj[key] = row[key]; return obj; }, {}), variations: [], }; acc.push(product); } if (row.variation_id) { const variation: any = Object.keys(row) .filter(key => key.startsWith('variation_')) .reduce((obj, key) => { obj[key.replace('variation_', '')] = row[key]; return obj; }, {}); variation.constitution = variation?.constitution?.map(item => { const product = item.sku ? { ...item, name: row.variation_product_name } : item; return product; }) || []; product.variations.push(variation); } product.constitution = product?.constitution?.map(item => { const productWithName = item.sku ? { ...item, name: row.product_name } : item; return productWithName; }) || []; return acc; }, []); return { items, total, current, pageSize, }; } /** * 检查 SKU 是否重复 * @param sku SKU 编码 * @param excludeSiteId 需要排除的站点 ID * @param excludeProductId 需要排除的产品 ID * @param excludeVariationId 需要排除的变体 ID * @returns 是否重复 */ async isSkuDuplicate( sku: string, excludeSiteId?: string, excludeProductId?: string, excludeVariationId?: string ): Promise { if (!sku) return false; const where: any = { sku }; const varWhere: any = { sku }; if (excludeVariationId) { varWhere.siteId = Not(excludeSiteId); varWhere.externalProductId = Not(excludeProductId); varWhere.externalVariationId = Not(excludeVariationId); } else { where.externalProductId = Not(excludeProductId); where.externalProductId = Not(excludeProductId); } const productDuplicate = await this.wpProductModel.findOne({ where, }); if (productDuplicate) { return true; } const variationDuplicate = await this.variationModel.findOne({ where: varWhere, }); return !!variationDuplicate; } /** * 设置产品或变体的构成成分 */ async setConstitution( id: number, isProduct: boolean, constitution: { sku: string; quantity: number }[] ): Promise { if (isProduct) { // 更新产品的 constitution const product = await this.wpProductModel.findOne({ where: { id } }); if (!product) { throw new Error(`未找到 ID 为 ${id} 的产品`); } product.constitution = constitution; await this.wpProductModel.save(product); } else { // 更新变体的 constitution const variation = await this.variationModel.findOne({ where: { id } }); if (!variation) { throw new Error(`未找到 ID 为 ${id} 的变体`); } variation.constitution = constitution; await this.variationModel.save(variation); } } async delWpProduct(siteId: string, productId: string) { const product = await this.wpProductModel.findOne({ where: { siteId, externalProductId: productId }, }); if (!product) throw new Error('未找到该商品'); await this.variationModel.delete({ siteId, externalProductId: productId }); await this.wpProductModel.delete({ siteId, externalProductId: productId }); } }