zksu
/
API
forked from yoone/API
1
0
Fork 0

feat: 增强产品同步功能并优化SKU生成逻辑

添加字典排序字段支持
优化产品同步流程,支持通过SKU同步
重构SKU模板生成逻辑,支持分类属性排序
完善产品导入导出功能,增加分类字段处理
统一产品操作方法,提升代码可维护性
This commit is contained in:
tikkhun 2026-01-08 15:03:11 +08:00
parent 93931f7915
commit cd0bcedfad
11 changed files with 377 additions and 232 deletions

17
package-lock.json generated
View File

@ -523,23 +523,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@faker-js/faker": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
"integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
"npm": ">=10"
}
},
"node_modules/@hapi/bourne": { "node_modules/@hapi/bourne": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz",

View File

@ -742,7 +742,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return data return data
} }
mapCreateProductParams(data: Partial<UnifiedProductDTO>): any { mapCreateProductParams(data: Partial<UnifiedProductDTO>): Partial<ShopyyProduct> {
// 构建 ShopYY 产品创建参数 // 构建 ShopYY 产品创建参数
const params: any = { const params: any = {
name: data.name || '', name: data.name || '',

View File

@ -133,9 +133,30 @@ export class WooCommerceAdapter implements ISiteAdapter {
} }
// 客户操作方法 // 客户操作方法
async getCustomer(where: {id: string | number}): Promise<UnifiedCustomerDTO> { async getCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>): Promise<UnifiedCustomerDTO> {
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.get(`customers/${where.id}`); // 根据提供的条件构建查询参数
let endpoint: string;
if (where.id) {
endpoint = `customers/${where.id}`;
} else if (where.email) {
// 使用邮箱查询客户
const res = await api.get('customers', { params: { email: where.email } });
if (!res.data || res.data.length === 0) {
throw new Error('Customer not found');
}
return this.mapPlatformToUnifiedCustomer(res.data[0]);
} else if (where.phone) {
// 使用电话查询客户
const res = await api.get('customers', { params: { search: where.phone } });
if (!res.data || res.data.length === 0) {
throw new Error('Customer not found');
}
return this.mapPlatformToUnifiedCustomer(res.data[0]);
} else {
throw new Error('Must provide at least one of id, email, or phone');
}
const res = await api.get(endpoint);
return this.mapPlatformToUnifiedCustomer(res.data); return this.mapPlatformToUnifiedCustomer(res.data);
} }
@ -158,7 +179,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]> { async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]> {
// 使用sdkGetAll获取所有客户数据不受分页限制 // 使用sdkGetAll获取所有客户数据不受分页限制
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
// 处理orderBy参数转换为WooCommerce API需要的格式 // 处理orderBy参数转换为WooCommerce API需要的格式
const requestParams = this.mapCustomerSearchParams(params || {}); const requestParams = this.mapCustomerSearchParams(params || {});
@ -168,20 +189,42 @@ export class WooCommerceAdapter implements ISiteAdapter {
} }
async createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> { async createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.post('customers', data); const res = await api.post('customers', data);
return this.mapPlatformToUnifiedCustomer(res.data); return this.mapPlatformToUnifiedCustomer(res.data);
} }
async updateCustomer(where: {id: string | number}, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> { async updateCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.put(`customers/${where.id}`, data); let customerId: string | number;
// 先根据条件获取客户ID
if (where.id) {
customerId = where.id;
} else {
// 如果没有提供ID则先查询客户
const customer = await this.getCustomer(where);
customerId = customer.id;
}
const res = await api.put(`customers/${customerId}`, data);
return this.mapPlatformToUnifiedCustomer(res.data); return this.mapPlatformToUnifiedCustomer(res.data);
} }
async deleteCustomer(where: {id: string | number}): Promise<boolean> { async deleteCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>): Promise<boolean> {
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
await api.delete(`customers/${where.id}`, { force: true }); let customerId: string | number;
// 先根据条件获取客户ID
if (where.id) {
customerId = where.id;
} else {
// 如果没有提供ID则先查询客户
const customer = await this.getCustomer(where);
customerId = customer.id;
}
await api.delete(`customers/${customerId}`, { force: true });
return true; return true;
} }
@ -225,7 +268,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]> { async getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]> {
// 使用sdkGetAll获取所有媒体数据不受分页限制 // 使用sdkGetAll获取所有媒体数据不受分页限制
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const media = await this.wpService.sdkGetAll(api, 'media', params); const media = await this.wpService.sdkGetAll(api, 'media', params);
return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem));
} }
@ -403,7 +446,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
// 订单操作方法 // 订单操作方法
async getOrder(where: {id: string | number}): Promise<UnifiedOrderDTO> { async getOrder(where: {id: string | number}): Promise<UnifiedOrderDTO> {
// 获取单个订单详情 // 获取单个订单详情
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.get(`orders/${where.id}`); const res = await api.get(`orders/${where.id}`);
return this.mapPlatformToUnifiedOrder(res.data); return this.mapPlatformToUnifiedOrder(res.data);
} }
@ -445,7 +488,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> { async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
// 使用sdkGetAll获取所有订单数据不受分页限制 // 使用sdkGetAll获取所有订单数据不受分页限制
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const orders = await this.wpService.sdkGetAll(api, 'orders', params); const orders = await this.wpService.sdkGetAll(api, 'orders', params);
return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order));
} }
@ -464,7 +507,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> { async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
// 创建订单并返回统一订单DTO // 创建订单并返回统一订单DTO
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.post('orders', data); const res = await api.post('orders', data);
return this.mapPlatformToUnifiedOrder(res.data); return this.mapPlatformToUnifiedOrder(res.data);
} }
@ -476,7 +519,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
async deleteOrder(where: {id: string | number}): Promise<boolean> { async deleteOrder(where: {id: string | number}): Promise<boolean> {
// 删除订单 // 删除订单
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
await api.delete(`orders/${where.id}`, { force: true }); await api.delete(`orders/${where.id}`, { force: true });
return true; return true;
} }
@ -541,14 +584,14 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getOrderNotes(orderId: string | number): Promise<any[]> { async getOrderNotes(orderId: string | number): Promise<any[]> {
// 获取订单备注列表 // 获取订单备注列表
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.get(`orders/${orderId}/notes`); const res = await api.get(`orders/${orderId}/notes`);
return res.data; return res.data;
} }
async createOrderNote(orderId: string | number, data: any): Promise<any> { async createOrderNote(orderId: string | number, data: any): Promise<any> {
// 创建订单备注 // 创建订单备注
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.post(`orders/${orderId}/notes`, data); const res = await api.post(`orders/${orderId}/notes`, data);
return res.data; return res.data;
} }
@ -559,7 +602,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
}): Promise<any> { }): Promise<any> {
throw new Error('暂未实现'); throw new Error('暂未实现');
// 取消订单履行 // 取消订单履行
// const api = (this.wpService as any).createApi(this.site, 'wc/v3'); // const api = this.wpService.createApi(this.site, 'wc/v3');
// try { // try {
// // 将订单状态改回处理中 // // 将订单状态改回处理中
@ -582,15 +625,105 @@ export class WooCommerceAdapter implements ISiteAdapter {
} }
// ========== 产品映射方法 ========== // ========== 产品映射方法 ==========
mapUnifiedToPlatformProduct(data: Partial<UnifiedProductDTO>) { mapUnifiedToPlatformProduct(data: Partial<UnifiedProductDTO>): Partial<WooProduct> {
return data; // 将统一产品DTO映射为WooCommerce产品数据
// 基本字段映射
const mapped: Partial<WooProduct> = {
id: data.id as number,
name: data.name,
type: data.type,
status: data.status,
sku: data.sku,
regular_price: data.regular_price,
sale_price: data.sale_price,
price: data.price,
stock_status: data.stock_status as 'instock' | 'outofstock' | 'onbackorder',
stock_quantity: data.stock_quantity,
// 映射更多WooCommerce产品特有的字段
// featured: data.featured,
// catalog_visibility: data.catalog_visibility,
// date_on_sale_from: data.date_on_sale_from,
// date_on_sale_to: data.date_on_sale_to,
// virtual: data.virtual,
// downloadable: data.downloadable,
// description: data.description,
// short_description: data.short_description,
// slug: data.slug,
// manage_stock: data.manage_stock,
// backorders: data.backorders as 'no' | 'notify' | 'yes',
// sold_individually: data.sold_individually,
// weight: data.weight,
// dimensions: data.dimensions,
// shipping_class: data.shipping_class,
// tax_class: data.tax_class,
};
// 映射图片数据
if (data.images && Array.isArray(data.images)) {
mapped.images = data.images.map(img => ({
id: img.id as number,
src: img.src,
name: img.name,
alt: img.alt,
}));
}
// 映射分类数据
if (data.categories && Array.isArray(data.categories)) {
mapped.categories = data.categories.map(cat => ({
// id: cat.id as number, //TODO
name: cat.name,
}));
}
// 映射标签数据
// TODO tags 应该可以设置
// if (data.tags && Array.isArray(data.tags)) {
// mapped.tags = data.tags.map(tag => {
// return ({
// // id: tag.id as number,
// name: tag.name,
// });
// });
// }
// 映射属性数据
if (data.attributes && Array.isArray(data.attributes)) {
mapped.attributes = data.attributes.map(attr => ({
// id 由于我们这个主要用来存,所以不映射 id
name: attr.name,
visible: attr.visible,
variation: attr.variation,
options: attr.options
}));
}
// 映射变体数据注意WooCommerce API 中变体通常通过单独的端点处理)
// 这里只映射变体的基本信息,具体创建/更新变体需要额外处理
if (data.variations && Array.isArray(data.variations)) {
// 对于WooProduct类型variations字段只存储变体ID
mapped.variations = data.variations.map(variation => variation.id as number);
}
// 映射下载数据(如果产品是可下载的)
// if (data.downloads && Array.isArray(data.downloads)) {
// mapped.downloads = data.downloads.map(download => ({
// id: download.id as number,
// name: download.name,
// file: download.file,
// }));
// }
return mapped;
} }
mapCreateProductParams(data: Partial<UnifiedProductDTO>) { mapCreateProductParams(data: Partial<UnifiedProductDTO>):Partial<WooProduct> {
return data; const {id,...mapped}= this.mapUnifiedToPlatformProduct(data);
// 创建不带 id
return mapped
} }
mapUpdateProductParams(data: Partial<UnifiedProductDTO>) { mapUpdateProductParams(data: Partial<UnifiedProductDTO>): Partial<WooProduct> {
return data; return this.mapUnifiedToPlatformProduct(data);
} }
mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial<WooProductSearchParams> { mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial<WooProductSearchParams> {
@ -649,14 +782,14 @@ export class WooCommerceAdapter implements ISiteAdapter {
return mapped; return mapped;
} }
mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { mapPlatformToUnifiedProduct(data: WooProduct): UnifiedProductDTO {
// 将 WooCommerce 产品数据映射为统一产品DTO // 将 WooCommerce 产品数据映射为统一产品DTO
// 保留常用字段与时间信息以便前端统一展示 // 保留常用字段与时间信息以便前端统一展示
// https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties
// 映射变体数据 // 映射变体数据
const mappedVariations = item.variations && Array.isArray(item.variations) const mappedVariations = data.variations && Array.isArray(data.variations)
? item.variations ? data.variations
.filter((variation: any) => typeof variation !== 'number') // 过滤掉数字类型的变体ID .filter((variation: any) => typeof variation !== 'number') // 过滤掉数字类型的变体ID
.map((variation: any) => { .map((variation: any) => {
// 将变体属性转换为统一格式 // 将变体属性转换为统一格式
@ -683,7 +816,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
return { return {
id: variation.id, id: variation.id,
name: variation.name || item.name, // 如果变体没有名称,使用父产品名称 name: variation.name || data.name, // 如果变体没有名称,使用父产品名称
sku: variation.sku || '', sku: variation.sku || '',
regular_price: String(variation.regular_price || ''), regular_price: String(variation.regular_price || ''),
sale_price: String(variation.sale_price || ''), sale_price: String(variation.sale_price || ''),
@ -697,34 +830,34 @@ export class WooCommerceAdapter implements ISiteAdapter {
: []; : [];
return { return {
id: item.id, id: data.id,
date_created: item.date_created, date_created: data.date_created,
date_modified: item.date_modified, date_modified: data.date_modified,
type: item.type, // simple grouped external variable type: data.type, // simple grouped external variable
status: item.status, // draft pending private publish status: data.status, // draft pending private publish
sku: item.sku, sku: data.sku,
name: item.name, name: data.name,
//价格 //价格
regular_price: item.regular_price, regular_price: data.regular_price,
sale_price: item.sale_price, sale_price: data.sale_price,
price: item.price, price: data.price,
stock_status: item.stock_status, stock_status: data.stock_status,
stock_quantity: item.stock_quantity, stock_quantity: data.stock_quantity,
images: (item.images || []).map((img: any) => ({ images: (data.images || []).map((img: any) => ({
id: img.id, id: img.id,
src: img.src, src: img.src,
name: img.name, name: img.name,
alt: img.alt, alt: img.alt,
})), })),
categories: (item.categories || []).map((c: any) => ({ categories: (data.categories || []).map((c: any) => ({
id: c.id, id: c.id,
name: c.name, name: c.name,
})), })),
tags: (item.tags || []).map((t: any) => ({ tags: (data.tags || []).map((t: any) => ({
id: t.id, id: t.id,
name: t.name, name: t.name,
})), })),
attributes: (item.attributes || []).map(attr => ({ attributes: (data.attributes || []).map(attr => ({
id: attr.id, id: attr.id,
name: attr.name || '', name: attr.name || '',
position: attr.position, position: attr.position,
@ -733,25 +866,39 @@ export class WooCommerceAdapter implements ISiteAdapter {
options: attr.options || [] options: attr.options || []
})), })),
variations: mappedVariations, variations: mappedVariations,
permalink: item.permalink, permalink: data.permalink,
raw: item, raw: data,
}; };
} }
async getProduct({id, sku}: {id?: string, sku?:string}){ // 判断是否是这个站点的sku
isSiteSkuThisSite(sku: string,){
return sku.startsWith(this.site.skuPrefix+'-');
}
async getProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>): Promise<UnifiedProductDTO>{
const { id, sku } = where;
if(id) return this.getProductById(id); if(id) return this.getProductById(id);
if(sku) return this.getProductBySku(sku) if(sku) return this.getProductBySku(sku);
return this.getProductById(id || sku || ''); throw new Error('必须提供id或sku参数');
} }
async getProductBySku(sku: string){ async getProductBySku(sku: string){
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); // const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.get(`products?sku=${sku}`); // const res = await api.get(`products`,{
const product = res.data[0]; // sku
// });
// const product = res.data[0];
const res = await this.wpService.getProducts(this.site,{
sku,
page:1,
per_page:1,
});
const product = res?.items?.[0];
if(!product) return null
return this.mapPlatformToUnifiedProduct(product); return this.mapPlatformToUnifiedProduct(product);
} }
// 产品操作方法 // 产品操作方法
async getProductById(id: string | number): Promise<UnifiedProductDTO> { async getProductById(id: string | number): Promise<UnifiedProductDTO> {
// 获取单个产品详情并映射为统一产品DTO // 获取单个产品详情并映射为统一产品DTO
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const res = await api.get(`products/${id}`); const res = await api.get(`products/${id}`);
const product = res.data; const product = res.data;
@ -792,7 +939,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
try { try {
// 批量获取该产品的所有变体数据 // 批量获取该产品的所有变体数据
const variations = await this.wpService.sdkGetAll( const variations = await this.wpService.sdkGetAll(
(this.wpService as any).createApi(this.site, 'wc/v3'), this.wpService.createApi(this.site, 'wc/v3'),
`products/${item.id}/variations` `products/${item.id}/variations`
); );
// 将完整的变体数据添加到产品对象中 // 将完整的变体数据添加到产品对象中
@ -818,7 +965,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> { async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> {
// 使用sdkGetAll获取所有产品数据不受分页限制 // 使用sdkGetAll获取所有产品数据不受分页限制
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const products = await this.wpService.sdkGetAll(api, 'products', params); const products = await this.wpService.sdkGetAll(api, 'products', params);
// 对于类型为 variable 的产品,需要加载完整的变体数据 // 对于类型为 variable 的产品,需要加载完整的变体数据
@ -848,41 +995,31 @@ export class WooCommerceAdapter implements ISiteAdapter {
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> { async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
// 创建产品并返回统一产品DTO // 创建产品并返回统一产品DTO
const res = await this.wpService.createProduct(this.site, data); const createData = this.mapCreateProductParams(data);
const res = await this.wpService.createProduct(this.site, createData);
return this.mapPlatformToUnifiedProduct(res); return this.mapPlatformToUnifiedProduct(res);
} }
async updateProduct(where: {id?: string | number, sku?: string}, data: Partial<UnifiedProductDTO>): Promise<boolean> { async updateProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>, data: Partial<UnifiedProductDTO>): Promise<boolean> {
// 更新产品并返回统一产品DTO // 更新产品并返回统一产品DTO
let productId: string; const product = await this.getProduct(where);
if (where.id) { if(!product){
productId = String(where.id); throw new Error('产品不存在');
} else if (where.sku) {
// 通过sku获取产品ID
const product = await this.getProductBySku(where.sku);
productId = String(product.id);
} else {
throw new Error('必须提供id或sku参数');
} }
const res = await this.wpService.updateProduct(this.site, productId, data as any); const updateData = this.mapUpdateProductParams(data);
const res = await this.wpService.updateProduct(this.site, String(product.id), updateData as any);
return res; return res;
} }
async deleteProduct(where: {id?: string | number, sku?: string}): Promise<boolean> { async deleteProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>): Promise<boolean> {
// 删除产品 // 删除产品
let productId: string; const product = await this.getProduct(where);
if (where.id) { if(!product){
productId = String(where.id); throw new Error('产品不存在');
} else if (where.sku) {
// 通过sku获取产品ID
const product = await this.getProductBySku(where.sku);
productId = String(product.id);
} else {
throw new Error('必须提供id或sku参数');
} }
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
try { try {
await api.delete(`products/${productId}`, { force: true }); await api.delete(`products/${product.id}`, { force: true });
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
@ -902,7 +1039,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
return data; return data;
} }
mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO {
// 将 WooCommerce 评论数据映射为统一评论DTO // 将 WooCommerce 评论数据映射为统一评论DTO
return { return {
id: item.id, id: item.id,
@ -913,7 +1050,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
rating: item.rating, rating: item.rating,
status: item.status, status: item.status,
date_created: item.date_created, date_created: item.date_created,
raw: item date_modified: item.date_modified,
}; };
} }
@ -937,23 +1074,31 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]> { async getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]> {
// 使用sdkGetAll获取所有评论数据不受分页限制 // 使用sdkGetAll获取所有评论数据不受分页限制
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params);
return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review));
} }
async createReview(data: any): Promise<UnifiedReviewDTO> { async createReview(data: CreateReviewDTO): Promise<UnifiedReviewDTO> {
const res = await this.wpService.createReview(this.site, data); const res = await this.wpService.createReview(this.site, data);
return this.mapPlatformToUnifiedReview(res); return this.mapPlatformToUnifiedReview(res);
} }
async updateReview(where: {id: number}, data: any): Promise<UnifiedReviewDTO> { async updateReview(where: Partial<Pick<UnifiedReviewDTO, 'id'>>, data: UpdateReviewDTO): Promise<UnifiedReviewDTO> {
const res = await this.wpService.updateReview(this.site, where.id, data); const { id } = where;
if (!id) {
throw new Error('必须提供评论ID');
}
const res = await this.wpService.updateReview(this.site, Number(id), data);
return this.mapPlatformToUnifiedReview(res); return this.mapPlatformToUnifiedReview(res);
} }
async deleteReview(where: {id: number}): Promise<boolean> { async deleteReview(where: Partial<Pick<UnifiedReviewDTO, 'id'>>): Promise<boolean> {
return await this.wpService.deleteReview(this.site, where.id); const { id } = where;
if (!id) {
throw new Error('必须提供评论ID');
}
return await this.wpService.deleteReview(this.site, Number(id));
} }
// ========== 订阅映射方法 ========== // ========== 订阅映射方法 ==========
@ -999,14 +1144,15 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]> { async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]> {
// 使用sdkGetAll获取所有订阅数据不受分页限制 // 使用sdkGetAll获取所有订阅数据不受分页限制
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params);
return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription));
} }
// ========== 变体映射方法 ========== // ========== 变体映射方法 ==========
mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO {
return data; // 使用mapVariation方法来实现统一的变体映射逻辑
return this.mapVariation(data);
} }
mapUnifiedToPlatformVariation(data: Partial<UnifiedProductVariationDTO>) { mapUnifiedToPlatformVariation(data: Partial<UnifiedProductVariationDTO>) {
return data; return data;
@ -1089,7 +1235,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
// 获取所有产品变体 // 获取所有产品变体
async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]> { async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]> {
try { try {
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const variations = await this.wpService.sdkGetAll(api, `products/${productId}/variations`, params); const variations = await this.wpService.sdkGetAll(api, `products/${productId}/variations`, params);
// 获取产品名称用于变体显示 // 获取产品名称用于变体显示
@ -1287,7 +1433,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
// 获取所有webhooks // 获取所有webhooks
async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]> { async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]> {
try { try {
const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const api = this.wpService.createApi(this.site, 'wc/v3');
const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params);
return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook));
} catch (error) { } catch (error) {
@ -1368,11 +1514,11 @@ export class WooCommerceAdapter implements ISiteAdapter {
return links; return links;
} }
batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array<string | number>; }): Promise<any> { batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array<string | number>; }): Promise<any> { batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
} }

