464 lines
15 KiB
TypeScript
464 lines
15 KiB
TypeScript
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';
|
|
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 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 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;
|
|
}
|
|
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<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.delete({ siteId, externalProductId: productId });
|
|
await this.wpProductModel.delete({ siteId, externalProductId: productId });
|
|
}
|
|
}
|