549 lines
17 KiB
TypeScript
549 lines
17 KiB
TypeScript
import { Product } from './../entity/product.entty';
|
||
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 { ProductStatus, ProductStockStatus } from '../enums/base.enum';
|
||
|
||
@Provide()
|
||
export class WpProductService {
|
||
@Config('wpSite')
|
||
sites: WpSite[];
|
||
|
||
@Inject()
|
||
private readonly wpApiService: WPService;
|
||
|
||
@InjectEntityModel(WpProduct)
|
||
wpProductModel: Repository<WpProduct>;
|
||
|
||
@InjectEntityModel(Variation)
|
||
variationModel: Repository<Variation>;
|
||
|
||
getSite(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.getSite(siteId);
|
||
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
|
||
.select([
|
||
'wp_product.id ',
|
||
'wp_product.externalProductId ',
|
||
])
|
||
.where('wp_product.siteId = :siteIds ', {
|
||
siteIds: siteId,
|
||
})
|
||
const rawResult = await externalProductIds.getRawMany();
|
||
|
||
const externalIds = rawResult.map(item => item.externalProductId);
|
||
|
||
const excludeValues = [];
|
||
|
||
const products = await this.wpApiService.getProducts(site);
|
||
for (const product of products) {
|
||
excludeValues.push(String(product.id));
|
||
const variations =
|
||
product.type === 'variable'
|
||
? await this.wpApiService.getVariations(site, product.id)
|
||
: [];
|
||
|
||
await this.syncProductAndVariations(site.id, product, variations);
|
||
}
|
||
|
||
const filteredIds = externalIds.filter(id => !excludeValues.includes(id));
|
||
if(filteredIds.length!=0){
|
||
await this.variationModel.createQueryBuilder('variation')
|
||
.update()
|
||
.set({ on_delete: true })
|
||
.where(" variation.externalProductId in (:...filteredId) ",{filteredId:filteredIds})
|
||
.execute();
|
||
|
||
this.wpProductModel.createQueryBuilder('wp_product')
|
||
.update()
|
||
.set({ on_delete: true })
|
||
.where(" wp_product.externalProductId in (:...filteredId) ",{filteredId:filteredIds})
|
||
.execute();
|
||
}
|
||
}
|
||
|
||
// 控制产品上下架
|
||
async updateProductStatus(id: number, status: ProductStatus, stock_status: ProductStockStatus) {
|
||
const wpProduct = await this.wpProductModel.findOneBy({ id });
|
||
const site = await this.getSite(wpProduct.siteId);
|
||
wpProduct.status = status;
|
||
wpProduct.stockStatus = stock_status;
|
||
const res = await this.wpApiService.updateProductStatus(site, wpProduct.externalProductId, status, stock_status);
|
||
if (res === true) {
|
||
this.wpProductModel.save(wpProduct);
|
||
return true;
|
||
} else {
|
||
return res;
|
||
}
|
||
}
|
||
|
||
async findProduct(
|
||
siteId: string,
|
||
externalProductId: string
|
||
): Promise<WpProduct | null> {
|
||
return await this.wpProductModel.findOne({
|
||
where: { siteId, externalProductId },
|
||
});
|
||
}
|
||
|
||
async findVariation(
|
||
siteId: string,
|
||
externalProductId: string,
|
||
externalVariationId: string
|
||
): Promise<Variation | null> {
|
||
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;
|
||
}
|
||
where.on_delete = false;
|
||
|
||
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) AND wp_product.on_delete = false ', {
|
||
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<boolean> {
|
||
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<void> {
|
||
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.createQueryBuilder('variation')
|
||
.update()
|
||
.set({ on_delete: true })
|
||
.where(" variation.externalProductId = :externalProductId ",{externalProductId:productId})
|
||
.execute();
|
||
|
||
const sums= await this.wpProductModel.createQueryBuilder('wp_product')
|
||
.update()
|
||
.set({ on_delete: true })
|
||
.where(" wp_product.externalProductId = :externalProductId ",{externalProductId:productId})
|
||
.execute();
|
||
|
||
console.log(sums);
|
||
//await this.variationModel.delete({ siteId, externalProductId: productId });
|
||
//await this.wpProductModel.delete({ siteId, externalProductId: productId });
|
||
}
|
||
|
||
|
||
|
||
async findProductsByName(name: string): Promise<WpProduct[]> {
|
||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||
const query = this.wpProductModel.createQueryBuilder('product');
|
||
|
||
// 保证 sku 不为空
|
||
query.where('product.sku IS NOT NULL AND product.on_delete = false');
|
||
|
||
if (nameFilter.length > 0 || name) {
|
||
const params: Record<string, string> = {};
|
||
const conditions: string[] = [];
|
||
|
||
// 英文名关键词全部匹配(AND)
|
||
if (nameFilter.length > 0) {
|
||
const nameConds = nameFilter.map((word, index) => {
|
||
const key = `name${index}`;
|
||
params[key] = `%${word}%`;
|
||
return `product.name LIKE :${key}`;
|
||
});
|
||
conditions.push(`(${nameConds.join(' AND ')})`);
|
||
}
|
||
|
||
// 中文名模糊匹配
|
||
if (name) {
|
||
params['nameCn'] = `%${name}%`;
|
||
conditions.push(`product.nameCn LIKE :nameCn`);
|
||
}
|
||
|
||
// 英文名关键词匹配 OR 中文名匹配
|
||
query.andWhere(`(${conditions.join(' OR ')})`, params);
|
||
}
|
||
|
||
query.take(50);
|
||
|
||
return await query.getMany();
|
||
}
|
||
}
|