View File

@ -698,10 +698,10 @@ export class ProductController {
// 从站点同步产品到本地 // 从站点同步产品到本地
@ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes }) @ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes })
@Post('/sync-from-site') @Post('/sync-from-site')
async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number }) { async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number ,sku: string}) {
try { try {
const { siteId, siteProductId } = body; const { siteId, siteProductId, sku } = body;
const product = await this.productService.syncProductFromSite(siteId, siteProductId); const product = await this.productService.syncProductFromSite(siteId, siteProductId, sku);
return successResponse(product); return successResponse(product);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -713,25 +713,26 @@ export class ProductController {
@Post('/batch-sync-from-site') @Post('/batch-sync-from-site')
async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) { async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) {
try { try {
const { siteId, siteProductIds } = body; throw new Error('批量同步产品到本地暂未实现');
const result = await this.productService.batchSyncFromSite(siteId, siteProductIds); // const { siteId, siteProductIds } = body;
// 将服务层返回的结果转换为统一格式 // const result = await this.productService.batchSyncFromSite(siteId, siteProductIds.map((id) => ({ siteProductId: id, sku: '' })));
const errors = result.errors.map((error: string) => { // // 将服务层返回的结果转换为统一格式
// 提取产品ID部分作为标识符 // const errors = result.errors.map((error: string) => {
const match = error.match(/站点产品ID (\d+) /); // // 提取产品ID部分作为标识符
const identifier = match ? match[1] : 'unknown'; // const match = error.match(/站点产品ID (\d+) /);
return { // const identifier = match ? match[1] : 'unknown';
identifier: identifier, // return {
error: error // identifier: identifier,
}; // error: error
}); // };
// });
return successResponse({ // return successResponse({
total: siteProductIds.length, // total: siteProductIds.length,
processed: result.synced + errors.length, // processed: result.synced + errors.length,
synced: result.synced, // synced: result.synced,
errors: errors // errors: errors
}); // });
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }

View File

@ -23,19 +23,38 @@ export default class TemplateSeeder implements Seeder {
const templates = [ const templates = [
{ {
name: 'product.sku', name: 'product.sku',
value: "<%= [it.category.shortName].concat(it.attributes.map(a => a.shortName)).join('-') %>", value: `<%
// 按分类判断属性排序逻辑
if (it.category.name === 'nicotine-pouches') {
// 1. 定义 nicotine-pouches 专属的属性固定顺序
const fixedOrder = ['brand','category', 'flavor', 'strength', 'humidity'];
sortedAttrShortNames = fixedOrder.map(attrKey => {
if(attrKey === 'category') return it.category.shortName
// 排序
const matchedAttr = it.attributes.find(a => a?.dict?.name === attrKey);
return matchedAttr ? matchedAttr.shortName : '';
}).filter(Boolean); // 移除空值,避免多余的 "-"
} else {
// 非目标分类,保留 attributes 原有顺序
sortedAttrShortNames = it.attributes.map(a => a.shortName);
}
// 4. 拼接分类名 + 排序后的属性名
%><%= sortedAttrShortNames.join('-') %><%
%>`,
description: '产品SKU模板', description: '产品SKU模板',
testData: JSON.stringify({ testData: JSON.stringify({
category: { "category": {
shortName: 'CAT', "name": "nicotine-pouches",
"shortName": "NP"
}, },
attributes: [ "attributes": [
{ shortName: 'BR' }, { "dict": {"name": "brand"},"shortName": "YOONE" },
{ shortName: 'FL' }, { "dict": {"name": "flavor"},"shortName": "FL" },
{ shortName: '10MG' }, { "dict": {"name": "strength"},"shortName": "10MG" },
{ shortName: 'DRY' }, { "dict": {"name": "humidity"},"shortName": "DRY" }
], ]
}), }),
}, },
{ {
name: 'product.title', name: 'product.title',

View File

@ -2,6 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger';
import { import {
UnifiedPaginationDTO, UnifiedPaginationDTO,
} from './api.dto'; } from './api.dto';
import { Dict } from '../entity/dict.entity';
// export class UnifiedOrderWhere{ // export class UnifiedOrderWhere{
// [] // []
// } // }
@ -137,6 +138,8 @@ export class UnifiedProductAttributeDTO {
@ApiProperty({ description: '变体属性值(单个值)', required: false }) @ApiProperty({ description: '变体属性值(单个值)', required: false })
option?: string; option?: string;
// 这个是属性的父级字典项
dict?: Dict;
} }
export class UnifiedProductVariationDTO { export class UnifiedProductVariationDTO {

View File

@ -117,9 +117,9 @@ export interface WooProduct {
// 购买备注 // 购买备注
purchase_note?: string; purchase_note?: string;
// 分类列表 // 分类列表
categories?: Array<{ id: number; name?: string; slug?: string }>; categories?: Array<{ id?: number; name?: string; slug?: string }>;
// 标签列表 // 标签列表
tags?: Array<{ id: number; name?: string; slug?: string }>; tags?: Array<{ id?: number; name?: string; slug?: string }>;
// 菜单排序 // 菜单排序
menu_order?: number; menu_order?: number;
// 元数据 // 元数据

View File

@ -29,6 +29,10 @@ export class Dict {
@OneToMany(() => DictItem, item => item.dict) @OneToMany(() => DictItem, item => item.dict)
items: DictItem[]; items: DictItem[];
// 排序
@Column({ default: 0, comment: '排序' })
sort: number;
// 是否可删除 // 是否可删除
@Column({ default: true, comment: '是否可删除' }) @Column({ default: true, comment: '是否可删除' })
deletable: boolean; deletable: boolean;

View File

@ -28,7 +28,7 @@ import { StockPoint } from '../entity/stock_point.entity';
import { StockService } from './stock.service'; import { StockService } from './stock.service';
import { TemplateService } from './template.service'; import { TemplateService } from './template.service';
import { SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchErrorItem, BatchOperationResult, SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
import { UnifiedProductDTO } from '../dto/site-api.dto'; import { UnifiedProductDTO } from '../dto/site-api.dto';
import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto'; import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto';
import { Category } from '../entity/category.entity'; import { Category } from '../entity/category.entity';
@ -225,7 +225,7 @@ export class ProductService {
where: { where: {
sku, sku,
}, },
relations: ['category', 'attributes', 'attributes.dict', 'siteSkus'] relations: ['category', 'attributes', 'attributes.dict']
}); });
} }
@ -1455,6 +1455,9 @@ export class ProductService {
} }
} }
// 处理分类字段
const category = val(rec.category);
return { return {
sku, sku,
name: val(rec.name), name: val(rec.name),
@ -1464,6 +1467,7 @@ export class ProductService {
promotionPrice: num(rec.promotionPrice), promotionPrice: num(rec.promotionPrice),
type: val(rec.type), type: val(rec.type),
siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined, siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined,
category, // 添加分类字段
attributes: attributes.length > 0 ? attributes : undefined, attributes: attributes.length > 0 ? attributes : undefined,
} as any; } as any;
@ -1483,10 +1487,15 @@ export class ProductService {
if (data.price !== undefined) dto.price = Number(data.price); if (data.price !== undefined) dto.price = Number(data.price);
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); // 处理分类字段
if (data.categoryId !== undefined) {
dto.categoryId = Number(data.categoryId);
} else if (data.category) {
// 如果是字符串需要后续在createProduct中处理
dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }];
}
// 默认值和特殊处理 // 默认值和特殊处理
dto.attributes = Array.isArray(data.attributes) ? data.attributes : []; dto.attributes = Array.isArray(data.attributes) ? data.attributes : [];
// 如果有组件信息,透传 // 如果有组件信息,透传
@ -1508,7 +1517,13 @@ export class ProductService {
if (data.price !== undefined) dto.price = Number(data.price); if (data.price !== undefined) dto.price = Number(data.price);
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); // 处理分类字段
if (data.categoryId !== undefined) {
dto.categoryId = Number(data.categoryId);
} else if (data.category) {
// 如果是字符串需要后续在updateProduct中处理
dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }];
}
if (data.type !== undefined) dto.type = data.type; if (data.type !== undefined) dto.type = data.type;
if (data.attributes !== undefined) dto.attributes = data.attributes; if (data.attributes !== undefined) dto.attributes = data.attributes;
@ -1548,8 +1563,8 @@ export class ProductService {
esc(p.price), esc(p.price),
esc(p.promotionPrice), esc(p.promotionPrice),
esc(p.type), esc(p.type),
esc(p.description), esc(p.description),
esc(p.category ? p.category.name || p.category.title : ''), // 添加分类字段
]; ];
// 属性数据 // 属性数据
@ -1575,9 +1590,9 @@ export class ProductService {
// 导出所有产品为 CSV 文本 // 导出所有产品为 CSV 文本
async exportProductsCSV(): Promise<string> { async exportProductsCSV(): Promise<string> {
// 查询所有产品及其属性(包含字典关系)和组成 // 查询所有产品及其属性(包含字典关系)、组成和分类
const products = await this.productModel.find({ const products = await this.productModel.find({
relations: ['attributes', 'attributes.dict', 'components'], relations: ['attributes', 'attributes.dict', 'components', 'category'],
order: { id: 'ASC' }, order: { id: 'ASC' },
}); });
@ -1612,8 +1627,8 @@ export class ProductService {
'price', 'price',
'promotionPrice', 'promotionPrice',
'type', 'type',
'description', 'description',
'category',
]; ];
// 动态属性表头 // 动态属性表头
@ -1640,7 +1655,7 @@ export class ProductService {
} }
// 从 CSV 导入产品;存在则更新,不存在则创建 // 从 CSV 导入产品;存在则更新,不存在则创建
async importProductsCSV(file: any): Promise<{ created: number; updated: number; errors: string[] }> { async importProductsCSV(file: any): Promise<BatchOperationResult> {
let buffer: Buffer; let buffer: Buffer;
if (Buffer.isBuffer(file)) { if (Buffer.isBuffer(file)) {
buffer = file; buffer = file;
@ -1676,19 +1691,19 @@ export class ProductService {
console.log('First record keys:', Object.keys(records[0])); console.log('First record keys:', Object.keys(records[0]));
} }
} catch (e: any) { } catch (e: any) {
return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] }; throw new Error(`CSV 解析失败:${e?.message || e}`)
} }
let created = 0; let created = 0;
let updated = 0; let updated = 0;
const errors: string[] = []; const errors: BatchErrorItem[] = [];
// 逐条处理记录 // 逐条处理记录
for (const rec of records) { for (const rec of records) {
try { try {
const data = this.transformCsvRecordToData(rec); const data = this.transformCsvRecordToData(rec);
if (!data) { if (!data) {
errors.push('缺少 SKU 的记录已跳过'); errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过'});
continue; continue;
} }
const { sku } = data; const { sku } = data;
@ -1708,11 +1723,11 @@ export class ProductService {
updated += 1; updated += 1;
} }
} catch (e: any) { } catch (e: any) {
errors.push(`产品${rec?.sku}导入失败:${e?.message || String(e)}`); errors.push({ identifier: '' + rec.sku, error: `产品${rec?.sku}导入失败:${e?.message || String(e)}`});
} }
} }
return { created, updated, errors }; return { total: records.length, processed: records.length - errors.length, created, updated, errors };
} }
// 将库存记录的 sku 添加到产品单品中 // 将库存记录的 sku 添加到产品单品中
@ -1831,9 +1846,7 @@ export class ProductService {
} }
// 将本地产品转换为站点API所需格式 // 将本地产品转换为站点API所需格式
const unifiedProduct = await this.convertLocalProductToUnifiedProduct(localProduct, params.siteSku); const unifiedProduct = await this.mapLocalToUnifiedProduct(localProduct, params.siteSku);
// 调用站点API的upsertProduct方法 // 调用站点API的upsertProduct方法
try { try {
@ -1842,7 +1855,7 @@ export class ProductService {
await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]); await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]);
return result; return result;
} catch (error) { } catch (error) {
throw new Error(`同步产品到站点失败: ${error.message}`); throw new Error(`同步产品到站点失败: ${error?.response?.data?.message??error.message}`);
} }
} }
@ -1869,9 +1882,6 @@ export class ProductService {
siteSku: item.siteSku siteSku: item.siteSku
}); });
// 然后绑定站点SKU
await this.bindSiteSkus(item.productId, [item.siteSku]);
results.synced++; results.synced++;
results.processed++; results.processed++;
} catch (error) { } catch (error) {
@ -1892,30 +1902,23 @@ export class ProductService {
* @param siteProductId ID * @param siteProductId ID
* @returns * @returns
*/ */
async syncProductFromSite(siteId: number, siteProductId: string | number): Promise<any> { async syncProductFromSite(siteId: number, siteProductId: string | number, sku: string): Promise<any> {
const adapter = await this.siteApiService.getAdapter(siteId);
const siteProduct = await adapter.getProduct({ id: siteProductId });
// 从站点获取产品信息 // 从站点获取产品信息
const siteProduct = await this.siteApiService.getProductFromSite(siteId, siteProductId);
if (!siteProduct) { if (!siteProduct) {
throw new Error(`站点产品 ID ${siteProductId} 不存在`); throw new Error(`站点产品 ID ${siteProductId} 不存在`);
} }
// 检查是否已存在相同SKU的本地产品
let localProduct = null;
if (siteProduct.sku) {
try {
localProduct = await this.findProductBySku(siteProduct.sku);
} catch (error) {
// 产品不存在,继续创建
}
}
// 将站点产品转换为本地产品格式 // 将站点产品转换为本地产品格式
const productData = await this.convertSiteProductToLocalProduct(siteProduct); const productData = await this.mapUnifiedToLocalProduct(siteProduct);
return await this.upsertProduct({sku}, productData);
if (localProduct) { }
async upsertProduct(where: Partial<Pick<Product,'id'| 'sku'>>, productData: any) {
const existingProduct = await this.productModel.findOne({ where: where});
if (existingProduct) {
// 更新现有产品 // 更新现有产品
const updateData: UpdateProductDTO = productData; const updateData: UpdateProductDTO = productData;
return await this.updateProduct(localProduct.id, updateData); return await this.updateProduct(existingProduct.id, updateData);
} else { } else {
// 创建新产品 // 创建新产品
const createData: CreateProductDTO = productData; const createData: CreateProductDTO = productData;
@ -1929,18 +1932,18 @@ export class ProductService {
* @param siteProductIds ID数组 * @param siteProductIds ID数组
* @returns * @returns
*/ */
async batchSyncFromSite(siteId: number, siteProductIds: (string | number)[]): Promise<{ synced: number, errors: string[] }> { async batchSyncFromSite(siteId: number, data: Array<{siteProductId:string, sku: string}>): Promise<{ synced: number, errors: string[] }> {
const results = { const results = {
synced: 0, synced: 0,
errors: [] errors: []
}; };
for (const siteProductId of siteProductIds) { for (const item of data) {
try { try {
await this.syncProductFromSite(siteId, siteProductId); await this.syncProductFromSite(siteId, item.siteProductId, item.sku);
results.synced++; results.synced++;
} catch (error) { } catch (error) {
results.errors.push(`站点产品ID ${siteProductId} 同步失败: ${error.message}`); results.errors.push(`站点产品ID ${item.siteProductId} 同步失败: ${error.message}`);
} }
} }
@ -1952,7 +1955,7 @@ export class ProductService {
* @param siteProduct * @param siteProduct
* @returns * @returns
*/ */
private async convertSiteProductToLocalProduct(siteProduct: any): Promise<CreateProductDTO> { private async mapUnifiedToLocalProduct(siteProduct: any): Promise<CreateProductDTO> {
const productData: any = { const productData: any = {
sku: siteProduct.sku, sku: siteProduct.sku,
name: siteProduct.name, name: siteProduct.name,
@ -2015,18 +2018,20 @@ export class ProductService {
* @param localProduct * @param localProduct
* @returns * @returns
*/ */
private async convertLocalProductToUnifiedProduct(localProduct: Product,siteSku?: string): Promise<Partial<UnifiedProductDTO>> { private async mapLocalToUnifiedProduct(localProduct: Product,siteSku?: string): Promise<Partial<UnifiedProductDTO>> {
const tags = localProduct.attributes?.map(a => ({name: a.name})) || [];
// 将本地产品数据转换为UnifiedProductDTO格式 // 将本地产品数据转换为UnifiedProductDTO格式
const unifiedProduct: any = { const unifiedProduct: any = {
id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在使用现有ID id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在使用现有ID
name: localProduct.nameCn || localProduct.name || localProduct.sku, name: localProduct.name,
type: 'simple', // 默认类型,可以根据实际需要调整 type: localProduct.type === 'single'? 'simple' : 'bundle', // 默认类型,可以根据实际需要调整
status: 'publish', // 默认状态,可以根据实际需要调整 status: 'publish', // 默认状态,可以根据实际需要调整
sku: siteSku || await this.templateService.render('site.product.sku', { sku: localProduct.sku }), sku: siteSku || await this.templateService.render('site.product.sku', { product: localProduct, sku: localProduct.sku }),
regular_price: String(localProduct.price || 0), regular_price: String(localProduct.price || 0),
sale_price: String(localProduct.promotionPrice || localProduct.price || 0), sale_price: String(localProduct.promotionPrice || localProduct.price || 0),
price: String(localProduct.price || 0), price: String(localProduct.price || 0),
// stock_status: localProduct.stockQuantity && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock', // TODO 库存暂时无法同步
// stock_status: localProduct.components && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock',
// stock_quantity: localProduct.stockQuantity || 0, // stock_quantity: localProduct.stockQuantity || 0,
// images: localProduct.images ? localProduct.images.map(img => ({ // images: localProduct.images ? localProduct.images.map(img => ({
// id: img.id, // id: img.id,
@ -2034,25 +2039,24 @@ export class ProductService {
// name: img.name || '', // name: img.name || '',
// alt: img.alt || '' // alt: img.alt || ''
// })) : [], // })) : [],
tags: [], tags,
categories: localProduct.category ? [{ categories: localProduct.category ? [{
id: localProduct.category.id, id: localProduct.category.id,
name: localProduct.category.name name: localProduct.category.name
}] : [], }] : [],
attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({ attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({
id: attr.id, id: attr.dict.id,
name: attr.name, name: attr.dict.name,
position: 0, position: attr.dict.sort || 0,
visible: true, visible: true,
variation: false, variation: false,
options: [attr.value] options: [attr.name]
})) : [], })) : [],
variations: [], variations: [],
date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(), date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(),
date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(), date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(),
raw: { raw: {
localProductId: localProduct.id, ...localProduct
localProductSku: localProduct.sku
} }
}; };

View File

@ -110,22 +110,18 @@ export class SiteApiService {
const adapter = await this.getAdapter(siteId); const adapter = await this.getAdapter(siteId);
// 首先尝试查找产品 // 首先尝试查找产品
if (product.sku) { if (!product.sku) {
// 如果没有提供ID但提供了SKU尝试通过SKU查找产品 throw new Error('产品SKU不能为空');
try {
// 尝试搜索具有相同SKU的产品
const existingProduct = await adapter.getProduct( { sku: product.sku });
if (existingProduct) {
// 找到现有产品,更新它
return await adapter.updateProduct({ id: existingProduct.id }, product);
}
// 产品不存在,执行创建
return await adapter.createProduct(product);
} catch (error) {
// 搜索失败,继续执行创建逻辑
console.log(`通过SKU搜索产品失败:`, error.message);
}
} }
// 尝试搜索具有相同SKU的产品
const existingProduct = await adapter.getProduct({ sku: product.sku });
if (existingProduct) {
// 找到现有产品,更新它
return await adapter.updateProduct({ id: existingProduct.id }, product);
}
// 产品不存在,执行创建
return await adapter.createProduct(product);
} }
/** /**
@ -175,17 +171,6 @@ export class SiteApiService {
return await adapter.getProducts(params); return await adapter.getProducts(params);
} }
/**
*
* @param siteId ID
* @param productId ID
* @returns
*/
async getProductFromSite(siteId: number, productId: string | number): Promise<any> {
const adapter = await this.getAdapter(siteId);
return await adapter.getProduct({ id: productId });
}
/** /**
* *
* @param siteId ID * @param siteId ID

View File

@ -44,7 +44,7 @@ export class WPService implements IPlatformService {
* @param site * @param site
* @param namespace API , wc/v3; wcs/v1 * @param namespace API , wc/v3; wcs/v1
*/ */
private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { public createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
return new WooCommerceRestApi({ return new WooCommerceRestApi({
url: site.apiUrl, url: site.apiUrl,
consumerKey: site.consumerKey, consumerKey: site.consumerKey,