feat(adapter): 公开映射方法以支持统一接口调用
将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名 修改webhook控制器以使用适配器映射方法处理订单数据
This commit is contained in:
parent
f2b1036286
commit
837254159a
|
|
@ -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}`,
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
// 移除配置中的站点数组,来源统一改为数据库
|
// 移除配置中的站点数组,来源统一改为数据库
|
||||||
|
|
||||||
|
|
@ -49,7 +48,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'];
|
||||||
|
|
@ -79,43 +78,44 @@ 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,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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
/**
|
/**
|
||||||
* 获取产品列表
|
* 获取产品列表
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue