diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 0cca7ff..1cf00fd 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -17,7 +17,10 @@ import { UnifiedAddressDTO, UnifiedShippingLineDTO, OrderFulfillmentStatus, - FulfillmentDTO + FulfillmentDTO, + CreateReviewDTO, + CreateVariationDTO, + UpdateReviewDTO } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { @@ -32,6 +35,7 @@ import { import { OrderStatus, } from '../enums/base.enum'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class ShopyyAdapter implements ISiteAdapter { shopyyFinancialStatusMap= { '200': '待支付', @@ -44,130 +48,181 @@ export class ShopyyAdapter implements ISiteAdapter { '290':"已取消", } constructor(private site: any, private shopyyService: ShopyyService) { - this.mapCustomer = this.mapCustomer.bind(this); - this.mapProduct = this.mapProduct.bind(this); - this.mapVariation = this.mapVariation.bind(this); - this.mapOrder = this.mapOrder.bind(this); - this.mapMedia = this.mapMedia.bind(this); - // this.mapSubscription = this.mapSubscription.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedVariation = this.mapPlatformToUnifiedVariation.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); } - private mapMedia(item: any): UnifiedMediaDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { + // 处理多地址结构 + const addresses = item.addresses || []; + const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); + + // 尝试从地址列表中获取billing和shipping + // 如果没有明确区分,默认使用默认地址或第一个地址 + const billingAddress = defaultAddress; + const shippingAddress = defaultAddress; + + const billing = { + first_name: billingAddress.first_name || item.first_name || '', + last_name: billingAddress.last_name || item.last_name || '', + fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), + company: billingAddress.company || '', + email: item.email || '', + phone: billingAddress.phone || item.contact || '', + address_1: billingAddress.address1 || '', + address_2: billingAddress.address2 || '', + city: billingAddress.city || '', + state: billingAddress.province || '', + postcode: billingAddress.zip || '', + country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' + }; + + const shipping = { + first_name: shippingAddress.first_name || item.first_name || '', + last_name: shippingAddress.last_name || item.last_name || '', + fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), + company: shippingAddress.company || '', + address_1: shippingAddress.address1 || '', + address_2: shippingAddress.address2 || '', + city: shippingAddress.city || '', + state: shippingAddress.province || '', + postcode: shippingAddress.zip || '', + country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' + }; + + return { + id: item.id || item.customer_id, + orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), + total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), + 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, + shipping, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), + raw: item, + }; + } + + mapUnifiedToPlatformCustomer(data: Partial) { + return data + } + + async getCustomer(id: string | number): Promise { + const customer = await this.shopyyService.getCustomer(this.site, id); + return this.mapPlatformToUnifiedCustomer(customer); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const { items, total, totalPages, page, per_page } = + await this.shopyyService.fetchCustomersPaged(this.site, params); + return { + items: items.map(this.mapPlatformToUnifiedCustomer.bind(this)), + total, + totalPages, + page, + per_page + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllCustomers 暂未实现 + throw new Error('Shopyy getAllCustomers 暂未实现'); + } + + async createCustomer(data: Partial): Promise { + const createdCustomer = await this.shopyyService.createCustomer(this.site, data); + return this.mapPlatformToUnifiedCustomer(createdCustomer); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + return this.mapPlatformToUnifiedCustomer(updatedCustomer); + } + + async deleteCustomer(id: string | number): Promise { + return await this.shopyyService.deleteCustomer(this.site, id); + } + + batchProcessCustomers?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { // 映射媒体项目 return { - id: item.id, - date_created: item.created_at, - date_modified: item.updated_at, - source_url: item.src, - title: item.alt || '', + id: data.id, + date_created: data.created_at, + date_modified: data.updated_at, + source_url: data.src, + title: data.alt || '', media_type: '', // Shopyy API未提供,暂时留空 mime_type: '', // Shopyy API未提供,暂时留空 }; } - private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { + mapUnifiedToPlatformMedia(data: Partial) { + return data + } + + async getMedia( + params: UnifiedSearchParamsDTO + ): Promise> { + const requestParams = this.mapMediaSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'media', // Shopyy的媒体API端点可能需要调整 + requestParams + ); + return { + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllMedia 暂未实现 + throw new Error('Shopyy getAllMedia 暂未实现'); + } + + async createMedia(file: any): Promise { + const createdMedia = await this.shopyyService.createMedia(this.site, file); + return this.mapPlatformToUnifiedMedia(createdMedia); + } + + async updateMedia(id: string | number, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + return this.mapPlatformToUnifiedMedia(updatedMedia); + } + + async deleteMedia(id: string | number): Promise { + return await this.shopyyService.deleteMedia(this.site, id); + } + + mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { return this.mapSearchParams(params) } - /** - * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 - * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 - */ - private mapSearchParams(params: UnifiedSearchParamsDTO): any { - // 处理分页参数 - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - - // 处理 where 条件 - const query: any = { - ...(params.where || {}), - page, - limit, - } - if(params.orderBy){ - const [field, dir] = Object.entries(params.orderBy)[0]; - query.order_by = dir === 'desc' ? 'desc' : 'asc'; - query.order_field = field - } - return query; - } - - private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { - // 映射产品状态 - function mapProductStatus(status: number) { - return status === 1 ? 'publish' : 'draft'; - } - return { - id: item.id, - name: item.name || item.title, - type: String(item.product_type ?? ''), - status: mapProductStatus(item.status), - sku: item.variant?.sku || '', - regular_price: String(item.variant?.price ?? ''), - sale_price: String(item.special_price ?? ''), - price: String(item.price ?? ''), - stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: item.inventory_quantity, - images: (item.images || []).map((img: any) => ({ - id: img.id || 0, - src: img.src, - name: '', - alt: img.alt || '', - // 排序 - position: img.position || '', - })), - attributes: (item.options || []).map(option => ({ - id: option.id || 0, - name: option.option_name || '', - options: (option.values || []).map(value => value.option_value || ''), - })), - tags: (item.tags || []).map((t: any) => ({ - id: t.id || 0, - name: t.name || '', - })), - // shopyy叫做专辑 - categories: item.collections.map((c: any) => ({ - id: c.id || 0, - name: c.title || '', - })), - variations: item.variants?.map(this.mapVariation.bind(this)) || [], - permalink: item.permalink, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : String(item.created_at ?? ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : String(item.updated_at ?? ''), - raw: item, - }; - } - - private mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { - // 映射变体 - return { - id: variant.id, - name: variant.sku || '', - sku: variant.sku || '', - regular_price: String(variant.price ?? ''), - sale_price: String(variant.special_price ?? ''), - price: String(variant.price ?? ''), - stock_status: - variant.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: variant.inventory_quantity, - }; - } - - shopyyOrderStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; - [100]: OrderStatus.PENDING, // 100 未完成 转为 pending - [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing - // 已发货 - - [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed - [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled - } - private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_address || {}; const shipping = (item as any).shipping_address || {}; @@ -336,99 +391,162 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } - shopyyFulfillmentStatusMap = { - // 未发货 - '300': OrderFulfillmentStatus.PENDING, - // 部分发货 - '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, - // 已发货 - '320': OrderFulfillmentStatus.FULFILLED, - // 已取消 - '330': OrderFulfillmentStatus.CANCELLED, - // 确认发货 + + mapUnifiedToPlatformOrder(data: Partial) { + return data } - private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { - // 处理多地址结构 - const addresses = item.addresses || []; - const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); - - // 尝试从地址列表中获取billing和shipping - // 如果没有明确区分,默认使用默认地址或第一个地址 - const billingAddress = defaultAddress; - const shippingAddress = defaultAddress; - - const billing = { - first_name: billingAddress.first_name || item.first_name || '', - last_name: billingAddress.last_name || item.last_name || '', - fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), - company: billingAddress.company || '', - email: item.email || '', - phone: billingAddress.phone || item.contact || '', - address_1: billingAddress.address1 || '', - address_2: billingAddress.address2 || '', - city: billingAddress.city || '', - state: billingAddress.province || '', - postcode: billingAddress.zip || '', - country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' - }; - - const shipping = { - first_name: shippingAddress.first_name || item.first_name || '', - last_name: shippingAddress.last_name || item.last_name || '', - fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), - company: shippingAddress.company || '', - address_1: shippingAddress.address1 || '', - address_2: shippingAddress.address2 || '', - city: shippingAddress.city || '', - state: shippingAddress.province || '', - postcode: shippingAddress.zip || '', - country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' - }; - - return { - id: item.id || item.customer_id, - orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), - total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), - 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, - shipping, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), - raw: item, - }; - } - mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { - return this.mapSearchParams(query) + mapCreateOrderParams(data: Partial): any { + return data } - async getProducts( + mapUpdateOrderParams(data: Partial): any { + // 构建 ShopYY 订单更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.status !== undefined) { + // 映射订单状态 + const statusMap = { + [OrderStatus.PENDING]: 100, // pending -> 100 未完成 + [OrderStatus.PROCESSING]: 110, // processing -> 110 待处理 + [OrderStatus.COMPLETED]: 180, // completed -> 180 已完成 + [OrderStatus.CANCEL]: 190 // cancel -> 190 取消 + }; + params.status = statusMap[data.status] || 100; + } + + if (data.payment_method !== undefined) { + params.payment_method = data.payment_method; + } + + if (data.billing) { + // 更新客户信息 + if (data.billing.first_name !== undefined) { + params.firstname = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.lastname = data.billing.last_name; + } + if (data.billing.email !== undefined) { + params.email = data.billing.email; + } + if (data.billing.phone !== undefined) { + params.phone = data.billing.phone; + } + + // 更新账单地址 + params.billing_address = params.billing_address || {}; + if (data.billing.first_name !== undefined) { + params.billing_address.first_name = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.billing_address.last_name = data.billing.last_name; + } + if (data.billing.company !== undefined) { + params.billing_address.company = data.billing.company; + } + if (data.billing.address_1 !== undefined) { + params.billing_address.address1 = data.billing.address_1; + } + if (data.billing.address_2 !== undefined) { + params.billing_address.address2 = data.billing.address_2; + } + if (data.billing.city !== undefined) { + params.billing_address.city = data.billing.city; + } + if (data.billing.state !== undefined) { + params.billing_address.province = data.billing.state; + } + if (data.billing.postcode !== undefined) { + params.billing_address.zip = data.billing.postcode; + } + if (data.billing.country !== undefined) { + params.billing_address.country_code = data.billing.country; + } + } + + if (data.shipping) { + // 更新送货地址 + params.shipping_address = params.shipping_address || {}; + if (data.shipping.first_name !== undefined) { + params.shipping_address.first_name = data.shipping.first_name; + } + if (data.shipping.last_name !== undefined) { + params.shipping_address.last_name = data.shipping.last_name; + } + if (data.shipping.company !== undefined) { + params.shipping_address.company = data.shipping.company; + } + if (data.shipping.address_1 !== undefined) { + params.shipping_address.address1 = data.shipping.address_1; + } + if (data.shipping.address_2 !== undefined) { + params.shipping_address.address2 = data.shipping.address_2; + } + if (data.shipping.city !== undefined) { + params.shipping_address.city = data.shipping.city; + } + if (data.shipping.state !== undefined) { + params.shipping_address.province = data.shipping.state; + } + if (data.shipping.postcode !== undefined) { + params.shipping_address.zip = data.shipping.postcode; + } + if (data.shipping.country !== undefined) { + params.shipping_address.country_code = data.shipping.country; + } + if (data.shipping.phone !== undefined) { + params.shipping_address.phone = data.shipping.phone; + } + } + + // 更新订单项 + if (data.line_items && data.line_items.length > 0) { + params.products = data.line_items.map((item: UnifiedOrderLineItemDTO) => ({ + product_id: item.product_id, + quantity: item.quantity, + // price: item.price || '0.00', + sku: item.sku || '', + })); + } + + // 更新物流信息 + if (data.shipping_lines && data.shipping_lines.length > 0) { + const shippingLine = data.shipping_lines[0]; + if (shippingLine.method_title !== undefined) { + params.shipping_method = shippingLine.method_title; + } + if (shippingLine.total !== undefined) { + params.shipping_price = shippingLine.total; + } + } + + // // 更新备注信息 + // if (data.note !== undefined) { + // params.note = data.note; + // } + + return params; + } + + async getOrder(id: string | number): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(id)); + return this.mapPlatformToUnifiedOrder(data); + } + + async getOrders( params: UnifiedSearchParamsDTO - ): Promise> { - // 转换搜索参数 - const requestParams = this.mapProductQuery(params); - const response = await this.shopyyService.fetchResourcePaged( + ): Promise> { + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, - 'products/list', - requestParams + 'orders', + normalizedParams ); - const { items = [], total, totalPages, page, per_page } = response; - const finalItems = items.map((item) => ({ - ...item, - permalink: `${this.site.websiteUrl}/products/${item.handle}`, - })).map(this.mapProduct.bind(this)) return { - items: finalItems as UnifiedProductDTO[], + items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), total, totalPages, page, @@ -436,34 +554,37 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + const data = await this.shopyyService.getAllOrders(this.site.id, params); + return data.map(this.mapPlatformToUnifiedOrder.bind(this)); } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapProduct(product); + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; } - async createProduct(data: Partial): Promise { - const res = await this.shopyyService.createProduct(this.site, data); - return this.mapProduct(res); - } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). - await this.shopyyService.updateProduct(this.site, String(id), data); - return true; + async createOrder(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateOrderParams(data); + const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); + return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateVariation(productId: string | number, variationId: string | number, data: any): Promise { - await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data); - return { ...data, id: variationId }; + async updateOrder(id: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateOrderParams(data); + return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + } + + async deleteOrder(id: string | number): Promise { + return await this.shopyyService.deleteOrder(this.site, id); } async getOrderNotes(orderId: string | number): Promise { @@ -474,22 +595,65 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.createOrderNote(this.site, orderId, data); } - async deleteProduct(id: string | number): Promise { - // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); - return true; + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + // 取消订单履行 + try { + // 调用 ShopyyService 的取消履行方法 + const cancelShipData = { + order_id: String(orderId), + fulfillment_id: data.shipment_id || '' + }; + const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); + + return { + success: result, + order_id: orderId, + shipment_id: data.shipment_id, + reason: data.reason, + cancelled_at: new Date().toISOString() + }; + } catch (error) { + throw new Error(`履行失败: ${error.message}`); + } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - return await this.shopyyService.batchProcessProducts(this.site, data); + async getOrderFulfillments(orderId: string | number): Promise { + return await this.shopyyService.getFulfillments(this.site, String(orderId)); } - /** - * 将统一的订单查询参数转换为 ShopYY 订单查询参数 - * 包含状态映射等特殊处理 - */ - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + + async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { + // 调用 Shopyy Service 的 createFulfillment 方法 + const fulfillmentData = { + tracking_number: data.tracking_number, + carrier_code: data.shipping_provider, + carrier_name: data.shipping_provider, + shipping_method: data.shipping_method || 'standard' + }; + + return await this.shopyyService.createFulfillment(this.site, String(orderId), fulfillmentData); + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: string; + tracking_provider?: string; + date_shipped?: string; + status_shipped?: string; + }): Promise { + return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + batchProcessOrders?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 首先使用通用参数转换 const baseParams = this.mapSearchParams(params); @@ -518,18 +682,205 @@ export class ShopyyAdapter implements ISiteAdapter { return baseParams; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - // 转换订单查询参数 - const normalizedParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + // 映射产品状态 + function mapProductStatus(status: number) { + return status === 1 ? 'publish' : 'draft'; + } return { - items: items.map(this.mapOrder.bind(this)), + id: item.id, + name: item.name || item.title, + type: String(item.product_type ?? ''), + status: mapProductStatus(item.status), + sku: item.variant?.sku || '', + regular_price: String(item.variant?.price ?? ''), + sale_price: String(item.special_price ?? ''), + price: String(item.price ?? ''), + stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: item.inventory_quantity, + images: (item.images || []).map((img: any) => ({ + id: img.id || 0, + src: img.src, + name: '', + alt: img.alt || '', + // 排序 + position: img.position || '', + })), + attributes: (item.options || []).map(option => ({ + id: option.id || 0, + name: option.option_name || '', + options: (option.values || []).map(value => value.option_value || ''), + })), + tags: (item.tags || []).map((t: any) => ({ + id: t.id || 0, + name: t.name || '', + })), + // shopyy叫做专辑 + categories: item.collections.map((c: any) => ({ + id: c.id || 0, + name: c.title || '', + })), + variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], + permalink: item.permalink, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : String(item.created_at ?? ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : String(item.updated_at ?? ''), + raw: item, + }; + } + + mapUnifiedToPlatformProduct(data: Partial) { + return data + } + + mapCreateProductParams(data: Partial): any { + // 构建 ShopYY 产品创建参数 + const params: any = { + name: data.name || '', + product_type: data.type || 1, // 默认简单产品 + status: this.mapStatus(data.status || 'publish'), + price: data.price || '0.00', + special_price: data.sale_price || '', + inventory_tracking: data.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: data.stock_quantity || 0, + }; + + // 添加变体信息 + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => ({ + sku: variation.sku || '', + price: variation.price || '0.00', + special_price: variation.sale_price || '', + inventory_tracking: variation.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: variation.stock_quantity || 0, + })); + } + + // 添加图片信息 + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息 + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息 + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + mapUpdateProductParams(data: Partial): any { + // 映射产品状态: publish -> 1, draft -> 0 + const mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 构建 ShopYY 产品更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.name !== undefined) params.name = data.name; + if (data.type !== undefined) params.product_type = data.type; + if (data.status !== undefined) params.status = mapStatus(data.status); + if (data.price !== undefined) params.price = data.price; + if (data.sale_price !== undefined) params.special_price = data.sale_price; + if (data.sku !== undefined) params.sku = data.sku; + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + // 添加变体信息(如果存在) + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => { + const variationParams: any = {}; + if (variation.id !== undefined) variationParams.id = variation.id; + if (variation.sku !== undefined) variationParams.sku = variation.sku; + if (variation.price !== undefined) variationParams.price = variation.price; + if (variation.sale_price !== undefined) variationParams.special_price = variation.sale_price; + if (variation.stock_quantity !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_quantity; + } + if (variation.stock_status !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_status === 'instock' ? (variation.stock_quantity || 1) : 0; + } + return variationParams; + }); + } + + // 添加图片信息(如果存在) + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + id: image.id, + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息(如果存在) + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息(如果存在) + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + async getProduct(id: string | number): Promise { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, id); + return this.mapPlatformToUnifiedProduct(product); + } + + async getProducts( + params: UnifiedSearchParamsDTO + ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); + const response = await this.shopyyService.fetchResourcePaged( + this.site, + 'products/list', + requestParams + ); + const { items = [], total, totalPages, page, per_page } = response; + const finalItems = items.map((item) => ({ + ...item, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, + })).map(this.mapPlatformToUnifiedProduct.bind(this)) + return { + items: finalItems as UnifiedProductDTO[], total, totalPages, page, @@ -537,207 +888,61 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams = { - where, - page: 1, - per_page: 1, - } - const data = await this.getOrders(searchParams); - return data.total || 0; + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllProducts 暂未实现 + throw new Error('Shopyy getAllProducts 暂未实现'); } - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - const data = await this.shopyyService.getAllOrders(this.site.id, params); - return data.map(this.mapOrder.bind(this)); + async createProduct(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateProductParams(data); + const res = await this.shopyyService.createProduct(this.site, requestParams); + return this.mapPlatformToUnifiedProduct(res); } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); - return this.mapOrder(data); + async updateProduct(id: string | number, data: Partial): Promise { + // Shopyy update returns boolean? + // shopyyService.updateProduct returns boolean. + // So I can't return the updated product. + // I have to fetch it again or return empty/input. + // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + // 使用映射方法转换参数 + const requestParams = this.mapUpdateProductParams(data); + await this.shopyyService.updateProduct(this.site, String(id), requestParams); + return true; } - async createOrder(data: Partial): Promise { - const createdOrder = await this.shopyyService.createOrder(this.site, data); - return this.mapOrder(createdOrder); + async deleteProduct(id: string | number): Promise { + // Use batch delete + await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + return true; } - async updateOrder(id: string | number, data: Partial): Promise { - return await this.shopyyService.updateOrder(this.site, String(id), data); + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + return await this.shopyyService.batchProcessProducts(this.site, data); } - async deleteOrder(id: string | number): Promise { - return await this.shopyyService.deleteOrder(this.site, id); + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) } - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - // 订单履行(发货) - try { - // 判断是否为部分发货(包含 items) - if (data.items && data.items.length > 0) { - // 部分发货 - const partShipData = { - order_number: String(orderId), - note: data.shipping_method || '', - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: '1', // 默认快递公司代码 - products: data.items.map(item => ({ - quantity: item.quantity, - order_product_id: String(item.order_item_id) - })) - }; - return await this.shopyyService.partFulfillOrder(this.site, partShipData); - } else { - // 批量发货(完整发货) - const batchShipData = { - order_number: String(orderId), - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: 1, // 默认快递公司代码 - note: data.shipping_method || '', - mode: null // 新增模式 - }; - return await this.shopyyService.batchFulfillOrders(this.site, batchShipData); - } - } catch (error) { - throw new Error(`履行失败: ${error.message}`); - } + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data } - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - // 取消订单履行 - try { - // 调用 ShopyyService 的取消履行方法 - const cancelShipData = { - order_id: String(orderId), - fulfillment_id: data.shipment_id || '' - }; - const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); - - return { - success: result, - order_id: orderId, - shipment_id: data.shipment_id, - reason: data.reason, - cancelled_at: new Date().toISOString() - }; - } catch (error) { - throw new Error(`取消履行失败: ${error.message}`); - } + mapUnifiedToPlatformReview(data: Partial) { + return data } - /** - * 获取订单履行信息 - * @param orderId 订单ID - * @returns 履行信息列表 - */ - async getOrderFulfillments(orderId: string | number): Promise { - return await this.shopyyService.getFulfillments(this.site, String(orderId)); + mapCreateReviewParams(data: CreateReviewDTO) { + return data } - /** - * 创建订单履行信息 - * @param orderId 订单ID - * @param data 履行数据 - * @returns 创建结果 - */ - async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { - // 调用 Shopyy Service 的 createFulfillment 方法 - const fulfillmentData = { - tracking_number: data.tracking_number, - carrier_code: data.shipping_provider, - carrier_name: data.shipping_provider, - shipping_method: data.shipping_method || 'standard' - }; - - return await this.shopyyService.createFulfillment(this.site, String(orderId), fulfillmentData); - } - - /** - * 更新订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @param data 更新数据 - * @returns 更新结果 - */ - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: string; - tracking_provider?: string; - date_shipped?: string; - status_shipped?: string; - }): Promise { - return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); - } - - /** - * 删除订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @returns 删除结果 - */ - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - throw new Error('Shopyy does not support subscriptions.'); - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllSubscriptions 暂未实现 - throw new Error('Shopyy getAllSubscriptions 暂未实现'); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapMediaSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'media', // Shopyy的媒体API端点可能需要调整 - requestParams - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllMedia 暂未实现 - throw new Error('Shopyy getAllMedia 暂未实现'); - } - - async createMedia(file: any): Promise { - const createdMedia = await this.shopyyService.createMedia(this.site, file); - return this.mapMedia(createdMedia); - } - - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); - return this.mapMedia(updatedMedia); - } - - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data } async getReviews( @@ -762,12 +967,21 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllReviews 暂未实现'); } - async getReview(id: string | number): Promise { - const review = await this.shopyyService.getReview(this.site, id); - return this.mapReview(review); + async createReview(data: any): Promise { + const createdReview = await this.shopyyService.createReview(this.site, data); + return this.mapReview(createdReview); } - private mapReview(review: any): UnifiedReviewDTO { + async updateReview(id: string | number, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, id, data); + return this.mapReview(updatedReview); + } + + async deleteReview(id: string | number): Promise { + return await this.shopyyService.deleteReview(this.site, id); + } + + mapReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -784,7 +998,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, @@ -799,38 +1013,133 @@ export class ShopyyAdapter implements ISiteAdapter { shopyyParams.status = where.status; } - // if (product_id) { - // shopyyParams.product_id = product_id; - // } - return shopyyParams; } - async createReview(data: any): Promise { - const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + mapUnifiedToPlatformSubscription(data: Partial) { + return data } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async getSubscriptions( + params: UnifiedSearchParamsDTO + ): Promise> { + throw new Error('Shopyy does not support subscriptions.'); } - // Webhook相关方法 - private mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllSubscriptions 暂未实现 + throw new Error('Shopyy getAllSubscriptions 暂未实现'); + } + + // ========== 产品变体映射方法 ========== + mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { + // 映射变体 return { - id: item.id, - name: item.webhook_name || `Webhook-${item.id}`, - topic: item.event_code || '', - delivery_url: item.url || '', - status: 'active', + id: variant.id, + name: variant.sku || '', + sku: variant.sku || '', + regular_price: String(variant.price ?? ''), + sale_price: String(variant.special_price ?? ''), + price: String(variant.price ?? ''), + stock_status: + variant.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: variant.inventory_quantity, }; } + mapUnifiedToPlatformVariation(data: Partial) { + return data + } + + mapCreateVariationParams(data: CreateVariationDTO) { + return data + } + + mapUpdateVariationParams(data: Partial): any { + // 构建 ShopYY 变体更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.id !== undefined) { + params.id = data.id; + } + if (data.sku !== undefined) { + params.sku = data.sku; + } + if (data.price !== undefined) { + params.price = data.price; + } + if (data.sale_price !== undefined) { + params.special_price = data.sale_price; + } + + // 处理库存信息 + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + return params; + } + + async getVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy getVariation 暂未实现'); + } + + async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getVariations 暂未实现'); + } + + async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getAllVariations 暂未实现'); + } + + async createVariation(productId: string | number, data: any): Promise { + throw new Error('Shopyy createVariation 暂未实现'); + } + + async updateVariation(productId: string | number, variationId: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateVariationParams(data); + await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), requestParams); + return { ...data, id: variationId }; + } + + async deleteVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy deleteVariation 暂未实现'); + } + + // ========== Webhook映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data + } + + mapUnifiedToPlatformWebhook(data: Partial) { + return data + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data + } + + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data + } + + async getWebhook(id: string | number): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, id); + return this.mapWebhook(webhook); + } + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { @@ -847,11 +1156,6 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllWebhooks 暂未实现'); } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); - } - async createWebhook(data: CreateWebhookDTO): Promise { const createdWebhook = await this.shopyyService.createWebhook(this.site, data); return this.mapWebhook(createdWebhook); @@ -866,6 +1170,17 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteWebhook(this.site, id); } + mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + return { + id: item.id, + name: item.webhook_name || `Webhook-${item.id}`, + topic: item.event_code || '', + delivery_url: item.url || '', + status: 'active', + }; + } + + // ========== 站点/其他方法 ========== async getLinks(): Promise> { // ShopYY站点的管理后台链接通常基于apiUrl构建 const url = this.site.websiteUrl @@ -885,59 +1200,58 @@ export class ShopyyAdapter implements ISiteAdapter { return links; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchCustomersPaged(this.site, params); - return { - items: items.map(this.mapCustomer.bind(this)), - total, - totalPages, + // ========== 辅助方法 ========== + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), page, - per_page - }; + limit, + } + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllCustomers 暂未实现 - throw new Error('Shopyy getAllCustomers 暂未实现'); + // 映射产品状态: publish -> 1, draft -> 0 + mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 映射库存状态: instock -> 1, outofstock -> 0 + mapStockStatus = (stockStatus: string) => { + return stockStatus === 'instock' ? 1 : 0; + }; + + shopyyOrderStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + [100]: OrderStatus.PENDING, // 100 未完成 转为 pending + [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing + // 已发货 + + [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed + [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); - return this.mapCustomer(customer); + shopyyFulfillmentStatusMap = { + // 未发货 + '300': OrderFulfillmentStatus.PENDING, + // 部分发货 + '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, + // 已发货 + '320': OrderFulfillmentStatus.FULFILLED, + // 已取消 + '330': OrderFulfillmentStatus.CANCELLED, + // 确认发货 } - - async createCustomer(data: Partial): Promise { - const createdCustomer = await this.shopyyService.createCustomer(this.site, data); - return this.mapCustomer(createdCustomer); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); - return this.mapCustomer(updatedCustomer); - } - - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); - } - - async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getVariations 暂未实现'); - } - - async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getAllVariations 暂未实现'); - } - - async getVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy getVariation 暂未实现'); - } - - async createVariation(productId: string | number, data: any): Promise { - throw new Error('Shopyy createVariation 暂未实现'); - } - - async deleteVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy deleteVariation 暂未实现'); - } -} +} \ No newline at end of file diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 9828a07..a5f2f0a 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -15,6 +15,8 @@ import { UpdateVariationDTO, UnifiedProductVariationDTO, UnifiedVariationPaginationDTO, + CreateReviewDTO, + UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { @@ -29,6 +31,7 @@ import { } from '../dto/woocommerce.dto'; import { Site } from '../entity/site.entity'; import { WPService } from '../service/wp.service'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 @@ -40,192 +43,236 @@ export class WooCommerceAdapter implements ISiteAdapter { this.mapOrder = this.mapOrder.bind(this); this.mapWebhook = this.mapWebhook.bind(this); } + batchProcessProducts?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + mapCreateVariationParams(data: CreateVariationDTO) { + throw new Error('Method not implemented.'); + } + mapUpdateVariationParams(data: UpdateVariationDTO) { + throw new Error('Method not implemented.'); + } - // 映射 WooCommerce webhook 到统一格式 - private mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { + return data; + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data; + } + + mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + // 将 WooCommerce 客户数据映射为统一客户DTO + // 包含基础信息地址信息与时间信息 return { - id: webhook.id.toString(), - name: webhook.name, - status: webhook.status, - topic: webhook.topic, - delivery_url: webhook.delivery_url, - secret: webhook.secret, - api_version: webhook.api_version, - date_created: webhook.date_created, - date_modified: webhook.date_modified, - // metadata: webhook.meta_data || [], + id: item.id, + avatar: item.avatar_url, + email: item.email, + orders: Number(item.orders ?? 0), + total_spend: Number(item.total_spent ?? 0), + first_name: item.first_name, + last_name: item.last_name, + username: item.username, + phone: item.billing?.phone || item.shipping?.phone, + billing: item.billing, + shipping: item.shipping, + date_created: item.date_created, + date_modified: item.date_modified, + raw: item, }; } - // 获取站点的 webhooks 列表 - async getWebhooks(params: UnifiedSearchParamsDTO): Promise { - try { - const result = await this.wpService.getWebhooks(this.site, params); - - return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; - } catch (error) { - throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取所有webhooks - async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { - try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); - } catch (error) { - throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { - try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 创建新的 webhook - async createWebhook(data: CreateWebhookDTO): Promise { - try { - const params = { - name: data.name, - status: 'active', // 默认状态为活跃 - topic: data.topic, - delivery_url: data.delivery_url, - secret: data.secret, - api_version: data.api_version || 'wp/v2', - }; - const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { - try { - const params = { - ...(data.name ? { name: data.name } : {}), - ...(data.status ? { status: data.status } : {}), - ...(data.topic ? { topic: data.topic } : {}), - ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), - ...(data.secret ? { secret: data.secret } : {}), - ...(data.api_version ? { api_version: data.api_version } : {}), - }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { - try { - await this.wpService.deleteWebhook(this.site, id); - return true; - } catch (error) { - throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - async getLinks(): Promise> { - const baseUrl = this.site.apiUrl; - const links = [ - { title: '访问网站', url: baseUrl }, - { title: '管理后台', url: `${baseUrl}/wp-admin/` }, - { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, - { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, - { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, - { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, - { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, - { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, - { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, - ]; - return links; - } - - createMedia(file: any): Promise { - throw new Error('Method not implemented.'); - } - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - - - - private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { + 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 : {}; + const mapped: any = { ...(params.search ? { search: params.search } : {}), - ...(where.status ? { status: where.status } : {}), page, per_page, }; + // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 + if (params.orderBy) { + // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } + if (typeof params.orderBy === 'string') { + const [field, direction = 'desc'] = params.orderBy.split(':'); + mapped.orderby = field; + mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; + } else if (typeof params.orderBy === 'object') { + const entries = Object.entries(params.orderBy); + if (entries.length > 0) { + const [field, direction] = entries[0]; + mapped.orderby = field; + mapped.order = direction === 'asc' ? 'asc' : 'desc'; + } + } + } + const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); - if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); - if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); - if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); - if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); - if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); - if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); - if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); - if (where.offset !== undefined) mapped.offset = Number(where.offset); - if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); - if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); - if (where.slug) mapped.slug = String(where.slug); - if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { - if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); - if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); - if (where.status) mapped.status = String(where.status); - } - if (where.type) mapped.type = String(where.type); - if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); - if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); - if (where.sku) mapped.sku = String(where.sku); - if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); - if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); - if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); - if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); - if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); - if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); - if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); - if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); - if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); - if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); - if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); - if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); - if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); + const toNumber = (value: any): number | undefined => { + if (value === undefined || value === null || value === '') return undefined; + const n = Number(value); + return Number.isFinite(n) ? n : undefined; + }; + + if (where.exclude) mapped.exclude = toArray(where.exclude); + if (where.include) mapped.include = toArray(where.include); + if (where.ids) mapped.include = toArray(where.ids); + if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); + + if (where.email) mapped.email = String(where.email); + const roleSource = where.role; + if (roleSource !== undefined) mapped.role = String(roleSource); return mapped; } - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 客户操作方法 + async getCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`customers/${id}`); + return this.mapCustomer(res.data); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapCustomerSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'customers', + requestParams + ); + return { + items: items.map((i: any) => this.mapCustomer(i)), + total, + totalPages, + page, + per_page, + + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有客户数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // 处理orderBy参数,转换为WooCommerce API需要的格式 + const requestParams = this.mapCustomerSearchParams(params || {}); + + const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); + return customers.map((customer: any) => this.mapCustomer(customer)); + } + + async createCustomer(data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('customers', data); + return this.mapCustomer(res.data); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.put(`customers/${id}`, data); + return this.mapCustomer(res.data); + } + + async deleteCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`customers/${id}`, { force: true }); + return true; + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { + return data; + } + mapUnifiedToPlatformMedia(data: Partial) { + return data; + } + + mapMedia(item: WpMedia): UnifiedMediaDTO { + // 将 WordPress 媒体数据映射为统一媒体DTO + // 兼容不同字段命名的时间信息 + return { + id: item.id, + title: + typeof item.title === 'string' + ? item.title + : item.title?.rendered || '', + media_type: item.media_type, + mime_type: item.mime_type, + source_url: item.source_url, + date_created: item.date_created ?? item.date, + date_modified: item.date_modified ?? item.modified, + }; + } + + // 媒体操作方法 + async getMedia(params: UnifiedSearchParamsDTO): Promise> { + // 获取媒体列表并映射为统一媒体DTO集合 + const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( + this.site, + params + ); + return { + items: items.map(this.mapMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有媒体数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const media = await this.wpService.sdkGetAll(api, 'media', params); + return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + } + + createMedia(file: any): Promise { + throw new Error('Method not implemented.'); + } + + async updateMedia(id: string | number, data: any): Promise { + // 更新媒体信息 + return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + } + + async deleteMedia(id: string | number): Promise { + // 删除媒体资源 + await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + return true; + } + + async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { + // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 + const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); + return result as any; + } + + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { + return data; + } + mapUnifiedToPlatformOrder(data: Partial) { + return data; + } + + mapCreateOrderParams(data: Partial) { + return data; + } + mapUpdateOrderParams(data: Partial) { + return data; + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 计算分页参数 const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); @@ -293,60 +340,328 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - private mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { + private buildFullAddress(addr: any): string { + 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(', '); + } + + mapOrder(item: WooOrder): UnifiedOrderDTO { + // 将 WooCommerce 订单数据映射为统一订单DTO + // 包含账单地址与收货地址以及创建与更新时间 + + // 映射物流追踪信息,将后端格式转换为前端期望的格式 + const fulfillments = (item.fulfillments || []).map((track: any) => ({ + tracking_number: track.tracking_number || '', + shipping_provider: track.shipping_provider || '', + shipping_method: track.shipping_method || '', + status: track.status || '', + date_created: track.date_created || '', + items: track.items || [], + })); + + return { + id: item.id, + number: item.number, + status: item.status, + currency: item.currency, + total: item.total, + customer_id: item.customer_id, + customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 + email: item.billing?.email || '', + customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), + refunds: item.refunds?.map?.(refund => ({ + id: refund.id, + reason: refund.reason, + total: refund.total, + })), + line_items: (item.line_items as any[]).map(li => ({ + ...li, + productId: li.product_id, + })), + customer_ip_address: item.customer_ip_address ?? '', + date_paid: item.date_paid ?? '', + utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', + device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', + source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', + billing: item.billing, + shipping: item.shipping, + billing_full_address: this.buildFullAddress(item.billing), + shipping_full_address: this.buildFullAddress(item.shipping), + payment_method: item.payment_method_title, + date_created: item.date_created, + date_modified: item.date_modified, + shipping_lines: item.shipping_lines, + fee_lines: item.fee_lines, + coupon_lines: item.coupon_lines, + fulfillments, + raw: item, + }; + } + + // 订单操作方法 + async getOrder(id: string | number): Promise { + // 获取单个订单详情 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${id}`); + return this.mapOrder(res.data); + } + + async getOrders(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + + // 并行获取所有订单的履行信息 + const ordersWithFulfillments = await Promise.all( + items.map(async (order: any) => { + try { + // 获取订单的履行信息 + const fulfillments = await this.getOrderFulfillments(order.id); + // 将履行信息添加到订单对象中 + return { + ...order, + fulfillments: fulfillments || [] + }; + } catch (error) { + // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 + console.error(`获取订单 ${order.id} 的履行信息失败:`, error); + return { + ...order, + fulfillments: [] + }; + } + }) + ); + + return { + items: ordersWithFulfillments.map(this.mapOrder), + total, + totalPages, + page, + per_page, + }; + } + + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订单数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const orders = await this.wpService.sdkGetAll(api, 'orders', params); + return orders.map((order: any) => this.mapOrder(order)); + } + + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + + async createOrder(data: Partial): Promise { + // 创建订单并返回统一订单DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('orders', data); + return this.mapOrder(res.data); + } + + async updateOrder(id: string | number, data: Partial): Promise { + // 更新订单并返回布尔结果 + return await this.wpService.updateOrder(this.site, String(id), data as any); + } + + async deleteOrder(id: string | number): Promise { + // 删除订单 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`orders/${id}`, { force: true }); + return true; + } + + async getOrderFulfillments(orderId: string | number): Promise { + return await this.wpService.getFulfillments(this.site, String(orderId)); + } + + async createOrderFulfillment(orderId: string | number, data: { + tracking_number: string; + shipping_provider: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; + }): Promise { + const shipmentData: any = { + shipping_provider: data.shipping_provider, + tracking_number: data.tracking_number, + }; + + if (data.shipping_method) { + shipmentData.shipping_method = data.shipping_method; + } + + if (data.status) { + shipmentData.status = data.status; + } + + if (data.date_created) { + shipmentData.date_created = data.date_created; + } + + if (data.items) { + shipmentData.items = data.items; + } + + const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData); + return response.data; + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: string; + shipping_provider?: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; + }): Promise { + return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + async getOrderNotes(orderId: string | number): Promise { + // 获取订单备注列表 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${orderId}/notes`); + return res.data; + } + + async createOrderNote(orderId: string | number, data: any): Promise { + // 创建订单备注 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post(`orders/${orderId}/notes`, data); + return res.data; + } + + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + throw new Error('暂未实现'); + // 取消订单履行 + // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // try { + // // 将订单状态改回处理中 + // await api.put(`orders/${orderId}`, { status: 'processing' }); + + // // 添加取消履行的备注 + // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; + // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); + + // return { + // success: true, + // order_id: orderId, + // shipment_id: data.shipment_id, + // reason: data.reason, + // cancelled_at: new Date().toISOString() + // }; + // } catch (error) { + // throw new Error(`取消履行失败: ${error.message}`); + // } + } + + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { + return data; + } + mapUnifiedToPlatformProduct(data: Partial) { + return data; + } + + mapCreateProductParams(data: Partial) { + return data; + } + mapUpdateProductParams(data: Partial) { + return data; + } + + 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 : {}; - const mapped: any = { ...(params.search ? { search: params.search } : {}), + ...(where.status ? { status: where.status } : {}), page, per_page, }; - // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 - if (params.orderBy) { - // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } - if (typeof params.orderBy === 'string') { - const [field, direction = 'desc'] = params.orderBy.split(':'); - mapped.orderby = field; - mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; - } else if (typeof params.orderBy === 'object') { - const entries = Object.entries(params.orderBy); - if (entries.length > 0) { - const [field, direction] = entries[0]; - mapped.orderby = field; - mapped.order = direction === 'asc' ? 'asc' : 'desc'; - } - } - } - const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - const toNumber = (value: any): number | undefined => { - if (value === undefined || value === null || value === '') return undefined; - const n = Number(value); - return Number.isFinite(n) ? n : undefined; - }; - - if (where.exclude) mapped.exclude = toArray(where.exclude); - if (where.include) mapped.include = toArray(where.include); - if (where.ids) mapped.include = toArray(where.ids); - if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); - - if (where.email) mapped.email = String(where.email); - const roleSource = where.role; - if (roleSource !== undefined) mapped.role = String(roleSource); + if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); + if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); + if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); + if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); + if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); + if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); + if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); + if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); + if (where.offset !== undefined) mapped.offset = Number(where.offset); + if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); + if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); + if (where.slug) mapped.slug = String(where.slug); + if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { + if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); + if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); + if (where.status) mapped.status = String(where.status); + } + if (where.type) mapped.type = String(where.type); + if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); + if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); + if (where.sku) mapped.sku = String(where.sku); + if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); + if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); + if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); + if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); + if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); + if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); + if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); + if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); + if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); + if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); + if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); + if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); + if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); return mapped; } - private mapProduct(item: WooProduct): UnifiedProductDTO { + mapProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -434,120 +749,41 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - private buildFullAddress(addr: any): string { - 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(', '); - } - private mapOrder(item: WooOrder): UnifiedOrderDTO { - // 将 WooCommerce 订单数据映射为统一订单DTO - // 包含账单地址与收货地址以及创建与更新时间 - // 映射物流追踪信息,将后端格式转换为前端期望的格式 - const fulfillments = (item.fulfillments || []).map((track: any) => ({ - tracking_number: track.tracking_number || '', - shipping_provider: track.shipping_provider || '', - shipping_method: track.shipping_method || '', - status: track.status || '', - date_created: track.date_created || '', - items: track.items || [], - })); + // 产品操作方法 + async getProduct(id: string | number): Promise { + // 获取单个产品详情并映射为统一产品DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); + const product = res.data; - return { - id: item.id, - number: item.number, - status: item.status, - currency: item.currency, - total: item.total, - customer_id: item.customer_id, - customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 - email: item.billing?.email || '', - customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), - refunds: item.refunds?.map?.(refund => ({ - id: refund.id, - reason: refund.reason, - total: refund.total, - })), - line_items: (item.line_items as any[]).map(li => ({ - ...li, - productId: li.product_id, - })), - customer_ip_address: item.customer_ip_address ?? '', - date_paid: item.date_paid ?? '', - utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', - device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', - source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', - billing: item.billing, - shipping: item.shipping, - billing_full_address: this.buildFullAddress(item.billing), - shipping_full_address: this.buildFullAddress(item.shipping), - payment_method: item.payment_method_title, - date_created: item.date_created, - date_modified: item.date_modified, - shipping_lines: item.shipping_lines, - fee_lines: item.fee_lines, - coupon_lines: item.coupon_lines, - fulfillments, - raw: item, - }; + // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 + if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { + try { + // 批量获取该产品的所有变体数据 + const variations = await this.wpService.sdkGetAll( + api, + `products/${product.id}/variations` + ); + // 将完整的变体数据添加到产品对象中 + product.variations = variations; + } catch (error) { + // 如果获取变体失败,保持原有的 ID 数组 + console.error(`获取产品 ${product.id} 的变体数据失败:`, error); + } + } + + return this.mapProduct(product); } - private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { - // 将 WooCommerce 订阅数据映射为统一订阅DTO - // 若缺少创建时间则回退为开始时间 - return { - id: item.id, - status: item.status, - customer_id: item.customer_id, - billing_period: item.billing_period, - billing_interval: item.billing_interval, - date_created: item.date_created ?? item.start_date, - date_modified: item.date_modified, - start_date: item.start_date, - next_payment_date: item.next_payment_date, - line_items: item.line_items, - raw: item, - }; - } - - private mapMedia(item: WpMedia): UnifiedMediaDTO { - // 将 WordPress 媒体数据映射为统一媒体DTO - // 兼容不同字段命名的时间信息 - return { - id: item.id, - title: - typeof item.title === 'string' - ? item.title - : item.title?.rendered || '', - media_type: item.media_type, - mime_type: item.mime_type, - source_url: item.source_url, - date_created: item.date_created ?? item.date, - date_modified: item.date_modified ?? item.modified, - }; - } - - async getProducts( - params: UnifiedSearchParamsDTO - ): Promise> { + async getProducts(params: UnifiedSearchParamsDTO): Promise> { // 获取产品列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products', + requestParams + ); // 对于类型为 variable 的产品,需要加载完整的变体数据 const productsWithVariations = await Promise.all( @@ -611,31 +847,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return productsWithVariations.map((product: any) => this.mapProduct(product)); } - async getProduct(id: string | number): Promise { - // 获取单个产品详情并映射为统一产品DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products/${id}`); - const product = res.data; - - // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 - if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { - try { - // 批量获取该产品的所有变体数据 - const variations = await this.wpService.sdkGetAll( - api, - `products/${product.id}/variations` - ); - // 将完整的变体数据添加到产品对象中 - product.variations = variations; - } catch (error) { - // 如果获取变体失败,保持原有的 ID 数组 - console.error(`获取产品 ${product.id} 的变体数据失败:`, error); - } - } - - return this.mapProduct(product); - } - async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); @@ -645,21 +856,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async updateProduct(id: string | number, data: Partial): Promise { // 更新产品并返回统一产品DTO const res = await this.wpService.updateProduct(this.site, String(id), data as any); - return res - } - - async getOrderNotes(orderId: string | number): Promise { - // 获取订单备注列表 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${orderId}/notes`); - return res.data; - } - - async createOrderNote(orderId: string | number, data: any): Promise { - // 创建订单备注 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post(`orders/${orderId}/notes`, data); - return res.data; + return res; } async deleteProduct(id: string | number): Promise { @@ -673,212 +870,22 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - // 批量处理产品增删改 - return await this.wpService.batchProcessProducts(this.site, data); + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data; + } + mapUnifiedToPlatformReview(data: Partial) { + return data; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - - // 并行获取所有订单的履行信息 - const ordersWithFulfillments = await Promise.all( - items.map(async (order: any) => { - try { - // 获取订单的履行信息 - const fulfillments = await this.getOrderFulfillments(order.id); - // 将履行信息添加到订单对象中 - return { - ...order, - fulfillments: fulfillments || [] - }; - } catch (error) { - // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 - console.error(`获取订单 ${order.id} 的履行信息失败:`, error); - return { - ...order, - fulfillments: [] - }; - } - }) - ); - - return { - items: ordersWithFulfillments.map(this.mapOrder), - total, - totalPages, - page, - per_page, - }; + mapCreateReviewParams(data: CreateReviewDTO) { + return data; + } + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams: UnifiedSearchParamsDTO = { - where, - page: 1, - per_page: 1, - }; - const requestParams = this.mapOrderSearchParams(searchParams); - const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - return total || 0; - } - - async getOrder(id: string | number): Promise { - // 获取单个订单详情 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); - } - - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订单数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); - } - - async createOrder(data: Partial): Promise { - // 创建订单并返回统一订单DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('orders', data); - return this.mapOrder(res.data); - } - - async updateOrder(id: string | number, data: Partial): Promise { - // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); - } - - async deleteOrder(id: string | number): Promise { - // 删除订单 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); - return true; - } - - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - throw new Error('暂无实现') - // 订单履行(发货) - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 更新订单状态为已完成 - // await api.put(`orders/${orderId}`, { status: 'completed' }); - - // // 如果提供了物流信息,添加到订单备注 - // if (data.tracking_number || data.shipping_provider) { - // const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - // } - - // return { - // success: true, - // order_id: orderId, - // fulfillment_id: `fulfillment_${orderId}_${Date.now()}`, - // tracking_number: data.tracking_number, - // shipping_provider: data.shipping_provider, - // fulfilled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`履行失败: ${error.message}`); - // } - } - - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - throw new Error('暂未实现') - // 取消订单履行 - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 将订单状态改回处理中 - // await api.put(`orders/${orderId}`, { status: 'processing' }); - - // // 添加取消履行的备注 - // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - - // return { - // success: true, - // order_id: orderId, - // shipment_id: data.shipment_id, - // reason: data.reason, - // cancelled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`取消履行失败: ${error.message}`); - // } - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取订阅列表并映射为统一订阅DTO集合 - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'subscriptions', - params - ); - return { - items: items.map(this.mapSubscription), - total, - totalPages, - page, - per_page, - - }; - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订阅数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取媒体列表并映射为统一媒体DTO集合 - const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( - this.site, - params - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有媒体数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const media = await this.wpService.sdkGetAll(api, 'media', params); - 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, @@ -893,17 +900,15 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getReviews( - params: UnifiedSearchParamsDTO - ): Promise { + // 评论操作方法 + async getReviews(params: UnifiedSearchParamsDTO): Promise { // 获取评论列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products/reviews', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products/reviews', + requestParams + ); return { items: items.map(this.mapReview.bind(this)), total, @@ -934,52 +939,42 @@ export class WooCommerceAdapter implements ISiteAdapter { return await this.wpService.deleteReview(this.site, id); } - async deleteMedia(id: string | number): Promise { - // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); - return true; + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data; + } + mapUnifiedToPlatformSubscription(data: Partial) { + return data; } - async updateMedia(id: string | number, data: any): Promise { - // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); - } - - async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { - // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 - const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); - return result as any; - } - - private mapCustomer(item: WooCustomer): UnifiedCustomerDTO { - // 将 WooCommerce 客户数据映射为统一客户DTO - // 包含基础信息地址信息与时间信息 + mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + // 将 WooCommerce 订阅数据映射为统一订阅DTO + // 若缺少创建时间则回退为开始时间 return { id: item.id, - avatar: item.avatar_url, - email: item.email, - orders: Number(item.orders ?? 0), - total_spend: Number(item.total_spent ?? 0), - first_name: item.first_name, - last_name: item.last_name, - username: item.username, - phone: item.billing?.phone || item.shipping?.phone, - billing: item.billing, - shipping: item.shipping, - date_created: item.date_created, + status: item.status, + customer_id: item.customer_id, + billing_period: item.billing_period, + billing_interval: item.billing_interval, + date_created: item.date_created ?? item.start_date, date_modified: item.date_modified, + start_date: item.start_date, + next_payment_date: item.next_payment_date, + line_items: item.line_items, raw: item, }; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const requestParams = this.mapCustomerSearchParams(params); + + // 订阅操作方法 + async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { + // 获取订阅列表并映射为统一订阅DTO集合 const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( this.site, - 'customers', - requestParams + 'subscriptions', + params ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map(this.mapSubscription), total, totalPages, page, @@ -988,101 +983,23 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有客户数据,不受分页限制 + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // 处理orderBy参数,转换为WooCommerce API需要的格式 - const requestParams = this.mapCustomerSearchParams(params || {}); - - const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); + return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); } - async getCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + // ========== 变体映射方法 ========== + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { + return data; } - - async createCustomer(data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('customers', data); - return this.mapCustomer(res.data); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); - } - - async deleteCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); - return true; - } - - async getOrderFulfillments(orderId: string | number): Promise { - return await this.wpService.getFulfillments(this.site, String(orderId)); - } - - async createOrderFulfillment(orderId: string | number, data: { - tracking_number: string; - shipping_provider: string; - shipping_method?: string; - status?: string; - date_created?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - const shipmentData: any = { - shipping_provider: data.shipping_provider, - tracking_number: data.tracking_number, - }; - - if (data.shipping_method) { - shipmentData.shipping_method = data.shipping_method; - } - - if (data.status) { - shipmentData.status = data.status; - } - - if (data.date_created) { - shipmentData.date_created = data.date_created; - } - - if (data.items) { - shipmentData.items = data.items; - } - - const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData); - return response.data; - } - - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - status?: string; - date_created?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); - } - - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + mapUnifiedToPlatformVariation(data: Partial) { + return data; } // 映射 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) => ({ @@ -1131,6 +1048,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + // 变体操作方法 // 获取产品变体列表 async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { try { @@ -1305,4 +1223,145 @@ export class WooCommerceAdapter implements ISiteAdapter { throw new Error(`删除产品变体失败: ${error instanceof Error ? error.message : String(error)}`); } } + + // ========== 网络钩子映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data; + } + mapUnifiedToPlatformWebhook(data: Partial) { + return data; + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data; + } + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data; + } + + // 映射 WooCommerce webhook 到统一格式 + mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + return { + id: webhook.id.toString(), + name: webhook.name, + status: webhook.status, + topic: webhook.topic, + delivery_url: webhook.delivery_url, + secret: webhook.secret, + api_version: webhook.api_version, + date_created: webhook.date_created, + date_modified: webhook.date_modified, + // metadata: webhook.meta_data || [], + }; + } + + // 网络钩子操作方法 + // 获取站点的 webhooks 列表 + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { + try { + const result = await this.wpService.getWebhooks(this.site, params); + + return { + items: (result.items as WooWebhook[]).map(this.mapWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; + } catch (error) { + throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取所有webhooks + async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { + try { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); + return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + } catch (error) { + throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取单个 webhook 详情 + async getWebhook(id: string | number): Promise { + try { + const result = await this.wpService.getWebhook(this.site, id); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 创建新的 webhook + async createWebhook(data: CreateWebhookDTO): Promise { + try { + const params = { + name: data.name, + status: 'active', // 默认状态为活跃 + topic: data.topic, + delivery_url: data.delivery_url, + secret: data.secret, + api_version: data.api_version || 'wp/v2', + }; + const result = await this.wpService.createWebhook(this.site, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 更新现有的 webhook + async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + try { + const params = { + ...(data.name ? { name: data.name } : {}), + ...(data.status ? { status: data.status } : {}), + ...(data.topic ? { topic: data.topic } : {}), + ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), + ...(data.secret ? { secret: data.secret } : {}), + ...(data.api_version ? { api_version: data.api_version } : {}), + }; + const result = await this.wpService.updateWebhook(this.site, id, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 删除指定的 webhook + async deleteWebhook(id: string | number): Promise { + try { + await this.wpService.deleteWebhook(this.site, id); + return true; + } catch (error) { + throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // ========== 其他方法 ========== + async getLinks(): Promise> { + const baseUrl = this.site.apiUrl; + const links = [ + { title: '访问网站', url: baseUrl }, + { title: '管理后台', url: `${baseUrl}/wp-admin/` }, + { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, + { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, + { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, + { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, + { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, + { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, + { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, + ]; + return links; + } + + batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } + + batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 5013390..f4b1c83 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -7,7 +7,6 @@ import { CancelFulfillmentDTO, CreateReviewDTO, CreateWebhookDTO, - FulfillmentDTO, UnifiedCustomerDTO, UnifiedCustomerPaginationDTO, UnifiedMediaPaginationDTO, @@ -986,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') @ApiOkResponse({ type: Object }) async cancelFulfillment( diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index ea6db0b..630e5ed 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,51 +19,71 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { + // ========== 客户映射方法 ========== /** - * 获取产品列表 + * 将平台客户数据转换为统一客户数据格式 + * @param data 平台特定客户数据 + * @returns 统一客户数据格式 */ - getProducts(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO; /** - * 获取所有产品 + * 将统一客户数据格式转换为平台客户数据 + * @param data 统一客户数据格式 + * @returns 平台特定客户数据 */ - getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformCustomer(data: Partial): any; /** - * 获取单个产品 + * 获取单个客户 */ - getProduct(id: string | number): Promise; + getCustomer(id: string | number): Promise; /** - * 获取订单列表 + * 获取客户列表 */ - getOrders(params: UnifiedSearchParamsDTO): Promise>; + getCustomers(params: UnifiedSearchParamsDTO): Promise>; /** - * 获取订单总数 + * 获取所有客户 */ - countOrders(params: Record): Promise; + getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; /** - * 获取所有订单 + * 创建客户 */ - getAllOrders(params?: UnifiedSearchParamsDTO): Promise; + createCustomer(data: Partial): Promise; /** - * 获取单个订单 + * 更新客户 */ - getOrder(id: string | number): Promise; + updateCustomer(id: string | number, data: Partial): Promise; /** - * 获取订阅列表 + * 删除客户 */ - getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + deleteCustomer(id: string | number): Promise; /** - * 获取所有订阅 + * 批量处理客户 */ - getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + batchProcessCustomers?(data: BatchOperationDTO): Promise; + + // ========== 媒体映射方法 ========== + /** + * 将平台媒体数据转换为统一媒体数据格式 + * @param data 平台特定媒体数据 + * @returns 统一媒体数据格式 + */ + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO; + + /** + * 将统一媒体数据格式转换为平台媒体数据 + * @param data 统一媒体数据格式 + * @returns 平台特定媒体数据 + */ + mapUnifiedToPlatformMedia(data: Partial): any; /** * 获取媒体列表 @@ -80,75 +100,69 @@ export interface ISiteAdapter { */ createMedia(file: any): Promise; + // ========== 订单映射方法 ========== /** - * 获取评论列表 + * 将平台订单数据转换为统一订单数据格式 + * @param data 平台特定订单数据 + * @returns 统一订单数据格式 */ - getReviews(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO; /** - * 获取所有评论 + * 将统一订单数据格式转换为平台订单数据 + * @param data 统一订单数据格式 + * @returns 平台特定订单数据 */ - getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformOrder(data: Partial): any; /** - * 创建评论 + * 将统一订单创建参数转换为平台订单创建参数 + * @param data 统一订单创建参数 + * @returns 平台订单创建参数 */ - createReview(data: CreateReviewDTO): Promise; + mapCreateOrderParams(data: Partial): any; /** - * 更新评论 + * 将统一订单更新参数转换为平台订单更新参数 + * @param data 统一订单更新参数 + * @returns 平台订单更新参数 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + mapUpdateOrderParams(data: Partial): any; /** - * 删除评论 + * 获取单个订单 */ - deleteReview(id: number): Promise; + getOrder(id: string | number): Promise; /** - * 创建产品 + * 获取订单列表 */ - createProduct(data: Partial): Promise; + getOrders(params: UnifiedSearchParamsDTO): Promise>; /** - * 更新产品 + * 获取所有订单 */ - updateProduct(id: string | number, data: Partial): Promise; + getAllOrders(params?: UnifiedSearchParamsDTO): Promise; /** - * 删除产品 + * 获取订单总数 */ - deleteProduct(id: string | number): Promise; + countOrders(params: Record): Promise; /** - * 获取产品变体列表 + * 创建订单 */ - getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + createOrder(data: Partial): Promise; /** - * 获取所有产品变体 + * 更新订单 */ - getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + updateOrder(id: string | number, data: Partial): Promise; /** - * 获取单个产品变体 + * 删除订单 */ - getVariation(productId: string | number, variationId: string | number): Promise; - - /** - * 创建产品变体 - */ - createVariation(productId: string | number, data: CreateVariationDTO): Promise; - - /** - * 更新产品变体 - */ - updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; - - /** - * 删除产品变体 - */ - deleteVariation(productId: string | number, variationId: string | number): Promise; + deleteOrder(id: string | number): Promise; /** * 获取订单备注 @@ -160,71 +174,6 @@ export interface ISiteAdapter { */ createOrderNote(orderId: string | number, data: any): Promise; - batchProcessProducts?(data: BatchOperationDTO): Promise; - - createOrder(data: Partial): Promise; - updateOrder(id: string | number, data: Partial): Promise; - deleteOrder(id: string | number): Promise; - - batchProcessOrders?(data: BatchOperationDTO): Promise; - - getCustomers(params: UnifiedSearchParamsDTO): Promise>; - getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; - getCustomer(id: string | number): Promise; - createCustomer(data: Partial): Promise; - updateCustomer(id: string | number, data: Partial): Promise; - deleteCustomer(id: string | number): Promise; - - batchProcessCustomers?(data: BatchOperationDTO): Promise; - - /** - * 获取webhooks列表 - */ - getWebhooks(params: UnifiedSearchParamsDTO): Promise; - - /** - * 获取所有webhooks - */ - getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; - - /** - * 获取单个webhook - */ - getWebhook(id: string | number): Promise; - - /** - * 创建webhook - */ - createWebhook(data: CreateWebhookDTO): Promise; - - /** - * 更新webhook - */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; - - /** - * 删除webhook - */ - deleteWebhook(id: string | number): Promise; - - /** - * 获取站点链接列表 - */ - getLinks(): Promise>; - - /** - * 订单履行(发货) - */ - fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise; - /** * 取消订单履行 */ @@ -272,4 +221,276 @@ export interface ISiteAdapter { * 删除订单履行信息 */ deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise; + + /** + * 批量处理订单 + */ + batchProcessOrders?(data: BatchOperationDTO): Promise; + + // ========== 产品映射方法 ========== + /** + * 将平台产品数据转换为统一产品数据格式 + * @param data 平台特定产品数据 + * @returns 统一产品数据格式 + */ + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO; + + /** + * 将统一产品数据格式转换为平台产品数据 + * @param data 统一产品数据格式 + * @returns 平台特定产品数据 + */ + mapUnifiedToPlatformProduct(data: Partial): any; + + /** + * 将统一产品创建参数转换为平台产品创建参数 + * @param data 统一产品创建参数 + * @returns 平台产品创建参数 + */ + mapCreateProductParams(data: Partial): any; + + /** + * 将统一产品更新参数转换为平台产品更新参数 + * @param data 统一产品更新参数 + * @returns 平台产品更新参数 + */ + mapUpdateProductParams(data: Partial): any; + + /** + * 获取单个产品 + */ + getProduct(id: string | number): Promise; + + /** + * 获取产品列表 + */ + getProducts(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有产品 + */ + getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品 + */ + createProduct(data: Partial): Promise; + + /** + * 更新产品 + */ + updateProduct(id: string | number, data: Partial): Promise; + + /** + * 删除产品 + */ + deleteProduct(id: string | number): Promise; + + /** + * 批量处理产品 + */ + batchProcessProducts?(data: BatchOperationDTO): Promise; + + // ========== 评论映射方法 ========== + /** + * 将平台评论数据转换为统一评论数据格式 + * @param data 平台特定评论数据 + * @returns 统一评论数据格式 + */ + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO; + + /** + * 将统一评论数据格式转换为平台评论数据 + * @param data 统一评论数据格式 + * @returns 平台特定评论数据 + */ + mapUnifiedToPlatformReview(data: Partial): any; + + /** + * 将统一评论创建参数转换为平台评论创建参数 + * @param data 统一评论创建参数 + * @returns 平台评论创建参数 + */ + mapCreateReviewParams(data: CreateReviewDTO): any; + + /** + * 将统一评论更新参数转换为平台评论更新参数 + * @param data 统一评论更新参数 + * @returns 平台评论更新参数 + */ + mapUpdateReviewParams(data: UpdateReviewDTO): any; + + /** + * 获取评论列表 + */ + getReviews(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有评论 + */ + getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建评论 + */ + createReview(data: CreateReviewDTO): Promise; + + /** + * 更新评论 + */ + updateReview(id: number, data: UpdateReviewDTO): Promise; + + /** + * 删除评论 + */ + deleteReview(id: number): Promise; + + // ========== 订阅映射方法 ========== + /** + * 将平台订阅数据转换为统一订阅数据格式 + * @param data 平台特定订阅数据 + * @returns 统一订阅数据格式 + */ + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO; + + /** + * 将统一订阅数据格式转换为平台订阅数据 + * @param data 统一订阅数据格式 + * @returns 平台特定订阅数据 + */ + mapUnifiedToPlatformSubscription(data: Partial): any; + + /** + * 获取订阅列表 + */ + getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有订阅 + */ + getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + + // ========== 产品变体映射方法 ========== + /** + * 将平台产品变体数据转换为统一产品变体数据格式 + * @param data 平台特定产品变体数据 + * @returns 统一产品变体数据格式 + */ + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO; + + /** + * 将统一产品变体数据格式转换为平台产品变体数据 + * @param data 统一产品变体数据格式 + * @returns 平台特定产品变体数据 + */ + mapUnifiedToPlatformVariation(data: Partial): any; + + /** + * 将统一产品变体创建参数转换为平台产品变体创建参数 + * @param data 统一产品变体创建参数 + * @returns 平台产品变体创建参数 + */ + mapCreateVariationParams(data: CreateVariationDTO): any; + + /** + * 将统一产品变体更新参数转换为平台产品变体更新参数 + * @param data 统一产品变体更新参数 + * @returns 平台产品变体更新参数 + */ + mapUpdateVariationParams(data: UpdateVariationDTO): any; + + /** + * 获取单个产品变体 + */ + getVariation(productId: string | number, variationId: string | number): Promise; + + /** + * 获取产品变体列表 + */ + getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有产品变体 + */ + getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品变体 + */ + createVariation(productId: string | number, data: CreateVariationDTO): Promise; + + /** + * 更新产品变体 + */ + updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; + + /** + * 删除产品变体 + */ + deleteVariation(productId: string | number, variationId: string | number): Promise; + + // ========== Webhook映射方法 ========== + /** + * 将平台Webhook数据转换为统一Webhook数据格式 + * @param data 平台特定Webhook数据 + * @returns 统一Webhook数据格式 + */ + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO; + + /** + * 将统一Webhook数据格式转换为平台Webhook数据 + * @param data 统一Webhook数据格式 + * @returns 平台特定Webhook数据 + */ + mapUnifiedToPlatformWebhook(data: Partial): 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(id: string | number): Promise; + + /** + * 获取webhooks列表 + */ + getWebhooks(params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有webhooks + */ + getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建webhook + */ + createWebhook(data: CreateWebhookDTO): Promise; + + /** + * 更新webhook + */ + updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + + /** + * 删除webhook + */ + deleteWebhook(id: string | number): Promise; + + // ========== 站点/其他方法 ========== + /** + * 获取站点链接列表 + */ + getLinks(): Promise>; } diff --git a/src/transformer/database.transformer.ts b/src/transformer/database.transformer.ts new file mode 100644 index 0000000..619ef04 --- /dev/null +++ b/src/transformer/database.transformer.ts @@ -0,0 +1 @@ +// 从 unified 到 数据库需要有个转换流程 diff --git a/src/transformer/file.transformer.ts b/src/transformer/file.transformer.ts new file mode 100644 index 0000000..b85ee23 --- /dev/null +++ b/src/transformer/file.transformer.ts @@ -0,0 +1 @@ +// 文件转换 diff --git a/src/transformer/shopyy.transformer.ts b/src/transformer/shopyy.transformer.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/transformer/woocommerce.adpater.ts b/src/transformer/woocommerce.adpater.ts new file mode 100644 index 0000000..e5b5c67 --- /dev/null +++ b/src/transformer/woocommerce.adpater.ts @@ -0,0 +1,8 @@ +import { UnifiedOrderDTO } from "../dto/site-api.dto"; + +export class ShipmentAdapter { + // 用于导出物流需要的数据 + mapFromOrder(order: UnifiedOrderDTO): any { + return order; + } +} \ No newline at end of file