779 lines
25 KiB
TypeScript
779 lines
25 KiB
TypeScript
import { Inject, Provide } from '@midwayjs/core';
|
||
import axios, { AxiosRequestConfig } from 'axios';
|
||
import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api';
|
||
import { WpProduct } from '../entity/wp_product.entity';
|
||
import { Variation } from '../entity/variation.entity';
|
||
import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto';
|
||
import { SiteService } from './site.service';
|
||
import { IPlatformService } from '../interface/platform.interface';
|
||
import * as FormData from 'form-data';
|
||
import * as fs from 'fs';
|
||
|
||
@Provide()
|
||
export class WPService implements IPlatformService {
|
||
@Inject()
|
||
private readonly siteService: SiteService;
|
||
|
||
/**
|
||
* 构建 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> = {}) {
|
||
const api = this.createApi(site, 'wc/v3');
|
||
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 ?? 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 };
|
||
}
|
||
|
||
/**
|
||
* 通过 SDK 聚合分页数据,返回全部数据
|
||
*/
|
||
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
|
||
// 直接传入较大的per_page参数,一次性获取所有数据
|
||
const { items } = await this.sdkGetPage<T>(api, resource, { ...params, per_page: 100 });
|
||
return items;
|
||
}
|
||
|
||
/**
|
||
* 获取 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, page: number = 1, pageSize: number = 100): Promise<any> {
|
||
const api = this.createApi(site, 'wc/v3');
|
||
return await this.sdkGetPage<WpProduct>(api, 'products', { page, per_page: pageSize });
|
||
}
|
||
|
||
|
||
// 导出 WooCommerce 产品为特殊CSV(平台特性)
|
||
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
|
||
const list = await this.getProducts(site, 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<Variation>(api, `products/${productId}/variations`, { page, per_page: pageSize });
|
||
}
|
||
|
||
async getVariation(
|
||
site: any,
|
||
productId: number,
|
||
variationId: number
|
||
): Promise<Variation> {
|
||
const api = this.createApi(site, 'wc/v3');
|
||
const res = await api.get(`products/${productId}/variations/${variationId}`);
|
||
return res.data as Variation;
|
||
}
|
||
|
||
async getOrder(
|
||
siteId: string,
|
||
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(site: any | number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||
// 如果传入的是站点ID,则获取站点配置
|
||
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
||
const api = this.createApi(siteConfig, 'wc/v3');
|
||
return await this.sdkGetPage<Record<string, any>>(api, 'orders', { page, per_page: pageSize });
|
||
}
|
||
|
||
/**
|
||
* 获取 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);
|
||
}
|
||
|
||
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: UpdateWpProductDTO
|
||
): 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);
|
||
}
|
||
|
||
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) {
|
||
console.error('更新产品库存失败:', error.response?.data || error.message);
|
||
// throw new Error(`更新产品库存失败: ${error.response?.data?.message || error.message}`);
|
||
// 为了不打断批量同步,这里记录错误但不抛出
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新 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) {
|
||
console.error('更新产品变体库存失败:', error.response?.data || error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新 WooCommerce 产品变体
|
||
* @param productId 产品 ID
|
||
* @param variationId 变体 ID
|
||
* @param data 更新的数据
|
||
*/
|
||
async updateVariation(
|
||
site: any,
|
||
productId: string,
|
||
variationId: string,
|
||
data: Partial<UpdateVariationDTO>
|
||
): Promise<boolean> {
|
||
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 {
|
||
await api.put(`products/${productId}/variations/${variationId}`, updateData);
|
||
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 createShipment(
|
||
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 deleteShipment(
|
||
site: any,
|
||
orderId: string,
|
||
trackingId: string,
|
||
): Promise<boolean> {
|
||
const apiUrl = site.apiUrl;
|
||
const { consumerKey, consumerSecret } = site;
|
||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
||
'base64'
|
||
);
|
||
|
||
console.log('del', orderId, trackingId);
|
||
// 删除接口: 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',
|
||
trackingId
|
||
),
|
||
headers: {
|
||
Authorization: `Basic ${auth}`,
|
||
},
|
||
};
|
||
try {
|
||
await axios.request(config);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('删除物流信息失败:', error.response?.data || error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量处理产品 (Create, Update, Delete)
|
||
* @param site 站点配置
|
||
* @param data 批量操作数据 { create?: [], update?: [], delete?: [] }
|
||
*/
|
||
async batchProcessProducts(
|
||
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/batch', data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('批量处理产品失败:', error.response?.data || error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取所有产品分类
|
||
* @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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 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
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 上传媒体文件
|
||
* @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(),
|
||
};
|
||
|
||
const response = await axios.post(url, formData, { headers });
|
||
return response.data;
|
||
}
|
||
|
||
/**
|
||
* 更新媒体信息
|
||
* @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;
|
||
}
|
||
|
||
/**
|
||
* 删除媒体文件
|
||
* @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<{ items: any[], total: number, totalPages: number }> {
|
||
const site = await this.siteService.get(siteId);
|
||
if (!site) {
|
||
throw new Error(`Site ${siteId} not found`);
|
||
}
|
||
|
||
if (site.type === 'shopyy') {
|
||
return { items: [], total: 0, totalPages: 0 };
|
||
}
|
||
|
||
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 }[];
|
||
}
|
||
}
|