import { ISiteAdapter } from '../interface/site-adapter.interface'; import { ShopyyService } from '../service/shopyy.service'; import { UnifiedCustomerDTO, UnifiedMediaDTO, UnifiedOrderDTO, UnifiedOrderLineItemDTO, UnifiedProductDTO, UnifiedProductVariationDTO, UnifiedSubscriptionDTO, UnifiedReviewPaginationDTO, UnifiedReviewDTO, UnifiedWebhookDTO, UnifiedWebhookPaginationDTO, CreateWebhookDTO, UpdateWebhookDTO, UnifiedAddressDTO } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { ShopyyCustomer, ShopyyOrder, ShopyyProduct, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; import { OrderStatus, } from '../enums/base.enum'; export class ShopyyAdapter implements ISiteAdapter { 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); } private mapMedia(item: any): UnifiedMediaDTO { // 映射媒体项目 return { id: item.id, date_created: item.created_at, date_modified: item.updated_at, source_url: item.src, title: item.alt || '', media_type: '', // Shopyy API未提供,暂时留空 mime_type: '', // Shopyy API未提供,暂时留空 }; } private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { const { search, page, per_page } = params; const shopyyParams: any = { page: page || 1, limit: per_page || 10, }; if (search) { shopyyParams.query = search; } return shopyyParams; } 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, 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, }; } shopyyOrderAutoNextStatusMap = {//订单状态 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 { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_address || {}; const shipping = (item as any).shipping_address || {}; // 构建账单地址对象 const billingObj: UnifiedAddressDTO = { first_name: billing.first_name || item.firstname || '', last_name: billing.last_name || item.lastname || '', fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(), company: billing.company || '', email: item.customer_email || item.email || '', phone: billing.phone || (item as any).telephone || '', address_1: billing.address1 || item.payment_address || '', address_2: billing.address2 || '', city: billing.city || item.payment_city || '', state: billing.province || item.payment_zone || '', postcode: billing.zip || item.payment_postcode || '', method_title: item.payment_method || '', country: billing.country_name || billing.country_code || item.payment_country || '', }; // 构建送货地址对象 const shippingObj: UnifiedAddressDTO = { first_name: shipping.first_name || item.firstname || '', last_name: shipping.last_name || item.lastname || '', fullname: shipping.name || '', company: shipping.company || '', address_1: shipping.address1 || (typeof item.shipping_address === 'string' ? item.shipping_address : '') || '', address_2: shipping.address2 || '', city: shipping.city || item.shipping_city || '', state: shipping.province || item.shipping_zone || '', postcode: shipping.zip || item.shipping_postcode || '', method_title: item.payment_method || '', country: shipping.country_name || shipping.country_code || item.shipping_country || '', }; // 构建送货地址对象 const shipping_lines: UnifiedShippingLineDTO[] = [ { id: item.fulfillments?.[0]?.id || 0, method_title: item.payment_method || '', method_id: item.payment_method || '', total: item.current_shipping_price.toExponential(2) || '0.00', total_tax: '0.00', taxes: [], meta_data: [], }, ]; // 格式化地址为字符串 const formatAddress = (addr: UnifiedAddressDTO) => { return [ addr.fullname, addr.company, addr.address_1, addr.address_2, addr.city, addr.state, addr.postcode, addr.country, addr.phone, ] .filter(Boolean) .join(', '); }; const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map( (p: any) => ({ id: p.id, name: p.product_title || p.name, product_id: p.product_id, quantity: p.quantity, total: String(p.price ?? ''), sku: p.sku || p.sku_code || '', price: String(p.price ?? ''), }) ); const currencySymbols: Record = { 'EUR': '€', 'USD': '$', 'GBP': '£', 'JPY': '¥', 'AUD': 'A$', 'CAD': 'C$', 'CHF': 'CHF', 'CNY': '¥', 'HKD': 'HK$', 'NZD': 'NZ$', 'SGD': 'S$' // 可以根据需要添加更多货币代码和符号 }; const originStatus = item.status; item.status = this.shopyyOrderAutoNextStatusMap[originStatus]; return { id: item.id || item.order_id, number: item.order_number || item.order_sn, status: String(item.status || item.order_status), currency: item.currency_code || item.currency, total: String(item.total_price ?? item.total_amount ?? ''), customer_id: item.customer_id || item.user_id, customer_name: item.customer_name || `${item.firstname} ${item.lastname}`.trim(), email: item.customer_email || item.email, customer_email: item.customer_email || item.email, line_items: lineItems, sales: lineItems, // 兼容前端 billing: billingObj, shipping: shippingObj, billing_full_address: formatAddress(billingObj), shipping_full_address: formatAddress(shippingObj), payment_method: item.payment_method, shipping_lines: shipping_lines || [], fee_lines: item.fee_lines || [], coupon_lines: item.coupon_lines || [], customer_ip_address: item.ip || '', device_type: item.source_device || '', utm_source: item.utm_source || '', source_type: 'shopyy', date_paid: typeof item.pay_at === 'number' ? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString() : null, refunds: [], currency_symbol: (currencySymbols[item.currency] || '$') || '', date_created: typeof item.created_at === 'number' ? new Date(item.created_at * 1000).toISOString() : item.date_added || (typeof item.created_at === 'string' ? item.created_at : ''), date_modified: typeof item.updated_at === 'number' ? new Date(item.updated_at * 1000).toISOString() : item.date_updated || item.last_modified || (typeof item.updated_at === 'string' ? item.updated_at : ''), raw: item, }; } 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, }; } async getProducts( params: UnifiedSearchParamsDTO ): Promise> { const response = await this.shopyyService.fetchResourcePaged( this.site, 'products/list', params ); 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[], total, totalPages, page, per_page, }; } async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { // Shopyy getAllProducts 暂未实现 throw new Error('Shopyy getAllProducts 暂未实现'); } async getProduct(id: string | number): Promise { // 使用ShopyyService获取单个产品 const product = await this.shopyyService.getProduct(this.site, id); return this.mapProduct(product); } 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 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 getOrderNotes(orderId: string | number): Promise { return await this.shopyyService.getOrderNotes(this.site, orderId); } async createOrderNote(orderId: string | number, data: any): Promise { 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 batchProcessProducts( data: { create?: any[]; update?: any[]; delete?: Array } ): Promise { return await this.shopyyService.batchProcessProducts(this.site, data); } async getOrders( params: UnifiedSearchParamsDTO ): Promise> { const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, 'orders', params ); return { items: items.map(this.mapOrder.bind(this)), total, totalPages, page, per_page, }; } async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { const data = await this.shopyyService.getAllOrders(this.site.id,params); return data.map(this.mapOrder.bind(this)); } async getOrder(id: string | number): Promise { const data = await this.shopyyService.getOrder(this.site.id, String(id)); return this.mapOrder(data); } async createOrder(data: Partial): Promise { const createdOrder = await this.shopyyService.createOrder(this.site, data); return this.mapOrder(createdOrder); } async updateOrder(id: string | number, data: Partial): Promise { return await this.shopyyService.updateOrder(this.site, String(id), data); } async deleteOrder(id: string | number): Promise { return await this.shopyyService.deleteOrder(this.site, id); } 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}`); } } async cancelFulfillment(orderId: string | number, data: { reason?: string; shipment_id?: string; }): Promise { // 取消订单履行 try { // 调用 ShopyyService 的取消履行方法 const cancelShipData = { order_id: String(orderId), fullfillment_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}`); } } /** * 获取订单履行信息 * @param orderId 订单ID * @returns 履行信息列表 */ async getOrderFulfillments(orderId: string | number): Promise { return await this.shopyyService.getFulfillments(this.site, String(orderId)); } /** * 创建订单履行信息 * @param orderId 订单ID * @param data 履行数据 * @returns 创建结果 */ async createOrderFulfillment(orderId: string | number, data: { tracking_number: string; tracking_provider: string; date_shipped?: string; status_shipped?: string; }): Promise { // 调用 Shopyy Service 的 createFulfillment 方法 const fulfillmentData = { tracking_number: data.tracking_number, carrier_code: data.tracking_provider, carrier_name: data.tracking_provider, shipping_method: data.status_shipped || '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); } async getReviews( params: UnifiedSearchParamsDTO ): Promise { const requestParams = this.mapReviewSearchParams(params); const { items, total, totalPages, page, per_page } = await this.shopyyService.getReviews( this.site, requestParams ); return { items: items.map(this.mapReview), total, totalPages, page, per_page, }; } async getAllReviews(params?: UnifiedSearchParamsDTO): Promise { // Shopyy getAllReviews 暂未实现 throw new Error('Shopyy getAllReviews 暂未实现'); } async getReview(id: string | number): Promise { const review = await this.shopyyService.getReview(this.site, id); return this.mapReview(review); } private mapReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, product_id: review.product_id || review.goods_id, author: review.author_name || review.username || '', email: review.author_email || review.user_email || '', content: review.comment || review.content || '', rating: Number(review.score || review.rating || 0), status: String(review.status || 'approved'), date_created: typeof review.created_at === 'number' ? new Date(review.created_at * 1000).toISOString() : String(review.created_at || review.date_added || '') }; } private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { const { search, page, per_page, where } = params; const shopyyParams: any = { page: page || 1, limit: per_page || 10, }; if (search) { shopyyParams.search = search; } if (where.status) { 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); } 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); } // Webhook相关方法 private 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 getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { items: items.map(this.mapWebhook), total, totalPages, page, per_page, }; } async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { // Shopyy getAllWebhooks 暂未实现 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); } async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { const updatedWebhook = await this.shopyyService.updateWebhook(this.site, id, data); return this.mapWebhook(updatedWebhook); } async deleteWebhook(id: string | number): Promise { return await this.shopyyService.deleteWebhook(this.site, id); } async getLinks(): Promise> { // ShopYY站点的管理后台链接通常基于apiUrl构建 const url = this.site.websiteUrl // 提取基础域名,去掉可能的路径部分 const baseUrl = url.replace(/\/api\/.*$/i, ''); const links = [ { title: '访问网站', url: baseUrl }, { title: '管理后台', url: `${baseUrl}/admin/` }, { title: '订单管理', url: `${baseUrl}/admin/orders.htm` }, { title: '产品管理', url: `${baseUrl}/admin/products.htm` }, { title: '客户管理', url: `${baseUrl}/admin/customers.htm` }, { title: '插件管理', url: `${baseUrl}/admin/apps.htm` }, { title: '店铺设置', url: `${baseUrl}/admin/settings.htm` }, { title: '营销中心', url: `${baseUrl}/admin/marketing.htm` }, ]; 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, page, per_page }; } async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { // Shopyy getAllCustomers 暂未实现 throw new Error('Shopyy getAllCustomers 暂未实现'); } async getCustomer(id: string | number): Promise { const customer = await this.shopyyService.getCustomer(this.site, id); return this.mapCustomer(customer); } 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 暂未实现'); } }