1324 lines
44 KiB
TypeScript
1324 lines
44 KiB
TypeScript
/**
|
||
*
|
||
* 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 }[];
|
||
}
|
||
}
|