diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 33d1e61..10c51fb 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -50,7 +50,7 @@ export class ShopyyAdapter implements ISiteAdapter { // this.mapSubscription = this.mapSubscription.bind(this); } - private mapMedia(item: any): UnifiedMediaDTO { + mapMedia(item: any): UnifiedMediaDTO { // 映射媒体项目 return { 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 shopyyParams: any = { page: page || 1, @@ -77,7 +77,7 @@ export class ShopyyAdapter implements ISiteAdapter { return shopyyParams; } - private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { // 映射产品状态 function mapProductStatus(status: number) { return status === 1 ? 'publish' : 'draft'; @@ -129,7 +129,7 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - private mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { + mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { // 映射变体 return { id: variant.id, @@ -152,7 +152,7 @@ export class ShopyyAdapter implements ISiteAdapter { [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled } - private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { + mapOrder(item: ShopyyOrder): UnifiedOrderDTO { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_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 defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); @@ -726,7 +726,7 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapReview(review); } - private mapReview(review: any): UnifiedReviewDTO { + mapReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { 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 shopyyParams: any = { page: page || 1, @@ -780,7 +780,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // Webhook相关方法 - private mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { return { id: item.id, name: item.webhook_name || `Webhook-${item.id}`, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index badf467..c407780 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -42,7 +42,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 映射 WooCommerce webhook 到统一格式 - private mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { return { id: webhook.id.toString(), name: webhook.name, @@ -169,7 +169,7 @@ export class WooCommerceAdapter implements ISiteAdapter { - private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { + mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; @@ -225,7 +225,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 计算分页参数 const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); @@ -293,7 +293,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - private mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { + mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; @@ -346,7 +346,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - private mapProduct(item: WooProduct): UnifiedProductDTO { + mapProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -449,7 +449,7 @@ export class WooCommerceAdapter implements ISiteAdapter { addr.phone ].filter(Boolean).join(', '); } - private mapOrder(item: WooOrder): UnifiedOrderDTO { + mapOrder(item: WooOrder): UnifiedOrderDTO { // 将 WooCommerce 订单数据映射为统一订单DTO // 包含账单地址与收货地址以及创建与更新时间 @@ -502,7 +502,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { // 将 WooCommerce 订阅数据映射为统一订阅DTO // 若缺少创建时间则回退为开始时间 return { @@ -520,7 +520,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - private mapMedia(item: WpMedia): UnifiedMediaDTO { + mapMedia(item: WpMedia): UnifiedMediaDTO { // 将 WordPress 媒体数据映射为统一媒体DTO // 兼容不同字段命名的时间信息 return { @@ -866,7 +866,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return media.map((mediaItem: any) => this.mapMedia(mediaItem)); } - private mapReview(item: any): UnifiedReviewDTO & { raw: any } { + mapReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -939,7 +939,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return result as any; } - private mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + mapCustomer(item: WooCustomer): UnifiedCustomerDTO { // 将 WooCommerce 客户数据映射为统一客户DTO // 包含基础信息地址信息与时间信息 return { @@ -1070,7 +1070,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 映射 WooCommerce 变体到统一格式 - private mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { + mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { // 将变体属性转换为统一格式 const mappedAttributes = variation.attributes && Array.isArray(variation.attributes) ? variation.attributes.map((attr: any) => ({ diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index 69a070d..37fcc5a 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -9,9 +9,10 @@ import { } from '@midwayjs/decorator'; import { Context } from '@midwayjs/koa'; import * as crypto from 'crypto'; - + import { SiteService } from '../service/site.service'; import { OrderService } from '../service/order.service'; +import { SiteApiService } from '../service/site-api.service'; @Controller('/webhook') export class WebhookController { @@ -27,9 +28,11 @@ export class WebhookController { @Logger() logger: ILogger; - + @Inject() private readonly siteService: SiteService; + @Inject() + private readonly siteApiService: SiteApiService; // 移除配置中的站点数组,来源统一改为数据库 @@ -45,7 +48,7 @@ export class WebhookController { @Query('siteId') siteIdStr: string, @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 topic = header['x-wc-webhook-topic']; const source = header['x-wc-webhook-source']; @@ -75,7 +78,80 @@ export class WebhookController { .update(rawBody) .digest('base64'); 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) { + 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) { + console.log(error); + } + } + + + + @Post('/shoppy') + async handleShoppyWebhook( + @Body() body: any, + @Query('siteId') siteIdStr: string, + @Query('signature') signature: string, + @Headers() header: any + ) { + console.log(`webhook shoppy`, siteIdStr, body, header) + const topic = header['x-oemsaas-event-type']; + // const source = header['x-oemsaas-shop-domain']; + const siteId = Number(siteIdStr); + + if (!signature) { + return { + code: HttpStatus.BAD_REQUEST, + success: false, + message: 'Signature missing', + }; + } + + //shopyy 无法提供加密字段校验,注释校验逻辑 + // const rawBody = this.ctx.request.rawBody; + // const hash = crypto + // .createHmac('sha256', this.secret) + // .update(rawBody) + // .digest('base64'); + const adapter = await this.siteApiService.getAdapter(siteId); + try { + if (this.secret === signature) { switch (topic) { case 'product.created': case 'product.updated': @@ -84,11 +160,12 @@ export class WebhookController { case 'product.deleted': // 不再写入本地,平台事件仅确认接收 break; - case 'order.created': - case 'order.updated': - await this.orderService.syncSingleOrder(siteId, body); + case 'orders/create': + case 'orders/update': + const order = adapter.mapOrder(body) + await this.orderService.syncSingleOrder(siteId, order); break; - case 'order.deleted': + case 'orders/delete': break; case 'customer.created': break; @@ -97,7 +174,7 @@ export class WebhookController { case 'customer.deleted': break; default: - console.log('Unhandled event:', body.event); + console.log('Unhandled event:', topic); } return { diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts index a579a33..56fce60 100644 --- a/src/entity/site.entity.ts +++ b/src/entity/site.entity.ts @@ -11,7 +11,10 @@ export class Site { apiUrl: string; @Column({ name: 'website_url', length: 255, nullable: true }) - websiteUrl: string; + websiteUrl?: string; + + @Column({ name: 'webhook_url', length: 255, nullable: true }) + webhookUrl?: string; @Column({ length: 255, nullable: true }) consumerKey?: string; diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 2e02cc3..9baf0c7 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -20,6 +20,12 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; 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; /** * 获取产品列表 */ diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts index 69a1495..e0fd0cf 100644 --- a/src/middleware/auth.middleware.ts +++ b/src/middleware/auth.middleware.ts @@ -21,6 +21,7 @@ export class AuthMiddleware implements IMiddleware { whiteList = [ '/user/login', '/webhook/woocommerce', + '/webhook/shoppy', '/logistics/getTrackingNumber', '/logistics/getListByTrackingId', '/product/categories/all',