import { ISiteAdapter } from '../interface/site-adapter.interface'; import { ShopyyService } from '../service/shopyy.service'; import { UnifiedAddressDTO, UnifiedCustomerDTO, UnifiedMediaDTO, UnifiedOrderDTO, UnifiedOrderLineItemDTO, UnifiedPaginationDTO, UnifiedProductDTO, UnifiedProductVariationDTO, UnifiedSearchParamsDTO, UnifiedSubscriptionDTO, UnifiedReviewPaginationDTO, UnifiedReviewDTO, UnifiedWebhookDTO, UnifiedWebhookPaginationDTO, CreateWebhookDTO, UpdateWebhookDTO, } from '../dto/site-api.dto'; import { ShopyyCustomer, ShopyyOrder, ShopyyProduct, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; 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, }; } 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 || '', 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 || '', country: shipping.country_name || shipping.country_code || item.shipping_country || '', }; // 格式化地址为字符串 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 || '', }) ); 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, line_items: lineItems, sales: lineItems, // 兼容前端 billing: billingObj, shipping: shippingObj, billing_full_address: formatAddress(billingObj), shipping_full_address: formatAddress(shippingObj), payment_method: item.payment_method, refunds: [], 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 { // Shopyy getAllOrders 暂未实现 throw new Error('Shopyy getAllOrders 暂未实现'); } async getOrder(id: string | number): Promise { const data = await this.shopyyService.getOrder(String(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 shipOrder(orderId: string | number, data: { tracking_number?: string; shipping_provider?: string; shipping_method?: string; items?: Array<{ order_item_id: number; quantity: number; }>; }): Promise { // 订单发货 try { // 更新订单状态为已发货 await this.shopyyService.updateOrder(this.site, String(orderId), { status: 'completed', meta_data: [ { key: '_tracking_number', value: data.tracking_number }, { key: '_shipping_provider', value: data.shipping_provider }, { key: '_shipping_method', value: data.shipping_method } ] }); // 添加发货备注 const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`; await this.shopyyService.createOrderNote(this.site, orderId, { note, customer_note: true }); return { success: true, order_id: orderId, shipment_id: `shipment_${orderId}_${Date.now()}`, tracking_number: data.tracking_number, shipping_provider: data.shipping_provider, shipped_at: new Date().toISOString() }; } catch (error) { throw new Error(`发货失败: ${error.message}`); } } async cancelShipOrder(orderId: string | number, data: { reason?: string; shipment_id?: string; }): Promise { // 取消订单发货 try { // 将订单状态改回处理中 await this.shopyyService.updateOrder(this.site, String(orderId), { status: 'processing', meta_data: [ { key: '_shipment_cancelled', value: 'yes' }, { key: '_shipment_cancelled_reason', value: data.reason } ] }); // 添加取消发货的备注 const note = `订单发货已取消${data.reason ? `,原因:${data.reason}` : ''}`; await this.shopyyService.createOrderNote(this.site, orderId, { 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> { 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); } }