API/src/service/wp.service.ts

1324 lines
44 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.

/**
*
* https://developer.wordpress.org/rest-api/reference/media/
*/
import { Inject, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios';
import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api';
import { SiteService } from './site.service';
import { IPlatformService } from '../interface/platform.interface';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import * as FormData from 'form-data';
import * as fs from 'fs';
import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
const MAX_PAGE_SIZE = 100;
@Provide()
export class WPService implements IPlatformService {
@Inject()
private readonly siteService: SiteService;
getCustomer(site: any, id: number): Promise<any> {
throw new Error('Method not implemented.');
}
/**
* 构建 URL,自动规范各段的斜杠,避免出现多 / 或少 / 导致请求失败
* 使用示例:this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId)
*/
private buildURL(base: string, ...parts: Array<string | number>): string {
// 去掉 base 末尾多余斜杠,但不影响协议中的 //
const baseSanitized = String(base).replace(/\/+$/g, '');
// 规范各段前后斜杠
const segments = parts
.filter((p) => p !== undefined && p !== null)
.map((p) => String(p))
.map((s) => s.replace(/^\/+|\/+$/g, ''))
.filter(Boolean);
const joined = [baseSanitized, ...segments].join('/');
// 折叠除协议外的多余斜杠,例如 https://example.com//a///b -> https://example.com/a/b
return joined.replace(/([^:])\/{2,}/g, '$1/');
}
/**
* 创建 WooCommerce SDK 实例
* @param site 站点配置
* @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1
*/
private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
return new WooCommerceRestApi({
url: site.apiUrl,
consumerKey: site.consumerKey,
consumerSecret: site.consumerSecret,
version: namespace,
});
}
/**
* 通用分页获取资源
*/
public async fetchResourcePaged<T>(site: any, resource: string, params: Record<string, any> = {}, namespace: WooCommerceRestApiVersion = 'wc/v3') {
const api = this.createApi(site, namespace);
return this.sdkGetPage<T>(api, resource, params);
}
/**
* 通过 SDK 获取单页数据,并返回数据与 totalPages
*/
private async sdkGetPage<T>(api: any, resource: string, params: Record<string, any> = {}) {
const page = params.page ?? 1;
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 登录页或错误页,请检查站点配置或权限');
}
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, page_size: per_page };
}
/**
* 通过 SDK 聚合分页数据,返回全部数据
* 使用并发方式获取所有分页数据,提高性能
* 默认按 date_created 倒序排列,确保获取最新的数据
*/
async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = MAX_PAGE_SIZE): Promise<T[]> {
return this.sdkGetAllConcurrent<T>(api, resource, params, maxPages);
}
/**
* 通过 SDK 聚合分页数据,使用并发方式获取所有分页数据
* 支持自定义并发数和最大页数限制
* 默认按 date_created 倒序排列,确保获取最新的数据
*/
private async sdkGetAllConcurrent<T>(
api: WooCommerceRestApi,
resource: string,
params: Record<string, any> = {},
maxPages: number = MAX_PAGE_SIZE,
concurrencyLimit: number = 5
): Promise<T[]> {
// 设置默认排序为 date_created 倒序,确保获取最新数据
const defaultParams = {
order: 'desc', // 倒序,优先获取最新数据
per_page: MAX_PAGE_SIZE,
...params
};
// 首先获取第一页数据,同时获取总页数信息
const firstPage = await this.sdkGetPage<T>(api, resource, { ...defaultParams, page: 1 });
const { items: firstPageItems, totalPages } = firstPage;
// 如果只有一页数据,直接返回
if (totalPages <= 1) {
return firstPageItems;
}
// 限制最大页数,避免过多的并发请求
const actualMaxPages = Math.min(totalPages, maxPages);
// 收集所有页面数据,从第二页开始
const allItems = [...firstPageItems];
let currentPage = 2;
// 使用并发限制,避免一次性发起过多请求
while (currentPage <= actualMaxPages) {
const batchPromises: Promise<T[]>[] = [];
const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1);
// 创建当前批次的并发请求
for (let i = 0; i < batchSize; i++) {
const page = currentPage + i;
const pagePromise = this.sdkGetPage<T>(api, resource, { ...defaultParams, page })
.then(pageResult => pageResult.items)
.catch(error => {
console.error(`获取第 ${page} 页数据失败:`, error);
return []; // 如果某页获取失败,返回空数组,不影响整体结果
});
batchPromises.push(pagePromise);
}
// 等待当前批次完成
const batchResults = await Promise.all(batchPromises);
// 合并当前批次的数据
for (const pageItems of batchResults) {
allItems.push(...pageItems);
}
// 移动到下一批次
currentPage += batchSize;
}
return allItems;
}
/**
* 获取 WordPress 数据
* @param wpApiUrl WordPress REST API 的基础地址
* @param endpoint API 端点路径(例如 wc/v3/products)
* @param consumerKey WooCommerce 的消费者密钥
* @param consumerSecret WooCommerce 的消费者密钥
*/
async fetchData<T>(
endpoint: string,
site: any,
param: Record<string, any> = {}
): Promise<T> {
try {
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site;
// 构建 URL,规避多/或少/问题
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
const response = await axios.request({
url,
headers: {
Authorization: `Basic ${auth}`,
},
method: 'GET',
...param,
});
return response.data;
} catch (error) {
throw error;
}
}
async fetchPagedData<T>(
endpoint: string,
site: any,
page: number = 1,
perPage: number = 100
): Promise<T[]> {
const allData: T[] = [];
const { apiUrl, consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
console.log(`!!!wpApiUrl, consumerKey, consumerSecret, auth`,site.apiUrl, consumerKey, consumerSecret, auth)
let hasMore = true;
while (hasMore) {
const config: AxiosRequestConfig = {
method: 'GET',
// 构建 URL,规避多/或少/问题
url: this.buildURL(apiUrl, '/wp-json', endpoint),
headers: {
Authorization: `Basic ${auth}`,
},
params: {
page,
per_page: perPage,
},
};
try {
const response = await axios.request(config);
// Append the current page data
allData.push(...response.data);
// Check for more pages
const totalPages = parseInt(
response.headers['x-wp-totalpages'] || '1',
10
);
hasMore = page < totalPages;
page += 1;
} catch (error) {
throw error;
}
}
return allData;
}
async getProducts(site: any, params: Record<string, any> = {}): Promise<any> {
const api = this.createApi(site, 'wc/v3');
const page = params.page ?? 1;
const per_page = params.per_page ?? params.pageSize ?? 100;
return await this.sdkGetPage<WooProduct>(api, 'products', { ...params, page, per_page });
}
async getProduct(site: any, id: number): Promise<any> {
const api = this.createApi(site, 'wc/v3');
const res = await api.get(`products/${id}`);
return res.data;
}
// 导出 WooCommerce 产品为特殊CSV平台特性
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
const list = await this.getProducts(site, { page, per_page: pageSize });
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
const rows = (list.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 csv;
}
async getVariations(site: any, productId: number, page: number = 1, pageSize: number = 100): Promise<any> {
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetPage<WooVariation>(api, `products/${productId}/variations`, { page, per_page: pageSize });
}
async getVariation(
site: any,
productId: number,
variationId: number
): Promise<WooVariation> {
const api = this.createApi(site, 'wc/v3');
const res = await api.get(`products/${productId}/variations/${variationId}`);
return res.data as WooVariation;
}
async getOrder(
siteId: number,
orderId: string
): Promise<Record<string, any>> {
const site = await this.siteService.get(siteId);
const api = this.createApi(site, 'wc/v3');
const res = await api.get(`orders/${orderId}`);
return res.data as Record<string, any>;
}
async getOrders(siteId: number,params: Record<string, any> = {}): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId);
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetAll<Record<string, any>>(api, 'orders', params);
}
/**
* 获取 WooCommerce Subscriptions
* 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions.
*/
async getSubscriptions(site: any | number, page: number = 1, pageSize: number = 100): Promise<any> {
// 如果传入的是站点ID则获取站点配置
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
// 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
const api = this.createApi(siteConfig, 'wc/v3');
return await this.sdkGetPage<Record<string, any>>(api, 'subscriptions', { page, per_page: pageSize });
}
async getOrderRefund(
siteId: string,
orderId: string,
refundId: number
): Promise<Record<string, any>> {
const site = await this.siteService.get(siteId);
const api = this.createApi(site, 'wc/v3');
const res = await api.get(`orders/${orderId}/refunds/${refundId}`);
return res.data as Record<string, any>;
}
async getOrderRefunds(
site: any | string,
orderId: number,
page: number = 1,
pageSize: number = 100
): Promise<any> {
// 如果传入的是站点ID则获取站点配置
const siteConfig = typeof site === 'string' ? await this.siteService.get(site) : site;
const api = this.createApi(siteConfig, 'wc/v3');
return await this.sdkGetPage<Record<string, any>>(api, `orders/${orderId}/refunds`, { page, per_page: pageSize });
}
async getOrderNote(
siteId: string,
orderId: number,
noteId: number
): Promise<Record<string, any>> {
const site = await this.siteService.get(siteId);
const api = this.createApi(site, 'wc/v3');
const res = await api.get(`orders/${orderId}/notes/${noteId}`);
return res.data as Record<string, any>;
}
async getOrderNotes(
site: any | string,
orderId: number,
page: number = 1,
pageSize: number = 100
): Promise<any> {
// 如果传入的是站点ID则获取站点配置
const siteConfig = typeof site === 'string' ? await this.siteService.get(site) : site;
const api = this.createApi(siteConfig, 'wc/v3');
return await this.sdkGetPage<Record<string, any>>(api, `orders/${orderId}/notes`, { page, per_page: pageSize });
}
/**
* 创建 WooCommerce 产品
* @param site 站点配置
* @param data 产品数据
*/
async createProduct(
site: any,
data: any
): Promise<any> {
const api = this.createApi(site, 'wc/v3');
// 确保价格为字符串
if (data.regular_price !== undefined && data.regular_price !== null) {
data.regular_price = String(data.regular_price);
}
if (data.sale_price !== undefined && data.sale_price !== null) {
data.sale_price = String(data.sale_price);
}
// 处理标签字段,如果为字符串数组则转换为 WooCommerce 所需的对象数组
if (Array.isArray((data as any).tags)) {
const tags = (data as any).tags;
(data as any).tags = tags.map((name: any) => ({ name }));
}
try {
const response = await api.post('products', data);
return response.data;
} catch (error) {
console.error('创建产品失败:', error.response?.data || error.message);
throw error;
}
}
/**
* 更新 WooCommerce 产品
* @param productId 产品 ID
* @param data 更新的数据
*/
async updateProduct(
site: any,
productId: string,
data: WooProduct
): Promise<any> {
const { regular_price, sale_price, ...params } = data;
const api = this.createApi(site, 'wc/v3');
const updateData: any = { ...params };
if (regular_price !== undefined && regular_price !== null) {
updateData.regular_price = String(regular_price);
}
if (sale_price !== undefined && sale_price !== null) {
updateData.sale_price = String(sale_price);
}
// 处理标签字段,如果为字符串数组则转换为 WooCommerce 所需的对象数组
if (Array.isArray(updateData.tags)) {
updateData.tags = updateData.tags.map((name: any) => ({ name }));
}
try {
const response = await api.put(`products/${productId}`, updateData);
return response.data;
} catch (error) {
console.error('更新产品失败:', error.response?.data || error.message);
throw new Error(`更新产品失败: ${error.response?.data?.message || error.message}`);
}
}
/**
* 更新 WooCommerce 产品 上下架状态
* @param productId 产品 ID
* @param status 状态
* @param stockStatus 库存状态
*/
async updateProductStatus(
site: any,
productId: string,
status: string,
stockStatus: string
): Promise<boolean> {
const api = this.createApi(site, 'wc/v3');
try {
await api.put(`products/${productId}`, {
status,
manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
stock_status: stockStatus,
});
return true;
} catch (error) {
console.error('更新产品上下架状态失败:', error.response?.data || error.message);
throw new Error(`更新产品上下架状态失败: ${error.response?.data?.message || error.message}`);
}
}
/**
* 更新 WooCommerce 产品库存
* @param site 站点配置
* @param productId 产品 ID
* @param quantity 库存数量
* @param stockStatus 库存状态
*/
async updateProductStock(
site: any,
productId: string,
quantity: number,
stockStatus: string
): Promise<boolean> {
const api = this.createApi(site, 'wc/v3');
try {
await api.put(`products/${productId}`, {
manage_stock: true,
stock_quantity: quantity,
stock_status: stockStatus,
});
return true;
} catch (error) {
const errorMessage = error.response?.data?.message || error.message || '更新产品库存失败';
throw new Error(`更新产品库存失败: ${errorMessage}`);
}
}
/**
* 更新 WooCommerce 产品变体库存
* @param site 站点配置
* @param productId 产品 ID
* @param variationId 变体 ID
* @param quantity 库存数量
* @param stockStatus 库存状态
*/
async updateProductVariationStock(
site: any,
productId: string,
variationId: string,
quantity: number,
stockStatus: string
): Promise<boolean> {
const api = this.createApi(site, 'wc/v3');
try {
await api.put(`products/${productId}/variations/${variationId}`, {
manage_stock: true,
stock_quantity: quantity,
stock_status: stockStatus,
});
return true;
} catch (error) {
const errorMessage = error.response?.data?.message || error.message || '更新产品变体库存失败';
throw new Error(`更新产品变体库存失败: ${errorMessage}`);
}
}
/**
* 更新 WooCommerce 产品变体
* @param productId 产品 ID
* @param variationId 变体 ID
* @param data 更新的数据
*/
async updateVariation(
site: any,
productId: string,
variationId: string,
data: Partial<WooVariation & any>
): Promise<WooVariation> {
const { regular_price, sale_price, ...params } = data;
const api = this.createApi(site, 'wc/v3');
const updateData: any = { ...params };
if (regular_price !== undefined && regular_price !== null) {
updateData.regular_price = String(regular_price);
}
if (sale_price !== undefined && sale_price !== null) {
updateData.sale_price = String(sale_price);
}
try {
const res = await api.put(`products/${productId}/variations/${variationId}`, updateData);
return res.data as WooVariation;
} catch (error) {
console.error('更新产品变体失败:', error.response?.data || error.message);
throw new Error(`更新产品变体失败: ${error.response?.data?.message || error.message}`);
}
}
/**
* 创建 WooCommerce 产品变体
* @param site 站点配置
* @param productId 产品 ID
* @param data 创建变体的数据
*/
async createVariation(
site: any,
productId: string,
data: Partial<WooVariation & any>
): Promise<WooVariation> {
const { regular_price, sale_price, ...params } = data;
const api = this.createApi(site, 'wc/v3');
const createData: any = { ...params };
if (regular_price !== undefined && regular_price !== null) {
createData.regular_price = String(regular_price);
}
if (sale_price !== undefined && sale_price !== null) {
createData.sale_price = String(sale_price);
}
try {
const res = await api.post(`products/${productId}/variations`, createData);
return res.data as WooVariation;
} catch (error) {
console.error('创建产品变体失败:', error.response?.data || error.message);
throw new Error(`创建产品变体失败: ${error.response?.data?.message || error.message}`);
}
}
/**
* 删除 WooCommerce 产品变体
* @param site 站点配置
* @param productId 产品 ID
* @param variationId 变体 ID
*/
async deleteVariation(
site: any,
productId: string,
variationId: string
): Promise<boolean> {
const api = this.createApi(site, 'wc/v3');
try {
await api.delete(`products/${productId}/variations/${variationId}`, { force: true });
return true;
} catch (error) {
console.error('删除产品变体失败:', error.response?.data || error.message);
throw new Error(`删除产品变体失败: ${error.response?.data?.message || error.message}`);
}
}
/**
* 更新 Order
*/
async updateOrder(
site: any,
orderId: string,
data: Record<string, any>
): Promise<boolean> {
const api = this.createApi(site, 'wc/v3');
try {
await api.put(`orders/${orderId}`, data);
return true;
} catch (error) {
console.error('更新订单失败:', error.response?.data || error.message);
throw new Error(`更新订单失败: ${error.response?.data?.message || error.message}`);
}
}
async createFulfillment(
site: any,
orderId: string,
data: Record<string, any>
) {
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
const config: AxiosRequestConfig = {
method: 'POST',
// 构建 URL,规避多/或少/问题
url: this.buildURL(
apiUrl,
'/wp-json',
'wc-ast/v3/orders',
orderId,
'shipment-trackings'
),
headers: {
Authorization: `Basic ${auth}`,
},
data,
};
return await axios.request(config);
}
async deleteFulfillment(
site: any,
orderId: string,
fulfillmentId: string,
): Promise<boolean> {
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
console.log('del', orderId, fulfillmentId);
// 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
const config: AxiosRequestConfig = {
method: 'DELETE',
// 构建 URL,规避多/或少/问题
url: this.buildURL(
apiUrl,
'/wp-json',
'wc-ast/v3/orders',
orderId,
'shipment-trackings',
fulfillmentId
),
headers: {
Authorization: `Basic ${auth}`,
},
};
try {
await axios.request(config);
return true;
} catch (error) {
const errorMessage = error.response?.data?.message || error.message || '删除物流信息失败';
throw new Error(`删除物流信息失败: ${errorMessage}`);
}
}
/**
* 获取订单履约跟踪信息
* @param site 站点配置
* @param orderId 订单ID
* @returns 履约跟踪信息列表
*/
async getFulfillments(
site: any,
orderId: string,
): Promise<any[]> {
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
const config: AxiosRequestConfig = {
method: 'GET',
url: this.buildURL(
apiUrl,
'/wp-json',
'wc-ast/v3/orders',
orderId,
'shipment-trackings'
),
headers: {
Authorization: `Basic ${auth}`,
},
};
try {
const response = await axios.request(config);
return response.data || [];
} catch (error) {
if (error.response?.status === 404) {
return [];
}
const errorMessage = error.response?.data?.message || error.message || '获取履约跟踪信息失败';
throw new Error(`获取履约跟踪信息失败: ${errorMessage}`);
}
}
/**
* 更新订单履约跟踪信息
* @param site 站点配置
* @param orderId 订单ID
* @param fulfillmentId 跟踪ID
* @param data 更新数据
* @returns 更新结果
*/
async updateFulfillment(
site: any,
orderId: string,
fulfillmentId: string,
data: {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}
): Promise<any> {
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
const fulfillmentData: any = {};
if (data.shipping_provider !== undefined) {
fulfillmentData.shipping_provider = data.shipping_provider;
}
if (data.tracking_number !== undefined) {
fulfillmentData.tracking_number = data.tracking_number;
}
if (data.shipping_method !== undefined) {
fulfillmentData.shipping_method = data.shipping_method;
}
if (data.status !== undefined) {
fulfillmentData.status = data.status;
}
if (data.date_created !== undefined) {
fulfillmentData.date_created = data.date_created;
}
if (data.items !== undefined) {
fulfillmentData.items = data.items;
}
const config: AxiosRequestConfig = {
method: 'PUT',
url: this.buildURL(
apiUrl,
'/wp-json',
'wc-ast/v3/orders',
orderId,
'shipment-trackings',
fulfillmentId
),
headers: {
Authorization: `Basic ${auth}`,
},
data: fulfillmentData,
};
try {
const response = await axios.request(config);
return response.data;
} catch (error) {
const errorMessage = error.response?.data?.message || error.message || '更新履约跟踪信息失败';
throw new Error(`更新履约跟踪信息失败: ${errorMessage}`);
}
}
/**
* 批量处理产品 (Create, Update, Delete)
* @param site 站点配置
* @param data 批量操作数据 { create?: [], update?: [], delete?: [] }
*/
async batchProcessProducts(
site: any,
data: BatchOperationDTO
): Promise<BatchOperationResultDTO> {
const api = this.createApi(site, 'wc/v3');
try {
const response = await api.post('products/batch', data);
const result = response.data;
// 转换 WooCommerce 批量操作结果为统一格式
const errors: Array<{identifier: string, error: string}> = [];
// WooCommerce 返回格式: { create: [...], update: [...], delete: [...] }
// 错误信息可能在每个项目的 error 字段中
const checkForErrors = (items: any[]) => {
items.forEach(item => {
if (item.error) {
errors.push({
identifier: String(item.id || item.sku || 'unknown'),
error: typeof item.error === 'string' ? item.error : JSON.stringify(item.error)
});
}
});
};
// 检查每个操作类型的结果中的错误
if (result.create) checkForErrors(result.create);
if (result.update) checkForErrors(result.update);
if (result.delete) checkForErrors(result.delete);
return {
total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0),
processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0),
created: result.create?.length || 0,
updated: result.update?.length || 0,
deleted: result.delete?.length || 0,
errors: errors
};
} catch (error) {
console.error('批量处理产品失败:', error.response?.data || error.message);
throw error;
}
}
/**
* 获取 api 客户端
* @param site 站点配置信息
* @returns api 客户端
*/
getApiClient(site: any): any {
return this.createApi(site);
}
/**
* 获取所有产品分类
* @param site 站点配置
*/
async getCategories(site: any): Promise<any[]> {
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetAll<any>(api, 'products/categories');
}
/**
* 批量处理产品分类
* @param site 站点配置
* @param data { create?: [], update?: [], delete?: [] }
*/
async batchProcessCategories(
site: any,
data: { create?: any[]; update?: any[]; delete?: any[] }
): Promise<any> {
const api = this.createApi(site, 'wc/v3');
try {
const response = await api.post('products/categories/batch', data);
return response.data;
} catch (error) {
console.error('批量处理产品分类失败:', error.response?.data || error.message);
throw error;
}
}
/**
* 获取所有产品标签
* @param site 站点配置
*/
async getTags(site: any): Promise<any[]> {
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetAll<any>(api, 'products/tags');
}
/**
* 批量处理产品标签
* @param site 站点配置
* @param data { create?: [], update?: [], delete?: [] }
*/
async batchProcessTags(
site: any,
data: { create?: any[]; update?: any[]; delete?: any[] }
): Promise<any> {
const api = this.createApi(site, 'wc/v3');
try {
const response = await api.post('products/tags/batch', data);
return response.data;
} catch (error) {
console.error('批量处理产品标签失败:', error.response?.data || error.message);
throw error;
}
}
async createReview(site: any, data: any): Promise<any> {
const api = this.createApi(site, 'wc/v3');
try {
const response = await api.post('products/reviews', data);
return response.data;
} catch (error) {
console.error('创建评论失败:', error.response?.data || error.message);
throw error;
}
}
async getReviews(site: any, page: number = 1, pageSize: number = 100): Promise<any> {
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetPage<any>(api, 'products/reviews', { page, per_page: pageSize });
}
async updateReview(site: any, reviewId: number, data: any): Promise<any> {
const api = this.createApi(site, 'wc/v3');
try {
const response = await api.put(`products/reviews/${reviewId}`, data);
return response.data;
} catch (error) {
console.error('更新评论失败:', error.response?.data || error.message);
throw error;
}
}
async deleteReview(site: any, reviewId: number): Promise<boolean> {
const api = this.createApi(site, 'wc/v3');
try {
await api.delete(`products/reviews/${reviewId}`, { force: true });
return true;
} catch (error) {
const errorMessage = error.response?.data?.message || error.message || '删除评论失败';
throw new Error(`删除评论失败: ${errorMessage}`);
}
}
/**
* 获取WooCommerce webhook列表
* @param site 站点配置
* @param params 查询参数
* @returns 分页webhook列表
*/
async getWebhooks(site: any, params: any): Promise<any> {
// WooCommerce API: GET /webhooks
const api = this.createApi(site, 'wc/v3');
return this.sdkGetPage<any>(api, 'webhooks', params);
}
/**
* 获取单个WooCommerce webhook
* @param site 站点配置
* @param webhookId webhook ID
* @returns webhook详情
*/
async getWebhook(site: any, webhookId: string | number): Promise<any> {
// WooCommerce API: GET /webhooks/{id}
const api = this.createApi(site, 'wc/v3');
const res = await api.get(`webhooks/${webhookId}`);
return res.data;
}
/**
* 创建WooCommerce webhook
* @param site 站点配置
* @param data webhook数据
* @returns 创建结果
*/
async createWebhook(site: any, data: any): Promise<any> {
// WooCommerce API: POST /webhooks
const api = this.createApi(site, 'wc/v3');
const res = await api.post('webhooks', data);
return res.data;
}
/**
* 更新WooCommerce webhook
* @param site 站点配置
* @param webhookId webhook ID
* @param data 更新数据
* @returns 更新结果
*/
async updateWebhook(site: any, webhookId: string | number, data: any): Promise<any> {
// WooCommerce API: PUT /webhooks/{id}
const api = this.createApi(site, 'wc/v3');
const res = await api.put(`webhooks/${webhookId}`, data);
return res.data;
}
/**
* 删除WooCommerce webhook
* @param site 站点配置
* @param webhookId webhook ID
* @returns 删除结果
*/
async deleteWebhook(site: any, webhookId: string | number): Promise<boolean> {
// WooCommerce API: DELETE /webhooks/{id}
const api = this.createApi(site, 'wc/v3');
try {
await api.delete(`webhooks/${webhookId}`, { force: true });
return true;
} catch (error) {
const errorMessage = error.response?.data?.message || error.message || '删除webhook失败';
throw new Error(`删除webhook失败: ${errorMessage}`);
}
}
/**
* 获取 WordPress 媒体库数据
* @param siteId 站点 ID
* @param page 页码
* @param perPage 每页数量
*/
async getMedia(siteId: number, page: number = 1, perPage: number = 20): Promise<{ items: any[], total: number, totalPages: number }> {
const site = await this.siteService.get(siteId, true);
if (!site) {
throw new Error('站点不存在');
}
const endpoint = 'wp/v2/media';
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
// 构建 URL,规避多/或少/问题
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: { page, per_page: perPage }
});
const total = Number(response.headers['x-wp-total'] || 0);
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
return {
items: response.data,
total,
totalPages
};
}
public async fetchMediaPaged(site: any, params: Record<string, any> = {}) {
const page = Number(params.page ?? 1);
const per_page = Number( 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
* @param file 文件对象
*/
async createMedia(siteId: number, file: any): Promise<any> {
const site = await this.siteService.get(siteId, true);
if (!site) {
throw new Error('站点不存在');
}
const endpoint = 'wp/v2/media';
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
const formData = new FormData();
// 假设 file 是 MidwayJS 的 file 对象
// MidwayJS 上传文件通常在 tmp 目录,需要读取流
formData.append('file', fs.createReadStream(file.data), {
filename: file.filename,
contentType: file.mimeType,
});
// Axios headers for multipart
const headers = {
Authorization: `Basic ${auth}`,
'Content-Disposition': `attachment; filename=${file.filename}`,
...formData.getHeaders(),
};
try {
const response = await axios.post(url, formData, {
headers,
// [关键修复] 允许 axios 发送大的请求体, Infinity 表示不限制大小
// 默认情况下, axios 对请求体大小有限制, 上传大文件时会中断
maxBodyLength: Infinity,
maxContentLength: Infinity,
// [建议] 为上传设置一个更长的超时时间 (例如 60 秒)
// 避免因网络波动或大文件上传时间长导致请求被客户端中断
timeout: 60000,
});
return response.data;
} catch (error) {
console.error('上传媒体文件失败:', error.response?.data || error.message);
// 增加对 EPIPE 错误的特定提示
if (error.code === 'EPIPE') {
console.error(
'检测到 EPIPE 错误, 这通常意味着服务器端提前关闭了连接.'
);
console.error(
'请检查服务器配置, 例如 Nginx 的 client_max_body_size 和 PHP 的 post_max_size / upload_max_filesize.'
);
}
throw error;
}
}
/**
* 更新媒体信息
* @param siteId 站点 ID
* @param mediaId 媒体 ID
* @param data 更新数据 (title, caption, description, alt_text)
*/
async updateMedia(siteId: number, mediaId: number, data: any): Promise<any> {
const site = await this.siteService.get(siteId, true);
if (!site) {
throw new Error('站点不存在');
}
const endpoint = `wp/v2/media/${mediaId}`;
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
const response = await axios.post(url, data, {
headers: { Authorization: `Basic ${auth}` },
});
return response.data;
}
async getMediaDetail(siteId: number, mediaId: number): Promise<any> {
// 函数说明 获取单个媒体的详细信息用于后续下载与转换
const site = await this.siteService.get(siteId, true);
if (!site) {
throw new Error('站点不存在');
}
const endpoint = `wp/v2/media/${mediaId}`;
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
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}` } });
return response.data;
}
async uploadMediaBufferWebp(siteId: number, buffer: Buffer, baseFilename: string): Promise<any> {
// 函数说明 以 Buffer 上传 webp 文件到 WordPress 媒体库
const site = await this.siteService.get(siteId, true);
if (!site) {
throw new Error('站点不存在');
}
const endpoint = 'wp/v2/media';
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
const form = new (FormData as any)();
const filename = `${baseFilename}.webp`;
// 条件判断 如果文件名为空则使用默认名称
form.append('file', buffer, { filename, contentType: 'image/webp' });
const headers = {
Authorization: `Basic ${auth}`,
'Content-Disposition': `attachment; filename=${filename}`,
...(form as any).getHeaders(),
};
const response = await axios.post(url, form, { headers });
return response.data;
}
async convertMediaToWebp(siteId: number, mediaIds: Array<number | string>): Promise<{ converted: any[]; failed: Array<{ id: number | string; error: string }> }> {
// 函数说明 批量将媒体转换为 webp 并上传为新媒体
const converted: any[] = [];
const failed: Array<{ id: number | string; error: string }> = [];
const sharp = require('sharp');
for (const id of mediaIds) {
try {
// 获取媒体详情用于下载源文件
const detail = await this.getMediaDetail(siteId, Number(id));
const srcUrl = detail?.source_url;
if (!srcUrl) {
throw new Error('source_url 不存在');
}
// 下载源文件为 Buffer
const resp = await axios.get(srcUrl, { responseType: 'arraybuffer', timeout: 30000,
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; Node.js Axios)',
} });
const inputBuffer = Buffer.from(resp.data);
// 条件判断 如果下载的 Buffer 为空则抛出错误
if (!inputBuffer || inputBuffer.length === 0) {
throw new Error('下载源文件失败');
}
// 使用 sharp 转换为 webp 格式
const webpBuffer: Buffer = await sharp(inputBuffer).webp({ quality: 85 }).toBuffer();
// 条件判断 如果转换结果为空则抛出错误
if (!webpBuffer || webpBuffer.length === 0) {
throw new Error('转换为 webp 失败');
}
// 生成基础文件名 使用原始文件名或媒体ID
const baseName = String(detail?.slug || detail?.title?.rendered || detail?.title || id).replace(/\s+/g, '-');
const uploaded = await this.uploadMediaBufferWebp(siteId, webpBuffer, baseName);
converted.push(uploaded);
} catch (e) {
failed.push({ id, error: (e as any)?.message || '未知错误' });
}
}
return { converted, failed };
}
/**
* 删除媒体文件
* @param siteId 站点 ID
* @param mediaId 媒体 ID
* @param force 是否强制删除(绕过回收站)
*/
async deleteMedia(siteId: number, mediaId: number, force: boolean = true): Promise<any> {
const site = await this.siteService.get(siteId, true);
if (!site) {
throw new Error('站点不存在');
}
const endpoint = `wp/v2/media/${mediaId}`;
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
const response = await axios.delete(url, {
headers: { Authorization: `Basic ${auth}` },
params: { force },
});
return response.data;
}
async getCustomers(siteId: number, page: number = 1, perPage: number = 20): Promise<any> {
const site = await this.siteService.get(siteId);
if (!site) {
throw new Error(`Site ${siteId} not found`);
}
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetPage<any>(api, 'customers', { page, per_page: perPage });
}
async ensureTags(site: any, tagNames: string[]): Promise<{ id: number; name: string }[]> {
if (!tagNames || tagNames.length === 0) return [];
const allTags = await this.getTags(site);
const existingTagMap = new Map(allTags.map((t) => [t.name, t.id]));
const missingTags = tagNames.filter((name) => !existingTagMap.has(name));
if (missingTags.length > 0) {
const createPayload = missingTags.map((name) => ({ name }));
const createdTagsResult = await this.batchProcessTags(site, { create: createPayload });
if (createdTagsResult && createdTagsResult.create) {
createdTagsResult.create.forEach((t) => {
if (t.id && t.name) existingTagMap.set(t.name, t.id);
});
}
}
return tagNames
.map((name) => {
const id = existingTagMap.get(name);
return id ? { id, name } : null;
})
.filter((t) => t !== null) as { id: number; name: string }[];
}
async ensureCategories(site: any, categoryNames: string[]): Promise<{ id: number; name: string }[]> {
if (!categoryNames || categoryNames.length === 0) return [];
const allCategories = await this.getCategories(site);
const existingCatMap = new Map(allCategories.map((c) => [c.name, c.id]));
const missingCategories = categoryNames.filter((name) => !existingCatMap.has(name));
if (missingCategories.length > 0) {
const createPayload = missingCategories.map((name) => ({ name }));
const createdCatsResult = await this.batchProcessCategories(site, { create: createPayload });
if (createdCatsResult && createdCatsResult.create) {
createdCatsResult.create.forEach((c) => {
if (c.id && c.name) existingCatMap.set(c.name, c.id);
});
}
}
return categoryNames
.map((name) => {
const id = existingCatMap.get(name);
return id ? { id, name } : null;
})
.filter((c) => c !== null) as { id: number; name: string }[];
}
}