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

679 lines
23 KiB
TypeScript

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<UnifiedPaginationDTO<UnifiedProductDTO>> {
const response = await this.shopyyService.fetchResourcePaged<ShopyyProduct>(
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 getProduct(id: string | number): Promise<UnifiedProductDTO> {
// 使用ShopyyService获取单个产品
const product = await this.shopyyService.getProduct(this.site, id);
return this.mapProduct(product);
}
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
const res = await this.shopyyService.createProduct(this.site, data);
return this.mapProduct(res);
}
async updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean> {
// 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<any> {
await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data);
return { ...data, id: variationId };
}
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 deleteProduct(id: string | number): Promise<boolean> {
// Use batch delete
await this.shopyyService.batchProcessProducts(this.site, { delete: [id] });
return true;
}
async batchProcessProducts(
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
): Promise<any> {
return await this.shopyyService.batchProcessProducts(this.site, data);
}
async getOrders(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
const { items, total, totalPages, page, per_page } =
await this.shopyyService.fetchResourcePaged<any>(
this.site,
'orders',
params
);
return {
items: items.map(this.mapOrder.bind(this)),
total,
totalPages,
page,
per_page,
};
}
async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
const data = await this.shopyyService.getOrder(String(this.site.id), String(id));
return this.mapOrder(data);
}
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
const createdOrder = await this.shopyyService.createOrder(this.site, data);
return this.mapOrder(createdOrder);
}
async updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
return await this.shopyyService.updateOrder(this.site, String(id), data);
}
async deleteOrder(id: string | number): Promise<boolean> {
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<any> {
// 订单发货
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<any> {
// 取消订单发货
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<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
throw new Error('Shopyy does not support subscriptions.');
}
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.mapMedia),
total,
totalPages,
page,
per_page,
};
}
async createMedia(file: any): Promise<UnifiedMediaDTO> {
const createdMedia = await this.shopyyService.createMedia(this.site, file);
return this.mapMedia(createdMedia);
}
async updateMedia(id: string | number, data: any): Promise<UnifiedMediaDTO> {
const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data);
return this.mapMedia(updatedMedia);
}
async deleteMedia(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteMedia(this.site, id);
}
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.mapReview),
total,
totalPages,
page,
per_page,
};
}
async getReview(id: string | number): Promise<UnifiedReviewDTO> {
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, status } = params;
const shopyyParams: any = {
page: page || 1,
limit: per_page || 10,
};
if (search) {
shopyyParams.search = search;
}
if (status) {
shopyyParams.status = status;
}
// if (product_id) {
// shopyyParams.product_id = product_id;
// }
return shopyyParams;
}
async createReview(data: any): Promise<UnifiedReviewDTO> {
const createdReview = await this.shopyyService.createReview(this.site, data);
return this.mapReview(createdReview);
}
async updateReview(id: string | number, data: any): Promise<UnifiedReviewDTO> {
const updatedReview = await this.shopyyService.updateReview(this.site, id, data);
return this.mapReview(updatedReview);
}
async deleteReview(id: string | number): Promise<boolean> {
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<UnifiedWebhookPaginationDTO> {
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 getWebhook(id: string | number): Promise<UnifiedWebhookDTO> {
const webhook = await this.shopyyService.getWebhook(this.site, id);
return this.mapWebhook(webhook);
}
async createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO> {
const createdWebhook = await this.shopyyService.createWebhook(this.site, data);
return this.mapWebhook(createdWebhook);
}
async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO> {
const updatedWebhook = await this.shopyyService.updateWebhook(this.site, id, data);
return this.mapWebhook(updatedWebhook);
}
async deleteWebhook(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteWebhook(this.site, id);
}
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;
}
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.mapCustomer.bind(this)),
total,
totalPages,
page,
per_page
};
}
async getCustomer(id: string | number): Promise<UnifiedCustomerDTO> {
const customer = await this.shopyyService.getCustomer(this.site, id);
return this.mapCustomer(customer);
}
async createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const createdCustomer = await this.shopyyService.createCustomer(this.site, data);
return this.mapCustomer(createdCustomer);
}
async updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data);
return this.mapCustomer(updatedCustomer);
}
async deleteCustomer(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteCustomer(this.site, id);
}
}