zksu
/
API
forked from yoone/API
1
0
Fork 0
API/src/adapter/shopyy.adapter.ts

1315 lines
45 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
UnifiedShippingLineDTO,
OrderFulfillmentStatus,
FulfillmentDTO,
CreateReviewDTO,
CreateVariationDTO,
UpdateReviewDTO,
} from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import {
ShopyyAllProductQuery,
ShopyyCustomer,
ShopyyOrder,
ShopyyOrderQuery,
ShopyyProduct,
ShopyyProductQuery,
ShopyyVariant,
ShopyyWebhook,
} from '../dto/shopyy.dto';
import {
OrderStatus,
} from '../enums/base.enum';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
export class ShopyyAdapter implements ISiteAdapter {
shopyyFinancialStatusMap= {
'200': '待支付',
'210': "支付中",
'220':"部分支付",
'230':"已支付",
'240':"支付失败",
'250':"部分退款",
'260':"已退款",
'290':"已取消",
}
constructor(private site: any, private shopyyService: ShopyyService) {
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);
}
// ========== 客户映射方法 ==========
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<UnifiedCustomerDTO>) {
return data
}
async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise<UnifiedCustomerDTO> {
if(!where.id && !where.email && !where.phone){
throw new Error('必须传入 id 或 email 或 phone')
}
const customer = await this.shopyyService.getCustomer(this.site, where.id);
return this.mapPlatformToUnifiedCustomer(customer);
}
async getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>> {
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<UnifiedCustomerDTO[]> {
// Shopyy getAllCustomers 暂未实现
throw new Error('Shopyy getAllCustomers 暂未实现');
}
async createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const createdCustomer = await this.shopyyService.createCustomer(this.site, data);
return this.mapPlatformToUnifiedCustomer(createdCustomer);
}
async updateCustomer(where: {id: string | number}, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data);
return this.mapPlatformToUnifiedCustomer(updatedCustomer);
}
async deleteCustomer(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteCustomer(this.site, where.id);
}
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO> {
throw new Error('Method not implemented.');
}
// ========== 媒体映射方法 ==========
mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO {
// 映射媒体项目
return {
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未提供,暂时留空
};
}
mapUnifiedToPlatformMedia(data: Partial<UnifiedMediaDTO>) {
return data
}
async getMedia(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
const requestParams = this.mapMediaSearchParams(params);
const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged<any>(
this.site,
'media', // Shopyy的媒体API端点可能需要调整
requestParams
);
return {
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
total,
totalPages,
page,
per_page,
};
}
async getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]> {
// Shopyy getAllMedia 暂未实现
throw new Error('Shopyy getAllMedia 暂未实现');
}
async createMedia(file: any): Promise<UnifiedMediaDTO> {
const createdMedia = await this.shopyyService.createMedia(this.site, file);
return this.mapPlatformToUnifiedMedia(createdMedia);
}
async updateMedia(where: {id: string | number}, data: any): Promise<UnifiedMediaDTO> {
const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data);
return this.mapPlatformToUnifiedMedia(updatedMedia);
}
async deleteMedia(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteMedia(this.site, where.id);
}
mapMediaSearchParams(params: UnifiedSearchParamsDTO): any {
return this.mapSearchParams(params)
}
// ========== 订单映射方法 ==========
mapPlatformToUnifiedOrder(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: Number(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<string, string> = {
'EUR': '€',
'USD': '$',
'GBP': '£',
'JPY': '¥',
'AUD': 'A$',
'CAD': 'C$',
'CHF': 'CHF',
'CNY': '¥',
'HKD': 'HK$',
'NZD': 'NZ$',
'SGD': 'S$'
// 可以根据需要添加更多货币代码和符号
};
// 映射订单状态,如果不存在则默认 pending
const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING;
const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status]
// 发货状态
const fulfillment_status = this.shopyyFulfillmentStatusMap[item.fulfillment_status];
return {
id: item.id || item.order_id,
number: item.order_number || item.order_sn,
status,
financial_status: finalcial_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', // FIXME
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 : ''),
fulfillment_status,
fulfillments: item.fulfillments?.map?.((f) => ({
id: f.id,
tracking_number: f.tracking_number || '',
shipping_provider: f.tracking_company || '',
shipping_method: f.tracking_company || '',
date_created: typeof f.created_at === 'number'
? new Date(f.created_at * 1000).toISOString()
: f.created_at || '',
// status: f.payment_tracking_status
})) || [],
raw: item,
};
}
mapUnifiedToPlatformOrder(data: Partial<UnifiedOrderDTO>) {
return data
}
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any {
return data
}
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): 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(where: {id: string | number}): Promise<UnifiedOrderDTO> {
const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
return this.mapPlatformToUnifiedOrder(data);
}
async getOrders(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
// 转换订单查询参数
const normalizedParams = this.mapOrderSearchParams(params);
const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged<any>(
this.site,
'orders',
normalizedParams
);
return {
items: items.map(this.mapPlatformToUnifiedOrder.bind(this)),
total,
totalPages,
page,
per_page,
};
}
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
const data = await this.shopyyService.getAllOrders(this.site.id, params);
return data.map(this.mapPlatformToUnifiedOrder.bind(this));
}
async countOrders(where: Record<string,any>): Promise<number> {
// 使用最小分页只获取总数
const searchParams = {
where,
page: 1,
per_page: 1,
}
const data = await this.getOrders(searchParams);
return data.total || 0;
}
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
// 使用映射方法转换参数
const requestParams = this.mapCreateOrderParams(data);
const createdOrder = await this.shopyyService.createOrder(this.site, requestParams);
return this.mapPlatformToUnifiedOrder(createdOrder);
}
async updateOrder(where: {id: string | number}, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
// 使用映射方法转换参数
const requestParams = this.mapUpdateOrderParams(data);
return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams);
}
async deleteOrder(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteOrder(this.site, where.id);
}
async getOrderNotes(orderId: string | number): Promise<any[]> {
return await this.shopyyService.getOrderNotes(this.site, orderId);
}
async createOrderNote(orderId: string | number, data: any): Promise<any> {
return await this.shopyyService.createOrderNote(this.site, orderId, data);
}
async cancelFulfillment(orderId: string | number, data: {
reason?: string;
shipment_id?: string;
}): Promise<any> {
// 取消订单履行
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 getOrderFulfillments(orderId: string | number): Promise<any[]> {
return await this.shopyyService.getFulfillments(this.site, String(orderId));
}
async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise<any> {
// 调用 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<any> {
return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
}
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId);
}
batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO> {
throw new Error('Method not implemented.');
}
mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<ShopyyOrderQuery> {
// 首先使用通用参数转换
const baseParams = this.mapSearchParams(params);
// 订单状态映射
const statusMap = {
'pending': '100', // 100 未完成
'processing': '110', // 110 待处理
'completed': "180", // 180 已完成(确认收货)
'cancelled': '190', // 190 取消
};
// 如果有状态参数,进行特殊映射
if (baseParams.status) {
const unifiedStatus = baseParams.status
if (statusMap[unifiedStatus]) {
baseParams.status = statusMap[unifiedStatus];
}
}
// 处理ID参数
if (baseParams.id) {
baseParams.ids = baseParams.id;
delete baseParams.id;
}
return baseParams;
}
// ========== 产品映射方法 ==========
mapPlatformToUnifiedProduct(item: ShopyyProduct): 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.mapPlatformToUnifiedVariation.bind(this)) || [],
permalink: `${this.site.websiteUrl}/products/${item.handle}`,
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<UnifiedProductDTO>) {
return data
}
mapCreateProductParams(data: Partial<UnifiedProductDTO>): Partial<ShopyyProduct> {
// 构建 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<UnifiedProductDTO>): 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(where: {id?: string | number, sku?: string}): Promise<UnifiedProductDTO> {
if(!where.id && !where.sku){
throw new Error('必须传入 id 或 sku')
}
if (where.id) {
// 使用ShopyyService获取单个产品
const product = await this.shopyyService.getProduct(this.site, where.id);
return this.mapPlatformToUnifiedProduct(product);
} else if (where.sku) {
// 通过sku获取产品
return this.getProductBySku(where.sku);
}
}
async getProducts(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedProductDTO>> {
// 转换搜索参数
const requestParams = this.mapProductQuery(params);
const response = await this.shopyyService.fetchResourcePaged<ShopyyProduct>(
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,
per_page,
};
}
mapAllProductParams(params: UnifiedSearchParamsDTO): Partial<ShopyyAllProductQuery>{
const mapped = {
...params.where,
} as any
if(params.per_page){mapped.limit = params.per_page}
return mapped
}
async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> {
// 转换搜索参数
const requestParams = this.mapAllProductParams(params);
const response = await this.shopyyService.request(
this.site,
'products',
'GET',
null,
requestParams
);
if(response.code !==0){
throw new Error(response.msg || '获取产品列表失败')
}
const { data = [] } = response;
const finalItems = data.map(this.mapPlatformToUnifiedProduct.bind(this))
return finalItems
}
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
// 使用映射方法转换参数
const requestParams = this.mapCreateProductParams(data);
const res = await this.shopyyService.createProduct(this.site, requestParams);
return this.mapPlatformToUnifiedProduct(res);
}
async updateProduct(where: {id?: string | number, sku?: string}, data: Partial<UnifiedProductDTO>): Promise<boolean> {
let productId: string;
if (where.id) {
productId = String(where.id);
} else if (where.sku) {
// 通过sku获取产品ID
const product = await this.getProductBySku(where.sku);
productId = String(product.id);
} else {
throw new Error('必须提供id或sku参数');
}
// 使用映射方法转换参数
const requestParams = this.mapUpdateProductParams(data);
await this.shopyyService.updateProduct(this.site, productId, requestParams);
return true;
}
async deleteProduct(where: {id?: string | number, sku?: string}): Promise<boolean> {
let productId: string | number;
if (where.id) {
productId = where.id;
} else if (where.sku) {
// 通过sku获取产品ID
const product = await this.getProductBySku(where.sku);
productId = product.id;
} else {
throw new Error('必须提供id或sku参数');
}
// Use batch delete
await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] });
return true;
}
// 通过sku获取产品详情的私有方法
private async getProductBySku(sku: string): Promise<UnifiedProductDTO> {
// 使用Shopyy API的搜索功能通过sku查询产品
const response = await this.getAllProducts({ where: {sku} });
console.log('getProductBySku', response)
const product = response?.[0]
if (!product) {
throw new Error(`未找到sku为${sku}的产品`);
}
return product
}
async batchProcessProducts(
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
): Promise<any> {
return await this.shopyyService.batchProcessProducts(this.site, data);
}
mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery {
return this.mapSearchParams(query)
}
mapAllProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery {
return this.mapSearchParams(query)
}
// ========== 评论映射方法 ==========
mapUnifiedToPlatformReview(data: Partial<UnifiedReviewDTO>) {
return data
}
mapCreateReviewParams(data: CreateReviewDTO) {
return data
}
mapUpdateReviewParams(data: UpdateReviewDTO) {
return data
}
async getReviews(
params: UnifiedSearchParamsDTO
): Promise<UnifiedReviewPaginationDTO> {
const requestParams = this.mapReviewSearchParams(params);
const { items, total, totalPages, page, per_page } = await this.shopyyService.getReviews(
this.site,
requestParams
);
return {
items: items.map(this.mapPlatformToUnifiedReview),
total,
totalPages,
page,
per_page,
};
}
async getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]> {
// Shopyy getAllReviews 暂未实现
throw new Error('Shopyy getAllReviews 暂未实现');
}
async createReview(data: any): Promise<UnifiedReviewDTO> {
const createdReview = await this.shopyyService.createReview(this.site, data);
return this.mapPlatformToUnifiedReview(createdReview);
}
async updateReview(where: {id: string | number}, data: any): Promise<UnifiedReviewDTO> {
const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data);
return this.mapPlatformToUnifiedReview(updatedReview);
}
async deleteReview(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteReview(this.site, where.id);
}
mapPlatformToUnifiedReview(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 || '')
};
}
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;
}
return shopyyParams;
}
// ========== 订阅映射方法 ==========
mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO {
return data
}
mapUnifiedToPlatformSubscription(data: Partial<UnifiedSubscriptionDTO>) {
return data
}
async getSubscriptions(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
throw new Error('Shopyy does not support subscriptions.');
}
async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]> {
// Shopyy getAllSubscriptions 暂未实现
throw new Error('Shopyy getAllSubscriptions 暂未实现');
}
// ========== 产品变体映射方法 ==========
mapPlatformToUnifiedVariation(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,
};
}
mapUnifiedToPlatformVariation(data: Partial<UnifiedProductVariationDTO>) {
return data
}
mapCreateVariationParams(data: CreateVariationDTO) {
return data
}
mapUpdateVariationParams(data: Partial<UnifiedProductVariationDTO>): 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<UnifiedProductVariationDTO> {
throw new Error('Shopyy getVariation 暂未实现');
}
async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<any> {
throw new Error('Shopyy getVariations 暂未实现');
}
async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]> {
throw new Error('Shopyy getAllVariations 暂未实现');
}
async createVariation(productId: string | number, data: any): Promise<UnifiedProductVariationDTO> {
throw new Error('Shopyy createVariation 暂未实现');
}
async updateVariation(productId: string | number, variationId: string | number, data: Partial<UnifiedProductVariationDTO>): Promise<any> {
// 使用映射方法转换参数
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<boolean> {
throw new Error('Shopyy deleteVariation 暂未实现');
}
// ========== Webhook映射方法 ==========
mapUnifiedToPlatformWebhook(data: Partial<UnifiedWebhookDTO>) {
return data
}
mapCreateWebhookParams(data: CreateWebhookDTO) {
return data
}
mapUpdateWebhookParams(data: UpdateWebhookDTO) {
return data
}
async getWebhook(where: {id: string | number}): Promise<UnifiedWebhookDTO> {
const webhook = await this.shopyyService.getWebhook(this.site, where.id);
return this.mapPlatformToUnifiedWebhook(webhook);
}
async getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO> {
const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params);
return {
items: items.map(this.mapPlatformToUnifiedWebhook),
total,
totalPages,
page,
per_page,
};
}
async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]> {
// Shopyy getAllWebhooks 暂未实现
throw new Error('Shopyy getAllWebhooks 暂未实现');
}
async createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO> {
const createdWebhook = await this.shopyyService.createWebhook(this.site, data);
return this.mapPlatformToUnifiedWebhook(createdWebhook);
}
async updateWebhook(where: {id: string | number}, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO> {
const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data);
return this.mapPlatformToUnifiedWebhook(updatedWebhook);
}
async deleteWebhook(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteWebhook(this.site, where.id);
}
mapPlatformToUnifiedWebhook(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<Array<{ title: string, url: string }>> {
// 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;
}
// ========== 辅助方法 ==========
/**
* 通用搜索参数转换方法,处理 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,
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;
}
// 映射产品状态: 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
}
shopyyFulfillmentStatusMap = {
// 未发货
'300': OrderFulfillmentStatus.PENDING,
// 部分发货
'310': OrderFulfillmentStatus.PARTIALLY_FULFILLED,
// 已发货
'320': OrderFulfillmentStatus.FULFILLED,
// 已取消
'330': OrderFulfillmentStatus.CANCELLED,
// 确认发货
}
}