feat(adapter): 公开映射方法以支持统一接口调用

将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名
修改webhook控制器以使用适配器映射方法处理订单数据
This commit is contained in:
zhuotianyuan 2026-01-07 11:54:34 +08:00
parent f2b1036286
commit 837254159a
4 changed files with 69 additions and 74 deletions

View File

@ -50,7 +50,7 @@ export class ShopyyAdapter implements ISiteAdapter {
// this.mapSubscription = this.mapSubscription.bind(this); // this.mapSubscription = this.mapSubscription.bind(this);
} }
private mapMedia(item: any): UnifiedMediaDTO { mapMedia(item: any): UnifiedMediaDTO {
// 映射媒体项目 // 映射媒体项目
return { return {
id: item.id, id: item.id,
@ -63,7 +63,7 @@ export class ShopyyAdapter implements ISiteAdapter {
}; };
} }
private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { mapMediaSearchParams(params: UnifiedSearchParamsDTO): any {
const { search, page, per_page } = params; const { search, page, per_page } = params;
const shopyyParams: any = { const shopyyParams: any = {
page: page || 1, page: page || 1,
@ -77,7 +77,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return shopyyParams; return shopyyParams;
} }
private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO {
// 映射产品状态 // 映射产品状态
function mapProductStatus(status: number) { function mapProductStatus(status: number) {
return status === 1 ? 'publish' : 'draft'; return status === 1 ? 'publish' : 'draft';
@ -129,7 +129,7 @@ export class ShopyyAdapter implements ISiteAdapter {
}; };
} }
private mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO {
// 映射变体 // 映射变体
return { return {
id: variant.id, id: variant.id,
@ -152,7 +152,7 @@ export class ShopyyAdapter implements ISiteAdapter {
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
} }
private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { mapOrder(item: ShopyyOrder): UnifiedOrderDTO {
// 提取账单和送货地址 如果不存在则为空对象 // 提取账单和送货地址 如果不存在则为空对象
const billing = (item as any).billing_address || {}; const billing = (item as any).billing_address || {};
const shipping = (item as any).shipping_address || {}; const shipping = (item as any).shipping_address || {};
@ -333,7 +333,7 @@ export class ShopyyAdapter implements ISiteAdapter {
// 确认发货 // 确认发货
} }
private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO {
// 处理多地址结构 // 处理多地址结构
const addresses = item.addresses || []; const addresses = item.addresses || [];
const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {});
@ -726,7 +726,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapReview(review); return this.mapReview(review);
} }
private mapReview(review: any): UnifiedReviewDTO { mapReview(review: any): UnifiedReviewDTO {
// 将ShopYY评论数据映射到统一评论DTO格式 // 将ShopYY评论数据映射到统一评论DTO格式
return { return {
id: review.id || review.review_id, id: review.id || review.review_id,
@ -743,7 +743,7 @@ export class ShopyyAdapter implements ISiteAdapter {
}; };
} }
private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { mapReviewSearchParams(params: UnifiedSearchParamsDTO): any {
const { search, page, per_page, where } = params; const { search, page, per_page, where } = params;
const shopyyParams: any = { const shopyyParams: any = {
page: page || 1, page: page || 1,
@ -780,7 +780,7 @@ export class ShopyyAdapter implements ISiteAdapter {
} }
// Webhook相关方法 // Webhook相关方法
private mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO {
return { return {
id: item.id, id: item.id,
name: item.webhook_name || `Webhook-${item.id}`, name: item.webhook_name || `Webhook-${item.id}`,

View File

@ -42,7 +42,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
} }
// 映射 WooCommerce webhook 到统一格式 // 映射 WooCommerce webhook 到统一格式
private mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO {
return { return {
id: webhook.id.toString(), id: webhook.id.toString(),
name: webhook.name, name: webhook.name,
@ -169,7 +169,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial<WooProductSearchParams> { mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial<WooProductSearchParams> {
const page = Number(params.page ?? 1); const page = Number(params.page ?? 1);
const per_page = Number(params.per_page ?? 20); const per_page = Number(params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {}; const where = params.where && typeof params.where === 'object' ? params.where : {};
@ -225,7 +225,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
return mapped; return mapped;
} }
private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<WooOrderSearchParams> { mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<WooOrderSearchParams> {
// 计算分页参数 // 计算分页参数
const page = Number(params.page ?? 1); const page = Number(params.page ?? 1);
const per_page = Number(params.per_page ?? 20); const per_page = Number(params.per_page ?? 20);
@ -293,7 +293,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
return mapped; return mapped;
} }
private mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record<string, any> { mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record<string, any> {
const page = Number(params.page ?? 1); const page = Number(params.page ?? 1);
const per_page = Number(params.per_page ?? 20); const per_page = Number(params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {}; const where = params.where && typeof params.where === 'object' ? params.where : {};
@ -346,7 +346,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
return mapped; return mapped;
} }
private mapProduct(item: WooProduct): UnifiedProductDTO { mapProduct(item: 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
@ -449,7 +449,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
addr.phone addr.phone
].filter(Boolean).join(', '); ].filter(Boolean).join(', ');
} }
private mapOrder(item: WooOrder): UnifiedOrderDTO { mapOrder(item: WooOrder): UnifiedOrderDTO {
// 将 WooCommerce 订单数据映射为统一订单DTO // 将 WooCommerce 订单数据映射为统一订单DTO
// 包含账单地址与收货地址以及创建与更新时间 // 包含账单地址与收货地址以及创建与更新时间
@ -502,7 +502,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
}; };
} }
private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO {
// 将 WooCommerce 订阅数据映射为统一订阅DTO // 将 WooCommerce 订阅数据映射为统一订阅DTO
// 若缺少创建时间则回退为开始时间 // 若缺少创建时间则回退为开始时间
return { return {
@ -520,7 +520,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
}; };
} }
private mapMedia(item: WpMedia): UnifiedMediaDTO { mapMedia(item: WpMedia): UnifiedMediaDTO {
// 将 WordPress 媒体数据映射为统一媒体DTO // 将 WordPress 媒体数据映射为统一媒体DTO
// 兼容不同字段命名的时间信息 // 兼容不同字段命名的时间信息
return { return {
@ -866,7 +866,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
return media.map((mediaItem: any) => this.mapMedia(mediaItem)); return media.map((mediaItem: any) => this.mapMedia(mediaItem));
} }
private mapReview(item: any): UnifiedReviewDTO & { raw: any } { mapReview(item: any): UnifiedReviewDTO & { raw: any } {
// 将 WooCommerce 评论数据映射为统一评论DTO // 将 WooCommerce 评论数据映射为统一评论DTO
return { return {
id: item.id, id: item.id,
@ -939,7 +939,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
return result as any; return result as any;
} }
private mapCustomer(item: WooCustomer): UnifiedCustomerDTO { mapCustomer(item: WooCustomer): UnifiedCustomerDTO {
// 将 WooCommerce 客户数据映射为统一客户DTO // 将 WooCommerce 客户数据映射为统一客户DTO
// 包含基础信息地址信息与时间信息 // 包含基础信息地址信息与时间信息
return { return {
@ -1070,7 +1070,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
} }
// 映射 WooCommerce 变体到统一格式 // 映射 WooCommerce 变体到统一格式
private mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO {
// 将变体属性转换为统一格式 // 将变体属性转换为统一格式
const mappedAttributes = variation.attributes && Array.isArray(variation.attributes) const mappedAttributes = variation.attributes && Array.isArray(variation.attributes)
? variation.attributes.map((attr: any) => ({ ? variation.attributes.map((attr: any) => ({

View File

@ -12,10 +12,7 @@ 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 {
@ -34,6 +31,8 @@ export class WebhookController {
@Inject() @Inject()
private readonly siteService: SiteService; private readonly siteService: SiteService;
@Inject()
private readonly siteApiService: SiteApiService;
// 移除配置中的站点数组,来源统一改为数据库 // 移除配置中的站点数组,来源统一改为数据库
@ -79,7 +78,14 @@ export class WebhookController {
.update(rawBody) .update(rawBody)
.digest('base64'); .digest('base64');
try { try {
if (hash === signature) { if (hash !== signature) {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
}
const adapter = await this.siteApiService.getAdapter(siteId);
switch (topic) { switch (topic) {
case 'product.created': case 'product.created':
case 'product.updated': case 'product.updated':
@ -90,7 +96,8 @@ export class WebhookController {
break; break;
case 'order.created': case 'order.created':
case 'order.updated': case 'order.updated':
await this.orderService.syncSingleOrder(siteId, body); const order = adapter.mapOrder(body)
await this.orderService.syncSingleOrder(siteId, order);
break; break;
case 'order.deleted': case 'order.deleted':
break; break;
@ -102,19 +109,12 @@ export class WebhookController {
break; break;
default: default:
console.log('Unhandled event:', body.event); console.log('Unhandled event:', body.event);
}
return { return {
code: 200, code: 200,
success: true, success: true,
message: 'Webhook processed successfully', message: 'Webhook processed successfully',
}; };
} else {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -130,23 +130,10 @@ 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 {
@ -162,6 +149,7 @@ 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) {
@ -174,7 +162,8 @@ export class WebhookController {
break; break;
case 'orders/create': case 'orders/create':
case 'orders/update': case 'orders/update':
await this.orderService.syncSingleOrder(siteId, bodys); const order = adapter.mapOrder(body)
await this.orderService.syncSingleOrder(siteId, order);
break; break;
case 'orders/delete': case 'orders/delete':
break; break;

View File

@ -20,6 +20,12 @@ 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;
/** /**
* *
*/ */