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 { // 去掉 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(api: any, resource: string, params: Record = {}) { const page = params.page ?? 1; const per_page = params.per_page ?? 100; const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page }); const data = res.data as T[]; const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1); return { items: data, totalPages, page, per_page }; } /** * 通过 SDK 聚合分页数据,返回全部数据 */ private async sdkGetAll(api: any, resource: string, params: Record = {}, maxPages: number = 50): Promise { const result: T[] = []; for (let page = 1; page <= maxPages; page++) { const { items, totalPages } = await this.sdkGetPage(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( endpoint: string, site: any, param: Record = {} ): Promise { 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( endpoint: string, site: any, page: number = 1, perPage: number = 100 ): Promise { 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 { const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll(api, 'products'); } async getVariations(site: any, productId: number): Promise { const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll(api, `products/${productId}/variations`); } async getVariation( site: any, productId: number, variationId: number ): Promise { 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> { 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; } async getOrders(siteId: string): Promise[]> { const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, 'orders'); } /** * 获取 WooCommerce Subscriptions * 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions。 * 返回所有分页合并后的订阅数组。 */ async getSubscriptions(siteId: string): Promise[]> { const site = await this.siteService.get(siteId); // 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3 const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, 'subscriptions'); } async getOrderRefund( siteId: string, orderId: string, refundId: number ): Promise> { 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; } async getOrderRefunds( siteId: string, orderId: number ): Promise[]> { const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, `orders/${orderId}/refunds`); } async getOrderNote( siteId: string, orderId: number, noteId: number ): Promise> { 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; } async getOrderNotes( siteId: string, orderId: number ): Promise[]> { const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, `orders/${orderId}/notes`); } async updateData( endpoint: string, site: any, data: Record ): Promise { 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 { 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 { 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 { 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 ): Promise { return await this.updateData(`/wc/v3/orders/${orderId}`, site, data); } async createShipment( site: any, orderId: string, data: Record ) { 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 { 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//shipment-trackings/ 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); } }