From 87b4039a67b4dde960c85d288843fca682cbcae5 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 11 Dec 2025 15:08:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=A2=E5=8D=95):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=BB=93=E6=9E=9C=E8=AE=A1=E6=95=B0=E5=92=8C?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E5=9C=B0=E5=9D=80=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在订单服务中添加成功和失败计数返回 为订单DTO添加账单和收货地址全称字段 统一各服务的同步结果返回格式 优化地址格式化逻辑和映射处理 --- src/adapter/shopyy.adapter.ts | 159 +++++++++++------ src/adapter/woocommerce.adapter.ts | 18 ++ src/dto/site-api.dto copy.ts | 264 ++++++++++++++++++++++++++++ src/dto/site-api.dto.ts | 6 + src/service/order.service.ts | 2 + src/service/shopyy.service.ts | 30 ++-- src/service/subscription.service.ts | 4 +- src/service/wp_product.service.ts | 4 +- 8 files changed, 412 insertions(+), 75 deletions(-) create mode 100644 src/dto/site-api.dto copy.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 8db04ac..c8a3b1b 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -60,86 +60,129 @@ export class ShopyyAdapter implements ISiteAdapter { } private mapOrder(item: any): UnifiedOrderDTO { + const billing = item.billing_address || {}; + const shipping = item.shipping_address || {}; + + const billingObj = { + first_name: billing.first_name || item.firstname || '', + last_name: billing.last_name || item.lastname || '', + fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(), + company: billing.company || '', + email: item.customer_email || item.email || '', + phone: billing.phone || item.telephone || '', + address_1: billing.address1 || item.payment_address || '', + address_2: billing.address2 || '', + city: billing.city || item.payment_city || '', + state: billing.province || item.payment_zone || '', + postcode: billing.zip || item.payment_postcode || '', + country: billing.country_name || billing.country_code || item.payment_country || '' + }; + + const shippingObj = { + first_name: shipping.first_name || item.firstname || '', + last_name: shipping.last_name || item.lastname || '', + fullname: shipping.name || '', + company: shipping.company || '', + address_1: shipping.address1 || item.shipping_address || '', + address_2: shipping.address2 || '', + city: shipping.city || item.shipping_city || '', + state: shipping.province || item.shipping_zone || '', + postcode: shipping.zip || item.shipping_postcode || '', + country: shipping.country_name || shipping.country_code || item.shipping_country || '' + }; + + const formatAddress = (addr: any) => { + return [ + addr.fullname, + addr.company, + addr.address_1, + addr.address_2, + addr.city, + addr.state, + addr.postcode, + addr.country, + addr.phone + ].filter(Boolean).join(', '); + }; + return { - id: item.order_id, - number: item.order_sn, - status: item.order_status, - currency: item.currency, - total: item.total_amount, - customer_id: item.user_id, - customer_name: `${item.firstname} ${item.lastname}`.trim(), - email: item.email, + id: item.id || item.order_id, + number: item.order_number || item.order_sn, + status: String(item.status || item.order_status), + currency: item.currency_code || item.currency, + total: String(item.total_price || item.total_amount), + customer_id: item.customer_id || item.user_id, + customer_name: item.customer_name || `${item.firstname} ${item.lastname}`.trim(), + email: item.customer_email || item.email, line_items: (item.products || []).map((p: any) => ({ id: p.id, - name: p.name, + name: p.product_title || p.name, product_id: p.product_id, quantity: p.quantity, - total: p.price, - sku: p.sku + total: String(p.price), + sku: p.sku || p.sku_code || '' })), sales: (item.products || []).map((p: any) => ({ id: p.id, - name: p.name, + name: p.product_title || p.name, product_id: p.product_id, productId: p.product_id, quantity: p.quantity, - total: p.price, - sku: p.sku + total: String(p.price), + sku: p.sku || p.sku_code || '' })), - billing: { - first_name: item.firstname, - last_name: item.lastname, - email: item.email, - phone: item.telephone, - address_1: item.payment_address, - city: item.payment_city, - state: item.payment_zone, - postcode: item.payment_postcode, - country: item.payment_country - }, - shipping: { - first_name: item.firstname, - last_name: item.lastname, - address_1: item.shipping_address, - city: item.shipping_city, - state: item.shipping_zone, - postcode: item.shipping_postcode, - country: item.shipping_country - }, + billing: billingObj, + shipping: shippingObj, + billing_full_address: formatAddress(billingObj), + shipping_full_address: formatAddress(shippingObj), payment_method: item.payment_method, - date_created: item.date_added, + date_created: item.created_at ? new Date(item.created_at * 1000).toISOString() : item.date_added, raw: item, }; } private mapCustomer(item: any): UnifiedCustomerDTO { + // 处理多地址结构 + const addresses = item.addresses || []; + const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); + + // 尝试从地址列表中获取billing和shipping + // 如果没有明确区分,默认使用默认地址或第一个地址 + const billing = defaultAddress; + const shipping = defaultAddress; + return { - id: item.customer_id, - first_name: item.firstname, - last_name: item.lastname, - fullname: item.customer_name, - email: item.customer_email, - phone: item.phone || '', + id: item.id || item.customer_id, + first_name: item.first_name || item.firstname || '', + last_name: item.last_name || item.lastname || '', + fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), + email: item.email || item.customer_email || '', + phone: item.contact || billing.phone || item.phone || '', billing: { - first_name: item.first_name, - last_name: item.last_name, - fullname: item.name, - email: item.email, - phone: item.phone || '', - address_1: item.address_1 || '', - city: item.city || '', - state: item.zone || '', - postcode: item.postcode || '', - country: item.country || '' + first_name: billing.first_name || item.first_name || '', + last_name: billing.last_name || item.last_name || '', + fullname: billing.name || `${billing.first_name || ''} ${billing.last_name || ''}`.trim(), + company: billing.company || '', + email: item.email || '', + phone: billing.phone || item.contact || '', + address_1: billing.address1 || '', + address_2: billing.address2 || '', + city: billing.city || '', + state: billing.province || '', + postcode: billing.zip || '', + country: billing.country_name || billing.country_code || item.country?.country_name || '' }, shipping: { - first_name: item.firstname, - last_name: item.lastname, - address_1: item.address_1 || '', - city: item.city || '', - state: item.zone || '', - postcode: item.postcode || '', - country: item.country || '' + first_name: shipping.first_name || item.first_name || '', + last_name: shipping.last_name || item.last_name || '', + fullname: shipping.name || `${shipping.first_name || ''} ${shipping.last_name || ''}`.trim(), + company: shipping.company || '', + address_1: shipping.address1 || '', + address_2: shipping.address2 || '', + city: shipping.city || '', + state: shipping.province || '', + postcode: shipping.zip || '', + country: shipping.country_name || shipping.country_code || item.country?.country_name || '' }, raw: item, }; diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index a2aeb4b..2b38e70 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -40,6 +40,22 @@ export class WooCommerceAdapter implements ISiteAdapter { } private mapOrder(item: any): UnifiedOrderDTO { + const formatAddress = (addr: any) => { + if (!addr) return ''; + const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); + return [ + name, + addr.company, + addr.address_1, + addr.address_2, + addr.city, + addr.state, + addr.postcode, + addr.country, + addr.phone + ].filter(Boolean).join(', '); + }; + return { id: item.id, number: item.number, @@ -59,6 +75,8 @@ export class WooCommerceAdapter implements ISiteAdapter { })), billing: item.billing, shipping: item.shipping, + billing_full_address: formatAddress(item.billing), + shipping_full_address: formatAddress(item.shipping), payment_method: item.payment_method_title, date_created: item.date_created, raw: item, diff --git a/src/dto/site-api.dto copy.ts b/src/dto/site-api.dto copy.ts new file mode 100644 index 0000000..066cba5 --- /dev/null +++ b/src/dto/site-api.dto copy.ts @@ -0,0 +1,264 @@ +import { ApiProperty } from '@midwayjs/swagger'; + +export class UnifiedPaginationDTO { + @ApiProperty({ description: '列表数据' }) + items: T[]; + + @ApiProperty({ description: '总数', example: 100 }) + total: number; + + @ApiProperty({ description: '当前页', example: 1 }) + page: number; + + @ApiProperty({ description: '每页数量', example: 20 }) + per_page: number; + + @ApiProperty({ description: '总页数', example: 5 }) + totalPages: number; +} + +export class UnifiedImageDTO { + @ApiProperty({ description: '图片ID' }) + id: number | string; + + @ApiProperty({ description: '图片URL' }) + src: string; + + @ApiProperty({ description: '图片名称' }) + name?: string; + + @ApiProperty({ description: '替代文本' }) + alt?: string; +} + +export class UnifiedProductDTO { + @ApiProperty({ description: '产品ID' }) + id: string | number; + + @ApiProperty({ description: '产品名称' }) + name: string; + + @ApiProperty({ description: '产品类型' }) + type: string; + + @ApiProperty({ description: '产品状态' }) + status: string; + + @ApiProperty({ description: '产品SKU' }) + sku: string; + + @ApiProperty({ description: '常规价格' }) + regular_price: string; + + @ApiProperty({ description: '销售价格' }) + sale_price: string; + + @ApiProperty({ description: '当前价格' }) + price: string; + + @ApiProperty({ description: '库存状态' }) + stock_status: string; + + @ApiProperty({ description: '库存数量' }) + stock_quantity: number; + + @ApiProperty({ description: '产品图片', type: [UnifiedImageDTO] }) + images: UnifiedImageDTO[]; + + @ApiProperty({ description: '产品标签', type: 'json' }) + tags?: string[]; + + @ApiProperty({ description: '产品属性', type: 'json' }) + attributes: any[]; + + @ApiProperty({ description: '产品变体', type: 'json' }) + variations?: any[]; + + @ApiProperty({ description: '创建时间' }) + date_created: string; + + @ApiProperty({ description: '更新时间' }) + date_modified: string; + + @ApiProperty({ description: '原始数据(保留备用)', type: 'json' }) + raw?: any; +} + +export class UnifiedOrderDTO { + @ApiProperty({ description: '订单ID' }) + id: string | number; + + @ApiProperty({ description: '订单号' }) + number: string; + + @ApiProperty({ description: '订单状态' }) + status: string; + + @ApiProperty({ description: '货币' }) + currency: string; + + @ApiProperty({ description: '总金额' }) + total: string; + + @ApiProperty({ description: '客户ID' }) + customer_id: number; + + @ApiProperty({ description: '客户姓名' }) + customer_name: string; + + @ApiProperty({ description: '客户邮箱' }) + email: string; + + @ApiProperty({ description: '订单项', type: 'json' }) + line_items: any[]; + + @ApiProperty({ description: '销售项(兼容前端)', type: 'json' }) + sales?: any[]; + + @ApiProperty({ description: '账单地址', type: 'json' }) + billing: any; + + @ApiProperty({ description: '收货地址', type: 'json' }) + shipping: any; + + @ApiProperty({ description: '账单地址全称' }) + billing_full_address?: string; + + @ApiProperty({ description: '收货地址全称' }) + shipping_full_address?: string; + + @ApiProperty({ description: '支付方式' }) + payment_method: string; + + @ApiProperty({ description: '创建时间' }) + date_created: string; + + @ApiProperty({ description: '原始数据', type: 'json' }) + raw?: any; +} + +export class UnifiedCustomerDTO { + @ApiProperty({ description: '客户ID' }) + id: string | number; + + @ApiProperty({ description: '邮箱' }) + email: string; + + @ApiProperty({ description: '名' }) + first_name?: string; + + @ApiProperty({ description: '姓' }) + last_name?: string; + + @ApiProperty({ description: '名字' }) + fullname?: string; + + @ApiProperty({ description: '用户名' }) + username?: string; + + @ApiProperty({ description: '电话' }) + phone?: string; + + @ApiProperty({ description: '账单地址', type: 'json' }) + billing?: any; + + @ApiProperty({ description: '收货地址', type: 'json' }) + shipping?: any; + + @ApiProperty({ description: '原始数据', type: 'json' }) + raw?: any; +} + +export class UnifiedSubscriptionDTO { + @ApiProperty({ description: '订阅ID' }) + id: string | number; + + @ApiProperty({ description: '订阅状态' }) + status: string; + + @ApiProperty({ description: '客户ID' }) + customer_id: number; + + @ApiProperty({ description: '计费周期' }) + billing_period: string; + + @ApiProperty({ description: '计费间隔' }) + billing_interval: number; + + @ApiProperty({ description: '开始时间' }) + start_date: string; + + @ApiProperty({ description: '下次支付时间' }) + next_payment_date: string; + + @ApiProperty({ description: '订单项', type: 'json' }) + line_items: any[]; + + @ApiProperty({ description: '原始数据', type: 'json' }) + raw?: any; +} + +export class UnifiedMediaDTO { + @ApiProperty({ description: '媒体ID' }) + id: number; + + @ApiProperty({ description: '标题' }) + title: string; + + @ApiProperty({ description: '媒体类型' }) + media_type: string; + + @ApiProperty({ description: 'MIME类型' }) + mime_type: string; + + @ApiProperty({ description: '源URL' }) + source_url: string; + + @ApiProperty({ description: '创建时间' }) + date_created: string; +} + +export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO { + @ApiProperty({ description: '列表数据', type: [UnifiedProductDTO] }) + items: UnifiedProductDTO[]; +} + +export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO { + @ApiProperty({ description: '列表数据', type: [UnifiedOrderDTO] }) + items: UnifiedOrderDTO[]; +} + +export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO { + @ApiProperty({ description: '列表数据', type: [UnifiedCustomerDTO] }) + items: UnifiedCustomerDTO[]; +} + +export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO { + @ApiProperty({ description: '列表数据', type: [UnifiedSubscriptionDTO] }) + items: UnifiedSubscriptionDTO[]; +} + +export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO { + @ApiProperty({ description: '列表数据', type: [UnifiedMediaDTO] }) + items: UnifiedMediaDTO[]; +} + +export class UnifiedSearchParamsDTO { + @ApiProperty({ description: '页码', example: 1 }) + page?: number; + + @ApiProperty({ description: '每页数量', example: 20 }) + per_page?: number; + + @ApiProperty({ description: '搜索关键词' }) + search?: string; + + @ApiProperty({ description: '状态' }) + status?: string; + + @ApiProperty({ description: '排序字段' }) + orderby?: string; + + @ApiProperty({ description: '排序方式' }) + order?: string; +} diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index 8fc1144..066cba5 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -121,6 +121,12 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '收货地址', type: 'json' }) shipping: any; + @ApiProperty({ description: '账单地址全称' }) + billing_full_address?: string; + + @ApiProperty({ description: '收货地址全称' }) + shipping_full_address?: string; + @ApiProperty({ description: '支付方式' }) payment_method: string; diff --git a/src/service/order.service.ts b/src/service/order.service.ts index aac76d0..4305bd8 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -117,6 +117,8 @@ export class OrderService { } return { success: failureCount === 0, + successCount, + failureCount, message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`, }; } diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index ca6cd66..b0bba1a 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -31,7 +31,7 @@ export class ShopyyService implements IPlatformService { * @returns 请求头 */ private buildHeaders(site: Site): Record { - if(!site?.token){ + if (!site?.token) { throw new Error(`获取站点${site?.name}数据,但失败,因为未设置站点令牌配置`) } return { @@ -52,7 +52,7 @@ export class ShopyyService implements IPlatformService { private async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise { const url = this.buildURL(site.apiUrl, endpoint); const headers = this.buildHeaders(site); - + const config: AxiosRequestConfig = { url, method, @@ -60,7 +60,7 @@ export class ShopyyService implements IPlatformService { params, data }; - + try { const response = await axios(config); return response.data; @@ -81,11 +81,11 @@ export class ShopyyService implements IPlatformService { limit: params.per_page || 20 }; const response = await this.request(site, endpoint, 'GET', null, requestParams); - if(response?.code !== 0){ + if (response?.code !== 0) { throw new Error(response?.msg) } return { - items: response.data.list || [], + items: (response.data.list || []) as T, total: response.data?.paginate?.total || 0, totalPages: response.data?.paginate?.pageTotal || 0, page: response.data?.paginate?.current || requestParams.page, @@ -106,7 +106,7 @@ export class ShopyyService implements IPlatformService { page, page_size: pageSize }); - + return { items: response.data || [], total: response.meta?.pagination?.total || 0, @@ -142,7 +142,7 @@ export class ShopyyService implements IPlatformService { page, page_size: pageSize }); - + return { items: response.data || [], total: response.meta?.pagination?.total || 0, @@ -175,13 +175,13 @@ export class ShopyyService implements IPlatformService { async getOrders(site: any | number, page: number = 1, pageSize: number = 100): Promise { // 如果传入的是站点ID,则获取站点配置 const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site; - + // ShopYY API: GET /orders const response = await this.request(siteConfig, 'orders', 'GET', null, { page, page_size: pageSize }); - + return { items: response.data || [], total: response.meta?.pagination?.total || 0, @@ -199,7 +199,7 @@ export class ShopyyService implements IPlatformService { */ async getOrder(siteId: string, orderId: string): Promise { const site = await this.siteService.get(Number(siteId)); - + // ShopYY API: GET /orders/{id} const response = await this.request(site, `orders/${orderId}`, 'GET'); return response.data; @@ -247,7 +247,7 @@ export class ShopyyService implements IPlatformService { // ShopYY产品状态映射 const shopyyStatus = status === 'publish' ? 1 : 0; const shopyyStockStatus = stockStatus === 'instock' ? 1 : 0; - + try { await this.request(site, `products/${productId}`, 'PUT', { status: shopyyStatus, @@ -312,7 +312,7 @@ export class ShopyyService implements IPlatformService { carrier_name: data.carrier_name, shipping_method: data.shipping_method }; - + const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', shipmentData); return response.data; } @@ -349,7 +349,7 @@ export class ShopyyService implements IPlatformService { page, page_size: pageSize }); - + return { items: response.data || [], total: response.meta?.pagination?.total || 0, @@ -425,8 +425,8 @@ export class ShopyyService implements IPlatformService { */ async fetchCustomersPaged(site: any, params: any): Promise { // ShopYY API: GET /customers - const { items, total, totalPages, page, per_page } = - await this.fetchResourcePaged(site, 'customers', params); + const { items, total, totalPages, page, per_page } = + await this.fetchResourcePaged(site, 'customers/list', params); return { items, total, diff --git a/src/service/subscription.service.ts b/src/service/subscription.service.ts index 0b17f1f..2f9fda1 100644 --- a/src/service/subscription.service.ts +++ b/src/service/subscription.service.ts @@ -35,11 +35,13 @@ export class SubscriptionService { } return { success: failureCount === 0, + successCount, + failureCount, message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`, }; } catch (error) { console.error('同步订阅失败:', error); - return { success: false, message: `同步失败: ${error.message}` }; + return { success: false, successCount: 0, failureCount: 0, message: `同步失败: ${error.message}` }; } } diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts index 6f20d91..ff9000a 100644 --- a/src/service/wp_product.service.ts +++ b/src/service/wp_product.service.ts @@ -606,11 +606,13 @@ export class WpProductService { } return { success: failureCount === 0, + successCount, + failureCount, message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`, }; } catch (error) { console.error('同步站点产品失败:', error); - return { success: false, message: `同步失败: ${error.message}` }; + return { success: false, successCount: 0, failureCount: 0, message: `同步失败: ${error.message}` }; } }