feat(service): 新增Wintopay物流服务并优化订单导出和物流处理 #70

Merged
longbot merged 8 commits from zhuotianyuan/API:main into main 2026-01-30 07:20:08 +00:00
4 changed files with 63 additions and 61 deletions
Showing only changes of commit e30cce45b7 - Show all commits

View File

@ -34,14 +34,14 @@ interface LogisticsUpdateResponse {
export class WintopayService { export class WintopayService {
@Inject() logger; @Inject() logger;
// 默认配置 // 默认配置
private config = { private config = {
//测试环境配置,在生产环境记得换掉 //测试环境配置,在生产环境记得换掉
apiBaseUrl: 'https://stage-merchant-api.wintopay.com', apiBaseUrl: 'https://stage-merchant-api.wintopay.com',
Authorization: 'Bearer kV8w1er8dFw9p9g2kb0mer398hD8hfWk', Authorization: 'Bearer kV8w1er8dFw9p9g2kb0mer398hD8hfWk',
}; };
// 发送请求 // 发送请求
private async sendRequest<T>(url: string, data: any): Promise<T> { private async sendRequest<T>(url: string, data: any): Promise<T> {
try { try {
const headers = { const headers = {

View File

@ -2873,7 +2873,7 @@ export class OrderService {
const logisticsAliases = await this.logisticsAliasModel.find(); const logisticsAliases = await this.logisticsAliasModel.find();
// 构建物流公司别名映射 // 构建物流公司别名映射
const logisticsAliasMap = new Map(logisticsAliases.map(alias => [ alias.logistics_alias,alias.logistics_company])); const logisticsAliasMap = new Map(logisticsAliases.map(alias => [alias.logistics_alias, alias.logistics_company]));
// 遍历数据行 // 遍历数据行
for (let i = 0; i < dataRows.length; i++) { for (let i = 0; i < dataRows.length; i++) {

View File

@ -1792,16 +1792,16 @@ export class ProductService {
// 这里判断 second 是否是数字 // 这里判断 second 是否是数字
return sku.includes('-MX-') || sku.includes('-Mixed-') || /^\d+$/.test(second) && /^\d+$/.test(last) return sku.includes('-MX-') || sku.includes('-Mixed-') || /^\d+$/.test(second) && /^\d+$/.test(last)
} }
async getComponentDetailFromSiteSku(siteProduct: { sku: string, name: string }, quantity: number = 1, site: Site): Promise<{ product: Product,parentProduct?: Product, quantity: number }[]> { async getComponentDetailFromSiteSku(siteProduct: { sku: string, name: string }, quantity: number = 1, site: Site): Promise<{ product: Product, parentProduct?: Product, quantity: number }[]> {
if (!siteProduct.sku) { if (!siteProduct.sku) {
throw new Error('siteSku 不能为空') throw new Error('siteSku 不能为空')
} }
const product = await this.getProductBySiteSku(siteProduct.sku, site) const product = await this.getProductBySiteSku(siteProduct.sku, site)
if (!product) return if (!product) return
if(!product?.components?.length){ if (!product?.components?.length) {
return [{ return [{
product, product,
quantity quantity

View File

@ -75,7 +75,7 @@ export class WPService implements IPlatformService {
} }
const data = res.data as T[]; const data = res.data as T[];
const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1); const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1);
const total = Number(res.headers?.['x-wp-total']?? 1) const total = Number(res.headers?.['x-wp-total'] ?? 1)
return { items: data, total, totalPages, page, per_page, page_size: per_page }; return { items: data, total, totalPages, page, per_page, page_size: per_page };
} }
@ -206,7 +206,7 @@ export class WPService implements IPlatformService {
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
); );
console.log(`!!!wpApiUrl, consumerKey, consumerSecret, auth`,site.apiUrl, consumerKey, consumerSecret, auth) console.log(`!!!wpApiUrl, consumerKey, consumerSecret, auth`, site.apiUrl, consumerKey, consumerSecret, auth)
let hasMore = true; let hasMore = true;
while (hasMore) { while (hasMore) {
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
@ -259,8 +259,8 @@ export class WPService implements IPlatformService {
// 导出 WooCommerce 产品为特殊CSV(平台特性) // 导出 WooCommerce 产品为特殊CSV(平台特性)
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> { async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
const list = await this.getProducts(site, { page, per_page: pageSize }); 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 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 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'); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
return csv; return csv;
} }
@ -289,7 +289,7 @@ export class WPService implements IPlatformService {
const res = await api.get(`orders/${orderId}`); const res = await api.get(`orders/${orderId}`);
return res.data as Record<string, any>; return res.data as Record<string, any>;
} }
async getOrders(siteId: number,params: Record<string, any> = {}): Promise<Record<string, any>[]> { async getOrders(siteId: number, params: Record<string, any> = {}): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId); const site = await this.siteService.get(siteId);
const api = this.createApi(site, 'wc/v3'); const api = this.createApi(site, 'wc/v3');
return await this.sdkGetAll<Record<string, any>>(api, 'orders', params); return await this.sdkGetAll<Record<string, any>>(api, 'orders', params);
@ -367,10 +367,10 @@ export class WPService implements IPlatformService {
const api = this.createApi(site, 'wc/v3'); const api = this.createApi(site, 'wc/v3');
// 确保价格为字符串 // 确保价格为字符串
if (data.regular_price !== undefined && data.regular_price !== null) { if (data.regular_price !== undefined && data.regular_price !== null) {
data.regular_price = String(data.regular_price); data.regular_price = String(data.regular_price);
} }
if (data.sale_price !== undefined && data.sale_price !== null) { if (data.sale_price !== undefined && data.sale_price !== null) {
data.sale_price = String(data.sale_price); data.sale_price = String(data.sale_price);
} }
// 处理标签字段,如果为字符串数组则转换为 WooCommerce 所需的对象数组 // 处理标签字段,如果为字符串数组则转换为 WooCommerce 所需的对象数组
if (Array.isArray((data as any).tags)) { if (Array.isArray((data as any).tags)) {
@ -805,7 +805,7 @@ export class WPService implements IPlatformService {
const result = response.data; const result = response.data;
// 转换 WooCommerce 批量操作结果为统一格式 // 转换 WooCommerce 批量操作结果为统一格式
const errors: Array<{identifier: string, error: string}> = []; const errors: Array<{ identifier: string, error: string }> = [];
// WooCommerce 返回格式: { create: [...], update: [...], delete: [...] } // WooCommerce 返回格式: { create: [...], update: [...], delete: [...] }
// 错误信息可能在每个项目的 error 字段中 // 错误信息可能在每个项目的 error 字段中
@ -1022,7 +1022,7 @@ export class WPService implements IPlatformService {
async getMedia(siteId: number, page: number = 1, perPage: number = 20): Promise<{ items: any[], total: number, totalPages: number }> { 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); const site = await this.siteService.get(siteId, true);
if (!site) { if (!site) {
throw new Error('站点不存在'); throw new Error('站点不存在');
} }
const endpoint = 'wp/v2/media'; const endpoint = 'wp/v2/media';
const apiUrl = site.apiUrl; const apiUrl = site.apiUrl;
@ -1046,7 +1046,7 @@ export class WPService implements IPlatformService {
}; };
} }
public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) { public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) {
const apiUrl = site.apiUrl; const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any; const { consumerKey, consumerSecret } = site as any;
const endpoint = 'wp/v2/media'; const endpoint = 'wp/v2/media';
@ -1061,15 +1061,15 @@ public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> =
} }
}); });
// 检查是否有错误信息 // 检查是否有错误信息
if(response?.data?.message){ if (response?.data?.message) {
throw new Error(`获取${apiUrl}条媒体文件失败,原因为${response.data.message}`) throw new Error(`获取${apiUrl}条媒体文件失败,原因为${response.data.message}`)
} }
if(!Array.isArray(response.data)) { if (!Array.isArray(response.data)) {
throw new Error(`获取${apiUrl}条媒体文件失败,原因为返回数据不是数组`); throw new Error(`获取${apiUrl}条媒体文件失败,原因为返回数据不是数组`);
} }
const total = Number(response.headers['x-wp-total'] || 0); const total = Number(response.headers['x-wp-total'] || 0);
const totalPages = Number(response.headers['x-wp-totalpages'] || 0); const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
return { items: response.data, total, totalPages, page:params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 }; return { items: response.data, total, totalPages, page: params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 };
} }
/** /**
* *
@ -1091,15 +1091,15 @@ public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> =
// 假设 file 是 MidwayJS 的 file 对象 // 假设 file 是 MidwayJS 的 file 对象
// MidwayJS 上传文件通常在 tmp 目录,需要读取流 // MidwayJS 上传文件通常在 tmp 目录,需要读取流
formData.append('file', fs.createReadStream(file.data), { formData.append('file', fs.createReadStream(file.data), {
filename: file.filename, filename: file.filename,
contentType: file.mimeType, contentType: file.mimeType,
}); });
// Axios headers for multipart // Axios headers for multipart
const headers = { const headers = {
Authorization: `Basic ${auth}`, Authorization: `Basic ${auth}`,
'Content-Disposition': `attachment; filename=${file.filename}`, 'Content-Disposition': `attachment; filename=${file.filename}`,
...formData.getHeaders(), ...formData.getHeaders(),
}; };
try { try {
@ -1205,10 +1205,12 @@ public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> =
throw new Error('source_url 不存在'); throw new Error('source_url 不存在');
} }
// 下载源文件为 Buffer // 下载源文件为 Buffer
const resp = await axios.get(srcUrl, { responseType: 'arraybuffer', timeout: 30000, const resp = await axios.get(srcUrl, {
headers: { responseType: 'arraybuffer', timeout: 30000,
'User-Agent': 'Mozilla/5.0 (compatible; Node.js Axios)', headers: {
} }); 'User-Agent': 'Mozilla/5.0 (compatible; Node.js Axios)',
}
});
const inputBuffer = Buffer.from(resp.data); const inputBuffer = Buffer.from(resp.data);
// 条件判断 如果下载的 Buffer 为空则抛出错误 // 条件判断 如果下载的 Buffer 为空则抛出错误
if (!inputBuffer || inputBuffer.length === 0) { if (!inputBuffer || inputBuffer.length === 0) {