diff --git a/src/config/config.default.ts b/src/config/config.default.ts index f826242..2a37fd0 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -42,6 +42,7 @@ import DictSeeder from '../db/seeds/dict.seeder'; import CategorySeeder from '../db/seeds/category.seeder'; import CategoryAttributeSeeder from '../db/seeds/category_attribute.seeder'; import { SiteSku } from '../entity/site-sku.entity'; +import { logisticsAlias } from '../entity/logistics_alias.entity'; export default { // use for cookie sign key, should change to your own and keep security @@ -88,6 +89,7 @@ export default { Area, CategoryAttribute, Category, + logisticsAlias, ], synchronize: true, logging: false, diff --git a/src/dto/logistics.dto.ts b/src/dto/logistics.dto.ts index 48841db..f0bd6ba 100644 --- a/src/dto/logistics.dto.ts +++ b/src/dto/logistics.dto.ts @@ -25,7 +25,7 @@ export class ShipmentBookDTO { shipmentPlatform: string; @ApiProperty() - @Rule(RuleType.string()) + @Rule(RuleType.any()) courierCompany: string; } diff --git a/src/entity/logistics_alias.entity.ts b/src/entity/logistics_alias.entity.ts new file mode 100644 index 0000000..bf2c475 --- /dev/null +++ b/src/entity/logistics_alias.entity.ts @@ -0,0 +1,34 @@ + +import { ApiProperty } from '@midwayjs/swagger'; +import { Entity, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('logistics_alias') +export class logisticsAlias { + @PrimaryGeneratedColumn() + id: number; + + @ApiProperty({ type: 'string' }) + @Column() + logistics_company: string + + @ApiProperty({ type: 'string' }) + @Column() + logistics_alias: string + + @ApiProperty({ type: 'string' }) + @Column() + platform: string + + + // 是否可删除 + @Column({ default: true, comment: '是否可删除' }) + deletable: boolean; + // 创建时间 + @CreateDateColumn() + createdAt: Date; + + // 更新时间 + @UpdateDateColumn() + updatedAt: Date; + +} \ No newline at end of file diff --git a/src/service/Wintopay.service.ts b/src/service/Wintopay.service.ts new file mode 100644 index 0000000..cf2491a --- /dev/null +++ b/src/service/Wintopay.service.ts @@ -0,0 +1,96 @@ +import { Inject, Provide } from '@midwayjs/core'; +import axios from 'axios'; +import dayjs = require('dayjs'); +import utc = require('dayjs/plugin/utc'); +import timezone = require('dayjs/plugin/timezone'); + +// 扩展dayjs功能 +dayjs.extend(utc); +dayjs.extend(timezone); + + +// Wintopay 物流更新请求接口 +interface LogisticsUpdateRequest { + trade_id: string; // 订单的流水号 + track_number: string; // 物流单号 + track_brand: string; // 物流公司编号 +} + +// Wintopay 物流更新响应接口 +interface LogisticsUpdateResponse { + code: string; + message: string; + data: { + trade_id: string; + track_brand: string; + track_number: string; + time: number; + }; + error: any; + request_id: string; +} + +@Provide() +export class WintopayService { + @Inject() logger; + + // 默认配置 + private config = { + //测试环境配置,在生产环境记得换掉 + apiBaseUrl: 'https://stage-merchant-api.wintopay.com', + Authorization: 'Bearer kV8w1er8dFw9p9g2kb0mer398hD8hfWk', + }; + + // 发送请求 + private async sendRequest(url: string, data: any): Promise { + try { + const headers = { + 'Content-Type': 'application/json', + 'Authorization': this.config.Authorization, + }; + + // 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误 + const response = await axios.post( + `${this.config.apiBaseUrl}${url}`, + data, + { + headers, + httpsAgent: new (require('https').Agent)({ + rejectUnauthorized: false + }) + } + ); + + return response.data; + } catch (error) { + this.logger.error('Wintopay API请求失败:', error); + throw error; + } + } + + + /** + * 更新订单物流信息 + * @param params 物流更新参数 + * @returns 物流更新响应 + */ + async logisticsUpdate(params: LogisticsUpdateRequest): Promise { + try { + this.logger.info('开始更新物流信息:', params); + + const response = await this.sendRequest('/v1/logistics/update', params); + + this.logger.info('物流更新成功:', response); + return response; + } catch (error: any) { + this.logger.error('物流更新失败:', error); + + // 处理API返回的错误 + if (error.response?.data) { + throw new Error(`物流更新失败: ${error.response.data.message || '未知错误'}`); + } + + throw new Error(`物流更新请求失败: ${error.message || '网络错误'}`); + } + } +} \ No newline at end of file diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index 2a58e59..d27fe94 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -327,17 +327,20 @@ export class LogisticsService { let resShipmentFee: any; if (data.shipmentPlatform === 'uniuni') { resShipmentFee = await this.uniExpressService.getRates(reqBody); + if (resShipmentFee.status !== 'SUCCESS') { + throw new Error(resShipmentFee.ret_msg); + } + return resShipmentFee.data.totalAfterTax * 100; } else if (data.shipmentPlatform === 'freightwaves') { const fre_reqBody = await this.convertToFreightwavesRateTry(data); resShipmentFee = await this.freightwavesService.rateTry(fre_reqBody); + return resShipmentFee.totalAmount * 100; } else { throw new Error('不支持的运单平台'); } - if (resShipmentFee.status !== 'SUCCESS') { - throw new Error(resShipmentFee.ret_msg); - } - return resShipmentFee.data.totalAfterTax * 100; + + } catch (e) { throw e; } @@ -360,12 +363,7 @@ export class LogisticsService { try { resShipmentOrder = await this.mepShipment(data, order); - // 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESS,tms.freightwaves状态为00000200 - if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') { - order.orderStatus = ErpOrderStatus.COMPLETED; - } else { - throw new Error('运单生成失败'); - } + order.orderStatus = ErpOrderStatus.COMPLETED; const dataSource = this.dataSourceManager.getDataSource('default'); let transactionError = undefined; let shipmentId = undefined; @@ -384,8 +382,8 @@ export class LogisticsService { unique_id = resShipmentOrder.data.uni_order_sn; state = resShipmentOrder.data.uni_status_code; } else { - co = resShipmentOrder.data?.shipOrderId; - unique_id = resShipmentOrder.data?.shipOrderId; + co = resShipmentOrder.shipOrderId; + unique_id = resShipmentOrder.shipOrderId; state = ErpOrderStatus.COMPLETED; } @@ -728,14 +726,20 @@ export class LogisticsService { }; // 添加运单 resShipmentOrder = await this.uniExpressService.createShipment(reqBody); + + // 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESS,tms.freightwaves状态为00000200 + if (resShipmentOrder.status !== 'SUCCESS') { + throw new Error('运单生成失败'); + } } if (data.shipmentPlatform === 'freightwaves') { + // 根据TMS系统对接说明文档格式化参数 const reqBody: any = { // shipCompany: 'UPSYYZ7000NEW', - shipCompany: data.courierCompany || "", - partnerOrderNumber: order.siteId + '-1-' + order.externalOrderId, + shipCompany: data.courierCompany, + partnerOrderNumber: order.siteId + '-' + order.externalOrderId, warehouseId: '25072621030107400060', shipper: { name: data.details.origin.contact_name, // 姓名 @@ -811,8 +815,8 @@ export class LogisticsService { return resShipmentOrder; } catch (error) { // 处理错误,例如记录日志或抛出异常 - throw new Error(`物流订单处理失败: ${error}`); - + throw new Error(`物流订单处理失败: ${error}`); + } } @@ -833,7 +837,7 @@ export class LogisticsService { // 转换为RateTryRequest格式 const r = { //shipCompany: 'UPSYYZ7000NEW', // 必填,但ShipmentFeeBookDTO中缺少 - shipCompany: data.courierCompany || "", + shipCompany: data.courierCompany, partnerOrderNumber: `order-${Date.now()}`, // 必填,使用时间戳生成 warehouseId: '25072621030107400060', // 可选,使用stockPointId转换 shipper: { diff --git a/src/service/order.service.ts b/src/service/order.service.ts index fb3baf1..4196090 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -42,6 +42,7 @@ import { UnifiedOrderDTO } from '../dto/site-api.dto'; import { CustomerService } from './customer.service'; import { ProductService } from './product.service'; import { Site } from '../entity/site.entity'; +import { logisticsAlias } from '../entity/logistics_alias.entity'; @Provide() export class OrderService { @@ -54,6 +55,9 @@ export class OrderService { @InjectEntityModel(Order) orderModel: Repository; + @InjectEntityModel(logisticsAlias) + logisticsAliasModel: Repository; + @InjectEntityModel(User) userModel: Repository; @@ -151,7 +155,7 @@ export class OrderService { where: { externalOrderId: String(order.id), siteId: siteId }, }); if (!existingOrder) { - this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status) + this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status) } // 同步单个订单 await this.syncSingleOrder(siteId, order); @@ -630,7 +634,7 @@ export class OrderService { await this.saveOrderItem(entity); // 为每个订单项创建对应的销售项(OrderSale) const site = await this.siteService.get(siteId); - await this.saveOrderSale(entity,site); + await this.saveOrderSale(entity, site); } } @@ -720,7 +724,7 @@ export class OrderService { */ // TODO 这里存的是库存商品实际 // 所以叫做 orderInventoryItems 可能更合适 - async saveOrderSale(orderItem: OrderItem,site:Site) { + async saveOrderSale(orderItem: OrderItem, site: Site) { const currentOrderSale = await this.orderSaleModel.find({ where: { siteId: orderItem.siteId, @@ -733,14 +737,13 @@ export class OrderService { if (!orderItem.sku) return; // 从数据库查询产品,关联查询组件 - const componentDetails = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name }, site); - if(!componentDetails?.length){ - return + const componentDetails = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name }, orderItem.quantity, site); + if (!componentDetails?.length) { + return } - const orderSales: OrderSale[] = componentDetails.map(({product, parentProduct, quantity}) => { + const orderSales: OrderSale[] = componentDetails.map(({ product, parentProduct, quantity }) => { if (!product) return null - console.log('product',product) const attrsObj = this.productService.getAttributesObject(product.attributes) const orderSale = plainToClass(OrderSale, { orderId: orderItem.orderId, @@ -2455,18 +2458,18 @@ export class OrderService { */ // TODO async exportOrder(ids: number[]) { - // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 + // 日期 订单号 姓名地址 邮箱 号码 盒数 换盒数 换货内容 快递号 商品1 数量1 商品2 数量2... interface ExportData { '日期': string; '订单号': string; '姓名地址': string; '邮箱': string; '号码': string; - '订单内容': string; '盒数': number; '换盒数': number; '换货内容': string; '快递号': string; + [key: string]: any; // 支持动态添加的商品和数量列 } try { @@ -2513,6 +2516,15 @@ export class OrderService { return acc; }, {} as Record); + // 计算最大商品数量 + let maxItemsCount = 0; + orders.forEach(order => { + const items = orderItemsByOrderId[order.id] || []; + if (items.length > maxItemsCount) { + maxItemsCount = items.length; + } + }); + // 构建导出数据 const exportDataList: ExportData[] = orders.map(order => { // 获取订单的订单项 @@ -2521,9 +2533,6 @@ export class OrderService { // 计算总盒数 const boxCount = items.reduce((total, item) => total + item.quantity, 0); - // 构建订单内容 - const orderContent = items.map(item => `${item.name} x ${item.quantity}`).join('; '); - // 构建姓名地址 const shipping = order.shipping; const billing = order.billing; @@ -2548,18 +2557,32 @@ export class OrderService { const exchangeBoxCount = 0; const exchangeContent = ''; - return { + // 构建基础数据对象 + const baseData: ExportData = { '日期': order.date_created?.toISOString().split('T')[0] || '', '订单号': order.externalOrderId || '', '姓名地址': nameAddress, '邮箱': order.customer_email || '', '号码': phone, - '订单内容': orderContent, '盒数': boxCount, '换盒数': exchangeBoxCount, '换货内容': exchangeContent, '快递号': trackingNumber }; + + // 添加商品和数量列 + items.forEach((item, index) => { + baseData[`商品${index + 1}`] = item.name; + baseData[`数量${index + 1}`] = item.quantity; + }); + + // 填充空值,确保所有行的列数一致 + for (let i = items.length; i < maxItemsCount; i++) { + baseData[`商品${i + 1}`] = ''; + baseData[`数量${i + 1}`] = ''; + } + + return baseData; }); // 返回CSV字符串内容给前端 @@ -2802,107 +2825,116 @@ export class OrderService { return result; } - // 从 CSV 导入产品;存在则更新,不存在则创建 - /** - * 导入 Wintopay 表格并回填物流信息 - * @param file 上传的文件 - * @returns 处理后的数据(包含更新的物流信息) - */ + // 从 CSV 导入产品;存在则更新,不存在则创建 + /** + * 导入 Wintopay 表格并回填物流信息 + * @param file 上传的文件 + * @returns 处理后的数据(包含更新的物流信息) + */ async importWintopayTable(file: any): Promise { - let updated = 0; - const errors: BatchErrorItem[] = []; - - // 解析文件获取工作表 - let buffer: Buffer; - if (Buffer.isBuffer(file)) { - buffer = file; - } else if (file?.data) { - if (typeof file.data === 'string') { - buffer = fs.readFileSync(file.data); - } else { - buffer = file.data; - } - } else { - throw new Error('无效的文件输入'); - } - - const workbook = xlsx.read(buffer, { type: 'buffer', codepage: 65001 }); - const worksheet = workbook.Sheets[workbook.SheetNames[0]]; - - // 获取表头和数据 - const jsonData = xlsx.utils.sheet_to_json(worksheet, { header: 1 }); - const headers = jsonData[0] as string[]; - const dataRows = jsonData.slice(1) as any[][]; - - // 查找各列的索引 - const columnIndices = { - orderNumber: headers.indexOf('订单号'), - logisticsCompany: headers.indexOf('物流公司'), - trackingNumber: headers.indexOf('单号-单元格文本格式'), - orderCreateTime: headers.indexOf('订单创建时间'), - orderEmail: headers.indexOf('订单邮箱'), - orderSite: headers.indexOf('订单网站'), - name: headers.indexOf('姓名'), - refund: headers.indexOf('退款'), - chargeback: headers.indexOf('拒付') - }; - - // 遍历数据行 - for (let i = 0; i < dataRows.length; i++) { - const row = dataRows[i]; - const orderNumber = row[columnIndices.orderNumber]; - - if (!orderNumber) { - errors.push({ identifier: `行 ${i + 2}`, error: '订单号为空' }); - continue; - } - - try { - let orderNumbers=""; - if (orderNumber.includes('_')&&orderNumber.includes('-')) { - orderNumbers = orderNumber.split('_')[0].toString(); - orderNumbers = orderNumbers.split('-')[1]; - } - // 通过订单号查询订单 - const order = await this.orderModel.findOne({ where: { externalOrderId: orderNumbers } }); - if (order) { - // 通过orderId查询fulfillments - const fulfillments = await this.orderFulfillmentModel.find({ where: { order_id: order.id } }); - - if (fulfillments && fulfillments.length > 0) { - const fulfillment = fulfillments[0]; // 假设每个订单只有一个物流信息 - // 回填物流信息 - if (columnIndices.logisticsCompany !== -1) { - row[columnIndices.logisticsCompany] = fulfillment.shipping_provider || ''; - } - if (columnIndices.trackingNumber !== -1) { - row[columnIndices.trackingNumber] = fulfillment.tracking_number || ''; - } - - updated++; - } - } - } catch (error) { - errors.push({ identifier: `行 ${i + 2}`, error: `处理失败: ${error.message}` }); - } - } - - // 将数据转换为对象数组,与 exportOrder 方法返回格式一致 - const resultData = dataRows.map((row, index) => { - const rowData: any = {}; - headers.forEach((header, colIndex) => { - rowData[header] = row[colIndex] || ''; - }); - // 添加行号信息 - rowData['行号'] = index + 2; - return rowData; - }); - - - // 返回XLSX buffer内容给前端 - // const xlsxBuffer = await this.exportToXlsx(resultData, { type: 'buffer' }); - return resultData; + let updated = 0; + const errors: BatchErrorItem[] = []; + // 解析文件获取工作表 + let buffer: Buffer; + if (Buffer.isBuffer(file)) { + buffer = file; + } else if (file?.data) { + if (typeof file.data === 'string') { + buffer = fs.readFileSync(file.data); + } else { + buffer = file.data; + } + } else { + throw new Error('无效的文件输入'); } + const workbook = xlsx.read(buffer, { type: 'buffer', codepage: 65001 }); + const worksheet = workbook.Sheets[workbook.SheetNames[0]]; + + // 获取表头和数据 + const jsonData = xlsx.utils.sheet_to_json(worksheet, { header: 1 }); + const headers = jsonData[0] as string[]; + const dataRows = jsonData.slice(1) as any[][]; + + // 查找各列的索引 + const columnIndices = { + orderNumber: headers.indexOf('订单号'), + logisticsCompany: headers.indexOf('物流公司'), + trackingNumber: headers.indexOf('单号-单元格文本格式'), + orderCreateTime: headers.indexOf('订单创建时间'), + orderEmail: headers.indexOf('订单邮箱'), + orderSite: headers.indexOf('订单网站'), + name: headers.indexOf('姓名'), + refund: headers.indexOf('退款'), + chargeback: headers.indexOf('拒付') + }; + + const logisticsAliases = await this.logisticsAliasModel.find(); + + // 构建物流公司别名映射 + const logisticsAliasMap = new Map(logisticsAliases.map(alias => [alias.logistics_alias, alias.logistics_company])); + + // 遍历数据行 + for (let i = 0; i < dataRows.length; i++) { + const row = dataRows[i]; + const orderNumber = row[columnIndices.orderNumber]; + + if (!orderNumber) { + errors.push({ identifier: `行 ${i + 2}`, error: '订单号为空' }); + continue; + } + + try { + let orderNumbers = orderNumber; + // 确保 orderNumber 是字符串类型 + const orderNumberStr = String(orderNumber); + if (orderNumberStr.includes('_') && orderNumberStr.includes('-')) { + orderNumbers = orderNumberStr.split('_')[0].toString(); + orderNumbers = orderNumbers.split('-')[1]; + } + // 通过订单号查询订单 + const order = await this.orderModel.findOne({ where: { externalOrderId: orderNumbers } }); + if (order) { + // 通过orderId查询fulfillments + const fulfillments = await this.orderFulfillmentModel.find({ where: { order_id: order.id } }); + + if (fulfillments && fulfillments.length > 0) { + const fulfillment = fulfillments[0]; // 假设每个订单只有一个物流信息 + + const shipping_provider = logisticsAliasMap.get(fulfillment.shipping_provider); + // 回填物流信息 + if (columnIndices.logisticsCompany !== -1) { + row[columnIndices.logisticsCompany] = shipping_provider || ''; + } + if (columnIndices.trackingNumber !== -1) { + row[columnIndices.trackingNumber] = fulfillment.tracking_number || ''; + } + + updated++; + } + } + } catch (error) { + errors.push({ identifier: `行 ${i + 2}`, error: `处理失败: ${error.message}` }); + } + } + + // 将数据转换为对象数组,与 exportOrder 方法返回格式一致 + const resultData = dataRows.map((row, index) => { + const rowData: any = {}; + headers.forEach((header, colIndex) => { + rowData[header] = row[colIndex] || ''; + }); + // 添加行号信息 + rowData['行号'] = index + 2; + return rowData; + }); + + + // 返回XLSX buffer内容给前端 + // const xlsxBuffer = await this.exportToXlsx(resultData, { type: 'buffer' }); + return resultData; + + } + } diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 7c14d37..a32f487 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1785,8 +1785,14 @@ export class ProductService { attributes: attributes.length > 0 ? attributes : undefined, } } - // 获取库存单品列表 - async getComponentDetailFromSiteSku(siteProduct: { sku: string, name?: string }, site: Site): Promise<{ product: Product,parentProduct?: Product, quantity: number }[]> { + isMixedSku(sku: string) { + const splitSKu = sku.split('-') + const last = splitSKu[splitSKu.length - 1] + const second = splitSKu[splitSKu.length - 2] + // 这里判断 second 是否是数字 + 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 }[]> { if (!siteProduct.sku) { throw new Error('siteSku 不能为空') } @@ -1795,23 +1801,22 @@ export class ProductService { if (!product) return - if(!product?.components?.length){ + if (!product?.components?.length) { return [{ product, - quantity:1 + quantity }] } return await Promise.all(product.components.map(async comp => { return { product: await this.productModel.findOne({ - where: { sku: comp.sku }, - relations: ['category', 'attributes', 'attributes.dict', 'components'] + where: { id: comp.productId }, }), parentProduct: product, // 这里得记录一下他的爸爸用来记录 - quantity: comp.quantity, + quantity: comp.quantity * quantity, } - })) + })) } // 准备创建产品的 DTO, 处理类型转换和默认值 @@ -2150,7 +2155,7 @@ export class ProductService { .leftJoinAndSelect('product.components', 'components') .leftJoinAndSelect('product.siteSkus', 'siteSku') .where('siteSku.sku LIKE :siteSku', { siteSku: `%${siteSku}%` }) - .orWhere('product.sku = :siteSku', { siteSku }) + .orWhere('product.sku = :siteSku', { siteSku }); if (site) { queryBuilder.orWhere('product.sku = :processedSku', { diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 3500bcb..8da7d40 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -16,7 +16,7 @@ import { WooProduct, WooVariation, WpMediaGetListParams } from '../dto/woocommer const MAX_PAGE_SIZE = 100; @Provide() export class WPService implements IPlatformService { - + @Inject() private readonly siteService: SiteService; @@ -75,7 +75,7 @@ export class WPService implements IPlatformService { } const data = res.data as T[]; 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 }; } @@ -94,9 +94,9 @@ export class WPService implements IPlatformService { * 默认按 date_created 倒序排列,确保获取最新的数据 */ private async sdkGetAllConcurrent( - api: WooCommerceRestApi, - resource: string, - params: Record = {}, + api: WooCommerceRestApi, + resource: string, + params: Record = {}, maxPages: number = MAX_PAGE_SIZE, concurrencyLimit: number = 5 ): Promise { @@ -118,7 +118,7 @@ export class WPService implements IPlatformService { // 限制最大页数,避免过多的并发请求 const actualMaxPages = Math.min(totalPages, maxPages); - + // 收集所有页面数据,从第二页开始 const allItems = [...firstPageItems]; let currentPage = 2; @@ -127,7 +127,7 @@ export class WPService implements IPlatformService { while (currentPage <= actualMaxPages) { const batchPromises: Promise[] = []; const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1); - + // 创建当前批次的并发请求 for (let i = 0; i < batchSize; i++) { const page = currentPage + i; @@ -137,18 +137,18 @@ export class WPService implements IPlatformService { console.error(`获取第 ${page} 页数据失败:`, error); return []; // 如果某页获取失败,返回空数组,不影响整体结果 }); - + batchPromises.push(pagePromise); } // 等待当前批次完成 const batchResults = await Promise.all(batchPromises); - + // 合并当前批次的数据 for (const pageItems of batchResults) { allItems.push(...pageItems); } - + // 移动到下一批次 currentPage += batchSize; } @@ -206,7 +206,7 @@ export class WPService implements IPlatformService { const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( '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; while (hasMore) { const config: AxiosRequestConfig = { @@ -259,8 +259,8 @@ export class WPService implements IPlatformService { // 导出 WooCommerce 产品为特殊CSV(平台特性) async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise { 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 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; } @@ -289,7 +289,7 @@ export class WPService implements IPlatformService { const res = await api.get(`orders/${orderId}`); return res.data as Record; } - async getOrders(siteId: number,params: Record = {}): Promise[]> { + async getOrders(siteId: number, params: Record = {}): Promise[]> { const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, 'orders', params); @@ -367,10 +367,10 @@ export class WPService implements IPlatformService { const api = this.createApi(site, 'wc/v3'); // 确保价格为字符串 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) { - data.sale_price = String(data.sale_price); + data.sale_price = String(data.sale_price); } // 处理标签字段,如果为字符串数组则转换为 WooCommerce 所需的对象数组 if (Array.isArray((data as any).tags)) { @@ -696,7 +696,7 @@ export class WPService implements IPlatformService { Authorization: `Basic ${auth}`, }, }; - + try { const response = await axios.request(config); return response.data || []; @@ -740,19 +740,19 @@ export class WPService implements IPlatformService { ); 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; } @@ -780,7 +780,7 @@ export class WPService implements IPlatformService { }, data: fulfillmentData, }; - + try { const response = await axios.request(config); return response.data; @@ -803,10 +803,10 @@ export class WPService implements IPlatformService { try { const response = await api.post('products/batch', data); const result = response.data; - + // 转换 WooCommerce 批量操作结果为统一格式 - const errors: Array<{identifier: string, error: string}> = []; - + const errors: Array<{ identifier: string, error: string }> = []; + // WooCommerce 返回格式: { create: [...], update: [...], delete: [...] } // 错误信息可能在每个项目的 error 字段中 const checkForErrors = (items: any[]) => { @@ -819,12 +819,12 @@ export class WPService implements IPlatformService { } }); }; - + // 检查每个操作类型的结果中的错误 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), @@ -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 }> { const site = await this.siteService.get(siteId, true); if (!site) { - throw new Error('站点不存在'); + throw new Error('站点不存在'); } const endpoint = 'wp/v2/media'; const apiUrl = site.apiUrl; @@ -1030,15 +1030,15 @@ export class WPService implements IPlatformService { // 构建 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, @@ -1046,7 +1046,7 @@ export class WPService implements IPlatformService { }; } -public async fetchMediaPaged(site: any, params: Partial = {}) { + public async fetchMediaPaged(site: any, params: Partial = {}) { const apiUrl = site.apiUrl; const { consumerKey, consumerSecret } = site as any; const endpoint = 'wp/v2/media'; @@ -1061,15 +1061,15 @@ public async fetchMediaPaged(site: any, params: Partial = } }); // 检查是否有错误信息 - if(response?.data?.message){ + if (response?.data?.message) { throw new Error(`获取${apiUrl}条媒体文件失败,原因为${response.data.message}`) } - if(!Array.isArray(response.data)) { + if (!Array.isArray(response.data)) { throw new Error(`获取${apiUrl}条媒体文件失败,原因为返回数据不是数组`); } 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: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 = // 假设 file 是 MidwayJS 的 file 对象 // MidwayJS 上传文件通常在 tmp 目录,需要读取流 formData.append('file', fs.createReadStream(file.data), { - filename: file.filename, - contentType: file.mimeType, + filename: file.filename, + contentType: file.mimeType, }); // Axios headers for multipart const headers = { - Authorization: `Basic ${auth}`, - 'Content-Disposition': `attachment; filename=${file.filename}`, - ...formData.getHeaders(), + Authorization: `Basic ${auth}`, + 'Content-Disposition': `attachment; filename=${file.filename}`, + ...formData.getHeaders(), }; try { @@ -1205,10 +1205,12 @@ public async fetchMediaPaged(site: any, params: Partial = 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 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) {