forked from yoone/API
1
0
Fork 0

feat: 重构站点API适配器接口并实现多平台支持

refactor(adapter): 重构适配器接口结构,分离不同实体的映射方法
feat(product): 增强产品同步功能,支持通过SKU查找和更新
fix(order): 修复订单同步和履约相关的问题
feat(template): 改进SKU模板逻辑,支持按分类属性排序
chore: 移除未使用的媒体控制器和相关代码
This commit is contained in:
tikkhun 2026-01-08 20:40:12 +08:00
parent 983ba47dbf
commit 3f5fb6adba
24 changed files with 2576 additions and 1706 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",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +0,0 @@
import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core';
import { WPService } from '../service/wp.service';
import { successResponse, errorResponse } from '../utils/response.util';
@Controller('/media')
export class MediaController {
@Inject()
wpService: WPService;
@Get('/list')
async list(
@Query('siteId') siteId: number,
@Query('page') page: number = 1,
@Query('pageSize') pageSize: number = 20
) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.getMedia(siteId, page, pageSize);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/upload')
async upload(@Fields() fields, @Files() files) {
try {
const siteId = fields.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
if (!files || files.length === 0) {
return errorResponse('file is required');
}
const file = files[0];
const result = await this.wpService.createMedia(siteId, file);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/update/:id')
async update(@Param('id') id: number, @Body() body) {
try {
const siteId = body.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
// 过滤出需要更新的字段
const { title, caption, description, alt_text } = body;
const data: any = {};
if (title !== undefined) data.title = title;
if (caption !== undefined) data.caption = caption;
if (description !== undefined) data.description = description;
if (alt_text !== undefined) data.alt_text = alt_text;
const result = await this.wpService.updateMedia(siteId, id, data);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Del('/:id')
async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.deleteMedia(siteId, id, force);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
}

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

@ -7,7 +7,6 @@ import {
CancelFulfillmentDTO, CancelFulfillmentDTO,
CreateReviewDTO, CreateReviewDTO,
CreateWebhookDTO, CreateWebhookDTO,
FulfillmentDTO,
UnifiedCustomerDTO, UnifiedCustomerDTO,
UnifiedCustomerPaginationDTO, UnifiedCustomerPaginationDTO,
UnifiedMediaPaginationDTO, UnifiedMediaPaginationDTO,
@ -106,7 +105,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateReview(id, body); const data = await adapter.updateReview({ id }, body);
this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -124,7 +123,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.deleteReview(id); const data = await adapter.deleteReview({ id });
this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -160,7 +159,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getWebhook(id); const data = await adapter.getWebhook({ id });
this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -199,7 +198,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateWebhook(id, body); const data = await adapter.updateWebhook({ id }, body);
this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -217,7 +216,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.deleteWebhook(id); const data = await adapter.deleteWebhook({ id });
this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -327,7 +326,7 @@ export class SiteApiController {
if (site.type === 'woocommerce') { if (site.type === 'woocommerce') {
const page = query.page || 1; const page = query.page || 1;
const perPage = (query.per_page) || 100; const perPage = (query.per_page) || 100;
const res = await this.siteApiService.wpService.getProducts(site, page, perPage); const res = await this.siteApiService.wpService.getProducts(site, { page, per_page: perPage });
const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity']; const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity'];
const rows = (res.items || []).map((p: any) => [p.id, p.name, p.type, p.status, p.sku, p.regular_price, p.sale_price, p.stock_status, p.stock_quantity]); const rows = (res.items || []).map((p: any) => [p.id, p.name, p.type, p.status, p.sku, p.regular_price, p.sale_price, p.stock_status, p.stock_quantity]);
const toCsvValue = (val: any) => { const toCsvValue = (val: any) => {
@ -360,7 +359,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getProduct(id); const data = await adapter.getProduct({ id });
// 如果获取到商品数据则增强ERP产品信息 // 如果获取到商品数据则增强ERP产品信息
if (data) { if (data) {
@ -485,7 +484,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateProduct(id, body); const data = await adapter.updateProduct({ id }, body);
this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -540,7 +539,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const success = await adapter.deleteProduct(id); const success = await adapter.deleteProduct({ id });
this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`);
return successResponse(success); return successResponse(success);
} catch (error) { } catch (error) {
@ -585,7 +584,7 @@ export class SiteApiController {
for (const item of body.update) { for (const item of body.update) {
try { try {
const id = item.id; const id = item.id;
const data = await adapter.updateProduct(id, item); const data = await adapter.updateProduct({ id }, item);
updated.push(data); updated.push(data);
} catch (e) { } catch (e) {
errors.push({ errors.push({
@ -598,7 +597,7 @@ export class SiteApiController {
if (body.delete?.length) { if (body.delete?.length) {
for (const id of body.delete) { for (const id of body.delete) {
try { try {
const ok = await adapter.deleteProduct(id); const ok = await adapter.deleteProduct({ id });
if (ok) deleted.push(id); if (ok) deleted.push(id);
else errors.push({ else errors.push({
identifier: String(id), identifier: String(id),
@ -672,6 +671,26 @@ export class SiteApiController {
} }
} }
@Get('/:siteId/orders/count')
@ApiOkResponse({ type: Object })
async countOrders(
@Param('siteId') siteId: number,
@Query() query: any
) {
this.logger.info(`[Site API] 获取订单总数开始, siteId: ${siteId}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const total = await adapter.countOrders(query);
this.logger.info(`[Site API] 获取订单总数成功, siteId: ${siteId}, total: ${total}`);
return successResponse({ total });
} catch (error) {
this.logger.error(`[Site API] 获取订单总数失败, siteId: ${siteId}, 错误信息: ${error.message}`);
return errorResponse(error.message);
}
}
@Get('/:siteId/customers/:customerId/orders') @Get('/:siteId/customers/:customerId/orders')
@ApiOkResponse({ type: UnifiedOrderPaginationDTO }) @ApiOkResponse({ type: UnifiedOrderPaginationDTO })
async getCustomerOrders( async getCustomerOrders(
@ -752,7 +771,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getOrder(id); const data = await adapter.getOrder({ id });
this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -824,7 +843,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const ok = await adapter.updateOrder(id, body); const ok = await adapter.updateOrder({ id }, body);
this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(ok); return successResponse(ok);
} catch (error) { } catch (error) {
@ -842,7 +861,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const ok = await adapter.deleteOrder(id); const ok = await adapter.deleteOrder({ id });
this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(ok); return successResponse(ok);
} catch (error) { } catch (error) {
@ -882,7 +901,7 @@ export class SiteApiController {
for (const item of body.update) { for (const item of body.update) {
try { try {
const id = item.id; const id = item.id;
const ok = await adapter.updateOrder(id, item); const ok = await adapter.updateOrder({ id }, item);
if (ok) updated.push(item); if (ok) updated.push(item);
else errors.push({ else errors.push({
identifier: String(item.id || 'unknown'), identifier: String(item.id || 'unknown'),
@ -899,7 +918,7 @@ export class SiteApiController {
if (body.delete?.length) { if (body.delete?.length) {
for (const id of body.delete) { for (const id of body.delete) {
try { try {
const ok = await adapter.deleteOrder(id); const ok = await adapter.deleteOrder({ id });
if (ok) deleted.push(id); if (ok) deleted.push(id);
else errors.push({ else errors.push({
identifier: String(id), identifier: String(id),
@ -966,25 +985,6 @@ export class SiteApiController {
} }
} }
@Post('/:siteId/orders/:id/fulfill')
@ApiOkResponse({ type: Object })
async fulfillOrder(
@Param('siteId') siteId: number,
@Param('id') id: string,
@Body() body: FulfillmentDTO
) {
this.logger.info(`[Site API] 订单履约开始, siteId: ${siteId}, orderId: ${id}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.fulfillOrder(id, body);
this.logger.info(`[Site API] 订单履约成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(data);
} catch (error) {
this.logger.error(`[Site API] 订单履约失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
return errorResponse(error.message);
}
}
@Post('/:siteId/orders/:id/cancel-fulfill') @Post('/:siteId/orders/:id/cancel-fulfill')
@ApiOkResponse({ type: Object }) @ApiOkResponse({ type: Object })
async cancelFulfillment( async cancelFulfillment(
@ -1050,13 +1050,13 @@ export class SiteApiController {
} }
} }
@Get('/:siteId/orders/:orderId/trackings') @Get('/:siteId/orders/:orderId/fulfillments')
@ApiOkResponse({ type: Object }) @ApiOkResponse({ type: Object })
async getOrderTrackings( async getOrderFulfillments(
@Param('siteId') siteId: number, @Param('siteId') siteId: number,
@Param('orderId') orderId: string @Param('orderId') orderId: string
) { ) {
this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`); this.logger.info(`[Site API] 获取订单履约信息开始, siteId: ${siteId}, orderId: ${orderId}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getOrderFulfillments(orderId); const data = await adapter.getOrderFulfillments(orderId);
@ -1435,7 +1435,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getCustomer(id); const data = await adapter.getCustomer({ id });
this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -1507,7 +1507,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateCustomer(id, body); const data = await adapter.updateCustomer({ id }, body);
this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -1525,7 +1525,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const success = await adapter.deleteCustomer(id); const success = await adapter.deleteCustomer({ id });
this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`);
return successResponse(success); return successResponse(success);
} catch (error) { } catch (error) {
@ -1561,7 +1561,7 @@ export class SiteApiController {
for (const item of body.update) { for (const item of body.update) {
try { try {
const id = item.id; const id = item.id;
const data = await adapter.updateCustomer(id, item); const data = await adapter.updateCustomer({ id }, item);
updated.push(data); updated.push(data);
} catch (e) { } catch (e) {
failed.push({ action: 'update', item, error: (e as any).message }); failed.push({ action: 'update', item, error: (e as any).message });
@ -1571,7 +1571,7 @@ export class SiteApiController {
if (body.delete?.length) { if (body.delete?.length) {
for (const id of body.delete) { for (const id of body.delete) {
try { try {
const ok = await adapter.deleteCustomer(id); const ok = await adapter.deleteCustomer({ id });
if (ok) deleted.push(id); if (ok) deleted.push(id);
else failed.push({ action: 'delete', id, error: 'delete failed' }); else failed.push({ action: 'delete', id, error: 'delete failed' });
} catch (e) { } catch (e) {

View File

@ -12,7 +12,10 @@ import * as crypto from 'crypto';
import { SiteService } from '../service/site.service'; import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service'; import { OrderService } from '../service/order.service';
import { SiteApiService } from '../service/site-api.service';
import {
UnifiedOrderDTO,
} from '../dto/site-api.dto';
@Controller('/webhook') @Controller('/webhook')
export class WebhookController { export class WebhookController {
@ -31,8 +34,6 @@ export class WebhookController {
@Inject() @Inject()
private readonly siteService: SiteService; private readonly siteService: SiteService;
@Inject()
private readonly siteApiService: SiteApiService;
// 移除配置中的站点数组,来源统一改为数据库 // 移除配置中的站点数组,来源统一改为数据库
@ -48,7 +49,7 @@ export class WebhookController {
@Query('siteId') siteIdStr: string, @Query('siteId') siteIdStr: string,
@Headers() header: any @Headers() header: any
) { ) {
console.log(`webhook woocommerce`, siteIdStr, body, header) console.log(`webhook woocommerce`, siteIdStr, body,header)
const signature = header['x-wc-webhook-signature']; const signature = header['x-wc-webhook-signature'];
const topic = header['x-wc-webhook-topic']; const topic = header['x-wc-webhook-topic'];
const source = header['x-wc-webhook-source']; const source = header['x-wc-webhook-source'];
@ -78,44 +79,43 @@ export class WebhookController {
.update(rawBody) .update(rawBody)
.digest('base64'); .digest('base64');
try { try {
if (hash !== signature) { if (hash === signature) {
switch (topic) {
case 'product.created':
case 'product.updated':
// 不再写入本地,平台事件仅确认接收
break;
case 'product.deleted':
// 不再写入本地,平台事件仅确认接收
break;
case 'order.created':
case 'order.updated':
await this.orderService.syncSingleOrder(siteId, body);
break;
case 'order.deleted':
break;
case 'customer.created':
break;
case 'customer.updated':
break;
case 'customer.deleted':
break;
default:
console.log('Unhandled event:', body.event);
}
return {
code: 200,
success: true,
message: 'Webhook processed successfully',
};
} else {
return { return {
code: 403, code: 403,
success: false, success: false,
message: 'Webhook verification failed', message: 'Webhook verification failed',
}; };
} }
const adapter = await this.siteApiService.getAdapter(siteId);
switch (topic) {
case 'product.created':
case 'product.updated':
// 不再写入本地,平台事件仅确认接收
break;
case 'product.deleted':
// 不再写入本地,平台事件仅确认接收
break;
case 'order.created':
case 'order.updated':
const order = adapter.mapOrder(body)
await this.orderService.syncSingleOrder(siteId, order);
break;
case 'order.deleted':
break;
case 'customer.created':
break;
case 'customer.updated':
break;
case 'customer.deleted':
break;
default:
console.log('Unhandled event:', body.event);
return {
code: 200,
success: true,
message: 'Webhook processed successfully',
};
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@ -130,10 +130,23 @@ export class WebhookController {
@Query('signature') signature: string, @Query('signature') signature: string,
@Headers() header: any @Headers() header: any
) { ) {
console.log(`webhook shoppy`, siteIdStr, body, header)
const topic = header['x-oemsaas-event-type']; const topic = header['x-oemsaas-event-type'];
// const source = header['x-oemsaas-shop-domain']; // const source = header['x-oemsaas-shop-domain'];
const siteId = Number(siteIdStr); const siteId = Number(siteIdStr);
const bodys = new UnifiedOrderDTO();
Object.assign(bodys, body);
// 从数据库获取站点配置
const site = await this.siteService.get(siteId, true);
// if (!site || !source?.includes(site.websiteUrl)) {
if (!site) {
console.log('domain not match');
return {
code: HttpStatus.BAD_REQUEST,
success: false,
message: 'domain not match',
};
}
if (!signature) { if (!signature) {
return { return {
@ -149,7 +162,6 @@ export class WebhookController {
// .createHmac('sha256', this.secret) // .createHmac('sha256', this.secret)
// .update(rawBody) // .update(rawBody)
// .digest('base64'); // .digest('base64');
const adapter = await this.siteApiService.getAdapter(siteId);
try { try {
if (this.secret === signature) { if (this.secret === signature) {
switch (topic) { switch (topic) {
@ -162,8 +174,7 @@ export class WebhookController {
break; break;
case 'orders/create': case 'orders/create':
case 'orders/update': case 'orders/update':
const order = adapter.mapOrder(body) await this.orderService.syncSingleOrder(siteId, bodys);
await this.orderService.syncSingleOrder(siteId, order);
break; break;
case 'orders/delete': case 'orders/delete':
break; break;

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

@ -4,6 +4,53 @@ export interface ShopyyTag {
id?: number; id?: number;
name?: string; name?: string;
} }
export interface ShopyyProductQuery {
page: string;
limit: string;
}
/**
* Shopyy
* Shopyy
* 参考文档: https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
*/
export class ShopyyAllProductQuery {
/** 分页大小,限制返回的商品数量 */
limit?: string;
/** 起始ID用于分页返回ID大于该值的商品 */
since_id?: string;
/** 商品ID精确匹配单个商品 */
id?: string;
/** 商品标题,支持模糊查询 */
title?: string;
/** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */
status?: string;
/** 商品SKU编码库存保有单位精确或模糊匹配 */
sku?: string;
/** 商品SPU编码标准化产品单元用于归类同款商品 */
spu?: string;
/** 商品分类ID筛选指定分类下的商品 */
collection_id?: string;
/** 变体价格最小值,筛选变体价格大于等于该值的商品 */
variant_price_min?: string;
/** 变体价格最大值,筛选变体价格小于等于该值的商品 */
variant_price_max?: string;
/** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */
variant_compare_at_price_min?: string;
/** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */
variant_compare_at_price_max?: string;
/** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */
variant_weight_min?: string;
/** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */
variant_weight_max?: string;
/** 商品创建时间最小值格式参考接口文档YYYY-MM-DD HH:mm:ss */
created_at_min?: string;
/** 商品创建时间最大值格式参考接口文档YYYY-MM-DD HH:mm:ss */
created_at_max?: string;
/** 商品更新时间最小值格式参考接口文档YYYY-MM-DD HH:mm:ss */
updated_at_min?: string;
/** 商品更新时间最大值格式参考接口文档YYYY-MM-DD HH:mm:ss */
updated_at_max?: string;
}
// 产品类型 // 产品类型
export interface ShopyyProduct { export interface ShopyyProduct {
// 产品主键 // 产品主键
@ -83,6 +130,42 @@ export interface ShopyyVariant {
position?: number | string; position?: number | string;
sku_code?: string; sku_code?: string;
} }
//
// 订单查询参数类型
export interface ShopyyOrderQuery {
// 订单ID集合 多个ID用','联接 例1,2,3
ids?: string;
// 订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消;
status?: string;
// 物流状态 300 未发货310 部分发货320 已发货330(确认收货)
fulfillment_status?: string;
// 支付状态 200 待支付210 支付中220 部分支付230 已支付240 支付失败250 部分退款260 已退款 290 已取消;
financial_status?: string;
// 支付时间 下限值
pay_at_min?: string;
// 支付时间 上限值
pay_at_max?: string;
// 创建开始时间
created_at_min?: number;
// 创建结束时间
created_at_max?: number;
// 更新时间开始
updated_at_min?: string;
// 更新时间结束
updated_at_max?: string;
// 起始ID
since_id?: string;
// 页码
page?: string;
// 每页条数
limit?: string;
// 排序字段默认id id=订单ID updated_at=最后更新时间 pay_at=支付时间
order_field?: string;
// 排序方式默认desc desc=降序 asc=升序
order_by?: string;
// 订单列表类型
group?: string;
}
// 订单类型 // 订单类型
export interface ShopyyOrder { export interface ShopyyOrder {
@ -219,7 +302,8 @@ export interface ShopyyOrder {
// 创建时间 // 创建时间
created_at?: number; created_at?: number;
// 发货商品表 id // 发货商品表 id
id?: number }>; id?: number
}>;
}>; }>;
shipping_zone_plans?: Array<{ shipping_zone_plans?: Array<{
shipping_price?: number | string; shipping_price?: number | string;

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{
// [] // []
// } // }
@ -17,6 +18,11 @@ export enum OrderFulfillmentStatus {
// 确认发货 // 确认发货
CONFIRMED, CONFIRMED,
} }
//
export class UnifiedProductWhere {
sku?: string;
[prop:string]:any
}
export class UnifiedTagDTO { export class UnifiedTagDTO {
// 标签DTO用于承载统一标签数据 // 标签DTO用于承载统一标签数据
@ApiProperty({ description: '标签ID' }) @ApiProperty({ description: '标签ID' })
@ -137,6 +143,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

@ -65,9 +65,6 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number; promotionPrice: number;
// 分类关联 // 分类关联
@ManyToOne(() => Category, category => category.products) @ManyToOne(() => Category, category => category.products)
@JoinColumn({ name: 'categoryId' }) @JoinColumn({ name: 'categoryId' })

View File

@ -20,51 +20,70 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
export interface ISiteAdapter { export interface ISiteAdapter {
mapOrder(order: any): UnifiedOrderDTO; // ========== 客户映射方法 ==========
mapWebhook(webhook:any):UnifiedWebhookDTO;
mapProduct(product:any): UnifiedProductDTO;
mapReview(data: any): UnifiedReviewDTO;
mapCustomer(data: any): UnifiedCustomerDTO;
mapMedia(data: any): UnifiedMediaDTO;
/** /**
* *
* @param data
* @returns
*/ */
getProducts(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedProductDTO>>; mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO;
/** /**
* *
* @param data
* @returns
*/ */
getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]>; mapUnifiedToPlatformCustomer(data: Partial<UnifiedCustomerDTO>): any;
/** /**
* *
*/ */
getProduct(id: string | number): Promise<UnifiedProductDTO>; getCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>): Promise<UnifiedCustomerDTO>;
/** /**
* *
*/ */
getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>; getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
/** /**
* *
*/ */
getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]>; getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]>;
/** /**
* *
*/ */
getOrder(id: string | number): Promise<UnifiedOrderDTO>; createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
/** /**
* *
*/ */
getSubscriptions(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>>; updateCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
/** /**
* *
*/ */
getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]>; deleteCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>): Promise<boolean>;
/**
*
*/
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
// ========== 媒体映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformMedia(data: Partial<UnifiedMediaDTO>): any;
/** /**
* *
@ -81,75 +100,69 @@ export interface ISiteAdapter {
*/ */
createMedia(file: any): Promise<UnifiedMediaDTO>; createMedia(file: any): Promise<UnifiedMediaDTO>;
// ========== 订单映射方法 ==========
/** /**
* *
* @param data
* @returns
*/ */
getReviews(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedReviewDTO>>; mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO;
/** /**
* *
* @param data
* @returns
*/ */
getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]>; mapUnifiedToPlatformOrder(data: Partial<UnifiedOrderDTO>): any;
/** /**
* *
* @param data
* @returns
*/ */
createReview(data: CreateReviewDTO): Promise<UnifiedReviewDTO>; mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any;
/** /**
* *
* @param data
* @returns
*/ */
updateReview(id: number, data: UpdateReviewDTO): Promise<UnifiedReviewDTO>; mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): any;
/** /**
* *
*/ */
deleteReview(id: number): Promise<boolean>; getOrder(where: Partial<Pick<UnifiedOrderDTO, 'id'>>): Promise<UnifiedOrderDTO>;
/** /**
* *
*/ */
createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO>; getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>;
/** /**
* *
*/ */
updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean>; getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]>;
/** /**
* *
*/ */
deleteProduct(id: string | number): Promise<boolean>; countOrders(params: Record<string, any>): Promise<number>;
/** /**
* *
*/ */
getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<UnifiedVariationPaginationDTO>; createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
/** /**
* *
*/ */
getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]>; updateOrder(where: Partial<Pick<UnifiedOrderDTO, 'id'>>, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
/** /**
* *
*/ */
getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO>; deleteOrder(where: Partial<Pick<UnifiedOrderDTO, 'id'>>): Promise<boolean>;
/**
*
*/
createVariation(productId: string | number, data: CreateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
deleteVariation(productId: string | number, variationId: string | number): Promise<boolean>;
/** /**
* *
@ -161,71 +174,6 @@ export interface ISiteAdapter {
*/ */
createOrderNote(orderId: string | number, data: any): Promise<any>; createOrderNote(orderId: string | number, data: any): Promise<any>;
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
deleteOrder(id: string | number): Promise<boolean>;
batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]>;
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
deleteCustomer(id: string | number): Promise<boolean>;
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
/**
* webhooks列表
*/
getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO>;
/**
* webhooks
*/
getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]>;
/**
* webhook
*/
getWebhook(id: string | number): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
deleteWebhook(id: string | number): Promise<boolean>;
/**
*
*/
getLinks(): Promise<Array<{title: string, url: string}>>;
/**
*
*/
fulfillOrder(orderId: string | number, data: {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any>;
/** /**
* *
*/ */
@ -273,4 +221,276 @@ export interface ISiteAdapter {
* *
*/ */
deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean>; deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean>;
/**
*
*/
batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
// ========== 产品映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformProduct(data: Partial<UnifiedProductDTO>): any;
/**
*
* @param data
* @returns
*/
mapCreateProductParams(data: Partial<UnifiedProductDTO>): any;
/**
*
* @param data
* @returns
*/
mapUpdateProductParams(data: Partial<UnifiedProductDTO>): any;
/**
*
*/
getProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>): Promise<UnifiedProductDTO>;
/**
*
*/
getProducts(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedProductDTO>>;
/**
*
*/
getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]>;
/**
*
*/
createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO>;
/**
*
*/
updateProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>, data: Partial<UnifiedProductDTO>): Promise<boolean>;
/**
*
*/
deleteProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>): Promise<boolean>;
/**
*
*/
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
// ========== 评论映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformReview(data: Partial<UnifiedReviewDTO>): any;
/**
*
* @param data
* @returns
*/
mapCreateReviewParams(data: CreateReviewDTO): any;
/**
*
* @param data
* @returns
*/
mapUpdateReviewParams(data: UpdateReviewDTO): any;
/**
*
*/
getReviews(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedReviewDTO>>;
/**
*
*/
getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]>;
/**
*
*/
createReview(data: CreateReviewDTO): Promise<UnifiedReviewDTO>;
/**
*
*/
updateReview(where: Partial<Pick<UnifiedReviewDTO, 'id'>>, data: UpdateReviewDTO): Promise<UnifiedReviewDTO>;
/**
*
*/
deleteReview(where: Partial<Pick<UnifiedReviewDTO, 'id'>>): Promise<boolean>;
// ========== 订阅映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformSubscription(data: Partial<UnifiedSubscriptionDTO>): any;
/**
*
*/
getSubscriptions(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>>;
/**
*
*/
getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]>;
// ========== 产品变体映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformVariation(data: Partial<UnifiedProductVariationDTO>): any;
/**
*
* @param data
* @returns
*/
mapCreateVariationParams(data: CreateVariationDTO): any;
/**
*
* @param data
* @returns
*/
mapUpdateVariationParams(data: UpdateVariationDTO): any;
/**
*
*/
getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO>;
/**
*
*/
getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<UnifiedVariationPaginationDTO>;
/**
*
*/
getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]>;
/**
*
*/
createVariation(productId: string | number, data: CreateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
deleteVariation(productId: string | number, variationId: string | number): Promise<boolean>;
// ========== Webhook映射方法 ==========
/**
* Webhook数据转换为统一Webhook数据格式
* @param data Webhook数据
* @returns Webhook数据格式
*/
mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO;
/**
* Webhook数据格式转换为平台Webhook数据
* @param data Webhook数据格式
* @returns Webhook数据
*/
mapUnifiedToPlatformWebhook(data: Partial<UnifiedWebhookDTO>): any;
/**
* Webhook创建参数转换为平台Webhook创建参数
* @param data Webhook创建参数
* @returns Webhook创建参数
*/
mapCreateWebhookParams(data: CreateWebhookDTO): any;
/**
* Webhook更新参数转换为平台Webhook更新参数
* @param data Webhook更新参数
* @returns Webhook更新参数
*/
mapUpdateWebhookParams(data: UpdateWebhookDTO): any;
/**
* webhook
*/
getWebhook(where: Partial<Pick<UnifiedWebhookDTO, 'id'>>): Promise<UnifiedWebhookDTO>;
/**
* webhooks列表
*/
getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO>;
/**
* webhooks
*/
getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]>;
/**
* webhook
*/
createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
updateWebhook(where: Partial<Pick<UnifiedWebhookDTO, 'id'>>, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
deleteWebhook(where: Partial<Pick<UnifiedWebhookDTO, 'id'>>): Promise<boolean>;
// ========== 站点/其他方法 ==========
/**
*
*/
getLinks(): Promise<Array<{ title: string, url: string }>>;
} }

View File

@ -75,10 +75,14 @@ export class SyncUniuniShipmentJob implements IJob{
'255': 'Gateway_To_Gateway_Transit' '255': 'Gateway_To_Gateway_Transit'
}; };
async onTick() { async onTick() {
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); try {
shipments.forEach(shipment => { const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
this.logisticsService.updateShipmentState(shipment); shipments.forEach(shipment => {
}); this.logisticsService.updateShipmentState(shipment);
});
} catch (error) {
this.logger.error(`更新运单状态失败 ${error.message}`);
}
} }
onComplete(result: any) { onComplete(result: any) {

View File

@ -202,7 +202,7 @@ export class OrderService {
try { try {
// 调用 WooCommerce API 获取订单 // 调用 WooCommerce API 获取订单
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const order = await adapter.getOrder(orderId); const order = await adapter.getOrder({ id: orderId });
// 检查订单是否已存在,以区分创建和更新 // 检查订单是否已存在,以区分创建和更新
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({

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

@ -1,3 +1,6 @@
/**
* https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
*/
import { ILogger, Inject, Provide } from '@midwayjs/core'; import { ILogger, Inject, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
import * as fs from 'fs'; import * as fs from 'fs';
@ -155,7 +158,7 @@ export class ShopyyService {
* @param params * @param params
* @returns * @returns
*/ */
private async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> { async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> {
const url = this.buildURL(site.apiUrl, endpoint); const url = this.buildURL(site.apiUrl, endpoint);
const headers = this.buildHeaders(site); const headers = this.buildHeaders(site);
@ -180,41 +183,19 @@ export class ShopyyService {
* *
*/ */
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) { public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
const page = Number(params.page || 1); const response = await this.request(site, endpoint, 'GET', null, params);
const limit = Number(params.per_page ?? 20); return this.mapPageResponse<T>(response,params);
const where = params.where && typeof params.where === 'object' ? params.where : {}; }
let orderby: string | undefined = params.orderby; mapPageResponse<T>(response:any,query: Record<string, any>){
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
}
}
// 映射统一入参到平台入参
const requestParams = {
...where,
...(params.search ? { search: params.search } : {}),
...(params.status ? { status: params.status } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
limit
};
this.logger.debug('ShopYY API请求分页参数:'+ JSON.stringify(requestParams));
const response = await this.request(site, endpoint, 'GET', null, requestParams);
if (response?.code !== 0) { if (response?.code !== 0) {
throw new Error(response?.msg) throw new Error(response?.msg)
} }
return { return {
items: (response.data.list || []) as T[], items: (response.data.list || []) as T[],
total: response.data?.paginate?.total || 0, total: response.data?.paginate?.total || 0,
totalPages: response.data?.paginate?.pageTotal || 0, totalPages: response.data?.paginate?.pageTotal || 0,
page: response.data?.paginate?.current || requestParams.page, page: response.data?.paginate?.current || query.page,
per_page: response.data?.paginate?.pagesize || requestParams.limit, per_page: response.data?.paginate?.pagesize || query.limit,
}; };
} }
@ -225,13 +206,13 @@ export class ShopyyService {
* @param pageSize * @param pageSize
* @returns * @returns
*/ */
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> { async getProducts(site: any, page: number = 1, pageSize: number = 100, where: Record<string, any> = {}): Promise<any> {
// ShopYY API: GET /products // ShopYY API: GET /products
// 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含 // 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含
const response = await this.request(site, 'products', 'GET', null, { const response = await this.request(site, 'products', 'GET', null, {
page, page,
page_size: pageSize, page_size: pageSize,
fields: 'id,name,sku,handle,status,type,stock_status,stock_quantity,images,regular_price,sale_price,tags,variations' ...where
}); });
return { return {

View File

@ -1,4 +1,4 @@
import { Inject, Provide } from '@midwayjs/core'; import { ILogger, Inject, Provide } from '@midwayjs/core';
import { ShopyyAdapter } from '../adapter/shopyy.adapter'; import { ShopyyAdapter } from '../adapter/shopyy.adapter';
import { WooCommerceAdapter } from '../adapter/woocommerce.adapter'; import { WooCommerceAdapter } from '../adapter/woocommerce.adapter';
import { ISiteAdapter } from '../interface/site-adapter.interface'; import { ISiteAdapter } from '../interface/site-adapter.interface';
@ -22,6 +22,9 @@ export class SiteApiService {
@Inject() @Inject()
productService: ProductService; productService: ProductService;
@Inject()
logger: ILogger;
async getAdapter(siteId: number): Promise<ISiteAdapter> { async getAdapter(siteId: number): Promise<ISiteAdapter> {
const site = await this.siteService.get(siteId, true); const site = await this.siteService.get(siteId, true);
if (!site) { if (!site) {
@ -110,36 +113,25 @@ export class SiteApiService {
const adapter = await this.getAdapter(siteId); const adapter = await this.getAdapter(siteId);
// 首先尝试查找产品 // 首先尝试查找产品
if (product.id) { if (!product.sku) {
try { throw new Error('产品SKU不能为空');
// 尝试获取产品以确认它是否存在
const existingProduct = await adapter.getProduct(product.id);
if (existingProduct) {
// 产品存在,执行更新
return await adapter.updateProduct(product.id, product);
}
} catch (error) {
// 如果获取产品失败,可能是因为产品不存在,继续执行创建逻辑
console.log(`产品 ${product.id} 不存在,将创建新产品:`, error.message);
}
} else if (product.sku) {
// 如果没有提供ID但提供了SKU尝试通过SKU查找产品
try {
// 尝试搜索具有相同SKU的产品
const searchResult = await adapter.getProducts({ where: { sku: product.sku } });
if (searchResult.items && searchResult.items.length > 0) {
const existingProduct = searchResult.items[0];
// 找到现有产品,更新它
return await adapter.updateProduct(existingProduct.id, product);
}
} catch (error) {
// 搜索失败,继续执行创建逻辑
console.log(`通过SKU搜索产品失败:`, error.message);
}
} }
// 尝试搜索具有相同SKU的产品
let existingProduct
try {
existingProduct = await adapter.getProduct({ sku: product.sku });
} catch (error) {
this.logger.error(`[Site API] 查找产品失败, siteId: ${siteId}, sku: ${product.sku}, 错误信息: ${error.message}`);
existingProduct = null
}
if (existingProduct) {
// 找到现有产品,更新它
return await adapter.updateProduct({ id: existingProduct.id }, product);
}
// 产品不存在,执行创建 // 产品不存在,执行创建
return await adapter.createProduct(product); return await adapter.createProduct(product);
} }
/** /**
@ -189,17 +181,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(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,
@ -240,9 +240,11 @@ export class WPService implements IPlatformService {
return allData; return allData;
} }
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> { async getProducts(site: any, params: Record<string, any> = {}): Promise<any> {
const api = this.createApi(site, 'wc/v3'); const api = this.createApi(site, 'wc/v3');
return await this.sdkGetPage<WooProduct>(api, 'products', { page, per_page: pageSize }); const page = params.page ?? 1;
const per_page = params.per_page ?? params.pageSize ?? 100;
return await this.sdkGetPage<WooProduct>(api, 'products', { ...params, page, per_page });
} }
async getProduct(site: any, id: number): Promise<any> { async getProduct(site: any, id: number): Promise<any> {
@ -254,7 +256,7 @@ export class WPService implements IPlatformService {
// 导出 WooCommerce 产品为特殊CSV平台特性 // 导出 WooCommerce 产品为特殊CSV平台特性
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> { async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
const list = await this.getProducts(site, page, pageSize); const list = await this.getProducts(site, { page, per_page: pageSize });
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity']; const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]); const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');

View File

@ -0,0 +1 @@
// 从 unified 到 数据库需要有个转换流程

View File

@ -0,0 +1 @@
// 文件转换

View File

View File

@ -0,0 +1,8 @@
import { UnifiedOrderDTO } from "../dto/site-api.dto";
export class ShipmentAdapter {
// 用于导出物流需要的数据
mapFromOrder(order: UnifiedOrderDTO): any {
return order;
}
}