API/src/service/wp_product.service.ts

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 });
}
}