feat: 统一分页查询参数处理与DTO增强

refactor(service): 重构分页查询参数处理逻辑
feat(dto): 增强统一分页DTO与搜索参数DTO
feat(controller): 实现批量导出与分页查询优化
docs(dto): 添加DTO字段注释与类型定义
This commit is contained in:
tikkhun 2025-12-15 15:18:12 +08:00
parent 3f3569995d
commit 8ef150c1ba
11 changed files with 1178 additions and 336 deletions

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ container
scripts
ai
tmp_uploads/
.trae

54
debug_sync.log Normal file

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,12 @@ import {
UnifiedSubscriptionDTO,
UnifiedCustomerDTO,
} from '../dto/site-api.dto';
import {
ShopyyProduct,
ShopyyOrder,
ShopyyCustomer,
ShopyyVariant,
} from '../dto/shopyy.dto';
export class ShopyyAdapter implements ISiteAdapter {
constructor(private site: any, private shopyyService: ShopyyService) { }
@ -16,19 +22,19 @@ export class ShopyyAdapter implements ISiteAdapter {
// return status === 1 ? 'publish' : 'draft';
// }
private mapProduct(item: any): UnifiedProductDTO {
private mapProduct(item: ShopyyProduct): UnifiedProductDTO {
function mapProductStatus(status: number) {
return status === 1 ? 'publish' : 'draft';
}
return {
id: item.id,
name: item.name || item.title,
type: item.product_type,
type: String(item.product_type ?? ''),
status: mapProductStatus(item.status),
sku: item.variant?.sku || '',
regular_price: item.variant?.price,
sale_price: item.special_price,
price: item.price,
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) => ({
@ -42,26 +48,26 @@ export class ShopyyAdapter implements ISiteAdapter {
attributes: [],
tags: item.tags || [],
variations: item.variants?.map(this.mapVariation.bind(this)) || [],
date_created: item.created_at,
date_modified: item.updated_at,
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,
};
}
mapVariation(mapVariation: any) {
mapVariation(mapVariation: ShopyyVariant) {
return {
id: mapVariation.id,
sku: mapVariation.sku || '',
regular_price: mapVariation.price,
sale_price: mapVariation.special_price,
price: mapVariation.price,
regular_price: String(mapVariation.price ?? ''),
sale_price: String(mapVariation.special_price ?? ''),
price: String(mapVariation.price ?? ''),
stock_status: mapVariation.inventory_tracking === 1 ? 'instock' : 'outofstock',
stock_quantity: mapVariation.inventory_quantity,
}
}
private mapOrder(item: any): UnifiedOrderDTO {
const billing = item.billing_address || {};
const shipping = item.shipping_address || {};
private mapOrder(item: ShopyyOrder): UnifiedOrderDTO {
const billing = (item as any).billing_address || {};
const shipping = (item as any).shipping_address || {};
const billingObj = {
first_name: billing.first_name || item.firstname || '',
@ -69,7 +75,7 @@ export class ShopyyAdapter implements ISiteAdapter {
fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(),
company: billing.company || '',
email: item.customer_email || item.email || '',
phone: billing.phone || item.telephone || '',
phone: billing.phone || (item as any).telephone || '',
address_1: billing.address1 || item.payment_address || '',
address_2: billing.address2 || '',
city: billing.city || item.payment_city || '',
@ -83,7 +89,7 @@ export class ShopyyAdapter implements ISiteAdapter {
last_name: shipping.last_name || item.lastname || '',
fullname: shipping.name || '',
company: shipping.company || '',
address_1: shipping.address1 || item.shipping_address || '',
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 || '',
@ -110,7 +116,7 @@ export class ShopyyAdapter implements ISiteAdapter {
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),
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,
@ -119,7 +125,7 @@ export class ShopyyAdapter implements ISiteAdapter {
name: p.product_title || p.name,
product_id: p.product_id,
quantity: p.quantity,
total: String(p.price),
total: String(p.price ?? ''),
sku: p.sku || p.sku_code || ''
})),
sales: (item.products || []).map((p: any) => ({
@ -128,7 +134,7 @@ export class ShopyyAdapter implements ISiteAdapter {
product_id: p.product_id,
productId: p.product_id,
quantity: p.quantity,
total: String(p.price),
total: String(p.price ?? ''),
sku: p.sku || p.sku_code || ''
})),
billing: billingObj,
@ -136,12 +142,19 @@ export class ShopyyAdapter implements ISiteAdapter {
billing_full_address: formatAddress(billingObj),
shipping_full_address: formatAddress(shippingObj),
payment_method: item.payment_method,
date_created: item.created_at ? new Date(item.created_at * 1000).toISOString() : item.date_added,
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: any): UnifiedCustomerDTO {
private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO {
// 处理多地址结构
const addresses = item.addresses || [];
const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {});
@ -153,6 +166,8 @@ export class ShopyyAdapter implements ISiteAdapter {
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(),
@ -184,6 +199,14 @@ export class ShopyyAdapter implements ISiteAdapter {
postcode: shipping.zip || '',
country: shipping.country_name || shipping.country_code || item.country?.country_name || ''
},
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,
};
}
@ -203,6 +226,7 @@ export class ShopyyAdapter implements ISiteAdapter {
totalPages,
page,
per_page,
page_size: per_page,
};
}
@ -269,6 +293,7 @@ export class ShopyyAdapter implements ISiteAdapter {
totalPages,
page,
per_page,
page_size: per_page,
};
}
@ -310,7 +335,8 @@ export class ShopyyAdapter implements ISiteAdapter {
total,
totalPages,
page,
per_page
per_page,
page_size: per_page
};
}

View File

@ -9,17 +9,31 @@ import {
UnifiedSubscriptionDTO,
UnifiedCustomerDTO,
} from '../dto/site-api.dto';
import {
WooProduct,
WooOrder,
WooSubscription,
WpMedia,
WooCustomer,
} from '../dto/woocommerce.dto';
export class WooCommerceAdapter implements ISiteAdapter {
// 构造函数接收站点配置与服务实例
constructor(private site: any, private wpService: WPService) {}
private mapProduct(item: any): UnifiedProductDTO {
private mapProduct(item: WooProduct): UnifiedProductDTO {
// 将 WooCommerce 产品数据映射为统一产品DTO
// 保留常用字段与时间信息以便前端统一展示
// https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties
return {
id: item.id,
name: item.name,
type: item.type,
status: item.status,
date_created: item.date_created,
date_modified: item.date_modified,
type: item.type, // simple grouped external variable
status: item.status, // draft pending private publish
sku: item.sku,
name: item.name,
//价格
regular_price: item.regular_price,
sale_price: item.sale_price,
price: item.price,
@ -33,13 +47,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
})),
attributes: item.attributes,
variations: item.variations,
date_created: item.date_created,
date_modified: item.date_modified,
raw: item,
};
}
private mapOrder(item: any): UnifiedOrderDTO {
private mapOrder(item: WooOrder): UnifiedOrderDTO {
// 地址格式化函数用于生成完整地址字符串
const formatAddress = (addr: any) => {
if (!addr) return '';
const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim();
@ -56,6 +70,8 @@ export class WooCommerceAdapter implements ISiteAdapter {
].filter(Boolean).join(', ');
};
// 将 WooCommerce 订单数据映射为统一订单DTO
// 包含账单地址与收货地址以及创建与更新时间
return {
id: item.id,
number: item.number,
@ -79,17 +95,22 @@ export class WooCommerceAdapter implements ISiteAdapter {
shipping_full_address: formatAddress(item.shipping),
payment_method: item.payment_method_title,
date_created: item.date_created,
date_modified: item.date_modified,
raw: item,
};
}
private mapSubscription(item: any): UnifiedSubscriptionDTO {
private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO {
// 将 WooCommerce 订阅数据映射为统一订阅DTO
// 若缺少创建时间则回退为开始时间
return {
id: item.id,
status: item.status,
customer_id: item.customer_id,
billing_period: item.billing_period,
billing_interval: item.billing_interval,
date_created: item.date_created ?? item.start_date,
date_modified: item.date_modified,
start_date: item.start_date,
next_payment_date: item.next_payment_date,
line_items: item.line_items,
@ -97,20 +118,27 @@ export class WooCommerceAdapter implements ISiteAdapter {
};
}
private mapMedia(item: any): UnifiedMediaDTO {
private mapMedia(item: WpMedia): UnifiedMediaDTO {
// 将 WordPress 媒体数据映射为统一媒体DTO
// 兼容不同字段命名的时间信息
return {
id: item.id,
title: item.title?.rendered || '',
title:
typeof item.title === 'string'
? item.title
: item.title?.rendered || '',
media_type: item.media_type,
mime_type: item.mime_type,
source_url: item.source_url,
date_created: item.date_created,
date_created: item.date_created ?? item.date,
date_modified: item.date_modified ?? item.modified,
};
}
async getProducts(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedProductDTO>> {
// 获取产品列表并使用统一分页结构返回
const { items, total, totalPages, page, per_page } =
await this.wpService.fetchResourcePaged<any>(
this.site,
@ -123,43 +151,51 @@ export class WooCommerceAdapter implements ISiteAdapter {
totalPages,
page,
per_page,
page_size: per_page,
};
}
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
// 获取单个产品详情并映射为统一产品DTO
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
const res = await api.get(`products/${id}`);
return this.mapProduct(res.data);
}
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
// 创建产品并返回统一产品DTO
const res = await this.wpService.createProduct(this.site, data);
return this.mapProduct(res);
}
async updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
// 更新产品并返回统一产品DTO
const res = await this.wpService.updateProduct(this.site, String(id), data as any);
return this.mapProduct(res);
}
async updateVariation(productId: string | number, variationId: string | number, data: any): Promise<any> {
// 更新变体信息并返回结果
const res = await this.wpService.updateVariation(this.site, String(productId), String(variationId), data);
return res;
}
async getOrderNotes(orderId: string | number): Promise<any[]> {
// 获取订单备注列表
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
const res = await api.get(`orders/${orderId}/notes`);
return res.data;
}
async createOrderNote(orderId: string | number, data: any): Promise<any> {
// 创建订单备注
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
const res = await api.post(`orders/${orderId}/notes`, data);
return res.data;
}
async deleteProduct(id: string | number): Promise<boolean> {
// 删除产品
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
try {
await api.delete(`products/${id}`, { force: true });
@ -172,12 +208,14 @@ export class WooCommerceAdapter implements ISiteAdapter {
async batchProcessProducts(
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
): Promise<any> {
// 批量处理产品增删改
return await this.wpService.batchProcessProducts(this.site, data);
}
async getOrders(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
// 获取订单列表并映射为统一订单DTO集合
const { items, total, totalPages, page, per_page } =
await this.wpService.fetchResourcePaged<any>(this.site, 'orders', params);
return {
@ -186,26 +224,31 @@ export class WooCommerceAdapter implements ISiteAdapter {
totalPages,
page,
per_page,
page_size: per_page,
};
}
async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
// 获取单个订单详情
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
const res = await api.get(`orders/${id}`);
return this.mapOrder(res.data);
}
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
// 创建订单并返回统一订单DTO
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
const res = await api.post('orders', data);
return this.mapOrder(res.data);
}
async updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
// 更新订单并返回布尔结果
return await this.wpService.updateOrder(this.site, String(id), data as any);
}
async deleteOrder(id: string | number): Promise<boolean> {
// 删除订单
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
await api.delete(`orders/${id}`, { force: true });
return true;
@ -214,6 +257,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
async getSubscriptions(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
// 获取订阅列表并映射为统一订阅DTO集合
const { items, total, totalPages, page, per_page } =
await this.wpService.fetchResourcePaged<any>(
this.site,
@ -226,45 +270,56 @@ export class WooCommerceAdapter implements ISiteAdapter {
totalPages,
page,
per_page,
page_size: per_page,
};
}
async getMedia(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
const { items, total, totalPages } = await this.wpService.getMedia(
this.site.id,
params.page || 1,
params.per_page || 20
// 获取媒体列表并映射为统一媒体DTO集合
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
this.site,
params
);
return {
items: items.map(this.mapMedia),
total,
totalPages,
page: params.page || 1,
per_page: params.per_page || 20,
page,
per_page,
page_size: per_page,
};
}
async deleteMedia(id: string | number): Promise<boolean> {
// 删除媒体资源
await this.wpService.deleteMedia(Number(this.site.id), Number(id), true);
return true;
}
async updateMedia(id: string | number, data: any): Promise<any> {
// 更新媒体信息
return await this.wpService.updateMedia(Number(this.site.id), Number(id), data);
}
private mapCustomer(item: any): UnifiedCustomerDTO {
private mapCustomer(item: WooCustomer): UnifiedCustomerDTO {
// 将 WooCommerce 客户数据映射为统一客户DTO
// 包含基础信息地址信息与时间信息
return {
id: item.id,
avatar: item.avatar_url,
email: item.email,
orders: Number(item.orders?? 0),
total_spend: Number(item.total_spent ?? 0),
first_name: item.first_name,
last_name: item.last_name,
username: item.username,
phone: item.billing?.phone || item.shipping?.phone,
billing: item.billing,
shipping: item.shipping,
date_created: item.date_created,
date_modified: item.date_modified,
raw: item,
};
}
@ -281,6 +336,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
totalPages,
page,
per_page,
page_size: per_page,
};
}

View File

@ -49,10 +49,42 @@ export class SiteApiController {
) {
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getProducts(query);
const header = ['id','name','type','status','sku','regular_price','sale_price','price','stock_status','stock_quantity'];
const rows = data.items.map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.price,p.stock_status,p.stock_quantity]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
const perPage = (query.page_size ?? query.per_page) || 100;
let page = 1;
const all: any[] = [];
while (true) {
const data = await adapter.getProducts({ ...query, page, per_page: perPage, page_size: perPage });
const items = data.items || [];
all.push(...items);
const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage));
if (!items.length || page >= totalPages) break;
page += 1;
}
let items = all;
if (query.ids) {
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
items = items.filter(i => ids.has(String(i.id)));
}
const header = ['id','name','type','status','sku','regular_price','sale_price','price','stock_status','stock_quantity','image_src'];
const rows = items.map((p: any) => [
p.id,
p.name,
p.type,
p.status,
p.sku,
p.regular_price,
p.sale_price,
p.price,
p.stock_status,
p.stock_quantity,
((p.images && p.images[0]?.src) || ''),
]);
const toCsvValue = (val: any) => {
const s = String(val ?? '');
const escaped = s.replace(/"/g, '""');
return `"${escaped}"`;
};
const csv = [header.map(toCsvValue).join(','), ...rows.map(r => r.map(toCsvValue).join(','))].join('\n');
return successResponse({ csv });
} catch (error) {
return errorResponse(error.message);
@ -69,15 +101,20 @@ export class SiteApiController {
const site = await this.siteApiService.siteService.get(siteId, true);
if (site.type === 'woocommerce') {
const page = query.page || 1;
const per_page = query.per_page || 100;
const res = await this.siteApiService.wpService.getProducts(site, page, per_page);
const perPage = (query.page_size ?? query.per_page) || 100;
const res = await this.siteApiService.wpService.getProducts(site, page, perPage);
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
return successResponse({ csv });
const toCsvValue = (val: any) => {
const s = String(val ?? '');
const escaped = s.replace(/"/g, '""');
return `"${escaped}"`;
};
const csv = [header.map(toCsvValue).join(','), ...rows.map(r => r.map(toCsvValue).join(','))].join('\n');
return successResponse({ csv });
}
if (site.type === 'shopyy') {
const res = await this.siteApiService.shopyyService.getProducts(site, query.page || 1, query.per_page || 100);
const res = await this.siteApiService.shopyyService.getProducts(site, query.page || 1, (query.page_size ?? query.per_page) || 100);
const header = ['id','name','type','status','sku','price','stock_status','stock_quantity'];
const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.price,p.stock_status,p.stock_quantity]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
@ -329,7 +366,12 @@ export class SiteApiController {
this.logger.info(`[Site API] 获取订单列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getOrders(query);
const where = { ...(query.where || {}) };
if (query.customer_id) {
where.customer = query.customer_id;
where.customer_id = query.customer_id;
}
const data = await adapter.getOrders({ ...query, where });
this.logger.info(`[Site API] 获取订单列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个订单`);
return successResponse(data);
} catch (error) {
@ -338,6 +380,26 @@ export class SiteApiController {
}
}
@Get('/:siteId/customers/:customerId/orders')
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
async getCustomerOrders(
@Param('siteId') siteId: number,
@Param('customerId') customerId: number,
@Query() query: UnifiedSearchParamsDTO
) {
this.logger.info(`[Site API] 获取客户订单列表开始, siteId: ${siteId}, customerId: ${customerId}, query: ${JSON.stringify(query)}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const where = { ...(query.where || {}), customer: customerId, customer_id: customerId };
const data = await adapter.getOrders({ ...query, where, customer_id: customerId });
this.logger.info(`[Site API] 获取客户订单列表成功, siteId: ${siteId}, customerId: ${customerId}, 共获取到 ${data.total} 个订单`);
return successResponse(data);
} catch (error) {
this.logger.error(`[Site API] 获取客户订单列表失败, siteId: ${siteId}, customerId: ${customerId}, 错误信息: ${error.message}`);
return errorResponse(error.message);
}
}
@Get('/:siteId/orders/export')
async exportOrders(
@Param('siteId') siteId: number,
@ -345,10 +407,44 @@ export class SiteApiController {
) {
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getOrders(query);
const header = ['id','number','status','currency','total','customer_id','customer_name','email','date_created'];
const rows = data.items.map((o: any) => [o.id,o.number,o.status,o.currency,o.total,o.customer_id,o.customer_name,o.email,o.date_created]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
const perPage = (query.page_size ?? query.per_page) || 100;
let page = 1;
const all: any[] = [];
while (true) {
const data = await adapter.getOrders({ ...query, page, per_page: perPage, page_size: perPage });
const items = data.items || [];
all.push(...items);
const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage));
if (!items.length || page >= totalPages) break;
page += 1;
}
let items = all;
if (query.ids) {
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
items = items.filter(i => ids.has(String(i.id)));
}
const header = ['id','number','status','currency','total','customer_id','customer_name','email','payment_method','phone','billing_full_address','shipping_full_address','date_created'];
const rows = items.map((o: any) => [
o.id,
o.number,
o.status,
o.currency,
o.total,
o.customer_id,
o.customer_name,
o.email,
o.payment_method,
(o.shipping?.phone || o.billing?.phone || ''),
(o.billing_full_address || ''),
(o.shipping_full_address || ''),
o.date_created,
]);
const toCsvValue = (val: any) => {
const s = String(val ?? '');
const escaped = s.replace(/"/g, '""');
return `"${escaped}"`;
};
const csv = [header.map(toCsvValue).join(','), ...rows.map(r => r.map(toCsvValue).join(','))].join('\n');
return successResponse({ csv });
} catch (error) {
return errorResponse(error.message);
@ -579,9 +675,24 @@ export class SiteApiController {
) {
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getSubscriptions(query);
const perPage = (query.page_size ?? query.per_page) || 100;
let page = 1;
const all: any[] = [];
while (true) {
const data = await adapter.getSubscriptions({ ...query, page, per_page: perPage, page_size: perPage });
const items = data.items || [];
all.push(...items);
const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage));
if (!items.length || page >= totalPages) break;
page += 1;
}
let items = all;
if (query.ids) {
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
items = items.filter(i => ids.has(String(i.id)));
}
const header = ['id','status','customer_id','billing_period','billing_interval','start_date','next_payment_date'];
const rows = data.items.map((s: any) => [s.id,s.status,s.customer_id,s.billing_period,s.billing_interval,s.start_date,s.next_payment_date]);
const rows = items.map((s: any) => [s.id,s.status,s.customer_id,s.billing_period,s.billing_interval,s.start_date,s.next_payment_date]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
return successResponse({ csv });
} catch (error) {
@ -614,9 +725,24 @@ export class SiteApiController {
) {
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getMedia(query);
const perPage = (query.page_size ?? query.per_page) || 100;
let page = 1;
const all: any[] = [];
while (true) {
const data = await adapter.getMedia({ ...query, page, per_page: perPage, page_size: perPage });
const items = data.items || [];
all.push(...items);
const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage));
if (!items.length || page >= totalPages) break;
page += 1;
}
let items = all;
if (query.ids) {
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
items = items.filter(i => ids.has(String(i.id)));
}
const header = ['id','title','media_type','mime_type','source_url','date_created'];
const rows = data.items.map((m: any) => [m.id,m.title,m.media_type,m.mime_type,m.source_url,m.date_created]);
const rows = items.map((m: any) => [m.id,m.title,m.media_type,m.mime_type,m.source_url,m.date_created]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
return successResponse({ csv });
} catch (error) {
@ -738,9 +864,49 @@ export class SiteApiController {
) {
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getCustomers(query);
const header = ['id','email','first_name','last_name','fullname','username','phone'];
const rows = data.items.map((c: any) => [c.id,c.email,c.first_name,c.last_name,c.fullname,c.username,c.phone]);
const perPage = (query.page_size ?? query.per_page) || 100;
let page = 1;
const all: any[] = [];
while (true) {
const data = await adapter.getCustomers({ ...query, page, per_page: perPage, page_size: perPage });
const items = data.items || [];
all.push(...items);
const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage));
if (!items.length || page >= totalPages) break;
page += 1;
}
let items = all;
if (query.ids) {
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
items = items.filter(i => ids.has(String(i.id)));
}
const header = ['id','email','first_name','last_name','fullname','username','phone','orders','total_spend','role','billing_full_address','shipping_full_address','date_created'];
const formatAddress = (addr: any) => [
addr?.fullname,
addr?.company,
addr?.address_1,
addr?.address_2,
addr?.city,
addr?.state,
addr?.postcode,
addr?.country,
addr?.phone,
].filter(Boolean).join(', ');
const rows = items.map((c: any) => [
c.id,
c.email,
c.first_name,
c.last_name,
c.fullname,
(c.username || c.raw?.username || ''),
(c.phone || c.billing?.phone || c.shipping?.phone || ''),
c.orders,
c.total_spend,
(c.role || c.raw?.role || ''),
formatAddress(c.billing || {}),
formatAddress(c.shipping || {}),
c.date_created,
]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
return successResponse({ csv });
} catch (error) {

282
src/dto/shopyy.dto.ts Normal file
View File

@ -0,0 +1,282 @@
// Shopyy 平台原始数据类型定义
// 仅包含当前映射逻辑所需字段以保持简洁与类型安全
// 产品类型
export interface ShopyyProduct {
// 产品主键
id: number;
// 产品名称或标题
name?: string;
title?: string;
// 产品类型
product_type?: string | number;
// 产品状态数值 1为发布 其他为草稿
status: number;
// 变体信息
variant?: {
sku?: string;
price?: string;
};
// 价格
special_price?: string;
price?: string;
// 库存追踪标识 1表示跟踪
inventory_tracking?: number;
// 库存数量
inventory_quantity?: number;
// 图片列表
images?: Array<{
id?: number;
src: string;
alt?: string;
position?: string | number;
}>;
// 主图
image?: {
src: string;
file_name?: string;
alt?: string;
file_size?: number;
width?: number;
height?: number;
id?: number;
position?: number | string;
file_type?: string;
};
// 标签
tags?: string[];
// 变体列表
variants?: ShopyyVariant[];
// 分类集合
collections?: Array<{ id?: number; title?: string }>;
// 规格选项列表
options?: Array<{
id?: number;
position?: number | string;
option_name?: string;
values?: Array<{ option_value?: string; id?: number; position?: number }>;
}>;
// 发布与标识
published_at?: string;
handle?: string;
spu?: string;
// 创建与更新时间
created_at?: string | number;
updated_at?: string | number;
}
// 变体类型
export interface ShopyyVariant {
id: number;
sku?: string;
price?: string;
special_price?: string;
inventory_tracking?: number;
inventory_quantity?: number;
available?: number;
barcode?: string;
weight?: number;
image?: { src: string; id?: number; file_name?: string; alt?: string; position?: number | string };
position?: number | string;
sku_code?: string;
}
// 订单类型
export interface ShopyyOrder {
// 主键与外部ID
id?: number;
order_id?: number;
// 订单号
order_number?: string;
order_sn?: string;
// 状态
status?: number | string;
order_status?: number | string;
// 币种
currency_code?: string;
currency?: string;
// 金额
total_price?: string | number;
total_amount?: string | number;
current_total_price?: string | number;
current_subtotal_price?: string | number;
current_shipping_price?: string | number;
current_tax_price?: string | number;
current_coupon_price?: string | number;
current_payment_price?: string | number;
// 客户ID
customer_id?: number;
user_id?: number;
// 客户信息
customer_name?: string;
firstname?: string;
lastname?: string;
customer_email?: string;
email?: string;
// 地址字段
billing_address?: {
first_name?: string;
last_name?: string;
name?: string;
company?: string;
phone?: string;
address1?: string;
address2?: string;
city?: string;
province?: string;
zip?: string;
country_name?: string;
country_code?: string;
};
shipping_address?: {
first_name?: string;
last_name?: string;
name?: string;
company?: string;
phone?: string;
address1?: string;
address2?: string;
city?: string;
province?: string;
zip?: string;
country_name?: string;
country_code?: string;
} | string;
telephone?: string;
payment_address?: string;
payment_city?: string;
payment_zone?: string;
payment_postcode?: string;
payment_country?: string;
shipping_city?: string;
shipping_zone?: string;
shipping_postcode?: string;
shipping_country?: string;
// 订单项集合
products?: Array<{
id?: number;
name?: string;
product_title?: string;
product_id?: number;
quantity?: number;
price?: string | number;
sku?: string;
sku_code?: string;
}>;
// 支付方式
payment_method?: string;
payment_id?: number;
payment_cards?: Array<{
store_id?: number;
card_len?: number;
card_suffix?: number;
year?: number;
payment_status?: number;
created_at?: number;
month?: number;
updated_at?: number;
payment_id?: number;
payment_interface?: string;
card_prefix?: number;
id?: number;
order_id?: number;
card?: string;
transaction_no?: string;
}>;
fulfillments?: Array<{
payment_tracking_status?: number;
note?: string;
updated_at?: number;
courier_code?: string;
courier_id?: number;
created_at?: number;
tracking_number?: string;
id?: number;
tracking_company?: string;
payment_tracking_result?: string;
payment_tracking_at?: number;
products?: Array<{ order_product_id?: number; quantity?: number; updated_at?: number; created_at?: number; id?: number }>;
}>;
shipping_zone_plans?: Array<{
shipping_price?: number | string;
updated_at?: number;
created_at?: number;
id?: number;
shipping_zone_name?: string;
shipping_zone_id?: number;
shipping_zone_plan_id?: number;
shipping_zone_plan_name?: string;
}>;
transaction?: {
note?: string;
amount?: number | string;
created_at?: number;
merchant_id?: string;
payment_type?: string;
merchant_account?: string;
updated_at?: number;
payment_id?: number;
admin_id?: number;
admin_name?: string;
id?: number;
payment_method?: string;
transaction_no?: string;
};
coupon_code?: string;
coupon_name?: string;
store_id?: number;
visitor_id?: string;
currency_rate?: string | number;
landing_page?: string;
note?: string;
admin_note?: string;
source_device?: string;
checkout_type?: string;
version?: string;
brand_id?: number;
tags?: string[];
financial_status?: number;
fulfillment_status?: number;
// 创建与更新时间可能为时间戳
created_at?: number | string;
date_added?: string;
updated_at?: number | string;
date_updated?: string;
last_modified?: string;
}
// 客户类型
export interface ShopyyCustomer {
// 主键与兼容ID
id?: number;
customer_id?: number;
// 姓名
first_name?: string;
firstname?: string;
last_name?: string;
lastname?: string;
fullname?: string;
customer_name?: string;
// 联系信息
email?: string;
customer_email?: string;
contact?: string;
phone?: string;
// 地址集合
addresses?: any[];
default_address?: any;
// 国家
country?: { country_name?: string };
// 统计字段
orders_count?: number;
order_count?: number;
orders?: number;
total_spent?: number | string;
total_spend_amount?: number | string;
total_spend_money?: number | string;
// 创建与更新时间可能为时间戳
created_at?: number | string;
date_added?: string;
updated_at?: number | string;
date_updated?: string;
}

View File

@ -1,264 +0,0 @@
import { ApiProperty } from '@midwayjs/swagger';
export class UnifiedPaginationDTO<T> {
@ApiProperty({ description: '列表数据' })
items: T[];
@ApiProperty({ description: '总数', example: 100 })
total: number;
@ApiProperty({ description: '当前页', example: 1 })
page: number;
@ApiProperty({ description: '每页数量', example: 20 })
per_page: number;
@ApiProperty({ description: '总页数', example: 5 })
totalPages: number;
}
export class UnifiedImageDTO {
@ApiProperty({ description: '图片ID' })
id: number | string;
@ApiProperty({ description: '图片URL' })
src: string;
@ApiProperty({ description: '图片名称' })
name?: string;
@ApiProperty({ description: '替代文本' })
alt?: string;
}
export class UnifiedProductDTO {
@ApiProperty({ description: '产品ID' })
id: string | number;
@ApiProperty({ description: '产品名称' })
name: string;
@ApiProperty({ description: '产品类型' })
type: string;
@ApiProperty({ description: '产品状态' })
status: string;
@ApiProperty({ description: '产品SKU' })
sku: string;
@ApiProperty({ description: '常规价格' })
regular_price: string;
@ApiProperty({ description: '销售价格' })
sale_price: string;
@ApiProperty({ description: '当前价格' })
price: string;
@ApiProperty({ description: '库存状态' })
stock_status: string;
@ApiProperty({ description: '库存数量' })
stock_quantity: number;
@ApiProperty({ description: '产品图片', type: [UnifiedImageDTO] })
images: UnifiedImageDTO[];
@ApiProperty({ description: '产品标签', type: 'json' })
tags?: string[];
@ApiProperty({ description: '产品属性', type: 'json' })
attributes: any[];
@ApiProperty({ description: '产品变体', type: 'json' })
variations?: any[];
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '更新时间' })
date_modified: string;
@ApiProperty({ description: '原始数据(保留备用)', type: 'json' })
raw?: any;
}
export class UnifiedOrderDTO {
@ApiProperty({ description: '订单ID' })
id: string | number;
@ApiProperty({ description: '订单号' })
number: string;
@ApiProperty({ description: '订单状态' })
status: string;
@ApiProperty({ description: '货币' })
currency: string;
@ApiProperty({ description: '总金额' })
total: string;
@ApiProperty({ description: '客户ID' })
customer_id: number;
@ApiProperty({ description: '客户姓名' })
customer_name: string;
@ApiProperty({ description: '客户邮箱' })
email: string;
@ApiProperty({ description: '订单项', type: 'json' })
line_items: any[];
@ApiProperty({ description: '销售项(兼容前端)', type: 'json' })
sales?: any[];
@ApiProperty({ description: '账单地址', type: 'json' })
billing: any;
@ApiProperty({ description: '收货地址', type: 'json' })
shipping: any;
@ApiProperty({ description: '账单地址全称' })
billing_full_address?: string;
@ApiProperty({ description: '收货地址全称' })
shipping_full_address?: string;
@ApiProperty({ description: '支付方式' })
payment_method: string;
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '原始数据', type: 'json' })
raw?: any;
}
export class UnifiedCustomerDTO {
@ApiProperty({ description: '客户ID' })
id: string | number;
@ApiProperty({ description: '邮箱' })
email: string;
@ApiProperty({ description: '名' })
first_name?: string;
@ApiProperty({ description: '姓' })
last_name?: string;
@ApiProperty({ description: '名字' })
fullname?: string;
@ApiProperty({ description: '用户名' })
username?: string;
@ApiProperty({ description: '电话' })
phone?: string;
@ApiProperty({ description: '账单地址', type: 'json' })
billing?: any;
@ApiProperty({ description: '收货地址', type: 'json' })
shipping?: any;
@ApiProperty({ description: '原始数据', type: 'json' })
raw?: any;
}
export class UnifiedSubscriptionDTO {
@ApiProperty({ description: '订阅ID' })
id: string | number;
@ApiProperty({ description: '订阅状态' })
status: string;
@ApiProperty({ description: '客户ID' })
customer_id: number;
@ApiProperty({ description: '计费周期' })
billing_period: string;
@ApiProperty({ description: '计费间隔' })
billing_interval: number;
@ApiProperty({ description: '开始时间' })
start_date: string;
@ApiProperty({ description: '下次支付时间' })
next_payment_date: string;
@ApiProperty({ description: '订单项', type: 'json' })
line_items: any[];
@ApiProperty({ description: '原始数据', type: 'json' })
raw?: any;
}
export class UnifiedMediaDTO {
@ApiProperty({ description: '媒体ID' })
id: number;
@ApiProperty({ description: '标题' })
title: string;
@ApiProperty({ description: '媒体类型' })
media_type: string;
@ApiProperty({ description: 'MIME类型' })
mime_type: string;
@ApiProperty({ description: '源URL' })
source_url: string;
@ApiProperty({ description: '创建时间' })
date_created: string;
}
export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO<UnifiedProductDTO> {
@ApiProperty({ description: '列表数据', type: [UnifiedProductDTO] })
items: UnifiedProductDTO[];
}
export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO<UnifiedOrderDTO> {
@ApiProperty({ description: '列表数据', type: [UnifiedOrderDTO] })
items: UnifiedOrderDTO[];
}
export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO<UnifiedCustomerDTO> {
@ApiProperty({ description: '列表数据', type: [UnifiedCustomerDTO] })
items: UnifiedCustomerDTO[];
}
export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO<UnifiedSubscriptionDTO> {
@ApiProperty({ description: '列表数据', type: [UnifiedSubscriptionDTO] })
items: UnifiedSubscriptionDTO[];
}
export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO<UnifiedMediaDTO> {
@ApiProperty({ description: '列表数据', type: [UnifiedMediaDTO] })
items: UnifiedMediaDTO[];
}
export class UnifiedSearchParamsDTO {
@ApiProperty({ description: '页码', example: 1 })
page?: number;
@ApiProperty({ description: '每页数量', example: 20 })
per_page?: number;
@ApiProperty({ description: '搜索关键词' })
search?: string;
@ApiProperty({ description: '状态' })
status?: string;
@ApiProperty({ description: '排序字段' })
orderby?: string;
@ApiProperty({ description: '排序方式' })
order?: string;
}

View File

@ -1,6 +1,7 @@
import { ApiProperty } from '@midwayjs/swagger';
export class UnifiedPaginationDTO<T> {
// 分页DTO用于承载统一分页信息与列表数据
@ApiProperty({ description: '列表数据' })
items: T[];
@ -13,11 +14,15 @@ export class UnifiedPaginationDTO<T> {
@ApiProperty({ description: '每页数量', example: 20 })
per_page: number;
@ApiProperty({ description: '每页数量别名', example: 20 })
page_size?: number;
@ApiProperty({ description: '总页数', example: 5 })
totalPages: number;
}
export class UnifiedImageDTO {
// 图片DTO用于承载统一图片数据
@ApiProperty({ description: '图片ID' })
id: number | string;
@ -32,6 +37,7 @@ export class UnifiedImageDTO {
}
export class UnifiedProductDTO {
// 产品DTO用于承载统一产品数据
@ApiProperty({ description: '产品ID' })
id: string | number;
@ -85,6 +91,7 @@ export class UnifiedProductDTO {
}
export class UnifiedOrderDTO {
// 订单DTO用于承载统一订单数据
@ApiProperty({ description: '订单ID' })
id: string | number;
@ -133,17 +140,36 @@ export class UnifiedOrderDTO {
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '更新时间' })
date_modified?: string;
@ApiProperty({ description: '原始数据', type: 'json' })
raw?: any;
}
export class UnifiedCustomerDTO {
// 客户DTO用于承载统一客户数据
@ApiProperty({ description: '客户ID' })
id: string | number;
@ApiProperty({ description: '头像URL' })
avatar?: string;
@ApiProperty({ description: '邮箱' })
email: string;
@ApiProperty({ description: '订单总数' })
orders?: number;
@ApiProperty({ description: '总花费' })
total_spend?: number;
@ApiProperty({ description: '创建时间' })
date_created?: string;
@ApiProperty({ description: '更新时间' })
date_modified?: string;
@ApiProperty({ description: '名' })
first_name?: string;
@ -170,6 +196,7 @@ export class UnifiedCustomerDTO {
}
export class UnifiedSubscriptionDTO {
// 订阅DTO用于承载统一订阅数据
@ApiProperty({ description: '订阅ID' })
id: string | number;
@ -185,6 +212,12 @@ export class UnifiedSubscriptionDTO {
@ApiProperty({ description: '计费间隔' })
billing_interval: number;
@ApiProperty({ description: '创建时间' })
date_created?: string;
@ApiProperty({ description: '更新时间' })
date_modified?: string;
@ApiProperty({ description: '开始时间' })
start_date: string;
@ -199,6 +232,7 @@ export class UnifiedSubscriptionDTO {
}
export class UnifiedMediaDTO {
// 媒体DTO用于承载统一媒体数据
@ApiProperty({ description: '媒体ID' })
id: number;
@ -216,49 +250,73 @@ export class UnifiedMediaDTO {
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '更新时间' })
date_modified?: string;
}
export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO<UnifiedProductDTO> {
// 产品分页DTO用于承载产品列表分页数据
@ApiProperty({ description: '列表数据', type: [UnifiedProductDTO] })
items: UnifiedProductDTO[];
}
export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO<UnifiedOrderDTO> {
// 订单分页DTO用于承载订单列表分页数据
@ApiProperty({ description: '列表数据', type: [UnifiedOrderDTO] })
items: UnifiedOrderDTO[];
}
export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO<UnifiedCustomerDTO> {
// 客户分页DTO用于承载客户列表分页数据
@ApiProperty({ description: '列表数据', type: [UnifiedCustomerDTO] })
items: UnifiedCustomerDTO[];
}
export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO<UnifiedSubscriptionDTO> {
// 订阅分页DTO用于承载订阅列表分页数据
@ApiProperty({ description: '列表数据', type: [UnifiedSubscriptionDTO] })
items: UnifiedSubscriptionDTO[];
}
export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO<UnifiedMediaDTO> {
// 媒体分页DTO用于承载媒体列表分页数据
@ApiProperty({ description: '列表数据', type: [UnifiedMediaDTO] })
items: UnifiedMediaDTO[];
}
export class UnifiedSearchParamsDTO {
// 统一查询参数DTO用于承载分页与筛选与排序参数
@ApiProperty({ description: '页码', example: 1 })
page?: number;
@ApiProperty({ description: '每页数量', example: 20 })
per_page?: number;
@ApiProperty({ description: '每页数量别名', example: 20 })
page_size?: number;
@ApiProperty({ description: '搜索关键词' })
search?: string;
@ApiProperty({ description: '状态' })
status?: string;
@ApiProperty({ description: '排序字段' })
@ApiProperty({ description: '客户ID,用于筛选订单' })
customer_id?: number;
@ApiProperty({ description: '过滤条件对象' })
where?: Record<string, any>;
@ApiProperty({ description: '排序对象,例如 { "sku": "desc" }' })
order?: Record<string, 'asc' | 'desc'> | string;
@ApiProperty({ description: '排序字段(兼容旧入参)' })
orderby?: string;
@ApiProperty({ description: '排序方式' })
order?: string;
@ApiProperty({ description: '排序方式(兼容旧入参)' })
orderDir?: 'asc' | 'desc';
@ApiProperty({ description: '选中ID列表,逗号分隔', required: false })
ids?: string;
}

389
src/dto/woocommerce.dto.ts Normal file
View File

@ -0,0 +1,389 @@
// WooCommerce 平台原始数据类型定义
// 仅包含当前映射逻辑所需字段以保持简洁与类型安全
// 产品类型
export interface WooProduct {
// 产品主键
id: number;
// 创建时间
date_created: string;
// 创建时间GMT
date_created_gmt: string;
// 更新时间
date_modified: string;
// 更新时间GMT
date_modified_gmt: string;
// 产品类型 simple grouped external variable
type: string;
// 产品状态 draft pending private publish
status: string;
// 是否为特色产品
featured: boolean;
// 目录可见性选项visible, catalog, search and hidden. Default is visible.
catalog_visibility: string;
// 常规价格
regular_price?: string;
// 促销价格
sale_price?: string;
// 当前价格
price?: string;
price_html?: string;
date_on_sale_from?: string; // Date the product is on sale from.
date_on_sale_from_gmt?: string; // Date the product is on sale from (GMT).
date_on_sale_to?: string; // Date the product is on sale to.
date_on_sale_to_gmt?: string; // Date the product is on sale to (GMT).
on_sale: boolean; // Whether the product is on sale.
purchasable: boolean; // Whether the product is purchasable.
total_sales: number; // Total sales for this product.
virtual: boolean; // Whether the product is virtual.
downloadable: boolean; // Whether the product is downloadable.
downloads: Array<{ id?: number; name?: string; file?: string }>; // Downloadable files for the product.
download_limit: number; // Download limit.
download_expiry: number; // Download expiry days.
external_url: string; // URL of the external product.
global_unique_id: string; // GTIN, UPC, EAN or ISBN - a unique identifier for each distinct product and service that can be purchased.
// 产品SKU
sku: string;
// 产品名称
name: string;
// 产品描述
description: string;
// 产品短描述
short_description: string;
// 产品永久链接
permalink: string;
// 产品URL路径
slug: string;
// 库存状态
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
// 库存数量
stock_quantity?: number;
// 是否管理库存
manage_stock?: boolean;
// 缺货预定设置 no notify yes
backorders?: 'no' | 'notify' | 'yes';
// 是否允许缺货预定 只读
backorders_allowed?: boolean;
// 是否处于缺货预定状态 只读
backordered?: boolean;
// 是否单独出售
sold_individually?: boolean;
// 重量
weight?: string;
// 尺寸
dimensions?: { length?: string; width?: string; height?: string };
// 是否需要运输 只读
shipping_required?: boolean;
// 运输是否计税 只读
shipping_taxable?: boolean;
// 运输类别 slug
shipping_class?: string;
// 运输类别ID 只读
shipping_class_id?: number;
// 图片列表
images?: Array<{ id: number; src: string; name?: string; alt?: string }>;
// 属性列表
attributes?: Array<{
id?: number;
name?: string;
position?: number;
visible?: boolean;
variation?: boolean;
options?: string[];
}>;
// 变体列表
variations?: number[];
// 默认变体属性
default_attributes?: Array<{ id?: number; name?: string; option?: string }>;
// 允许评论
reviews_allowed?: boolean;
// 平均评分 只读
average_rating?: string;
// 评分数量 只读
rating_count?: number;
// 相关产品ID列表 只读
related_ids?: number[];
// 追加销售产品ID列表
upsell_ids?: number[];
// 交叉销售产品ID列表
cross_sell_ids?: number[];
// 父产品ID
parent_id?: number;
// 购买备注
purchase_note?: string;
// 分类列表
categories?: Array<{ id: number; name?: string; slug?: string }>;
// 标签列表
tags?: Array<{ id: number; name?: string; slug?: string }>;
// 菜单排序
menu_order?: number;
// 元数据
meta_data?: Array<{ id?: number; key: string; value: any }>;
}
// 订单类型
export interface WooOrder {
// 订单主键
id: number;
// 父订单ID
parent_id?: number;
// 订单号
number: string;
// 订单键 只读
order_key?: string;
// 创建来源
created_via?: string;
// WooCommerce版本 只读
version?: string;
// 状态
status: string;
// 币种
currency: string;
// 价格是否含税 只读
prices_include_tax?: boolean;
// 总金额
total: string;
// 总税额 只读
total_tax?: string;
// 折扣总额 只读
discount_total?: string;
// 折扣税额 只读
discount_tax?: string;
// 运费总额 只读
shipping_total?: string;
// 运费税额 只读
shipping_tax?: string;
// 购物车税额 只读
cart_tax?: string;
// 客户ID
customer_id: number;
// 客户IP 只读
customer_ip_address?: string;
// 客户UA 只读
customer_user_agent?: string;
// 客户备注
customer_note?: string;
// 账单信息
billing?: {
first_name?: string;
last_name?: string;
email?: string;
company?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
phone?: string;
fullname?: string;
};
// 收货信息
shipping?: {
first_name?: string;
last_name?: string;
company?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
phone?: string;
fullname?: string;
};
// 订单项
line_items?: Array<{
product_id?: number;
variation_id?: number;
quantity?: number;
subtotal?: string;
subtotal_tax?: string;
total?: string;
total_tax?: string;
name?: string;
sku?: string;
price?: number;
meta_data?: Array<{ key: string; value: any }>;
[key: string]: any;
}>;
// 税费行 只读
tax_lines?: Array<{
id?: number;
rate_code?: string;
rate_id?: number;
label?: string;
tax_total?: string;
shipping_tax_total?: string;
compound?: boolean;
meta_data?: any[];
}>;
// 物流费用行
shipping_lines?: Array<{
id?: number;
method_title?: string;
method_id?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}>;
// 手续费行
fee_lines?: Array<{
id?: number;
name?: string;
tax_class?: string;
tax_status?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}>;
// 优惠券行
coupon_lines?: Array<{
id?: number;
code?: string;
discount?: string;
discount_tax?: string;
meta_data?: any[];
}>;
// 退款列表 只读
refunds?: Array<{
id?: number;
reason?: string;
total?: string;
[key: string]: any;
}>;
// 支付方式标题
payment_method_title?: string;
// 支付方式ID
payment_method?: string;
// 交易ID
transaction_id?: string;
// 已支付时间
date_paid?: string;
date_paid_gmt?: string;
// 完成时间
date_completed?: string;
date_completed_gmt?: string;
// 购物车hash 只读
cart_hash?: string;
// 设置为已支付 写入专用
set_paid?: boolean;
// 元数据
meta_data?: Array<{ id?: number; key: string; value: any }>;
// 创建与更新时间
date_created: string;
date_created_gmt?: string;
date_modified?: string;
date_modified_gmt?: string;
}
// 订阅类型
export interface WooSubscription {
// 订阅主键
id: number;
// 订阅状态
status: string;
// 客户ID
customer_id: number;
// 计费周期
billing_period?: string;
// 计费间隔
billing_interval?: number;
// 开始时间
start_date?: string;
// 下次支付时间
next_payment_date?: string;
// 订阅项
line_items?: any[];
// 创建时间
date_created?: string;
// 更新时间
date_modified?: string;
}
// WordPress 媒体类型
export interface WpMedia {
// 媒体主键
id: number;
// 标题可能为字符串或包含rendered的对象
title?: { rendered?: string } | string;
// 媒体类型
media_type?: string;
// MIME类型
mime_type?: string;
// 源地址
source_url?: string;
// 创建时间兼容date字段
date_created?: string;
date?: string;
// 更新时间兼容modified字段
date_modified?: string;
modified?: string;
}
// 客户类型
export interface WooCustomer {
// 客户主键
id: number;
// 头像URL
avatar_url?: string;
// 邮箱
email: string;
// 订单总数
orders?: number;
// 总花费
total_spent?: number | string;
// 名
first_name?: string;
// 姓
last_name?: string;
// 用户名
username?: string;
// 角色 只读
role?: string;
// 密码 写入专用
password?: string;
// 账单信息
billing?: {
first_name?: string;
last_name?: string;
email?: string;
company?: string;
phone?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
};
// 收货信息
shipping?: {
first_name?: string;
last_name?: string;
company?: string;
phone?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
};
// 是否为付费客户 只读
is_paying_customer?: boolean;
// 元数据
meta_data?: Array<{ id?: number; key: string; value: any }>;
// 创建时间
date_created?: string;
date_created_gmt?: string;
// 更新时间
date_modified?: string;
date_modified_gmt?: string;
}

View File

@ -74,11 +74,28 @@ export class ShopyyService implements IPlatformService {
*
*/
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
// 映射 params 字段: page -> page, per_page -> limit
const page = Number(params.page || 1);
const limit = Number(params.page_size ?? params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {};
let orderby: string | undefined = params.orderby;
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
}
}
// 映射统一入参到平台入参
const requestParams = {
...params,
page: params.page || 1,
limit: params.per_page || 20
...where,
...(params.search ? { search: params.search } : {}),
...(params.status ? { status: params.status } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
limit
};
const response = await this.request(site, endpoint, 'GET', null, requestParams);
if (response?.code !== 0) {
@ -89,7 +106,8 @@ export class ShopyyService implements IPlatformService {
total: response.data?.paginate?.total || 0,
totalPages: response.data?.paginate?.pageTotal || 0,
page: response.data?.paginate?.current || requestParams.page,
per_page: response.data?.paginate?.pagesize || requestParams.limit
per_page: response.data?.paginate?.pagesize || requestParams.limit,
page_size: response.data?.paginate?.pagesize || requestParams.limit
};
}

View File

@ -51,7 +51,29 @@ export class WPService implements IPlatformService {
*/
public async fetchResourcePaged<T>(site: any, resource: string, params: Record<string, any> = {}) {
const api = this.createApi(site, 'wc/v3');
return this.sdkGetPage<T>(api, resource, params);
const page = Number(params.page ?? 1);
const per_page = Number(params.page_size ?? params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {};
let orderby: string | undefined = params.orderby;
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
}
}
const requestParams = {
...where,
...(params.search ? { search: params.search } : {}),
...(params.status ? { status: params.status } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
per_page
};
return this.sdkGetPage<T>(api, resource, requestParams);
}
/**
@ -59,7 +81,7 @@ export class WPService implements IPlatformService {
*/
private async sdkGetPage<T>(api: any, resource: string, params: Record<string, any> = {}) {
const page = params.page ?? 1;
const per_page = params.per_page ?? 100;
const per_page = params.per_page ?? params.page_size ?? 100;
const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page });
if (res?.headers?.['content-type']?.includes('text/html')) {
throw new Error('接口返回了 text/html,可能为 WordPress 登录页或错误页,请检查站点配置或权限');
@ -67,7 +89,7 @@ export class WPService implements IPlatformService {
const data = res.data as T[];
const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1);
const total = Number(res.headers?.['x-wp-total']?? 1)
return { items: data, total, totalPages, page, per_page };
return { items: data, total, totalPages, page, per_page, page_size: per_page };
}
/**
@ -630,6 +652,40 @@ export class WPService implements IPlatformService {
};
}
public async fetchMediaPaged(site: any, params: Record<string, any> = {}) {
const page = Number(params.page ?? 1);
const per_page = Number(params.page_size ?? params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {};
let orderby: string | undefined = params.orderby;
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
}
}
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
const endpoint = 'wp/v2/media';
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
const response = await axios.get(url, {
headers: { Authorization: `Basic ${auth}` },
params: {
...where,
...(params.search ? { search: params.search } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
per_page
}
});
const total = Number(response.headers['x-wp-total'] || 0);
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
return { items: response.data, total, totalPages, page, per_page, page_size: per_page };
}
/**
*
* @param siteId ID