411 lines
12 KiB
TypeScript
411 lines
12 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 { ProductStatus, ProductStockStatus } from '../enums/base.enum';
|
||
import { SiteService } from './site.service';
|
||
|
||
@Provide()
|
||
export class WPService {
|
||
@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,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 通过 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[]> {
|
||
const result: T[] = [];
|
||
for (let page = 1; page <= maxPages; page++) {
|
||
const { items, totalPages } = await this.sdkGetPage<T>(api, resource, { ...params, page });
|
||
result.push(...items);
|
||
if (page >= totalPages) break;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 获取 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): Promise<WpProduct[]> {
|
||
const api = this.createApi(site, 'wc/v3');
|
||
return await this.sdkGetAll<WpProduct>(api, 'products');
|
||
}
|
||
|
||
async getVariations(site: any, productId: number): Promise<Variation[]> {
|
||
const api = this.createApi(site, 'wc/v3');
|
||
return await this.sdkGetAll<Variation>(api, `products/${productId}/variations`);
|
||
}
|
||
|
||
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(
|
||
siteId: string,
|
||
after: string,
|
||
before: string
|
||
): 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', { after: after,
|
||
before: before });
|
||
}
|
||
|
||
/**
|
||
* 获取 WooCommerce Subscriptions
|
||
* 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions。
|
||
* 返回所有分页合并后的订阅数组。
|
||
*/
|
||
async getSubscriptions(siteId: string): Promise<Record<string, any>[]> {
|
||
const site = await this.siteService.get(siteId);
|
||
// 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
|
||
const api = this.createApi(site, 'wc/v3');
|
||
return await this.sdkGetAll<Record<string, any>>(api, 'subscriptions');
|
||
|
||
}
|
||
|
||
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(
|
||
siteId: string,
|
||
orderId: number
|
||
): 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/${orderId}/refunds`);
|
||
}
|
||
|
||
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(
|
||
siteId: string,
|
||
orderId: number
|
||
): 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/${orderId}/notes`);
|
||
}
|
||
|
||
async updateData<T>(
|
||
endpoint: string,
|
||
site: any,
|
||
data: Record<string, any>
|
||
): Promise<Boolean> {
|
||
const apiUrl = site.apiUrl;
|
||
const { consumerKey, consumerSecret } = site;
|
||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
||
'base64'
|
||
);
|
||
const config: AxiosRequestConfig = {
|
||
method: 'PUT',
|
||
// 构建 URL,规避多/或少/问题
|
||
url: this.buildURL(apiUrl, '/wp-json', endpoint),
|
||
headers: {
|
||
Authorization: `Basic ${auth}`,
|
||
},
|
||
data,
|
||
};
|
||
try {
|
||
await axios.request(config);
|
||
return true;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新 WooCommerce 产品
|
||
* @param productId 产品 ID
|
||
* @param data 更新的数据
|
||
*/
|
||
async updateProduct(
|
||
site: any,
|
||
productId: string,
|
||
data: UpdateWpProductDTO
|
||
): Promise<Boolean> {
|
||
const { regular_price, sale_price, ...params } = data;
|
||
return await this.updateData(`/wc/v3/products/${productId}`, site, {
|
||
...params,
|
||
regular_price: regular_price ? regular_price.toString() : null,
|
||
sale_price: sale_price ? sale_price.toString() : null,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新 WooCommerce 产品 上下架状态
|
||
* @param productId 产品 ID
|
||
* @param status 状态
|
||
* @param stock_status 上下架状态
|
||
*/
|
||
async updateProductStatus(
|
||
site: any,
|
||
productId: string,
|
||
status: ProductStatus,
|
||
stock_status: ProductStockStatus
|
||
): Promise<Boolean> {
|
||
const res = await this.updateData(`/wc/v3/products/${productId}`, site, {
|
||
status,
|
||
manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
|
||
stock_status,
|
||
});
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 更新 WooCommerce 产品变体
|
||
* @param productId 产品 ID
|
||
* @param variationId 变体 ID
|
||
* @param data 更新的数据
|
||
*/
|
||
async updateVariation(
|
||
site: any,
|
||
productId: string,
|
||
variationId: string,
|
||
data: UpdateVariationDTO
|
||
): Promise<Boolean> {
|
||
const { regular_price, sale_price, ...params } = data;
|
||
return await this.updateData(
|
||
`/wc/v3/products/${productId}/variations/${variationId}`,
|
||
site,
|
||
{
|
||
...params,
|
||
regular_price: regular_price ? regular_price.toString() : null,
|
||
sale_price: sale_price ? sale_price.toString() : null,
|
||
}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 更新 Order
|
||
*/
|
||
async updateOrder(
|
||
site: any,
|
||
orderId: string,
|
||
data: Record<string, any>
|
||
): Promise<Boolean> {
|
||
return await this.updateData(`/wc/v3/orders/${orderId}`, site, data);
|
||
}
|
||
|
||
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}`,
|
||
},
|
||
};
|
||
return await axios.request(config);
|
||
}
|
||
} |