import { Inject, Logger, Provide } from '@midwayjs/core'; import { WPService } from './wp.service'; import { Order } from '../entity/order.entity'; import { In, Like, Repository } from 'typeorm'; import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { plainToClass } from 'class-transformer'; import { OrderItem } from '../entity/order_item.entity'; import { OrderSale } from '../entity/order_sale.entity'; import { Product } from '../entity/product.entity'; import { OrderFee } from '../entity/order_fee.entity'; import { OrderRefund } from '../entity/order_refund.entity'; import { OrderRefundItem } from '../entity/order_refund_item.entity'; import { OrderCoupon } from '../entity/order_coupon.entity'; import { OrderShipping } from '../entity/order_shipping.entity'; import { OrderFulfillment } from '../entity/order_fulfillment.entity'; import { Shipment } from '../entity/shipment.entity'; import { Customer } from '../entity/customer.entity'; import { ErpOrderStatus, OrderStatus, StockRecordOperationType, } from '../enums/base.enum'; import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; import dayjs = require('dayjs'); import { OrderDetailRes } from '../dto/reponse.dto'; import { OrderNote } from '../entity/order_note.entity'; import { User } from '../entity/user.entity'; import { SiteService } from './site.service'; import { ShipmentItem } from '../entity/shipment_item.entity'; import { UpdateStockDTO } from '../dto/stock.dto'; import { StockService } from './stock.service'; import { OrderItemOriginal } from '../entity/order_item_original.entity'; import { SiteApiService } from './site-api.service'; import { SyncOperationResult } from '../dto/api.dto'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { UnifiedOrderDTO } from '../dto/site-api.dto'; import { CustomerService } from './customer.service'; import { ProductService } from './product.service'; @Provide() export class OrderService { @Inject() wpService: WPService; @Inject() stockService: StockService; @InjectEntityModel(Order) orderModel: Repository; @InjectEntityModel(User) userModel: Repository; @InjectEntityModel(OrderItem) orderItemModel: Repository; @InjectEntityModel(OrderItem) orderItemOriginalModel: Repository; @InjectEntityModel(OrderSale) orderSaleModel: Repository; @InjectEntityModel(Product) productModel: Repository; @InjectEntityModel(OrderFee) orderFeeModel: Repository; @InjectEntityModel(OrderRefund) orderRefundModel: Repository; @InjectEntityModel(OrderRefundItem) orderRefundItemModel: Repository; @InjectEntityModel(OrderCoupon) orderCouponModel: Repository; @InjectEntityModel(OrderShipping) orderShippingModel: Repository; @InjectEntityModel(OrderFulfillment) orderFulfillmentModel: Repository; @InjectEntityModel(Shipment) shipmentModel: Repository; @InjectEntityModel(ShipmentItem) shipmentItemModel: Repository; @InjectEntityModel(OrderNote) orderNoteModel: Repository; @Inject() dataSourceManager: TypeORMDataSourceManager; @InjectEntityModel(Customer) customerModel: Repository; @Inject() siteService: SiteService; @Inject() siteApiService: SiteApiService; @Inject() customerService: CustomerService; @Logger() logger; // 注入 Logger 实例 @Inject() productService: ProductService; /** * 批量同步订单 * 流程说明: * 1. 调用 WooCommerce API 获取订单列表 * 2. 遍历每个订单,检查数据库中是否已存在 * 3. 调用 syncSingleOrder 同步单个订单 * 4. 统计同步结果(创建数、更新数、错误数) * * 涉及实体: Order * * @param siteId 站点ID * @param params 查询参数 * @returns 同步操作结果 */ async syncOrders(siteId: number, params: Record = {}): Promise { // 调用 WooCommerce API 获取订单 const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); // 初始化同步结果对象 const syncResult: SyncOperationResult = { total: result.length, processed: 0, synced: 0, created: 0, updated: 0, errors: [] }; console.log('开始进入循环同步订单', result.length, '个订单') // 遍历每个订单进行同步 for (const order of result) { try { // 检查订单是否已存在,以区分创建和更新 const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: String(order.id), siteId: siteId }, }); if (!existingOrder) { console.log("数据库中不存在", order.id, '订单状态:', order.status) } // 同步单个订单 await this.syncSingleOrder(siteId, order); // 统计结果 syncResult.processed++; syncResult.synced++; // 根据订单是否存在,更新创建或更新计数 if (existingOrder) { syncResult.updated++; } else { syncResult.created++; } // console.log('updated', syncResult.updated, 'created:', syncResult.created) } catch (error) { // 记录错误但不中断整个同步过程 syncResult.errors.push({ identifier: String(order.id), error: error.message || '同步失败' }); syncResult.processed++; } } console.log('同步完成', syncResult.updated, 'created:', syncResult.created) this.logger.debug('syncOrders result', syncResult) return syncResult; } /** * 根据订单ID同步单个订单 * 流程说明: * 1. 调用 WooCommerce API 获取指定订单 * 2. 检查数据库中是否已存在该订单 * 3. 调用 syncSingleOrder 同步订单数据 * 4. 统计同步结果 * * 涉及实体: Order * * @param siteId 站点ID * @param orderId 订单ID * @returns 同步操作结果 */ async syncOrderById(siteId: number, orderId: string): Promise { const syncResult: SyncOperationResult = { total: 1, processed: 0, synced: 0, created: 0, updated: 0, errors: [] }; try { // 调用 WooCommerce API 获取订单 const adapter = await this.siteApiService.getAdapter(siteId); const order = await adapter.getOrder({ id: orderId }); // 检查订单是否已存在,以区分创建和更新 const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: String(order.id), siteId: siteId }, }); if (!existingOrder) { console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status) } // 同步单个订单 await this.syncSingleOrder(siteId, order, true); // 统计结果 syncResult.processed = 1; syncResult.synced = 1; if (existingOrder) { syncResult.updated = 1; } else { syncResult.created = 1; } return syncResult; } catch (error) { // 记录错误 syncResult.errors.push({ identifier: orderId, error: error.message || '同步失败' }); syncResult.processed = 1; return syncResult; } } /** * 订单状态自动切换映射表 * 用于将 WordPress 订单状态转换为系统内部状态 */ orderAutoNextStatusMap = { [OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded } /** * 自动更新订单状态 * 流程说明: * 1. 检查订单状态是否需要转换 * 2. 如果需要转换,先同步状态到 WooCommerce * 3. 然后将订单状态切换到下一状态 * * 涉及实体: Order * * @param siteId 站点ID * @param order 订单对象 */ async autoUpdateOrderStatus(siteId: number, order: any) { // console.log('更新订单状态', order.status, '=>', this.orderAutoNextStatusMap[order.status]) // 其他状态保持不变 const originStatus = order.status; // 如果有值就赋值 if (!this.orderAutoNextStatusMap[originStatus]) { return } try { const site = await this.siteService.get(siteId); // 仅处理 WooCommerce 站点 if (site.type !== 'woocommerce') { return } // 将订单状态同步到 WooCommerce,然后切换至下一状态 await this.wpService.updateOrder(site, String(order.id), { status: order.status }); order.status = this.orderAutoNextStatusMap[originStatus]; } catch (error) { console.error('更新订单状态失败,原因为:', error) } } async getOrderByExternalOrderId(siteId: number, externalOrderId: string) { return await this.orderModel.findOne({ where: { externalOrderId: String(externalOrderId), siteId }, }); } /** * 同步单个订单 * 流程说明: * 1. 从订单数据中解构出各个子项(line_items, shipping_lines, fee_lines, coupon_lines, refunds, fulfillments) * 2. 检查数据库中是否已存在该订单 * 3. 自动更新订单状态(如果需要) * 4. 如果订单状态为 AUTO_DRAFT,则跳过处理 * 5. 如果订单从未完成变为完成状态,则更新库存并返回 * 6. 如果订单不可编辑且不强制更新,则跳过处理 * 7. 保存订单主数据 * 8. 保存订单项(OrderItem) * 9. 保存退款信息(OrderRefund, OrderRefundItem) * 10. 保存费用信息(OrderFee) * 11. 保存优惠券信息(OrderCoupon) * 12. 保存配送信息(OrderShipping) * 13. 保存履约信息(OrderFulfillment) * * 涉及实体: Order, OrderItem, OrderRefund, OrderRefundItem, OrderFee, OrderCoupon, OrderShipping, OrderFulfillment * * @param siteId 站点ID * @param order 订单数据 * @param forceUpdate 是否强制更新 */ async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) { // 从订单数据中解构出各个子项 let { line_items, shipping_lines, fee_lines, coupon_lines, refunds, fulfillments, // 物流信息 ...orderData } = order; // console.log('同步进单个订单', order) // 如果订单状态为 AUTO_DRAFT,则跳过处理 if (order.status === OrderStatus.AUTO_DRAFT) { this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id) return; } // 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断 // if(!order.is_editable && !forceUpdate){ // this.logger.debug('订单不可编辑,跳过处理', siteId, order.id) // return; // } // 自动转换远程订单的状态(如果需要) await this.autoUpdateOrderStatus(siteId, order); // 这里的 saveOrder 已经包括了创建订单和更新订单 let orderRecord: Order = await this.saveOrder(siteId, orderData); // 如果订单从未完成变为完成状态,则更新库存 if ( orderRecord && orderRecord.orderStatus !== ErpOrderStatus.COMPLETED && orderData.status === OrderStatus.COMPLETED ) { await this.updateStock(orderRecord); // 不再直接返回,继续执行后续的更新操作 } const externalOrderId = String(order.id); const orderId = orderRecord.id; // 保存订单项 await this.saveOrderItems({ siteId, orderId, externalOrderId: String(externalOrderId), orderItems: line_items, }); // 保存退款信息 await this.saveOrderRefunds({ siteId, orderId, externalOrderId , refunds, }); // 保存费用信息 await this.saveOrderFees({ siteId, orderId, externalOrderId, fee_lines, }); // 保存优惠券信息 await this.saveOrderCoupons({ siteId, orderId, externalOrderId, coupon_lines, }); // 保存配送信息 await this.saveOrderShipping({ siteId, orderId, externalOrderId, shipping_lines, }); // 保存履约信息 await this.saveOrderFulfillments({ siteId, orderId, externalOrderId, fulfillments: fulfillments, }); } /** * 更新订单库存 * 流程说明: * 1. 查询订单的所有销售项(OrderSale) * 2. 遍历每个销售项,创建库存更新记录 * 3. 调用 StockService 更新库存(出库操作) * * 涉及实体: OrderSale, Stock * * @param existingOrder 已存在的订单 */ async updateStock(existingOrder: Order) { const items = await this.orderSaleModel.find({ where: { orderId: existingOrder.id }, }); if (!items) return; const stockPointId = 2; // ['YT', 'NT', 'BC', 'AB', 'SK'].some( // v => // v.toLowerCase() === // ( // existingOrder?.shipping?.state || existingOrder?.billing?.state // ).toLowerCase() // ) // ? 3 // : 2; for (const item of items) { const updateStock = new UpdateStockDTO(); updateStock.stockPointId = stockPointId; updateStock.sku = item.sku; updateStock.quantityChange = item.quantity; updateStock.operationType = StockRecordOperationType.OUT; updateStock.operatorId = 1; updateStock.note = `订单${existingOrder.externalOrderId} 出库`; await this.stockService.updateStock(updateStock); } } /** * 保存订单主数据 * 流程说明: * 1. 将外部订单ID转换为字符串 * 2. 创建订单实体对象 * 3. 检查数据库中是否已存在该订单 * 4. 如果存在: * - 检查是否可以更新 ERP 状态 * - 如果可以,则更新订单状态并保存 * 5. 如果不存在: * - 映射订单状态 * - 创建或更新客户信息 * - 保存新订单 * * 涉及实体: Order, Customer * * @param siteId 站点ID * @param order 订单数据 * @returns 保存后的订单实体 */ // 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等 async saveOrder(siteId: number, order: Omit): Promise { // 将外部订单ID转换为字符串 const externalOrderId = String(order.id) delete order.id // 创建订单实体对象 const entity = plainToClass(Order, { ...order, externalOrderId, siteId }); // 检查数据库中是否已存在该订单 const existingOrder = await this.orderModel.findOne({ where: { externalOrderId, siteId: siteId }, }); // 如果订单已存在 if (existingOrder) { // 检查是否可以更新 ERP 状态 if (this.canUpdateErpStatus(existingOrder.orderStatus)) { entity.orderStatus = this.mapOrderStatus(entity.status); } else { // 如果不能更新 ERP 状态,则保留原有的 orderStatus entity.orderStatus = existingOrder.orderStatus; } // 更新订单数据(包括 shipping、billing 等字段) await this.orderModel.update(existingOrder.id, entity); entity.id = existingOrder.id; return entity; } // 如果订单不存在,则映射订单状态 entity.orderStatus = this.mapOrderStatus(entity.status); // 创建或更新客户信息 await this.customerService.upsertCustomer({ email: order.customer_email, site_id: siteId, origin_id: String(order.customer_id), billing: order.billing, shipping: order.shipping, first_name: order?.billing?.first_name || order?.shipping?.first_name, last_name: order?.billing?.last_name || order?.shipping?.last_name, fullname: order?.billing?.fullname || order?.shipping?.fullname, phone: order?.billing?.phone || order?.shipping?.phone, // tags:['fromOrder'] }); // const customer = await this.customerModel.findOne({ // where: { email: order.customer_email }, // }); // if (!customer) { // // 这里用 customer create // await this.customerModel.save({ // email: order.customer_email, // site_id: siteId, // origin_id: String(order.customer_id), // billing: order.billing, // shipping: order.shipping, // phone: order?.billing?.phone || order?.shipping?.phone, // }); // } // 保存新订单 return await this.orderModel.save(entity); } /** * 检查是否可以更新 ERP 状态 * 某些状态不允许被覆盖,如: AFTER_SALE_PROCESSING, PENDING_RESHIPMENT, PENDING_REFUND * * @param currentErpStatus 当前 ERP 状态 * @returns 是否可以更新 */ canUpdateErpStatus(currentErpStatus: string): boolean { const nonOverridableStatuses = [ 'AFTER_SALE_PROCESSING', 'PENDING_RESHIPMENT', 'PENDING_REFUND', ]; // 如果当前 ERP 状态不可覆盖,则禁止更新 return !nonOverridableStatuses.includes(currentErpStatus); } /** * 映射订单状态 * 将 WooCommerce 订单状态转换为 ERP 订单状态 * * @param status WooCommerce 订单状态 * @returns ERP 订单状态 */ mapOrderStatus(status: OrderStatus): ErpOrderStatus { switch (status) { case OrderStatus.PENDING: return ErpOrderStatus.PENDING; case OrderStatus.PROCESSING: return ErpOrderStatus.PROCESSING; case OrderStatus.COMPLETED: return ErpOrderStatus.COMPLETED; case OrderStatus.CANCEL: return ErpOrderStatus.CANCEL; case OrderStatus.REFUNDED: return ErpOrderStatus.REFUNDED; case OrderStatus.FAILED: return ErpOrderStatus.FAILED; case OrderStatus.RETURN_REQUESTED: return ErpOrderStatus.RETURN_REQUESTED; case OrderStatus.RETURN_APPROVED: return ErpOrderStatus.RETURN_APPROVED; case OrderStatus.RETURN_CANCELLED: return ErpOrderStatus.RETURN_CANCELLED; default: return ErpOrderStatus.PENDING; } } /** * 保存订单项 * 流程说明: * 1. 查询数据库中已存在的订单项 * 2. 找出需要删除的订单项(在数据库中存在但在新数据中不存在) * 3. 删除这些订单项及其对应的销售项(OrderSale) * 4. 遍历新的订单项,设置相关字段 * 5. 保存每个订单项 * 6. 为每个订单项创建对应的销售项(OrderSale) * * 涉及实体: OrderItem, OrderSale * * @param params 订单项参数 */ async saveOrderItems(params: { siteId: number; orderId: number; externalOrderId: string; orderItems: Record[]; }) { // 查询数据库中已存在的订单项 const { siteId, orderId, externalOrderId, orderItems } = params; const currentOrderItems = await this.orderItemModel.find({ where: { siteId, externalOrderId: externalOrderId }, }); // 找出需要删除的订单项(在数据库中存在但在新数据中不存在) const syncedOrderItemIds = new Set(orderItems.map(v => String(v.id))); const orderItemToDelete = currentOrderItems.filter( db => !syncedOrderItemIds.has(String(db.externalOrderItemId)) ); // 删除这些订单项及其对应的销售项(OrderSale) if (orderItemToDelete.length > 0) { const idsToDelete = orderItemToDelete.map(v => v.id); await this.orderItemModel.delete(idsToDelete); const itemIdsToDelete = orderItemToDelete.map(v => v.externalOrderItemId); await this.orderSaleModel.delete({ siteId, externalOrderItemId: In(itemIdsToDelete), }); } // 遍历新的订单项,设置相关字段并保存 for (const orderItem of orderItems) { orderItem.siteId = siteId; orderItem.externalOrderId = externalOrderId; orderItem.externalOrderItemId = String(orderItem.id); orderItem.orderId = orderId; orderItem.externalProductId = String(orderItem.product_id); orderItem.externalVariationId = String(orderItem.variation_id); delete orderItem.id; const entity = plainToClass(OrderItem, orderItem); // 保存订单项 await this.saveOrderItem(entity); // 为每个订单项创建对应的销售项(OrderSale) await this.saveOrderSale(entity); } } /** * 保存单个订单项 * 流程说明: * 1. 检查数据库中是否已存在该订单项 * 2. 如果存在,则更新订单项 * 3. 如果不存在,则创建新订单项 * * 涉及实体: OrderItem * * @param orderItem 订单项实体 */ async saveOrderItem(orderItem: OrderItem) { const existingOrderItem = await this.orderItemModel.findOne({ where: { externalOrderId: orderItem.externalOrderId, siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId, }, }); if ( existingOrderItem && existingOrderItem.quantity === orderItem.quantity ) { return; } if (existingOrderItem) { await this.orderItemModel.update(existingOrderItem.id, orderItem); } else { await this.orderItemModel.save(orderItem); } } /** * 保存单个订单项(备用方法) * 流程说明: * 1. 检查数据库中是否已存在该订单项 * 2. 如果数量相同,则跳过处理 * 3. 如果存在但数量不同,则更新订单项 * 4. 如果不存在,则创建新订单项 * * 注意: 该方法与 saveOrderItem 功能相同,可能是备用或待删除的方法 * * 涉及实体: OrderItem * * @param orderItem 订单项实体 */ async saveOrderItemsG(orderItem: OrderItem) { const existingOrderItem = await this.orderItemModel.findOne({ where: { externalOrderId: orderItem.externalOrderId, siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId, }, }); if ( existingOrderItem && existingOrderItem.quantity === orderItem.quantity ) { return; } if (existingOrderItem) { await this.orderItemModel.update(existingOrderItem.id, orderItem); } else { await this.orderItemModel.save(orderItem); } } /** * 保存订单销售项 * 流程说明: * 1. 查询数据库中已存在的销售项 * 2. 删除已存在的销售项 * 3. 如果订单项没有 SKU,则跳过处理 * 4. 查询产品信息(包含组件信息) * 5. 如果产品有组件,则为每个组件创建销售项 * 6. 如果产品没有组件,则直接创建销售项 * 7. 保存所有销售项 * * 涉及实体: OrderSale, Product * * @param orderItem 订单项实体 */ // TODO 这里存的是库存商品实际 // 所以叫做 orderInventoryItems 可能更合适 async saveOrderSale(orderItem: OrderItem) { const currentOrderSale = await this.orderSaleModel.find({ where: { siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId, }, }); if (currentOrderSale.length > 0) { await this.orderSaleModel.delete(currentOrderSale.map(v => v.id)); } if (!orderItem.sku) return; // 从数据库查询产品,关联查询组件 const product = await this.productModel.findOne({ where: { siteSkus: Like(`%${orderItem.sku}%`) }, relations: ['components','attributes','attributes.dict'], }); if (!product) return; const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => { return { product: await this.productModel.findOne({ where: { sku: comp.sku }, relations: ['components', 'attributes','attributes.dict'], }), quantity: comp.quantity * orderItem.quantity, } })) : [{ product, quantity: orderItem.quantity }] const orderSales: OrderSale[] = componentDetails.map(componentDetail => { if (!componentDetail.product) return null const attrsObj = this.productService.getAttributesObject(product.attributes) const orderSale = plainToClass(OrderSale, { orderId: orderItem.orderId, siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId,// 原始 itemId parentProductId: product.id, // 父产品 ID 用于统计套餐 如果是单品则不记录 productId: componentDetail.product.id, isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品 name: componentDetail.product.name, quantity: componentDetail.quantity * orderItem.quantity, sku: componentDetail.product.sku, // 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。 brand: attrsObj?.['brand']?.name, version: attrsObj?.['version']?.name, strength: attrsObj?.['strength']?.name, flavor: attrsObj?.['flavor']?.name, humidity: attrsObj?.['humidity']?.name, size: attrsObj?.['size']?.name, category: componentDetail.product.category.name, }); return orderSale }).filter(v => v !== null) console.log("orderSales",orderSales) if (orderSales.length > 0) { await this.orderSaleModel.save(orderSales); } } // // extract stren // extractNumberFromString(str: string): number { // if (!str) return 0; // const num = parseInt(str, 10); // return isNaN(num) ? 0 : num; // } /** * 保存订单退款信息 * 流程说明: * 1. 遍历退款列表,为每个退款项获取详细信息 * 2. 设置退款相关字段(orderId、siteId、externalOrderId、externalRefundId) * 3. 检查数据库中是否已存在该退款 * 4. 如果存在,则更新退款信息 * 5. 如果不存在,则创建新退款 * 6. 保存退款项(OrderRefundItem) * * 涉及实体: OrderRefund, OrderRefundItem * * @param params 退款参数 */ async saveOrderRefunds({ siteId, orderId, externalOrderId, refunds, }: { siteId: number; orderId: number; externalOrderId: string; refunds: Record[]; }) { for (const item of refunds) { const refund = await this.wpService.getOrderRefund( String(siteId), externalOrderId, item.id ); const refundItems = refund.line_items; refund.orderId = orderId; refund.siteId = siteId; refund.externalOrderId = externalOrderId; refund.externalRefundId = item.id; delete refund.id; const entity = plainToClass(OrderRefund, refund); const existingOrderRefund = await this.orderRefundModel.findOne({ where: { siteId, externalOrderId: externalOrderId, externalRefundId: item.id, }, }); let refundId; if (existingOrderRefund) { await this.orderRefundModel.update(existingOrderRefund.id, entity); refundId = existingOrderRefund.id; } else { refundId = (await this.orderRefundModel.save(entity)).id; } this.saveOrderRefundItems({ refundItems, siteId, refundId, externalRefundId: item.id, }); } } /** * 保存订单退款项 * 流程说明: * 1. 遍历退款项列表 * 2. 设置退款项相关字段(externalRefundItemId、externalProductId、externalVariationId) * 3. 创建退款项实体 * 4. 检查数据库中是否已存在该退款项 * 5. 如果存在,则更新退款项 * 6. 如果不存在,则创建新退款项 * * 涉及实体: OrderRefundItem * * @param params 退款项参数 */ async saveOrderRefundItems({ refundItems, siteId, refundId, externalRefundId, }) { for (const item of refundItems) { item.externalRefundItemId = item.id; item.externalProductId = item.product_id; item.externalVariationId = item.variation_id; delete item.id; const refundItem = plainToClass(OrderRefundItem, { refundId, siteId, externalRefundId, ...item, }); const existingOrderRefundItem = await this.orderRefundItemModel.findOne({ where: { siteId, externalRefundId, externalRefundItemId: refundItem.externalRefundItemId, }, }); if (existingOrderRefundItem) { await this.orderRefundItemModel.update( existingOrderRefundItem.id, refundItem ); } else { await this.orderRefundItemModel.save(refundItem); } } } /** * 保存订单费用信息 * 流程说明: * 1. 查询数据库中已存在的费用项 * 2. 找出需要删除的费用项(在数据库中存在但在新数据中不存在) * 3. 删除这些费用项 * 4. 遍历新的费用项,设置相关字段 * 5. 检查数据库中是否已存在该费用项 * 6. 如果存在,则更新费用项 * 7. 如果不存在,则创建新费用项 * * 涉及实体: OrderFee * * @param params 费用参数 */ async saveOrderFees({ siteId, orderId, externalOrderId, fee_lines }) { const currentOrderFees = await this.orderFeeModel.find({ where: { siteId, externalOrderId }, }); const syncedFeeIds = new Set(fee_lines.map(v => String(v.id))); const toDeleteIds = currentOrderFees .filter(db => !syncedFeeIds.has(db.externalOrderFeeId)) .map(db => db.id); if (toDeleteIds.length > 0) { await this.orderFeeModel.delete(toDeleteIds); } for (const fee of fee_lines) { fee.externalOrderFeeId = String(fee.id); delete fee.id; const entity = plainToClass(OrderFee, { siteId, orderId, externalOrderId, ...fee, }); const db = await this.orderFeeModel.findOne({ where: { siteId, externalOrderId, externalOrderFeeId: entity.externalOrderFeeId, }, }); if (db) { await this.orderFeeModel.update(db.id, entity); } else { await this.orderFeeModel.save(entity); } } } /** * 保存订单优惠券信息 * 流程说明: * 1. 遍历优惠券列表 * 2. 设置优惠券相关字段(externalOrderCouponId、siteId、orderId、externalOrderId) * 3. 创建优惠券实体 * 4. 检查数据库中是否已存在该优惠券 * 5. 如果存在,则更新优惠券 * 6. 如果不存在,则创建新优惠券 * * 涉及实体: OrderCoupon * * @param params 优惠券参数 */ async saveOrderCoupons({ siteId, externalOrderId, orderId, coupon_lines }) { for (const item of coupon_lines) { item.externalOrderCouponId = item.id; item.siteId = siteId; item.orderId = orderId; item.externalOrderId = externalOrderId; delete item.id; const coupon = plainToClass(OrderCoupon, item); const existingOrderCoupon = await this.orderCouponModel.findOne({ where: { siteId, externalOrderId, externalOrderCouponId: coupon.externalOrderCouponId, }, }); if (existingOrderCoupon) { await this.orderCouponModel.update(existingOrderCoupon.id, coupon); } else { await this.orderCouponModel.save(coupon); } } } /** * 保存订单配送信息 * 流程说明: * 1. 遍历配送列表 * 2. 设置配送相关字段(externalOrderShippingId、siteId、orderId、externalOrderId) * 3. 创建配送实体 * 4. 检查数据库中是否已存在该配送 * 5. 如果存在,则更新配送 * 6. 如果不存在,则创建新配送 * * 涉及实体: OrderShipping * * @param params 配送参数 */ async saveOrderShipping({ siteId, orderId, externalOrderId, shipping_lines, }) { for (const item of shipping_lines) { item.externalOrderShippingId = item.id; item.siteId = siteId; item.orderId = orderId; item.externalOrderId = externalOrderId; delete item.id; const shipping = plainToClass(OrderShipping, item); const existingOrderShipping = await this.orderShippingModel.findOne({ where: { siteId, externalOrderId, externalOrderShippingId: shipping.externalOrderShippingId, }, }); if (existingOrderShipping) { await this.orderShippingModel.update( existingOrderShipping.id, shipping ); } else { await this.orderShippingModel.save(shipping); } } } /** * 保存订单履约信息 * 流程说明: * 1. 检查履约列表是否存在,如果不存在则跳过 * 2. 遍历履约列表 * 3. 设置履约相关字段(externalFulfillmentId、siteId、orderId、externalOrderId) * 4. 转换时间戳为日期格式 * 5. 创建履约实体 * 6. 检查数据库中是否已存在该履约 * 7. 如果存在,则更新履约 * 8. 如果不存在,则创建新履约 * * 涉及实体: OrderFulfillment * * @param params 履约参数 */ async saveOrderFulfillments({ siteId, orderId, externalOrderId, fulfillments, }) { // 如果履约列表不存在,则跳过处理 if (!fulfillments || !Array.isArray(fulfillments) || fulfillments.length === 0) { return; } // 遍历履约列表 for (const item of fulfillments) { // 设置履约相关字段 item.external_fulfillment_id = String(item.id); item.site_id = siteId; item.order_id = orderId; item.external_order_id = String(externalOrderId); // 删除原始 ID delete item.id; // 转换时间戳为日期格式 if (item.date_created && typeof item.date_created === 'number') { item.date_created = new Date(item.date_created * 1000); } else if (item.date_created && typeof item.date_created === 'string') { item.date_created = new Date(item.date_created); } // 创建履约实体 const fulfillment = plainToClass(OrderFulfillment, item); // 检查数据库中是否已存在该履约 const existingOrderFulfillment = await this.orderFulfillmentModel.findOne({ where: { site_id: siteId, external_order_id: externalOrderId, external_fulfillment_id: fulfillment.external_fulfillment_id, }, }); // 如果存在,则更新履约 if (existingOrderFulfillment) { await this.orderFulfillmentModel.update( existingOrderFulfillment.id, fulfillment ); } else { // 如果不存在,则创建新履约 await this.orderFulfillmentModel.save(fulfillment); } } } /** * 获取订单列表 * 流程说明: * 1. 构建基础SQL查询,包含订单基本信息、客户统计、订阅信息、物流信息 * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、状态、关键字等) * 3. 检查用户权限,如果用户有"order-10-days"权限且未指定日期范围,则限制查询最近10天的订单 * 4. 执行总数查询 * 5. 添加分页和排序,执行主查询 * 6. 返回订单列表和分页信息 * * 涉及实体: Order, Subscription, Shipment, Customer * * @param params 查询参数 * @param userId 用户ID(用于权限检查) * @returns 订单列表和分页信息 */ async getOrders({ externalOrderId, siteId, startDate, endDate, status, keyword, current, pageSize, customer_email, payment_method, billing_phone, isSubscriptionOnly = false, }, userId = undefined) { const parameters: any[] = []; // 基础查询 let sqlQuery = ` SELECT o.id as id, o.externalOrderId as externalOrderId, o.siteId as siteId, o.date_paid as date_paid, o.total as total, o.date_created as date_created, o.customer_email as customer_email, o.exchange_frequency as exchange_frequency, o.transaction_id as transaction_id, o.orderStatus as orderStatus, o.customer_ip_address as customer_ip_address, o.device_type as device_type, o.source_type as source_type, o.utm_source as utm_source, o.customer_note as customer_note, o.shipping as shipping, o.billing as billing, o.payment_method as payment_method, cs.order_count as order_count, cs.total_spent as total_spent, CASE WHEN EXISTS ( SELECT 1 FROM subscription s WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId ) THEN 1 ELSE 0 END AS isSubscription, ( SELECT COALESCE( JSON_ARRAYAGG( JSON_OBJECT( 'id', s.id, 'externalSubscriptionId', s.externalSubscriptionId, 'status', s.status, 'currency', s.currency, 'total', s.total, 'billing_period', s.billing_period, 'billing_interval', s.billing_interval, 'customer_id', s.customer_id, 'customer_email', s.customer_email, 'parent_id', s.parent_id, 'start_date', s.start_date, 'trial_end', s.trial_end, 'next_payment_date', s.next_payment_date, 'end_date', s.end_date, 'line_items', s.line_items, 'meta_data', s.meta_data ) ), JSON_ARRAY() ) FROM subscription s WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId ) AS related, COALESCE( JSON_ARRAYAGG( CASE WHEN s.id IS NOT NULL THEN JSON_OBJECT( 'state', s.state, 'tracking_provider', s.tracking_provider, 'primary_tracking_number', s.primary_tracking_number ) END ), JSON_ARRAY() ) as shipmentList, COALESCE( JSON_ARRAYAGG( CASE WHEN fulfillment.id IS NOT NULL THEN JSON_OBJECT( 'id', fulfillment.id, 'externalFulfillmentId', fulfillment.external_fulfillment_id, 'orderId', fulfillment.order_id, 'siteId', fulfillment.site_id, 'status', fulfillment.status, 'createdAt', fulfillment.created_at, 'updatedAt', fulfillment.updated_at, 'tracking_number', fulfillment.tracking_number, 'shipping_provider', fulfillment.shipping_provider, 'shipping_method', fulfillment.shipping_method, 'items', fulfillment.items ) END ), JSON_ARRAY() ) as fulfillments FROM \`order\` o LEFT JOIN ( SELECT o.customer_email AS customer_email, COUNT(o.id) AS order_count, SUM(o.total) AS total_spent FROM \`order\` o WHERE o.status IN ('processing', 'completed') GROUP BY o.customer_email ) cs ON cs.customer_email = o.customer_email LEFT JOIN order_shipment os ON os.order_id = o.id LEFT JOIN shipment s ON s.id = os.shipment_id LEFT JOIN order_fulfillment fulfillment ON fulfillment.order_id = o.id WHERE 1=1 `; // 计算总数 let totalQuery = ` SELECT COUNT(*) as total FROM \`order\` o LEFT JOIN ( SELECT o.customer_email AS customer_email FROM \`order\` o WHERE o.status IN ('processing', 'completed') GROUP BY o.customer_email ) cs ON cs.customer_email = o.customer_email WHERE 1=1 `; // 动态添加过滤条件 if (externalOrderId) { sqlQuery += ` AND o.externalOrderId = ?`; totalQuery += ` AND o.externalOrderId = ?`; parameters.push(externalOrderId); } if (siteId) { sqlQuery += ` AND o.siteId = ?`; totalQuery += ` AND o.siteId = ?`; parameters.push(siteId); } if (startDate) { sqlQuery += ` AND o.date_paid >= ?`; totalQuery += ` AND o.date_paid >= ?`; parameters.push(startDate); } if (endDate) { sqlQuery += ` AND o.date_paid <= ?`; totalQuery += ` AND o.date_paid <= ?`; parameters.push(endDate); } // 支付方式筛选(使用参数化,避免SQL注入) if (payment_method) { sqlQuery += ` AND o.payment_method LIKE ?`; totalQuery += ` AND o.payment_method LIKE ?`; parameters.push(`%${payment_method}%`); } const user = await this.userModel.findOneBy({ id: userId }); if (user?.permissions?.includes('order-10-days') && !startDate && !endDate) { sqlQuery += ` AND o.date_created >= ?`; totalQuery += ` AND o.date_created >= ?`; const tenDaysAgo = new Date(); tenDaysAgo.setDate(tenDaysAgo.getDate() - 10); parameters.push(tenDaysAgo.toISOString()); } // 处理 status 参数 if (status) { if (Array.isArray(status)) { sqlQuery += ` AND o.orderStatus IN (${status .map(() => '?') .join(', ')})`; totalQuery += ` AND o.orderStatus IN (${status .map(() => '?') .join(', ')})`; parameters.push(...status); } else { sqlQuery += ` AND o.orderStatus = ?`; totalQuery += ` AND o.orderStatus = ?`; parameters.push(status); } } // 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储) if (isSubscriptionOnly) { const subCond = ` AND ( EXISTS ( SELECT 1 FROM subscription s WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId ) ) `; sqlQuery += subCond; totalQuery += subCond; } if (customer_email) { sqlQuery += ` AND o.customer_email LIKE ?`; totalQuery += ` AND o.customer_email LIKE ?`; parameters.push(`%${customer_email}%`); } if (billing_phone) { sqlQuery += ` AND o.billing_phone LIKE ?`; totalQuery += ` AND o.billing_phone LIKE ?`; parameters.push(`%${billing_phone}%`); } // 关键字搜索 if (keyword) { sqlQuery += ` AND EXISTS ( SELECT 1 FROM order_item oi WHERE oi.orderId = o.id AND oi.name LIKE ? ) `; totalQuery += ` AND EXISTS ( SELECT 1 FROM order_item oi WHERE oi.orderId = o.id AND oi.name LIKE ? ) `; parameters.push(`%${keyword}%`); } // 执行获取总数的查询 const totalResult = await this.orderModel.query(totalQuery, parameters); const total = totalResult[0]?.total || 0; // 添加分页到主查询 sqlQuery += ` GROUP BY o.id ORDER BY o.date_paid DESC LIMIT ? OFFSET ? `; parameters.push(pageSize, (current - 1) * pageSize); // 执行查询 const orders = await this.orderModel.query(sqlQuery, parameters); return { items: orders, total, current, pageSize }; } /** * 获取订单状态统计 * 流程说明: * 1. 构建查询,按订单状态分组统计订单数量 * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、关键字等) * 3. 支持关键字搜索,检查订单项名称是否包含关键字 * 4. 支持订阅订单过滤 * 5. 返回各状态的订单数量统计 * * 涉及实体: Order, OrderItem, Subscription * * @param params 查询参数 * @returns 订单状态统计列表 */ async getOrderStatus({ externalOrderId, siteId, startDate, endDate, keyword, customer_email, billing_phone, isSubscriptionOnly = false, }: any) { const query = this.orderModel .createQueryBuilder('order') .select('order.orderStatus', 'status') .addSelect('COUNT(*)', 'count') .groupBy('order.orderStatus'); if (externalOrderId) { query.andWhere('order.externalOrderId = :externalOrderId', { externalOrderId, }); } if (siteId) { query.andWhere('order.siteId = :siteId', { siteId }); } if (startDate) { query.andWhere('order.date_created >= :startDate', { startDate }); } if (endDate) { query.andWhere('order.date_created <= :endDate', { endDate }); } if (customer_email) query.andWhere('order.customer_email LIKE :customer_email', { customer_email: `%${customer_email}%`, }); // 🔥 关键字搜索:检查 order_item.name 是否包含 keyword if (keyword) { query.andWhere( `EXISTS ( SELECT 1 FROM order_item oi WHERE oi.orderId = order.id AND oi.name LIKE :keyword )`, { keyword: `%${keyword}%` } ); } if (isSubscriptionOnly) { query.andWhere(`( EXISTS ( SELECT 1 FROM subscription s WHERE s.siteId = order.siteId AND s.parent_id = order.externalOrderId ) )`); } return await query.getRawMany(); } /** * 获取订单销售统计 * 流程说明: * 1. 查询总条数(按产品ID去重) * 2. 分页查询产品基础信息(产品ID、名称、总数量、订单数) * 3. 批量统计当前页产品的历史复购情况(第1次、第2次、第3次、>3次订单的数量和盒数) * 4. 统计总量(时间段内的总数量、YOONE各规格数量、ZEX数量) * 5. 支持按产品名称关键字过滤 * 6. 支持排除套餐订单(exceptPackage) * * 涉及实体: OrderSale, Order * * @param params 查询参数 * @returns 销售统计和分页信息 */ async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'); startDate = (startDate as any) || defaultStart as any; endDate = (endDate as any) || defaultEnd as any; const offset = (current - 1) * pageSize; // ------------------------- // 1. 查询总条数 // ------------------------- const countParams: any[] = [startDate, endDate]; let countSql = ` SELECT COUNT(DISTINCT os.productId) AS totalCount FROM order_sale os INNER JOIN \`order\` o ON o.id = os.orderId WHERE o.date_paid BETWEEN ? AND ? AND o.status IN ('completed','processing') `; if (siteId) { countSql += ' AND os.siteId = ?'; countParams.push(siteId); } if (nameKeywords.length > 0) { countSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; countParams.push(...nameKeywords.map(w => `%${w}%`)); } const [countResult] = await this.orderSaleModel.query(countSql, countParams); const totalCount = Number(countResult?.totalCount || 0); // ------------------------- // 2. 分页查询 product 基础信息 // ------------------------- const itemParams: any[] = [startDate, endDate]; let nameCondition = ''; if (nameKeywords.length > 0) { nameCondition = ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; itemParams.push(...nameKeywords.map(w => `%${w}%`)); } let itemSql = ` SELECT os.productId, os.name, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders FROM order_sale os INNER JOIN \`order\` o ON o.id = os.orderId WHERE o.date_paid BETWEEN ? AND ? AND o.status IN ('completed','processing') `; if (siteId) { itemSql += ' AND os.siteId = ?'; itemParams.push(siteId); } if (exceptPackage) { itemSql += ` AND os.orderId IN ( SELECT orderId FROM order_sale GROUP BY orderId HAVING COUNT(*) = 1 ) `; } itemSql += nameCondition; itemSql += ` GROUP BY os.productId, os.name ORDER BY totalQuantity DESC LIMIT ? OFFSET ? `; itemParams.push(pageSize, offset); const items = await this.orderSaleModel.query(itemSql, itemParams); // ------------------------- // 3. 批量统计当前页 product 历史复购 // ------------------------- if (items.length > 0) { const productIds = items.map(i => i.productId); const pcParams: any[] = [...productIds, startDate, endDate]; if (siteId) pcParams.push(siteId); let pcSql = ` SELECT os.productId, SUM(CASE WHEN t.purchaseIndex = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount, COUNT(DISTINCT CASE WHEN t.purchaseIndex = 1 THEN os.orderId END) AS firstOrderCount, SUM(CASE WHEN t.purchaseIndex = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount, COUNT(DISTINCT CASE WHEN t.purchaseIndex = 2 THEN os.orderId END) AS secondOrderCount, SUM(CASE WHEN t.purchaseIndex = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount, COUNT(DISTINCT CASE WHEN t.purchaseIndex = 3 THEN os.orderId END) AS thirdOrderCount, SUM(CASE WHEN t.purchaseIndex > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount, COUNT(DISTINCT CASE WHEN t.purchaseIndex > 3 THEN os.orderId END) AS moreThirdOrderCount FROM order_sale os INNER JOIN ( SELECT o2.id AS orderId, @idx := IF(@prev_email = o2.customer_email, @idx + 1, 1) AS purchaseIndex, @prev_email := o2.customer_email FROM \`order\` o2 CROSS JOIN (SELECT @idx := 0, @prev_email := '') vars WHERE o2.status IN ('completed','processing') ORDER BY o2.customer_email, o2.date_paid ) t ON t.orderId = os.orderId WHERE os.productId IN (${productIds.map(() => '?').join(',')}) AND os.orderId IN ( SELECT id FROM \`order\` WHERE date_paid BETWEEN ? AND ? ${siteId ? 'AND siteId = ?' : ''} ) `; if (exceptPackage) { pcSql += ` AND os.orderId IN ( SELECT orderId FROM order_sale GROUP BY orderId HAVING COUNT(*) = 1 ) `; } pcSql += ` GROUP BY os.productId `; console.log('------3.5-----', pcSql, pcParams, exceptPackage); const pcResults = await this.orderSaleModel.query(pcSql, pcParams); const pcMap = new Map(); pcResults.forEach(r => pcMap.set(r.productId, r)); items.forEach(i => { const r = pcMap.get(i.productId) || {}; i.firstOrderYOONEBoxCount = Number(r.firstOrderYOONEBoxCount || 0); i.firstOrderCount = Number(r.firstOrderCount || 0); i.secondOrderYOONEBoxCount = Number(r.secondOrderYOONEBoxCount || 0); i.secondOrderCount = Number(r.secondOrderCount || 0); i.thirdOrderYOONEBoxCount = Number(r.thirdOrderYOONEBoxCount || 0); i.thirdOrderCount = Number(r.thirdOrderCount || 0); i.moreThirdOrderYOONEBoxCount = Number(r.moreThirdOrderYOONEBoxCount || 0); i.moreThirdOrderCount = Number(r.moreThirdOrderCount || 0); }); } // ------------------------- // 4. 总量统计(时间段 + siteId) // ------------------------- const totalParams: any[] = [startDate, endDate]; const yooneParams: any[] = [startDate, endDate]; let totalSql = ` SELECT SUM(os.quantity) AS totalQuantity FROM order_sale os INNER JOIN \`order\` o ON o.id = os.orderId WHERE o.date_paid BETWEEN ? AND ? AND o.status IN ('completed','processing') `; let yooneSql = ` SELECT SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '3mg' THEN os.quantity ELSE 0 END) AS yoone3Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '6mg' THEN os.quantity ELSE 0 END) AS yoone6Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '9mg' THEN os.quantity ELSE 0 END) AS yoone9Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '15mg' THEN os.quantity ELSE 0 END) AS yoone15Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '18mg' THEN os.quantity ELSE 0 END) AS yoone18Quantity, SUM(CASE WHEN os.brand = 'zex' THEN os.quantity ELSE 0 END) AS zexQuantity FROM order_sale os INNER JOIN \`order\` o ON o.id = os.orderId WHERE o.date_paid BETWEEN ? AND ? AND o.status IN ('completed','processing') `; if (siteId) { totalSql += ' AND os.siteId = ?'; totalParams.push(siteId); yooneSql += ' AND os.siteId = ?'; yooneParams.push(siteId); } if (nameKeywords.length > 0) { totalSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; totalParams.push(...nameKeywords.map(w => `%${w}%`)); } const [totalResult] = await this.orderSaleModel.query(totalSql, totalParams); const [yooneResult] = await this.orderSaleModel.query(yooneSql, yooneParams); return { items, total: totalCount, // ✅ 总条数 totalQuantity: Number(totalResult.totalQuantity || 0), yoone3Quantity: Number(yooneResult.yoone3Quantity || 0), yoone6Quantity: Number(yooneResult.yoone6Quantity || 0), yoone9Quantity: Number(yooneResult.yoone9Quantity || 0), yoone12Quantity: Number(yooneResult.yoone12Quantity || 0), yoone12QuantityNew: Number(yooneResult.yoone12QuantityNew || 0), yoone15Quantity: Number(yooneResult.yoone15Quantity || 0), yoone18Quantity: Number(yooneResult.yoone18Quantity || 0), zexQuantity: Number(yooneResult.zexQuantity || 0), current, pageSize, }; } /** * 获取订单项统计 * 流程说明: * 1. 使用CTE计算每个客户对每个产品的购买次数 * 2. 查询订单项统计信息(外部产品ID、变体ID、名称、总数量、订单数) * 3. 统计不同购买次数的订单数(第1次、第2次、第3次、>3次) * 4. 支持按产品名称关键字过滤 * 5. 支持分页查询 * 6. 返回订单项统计和分页信息 * * 涉及实体: OrderItem, Order * * @param params 查询参数 * @returns 订单项统计和分页信息 */ async getOrderItems({ current, pageSize, siteId, startDate, endDate, sku, name, }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'); startDate = (startDate as any) || defaultStart as any; endDate = (endDate as any) || defaultEnd as any; // 分页查询 let sqlQuery = ` WITH product_purchase_counts AS ( SELECT o.customer_email,oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name, COUNT(DISTINCT o.id,oi.siteId,oi.externalProductId,oi.externalVariationId) AS order_count FROM \`order\` o JOIN order_item oi ON o.id = oi.orderId WHERE o.status IN ('completed', 'processing') GROUP BY o.customer_email, oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name ) SELECT oi.externalProductId AS externalProductId, oi.externalVariationId AS externalVariationId, oi.name AS name, SUM(oi.quantity) AS totalQuantity, COUNT(distinct oi.orderId) AS totalOrders, COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount, COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount, COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount, COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount FROM order_item oi INNER JOIN \`order\` o ON o.id = oi.orderId INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.externalProductId = oi.externalProductId AND pc.externalVariationId = oi.externalVariationId WHERE o.date_created BETWEEN ? AND ? AND o.status IN ('processing', 'completed') `; const parameters: any[] = [startDate, endDate]; if (siteId) { sqlQuery += ' AND oi.siteId = ?'; parameters.push(siteId); } if (nameKeywords.length > 0) { sqlQuery += ' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND '); parameters.push(...nameKeywords.map(word => `%${word}%`)); } sqlQuery += ` GROUP BY oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name ORDER BY totalQuantity DESC `; sqlQuery += ' LIMIT ? OFFSET ?'; parameters.push(pageSize, (current - 1) * pageSize); // 执行查询并传递参数 const items = await this.orderSaleModel.query(sqlQuery, parameters); let totalCountQuery = ` SELECT COUNT(DISTINCT oi.siteId,oi.externalProductId,oi.externalVariationId) AS totalCount FROM order_item oi INNER JOIN \`order\` o ON o.id = oi.orderId WHERE o.date_created BETWEEN ? AND ? AND o.status IN ('processing', 'completed') `; const totalCountParameters: any[] = [startDate, endDate]; if (siteId) { totalCountQuery += ' AND oi.siteId = ?'; totalCountParameters.push(siteId); } if (nameKeywords.length > 0) { totalCountQuery += ' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND '); totalCountParameters.push(...nameKeywords.map(word => `%${word}%`)); } const totalCountResult = await this.orderSaleModel.query( totalCountQuery, totalCountParameters ); let totalQuantityQuery = ` SELECT SUM(oi.quantity) AS totalQuantity FROM order_item oi INNER JOIN \`order\` o ON o.id = oi.orderId WHERE o.date_created BETWEEN ? AND ? AND o.status IN ('processing', 'completed') `; const totalQuantityParameters: any[] = [startDate, endDate]; if (siteId) { totalQuantityQuery += ' AND oi.siteId = ?'; totalQuantityParameters.push(siteId); } if (nameKeywords.length > 0) { totalQuantityQuery += ' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND '); totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`)); } const totalQuantityResult = await this.orderSaleModel.query( totalQuantityQuery, totalQuantityParameters ); return { items, total: totalCountResult[0]?.totalCount, totalQuantity: Number( totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) ), current, pageSize, }; } /** * 获取订单项列表 * 流程说明: * 1. 构建SQL查询,关联订单表,获取订单项和订单信息 * 2. 检查订单项是否为订阅项(通过meta_data中的特定key判断) * 3. 根据参数动态添加过滤条件(日期范围、站点ID、产品名称、外部产品ID、变体ID) * 4. 执行总数查询 * 5. 添加分页和排序,执行主查询 * 6. 返回订单项列表和分页信息 * * 涉及实体: OrderItem, Order * * @param params 查询参数 * @returns 订单项列表和分页信息 */ async getOrderItemList({ siteId, startDate, endDate, current, pageSize, name, externalProductId, externalVariationId, }: any) { const params: any[] = []; let sql = ` SELECT oi.*, o.id AS orderId, o.externalOrderId AS orderExternalOrderId, o.date_created AS orderDateCreated, o.customer_email AS orderCustomerEmail, o.orderStatus AS orderStatus, o.siteId AS orderSiteId, CASE WHEN JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"is_subscription"') OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcs_bought_as_subscription"') OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcsatt_scheme"') OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_subscription"') THEN 1 ELSE 0 END AS isSubscriptionItem FROM order_item oi INNER JOIN \`order\` o ON o.id = oi.orderId WHERE 1=1 `; let countSql = ` SELECT COUNT(*) AS total FROM order_item oi INNER JOIN \`order\` o ON o.id = oi.orderId WHERE 1=1 `; const pushFilter = (cond: string, value: any) => { sql += cond; countSql += cond; params.push(value); }; if (startDate) pushFilter(' AND o.date_created >= ?', startDate); if (endDate) pushFilter(' AND o.date_created <= ?', endDate); if (siteId) pushFilter(' AND oi.siteId = ?', siteId); if (name) { pushFilter(' AND oi.name LIKE ?', `%${name}%`); } if (externalProductId) pushFilter(' AND oi.externalProductId = ?', externalProductId); if (externalVariationId) pushFilter(' AND oi.externalVariationId = ?', externalVariationId); sql += ' ORDER BY o.date_created DESC LIMIT ? OFFSET ?'; const listParams = [...params, pageSize, (current - 1) * pageSize]; const items = await this.orderItemModel.query(sql, listParams); const [countRow] = await this.orderItemModel.query(countSql, params); const total = Number(countRow?.total || 0); return { items, total, current, pageSize }; } /** * 获取订单详情 * 流程说明: * 1. 查询订单基本信息 * 2. 查询站点信息 * 3. 查询订单项 * 4. 查询订单销售项 * 5. 查询退款信息和退款项 * 6. 查询订单备注(包含用户名) * 7. 查询物流信息(包含物流项) * 8. 查询关联的订阅和相关订单 * 9. 返回完整的订单详情 * * 涉及实体: Order, Site, OrderItem, OrderSale, OrderRefund, OrderRefundItem, OrderNote, Shipment, ShipmentItem, Subscription * * @param id 订单ID * @returns 订单详情 */ async getOrderDetail(id: number): Promise { const order = await this.orderModel.findOne({ where: { id } }); const site = await this.siteService.get(Number(order.siteId), true); const items = await this.orderItemModel.find({ where: { orderId: id } }); const sales = await this.orderSaleModel.find({ where: { orderId: id } }); const refunds = await this.orderRefundModel.find({ where: { orderId: id }, }); const refundItems = await this.orderRefundItemModel.find({ where: { refundId: In(refunds.map(refund => refund.id)) }, }); const notes = await this.orderNoteModel .createQueryBuilder('order_note') .leftJoin(User, 'u', 'u.id=order_note.userId') .select(['order_note.*', 'u.username as username']) .where('order_note.orderId=:orderId', { orderId: id }) .getRawMany(); const getShipmentSql = ` SELECT s.*, CAST(CONCAT('[', GROUP_CONCAT(DISTINCT o.externalOrderId SEPARATOR ','), ']') AS JSON) AS orderIds FROM shipment s JOIN order_shipment os ON os.shipment_id = s.id JOIN \`order\` o ON os.order_id = o.id WHERE s.id IN ( SELECT shipment_id FROM order_shipment WHERE order_id = ? ) GROUP BY s.id; `; const shipment = await this.shipmentModel.query(getShipmentSql, [id, id]); if (shipment && shipment.length) { for (const v of shipment) { v.items = await this.shipmentItemModel.findBy({ shipment_id: v.id }); } } // 关联数据:订阅与相关订单(用于前端关联展示) let relatedList: any[] = []; try { const related = await this.getRelatedByOrder(id); const subs = Array.isArray(related?.subscriptions) ? related.subscriptions : []; const ords = Array.isArray(related?.orders) ? related.orders : []; const seen = new Set(); const merge = [...subs, ...ords]; for (const it of merge) { const key = it?.externalSubscriptionId ? `sub:${it.externalSubscriptionId}` : it?.externalOrderId ? `ord:${it.externalOrderId}` : `id:${it?.id}`; if (!seen.has(key)) { seen.add(key); relatedList.push(it); } } } catch (error) { // 关联查询失败不影响详情返回 } return { ...order, name: site?.name, // Site 实体无邮箱字段,这里返回空字符串保持兼容 email: '', items, sales, refundItems, notes, shipment, related: relatedList, }; } /** * 获取订单关联信息 * 流程说明: * 1. 查询订单基本信息 * 2. 查询关联的订阅信息(通过parent_id关联) * 3. 返回订单和订阅信息 * * 涉及实体: Order, Subscription * * @param orderId 订单ID * @returns 订单和关联的订阅信息 */ async getRelatedByOrder(orderId: number) { const order = await this.orderModel.findOne({ where: { id: orderId } }); if (!order) throw new Error('订单不存在'); const siteId = order.siteId; const subSql = ` SELECT * FROM subscription s WHERE s.siteId = ? AND s.parent_id = ? `; const subscriptions = await this.orderModel.query(subSql, [siteId, order.externalOrderId]); return { order, subscriptions, orders: [], }; } /** * 删除订单 * 流程说明: * 1. 查询订单是否存在 * 2. 删除订单配送信息 * 3. 删除订单销售项 * 4. 删除退款信息和退款项 * 5. 删除订单项 * 6. 删除订单费用 * 7. 删除订单优惠券 * 8. 删除订单主数据 * * 涉及实体: Order, OrderShipping, OrderSale, OrderRefund, OrderRefundItem, OrderItem, OrderFee, OrderCoupon * * @param id 订单ID */ async delOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error('订单不存在'); await this.orderShippingModel.delete({ orderId: id }); await this.orderSaleModel.delete({ orderId: id }); const refunds = await this.orderRefundModel.find({ where: { orderId: id }, }); if (refunds.length > 0) { for (const refund of refunds) { await this.orderRefundItemModel.delete({ refundId: refund.id }); await this.orderRefundModel.delete({ id: refund.id }); } } await this.orderItemModel.delete({ orderId: id }); await this.orderFeeModel.delete({ orderId: id }); await this.orderCouponModel.delete({ orderId: id }); await this.orderModel.delete({ id }); } /** * 创建订单备注 * 流程说明: * 1. 接收用户ID和备注数据 * 2. 创建订单备注实体,关联用户ID * 3. 保存订单备注 * * 涉及实体: OrderNote * * @param userId 用户ID * @param data 备注数据 * @returns 保存后的订单备注 */ async createNote(userId: number, data: CreateOrderNoteDTO) { return await this.orderNoteModel.save({ ...data, userId, }); } /** * 根据订单号获取订单 * 流程说明: * 1. 根据订单号模糊查询订单(仅查询处理中和待补发的订单) * 2. 批量获取订单涉及的站点名称 * 3. 构建站点ID到站点名称的映射 * 4. 返回订单列表,包含订单号、ID和站点名称 * * 涉及实体: Order, Site * * @param id 订单号 * @returns 订单列表(包含订单号、ID和站点名称) */ async getOrderByNumber(id: string) { const orders = await this.orderModel.find({ where: { externalOrderId: Like(id), orderStatus: In([ ErpOrderStatus.PROCESSING, ErpOrderStatus.PENDING_RESHIPMENT, ]), }, }); // 批量获取订单涉及的站点名称,避免使用配置文件 const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false); const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name])); return orders.map(order => ({ externalOrderId: order.externalOrderId, id: order.id, name: siteMap.get(String(order.siteId)) || '', })); } /** * 取消订单 * 流程说明: * 1. 查询订单是否存在 * 2. 查询订单所属站点 * 3. 如果订单状态不是已取消,则调用WooCommerce API更新订单状态为已取消 * 4. 更新本地订单状态为已取消 * 5. 更新订单ERP状态为已取消 * * 涉及实体: Order, Site * * @param id 订单ID */ async cancelOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); const site = await this.siteService.get(Number(order.siteId), true); if (order.status !== OrderStatus.CANCEL) { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.CANCEL, }); order.status = OrderStatus.CANCEL; } order.orderStatus = ErpOrderStatus.CANCEL; await this.orderModel.save(order); } /** * 退款订单 * 流程说明: * 1. 查询订单是否存在 * 2. 查询订单所属站点 * 3. 如果订单状态不是已退款,则调用WooCommerce API更新订单状态为已退款 * 4. 更新本地订单状态为已退款 * 5. 更新订单ERP状态为已退款 * * 涉及实体: Order, Site * * @param id 订单ID */ async refundOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); const site = await this.siteService.get(Number(order.siteId), true); if (order.status !== OrderStatus.REFUNDED) { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.REFUNDED, }); order.status = OrderStatus.REFUNDED; } order.orderStatus = ErpOrderStatus.REFUNDED; await this.orderModel.save(order); } /** * 完成订单 * 流程说明: * 1. 查询订单是否存在 * 2. 查询订单所属站点 * 3. 如果订单状态不是已完成,则调用WooCommerce API更新订单状态为已完成 * 4. 更新本地订单状态为已完成 * 5. 更新订单ERP状态为已完成 * * 涉及实体: Order, Site * * @param id 订单ID */ async completedOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); const site = await this.siteService.get(order.siteId); if (order.status !== OrderStatus.COMPLETED) { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.COMPLETED, }); order.status = OrderStatus.COMPLETED; } order.orderStatus = ErpOrderStatus.COMPLETED; await this.orderModel.save(order); } /** * 更改订单状态 * 流程说明: * 1. 查询订单是否存在 * 2. 更新订单ERP状态 * 3. 保存订单 * * 涉及实体: Order * * @param id 订单ID * @param status ERP订单状态 */ async changeStatus(id: number, status: ErpOrderStatus) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); order.orderStatus = status; await this.orderModel.save(order); } /** * 创建订单 * 流程说明: * 1. 验证必需参数siteId是否存在 * 2. 获取默认数据源 * 3. 在事务中处理订单创建 * 4. 保存订单基本信息(站点ID、外部订单号、状态、货币、日期、客户信息等) * 5. 遍历销售项目列表 * 6. 根据SKU查询产品信息 * 7. 保存订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) * * 涉及实体: Order, OrderSale, Product * * @param data 订单数据 * @returns 创建的订单 */ async createOrder(data: Record) { // 从数据中解构出需要用的属性 const { siteId, sales, total, billing, customer_email, billing_phone } = data; // 如果没有 siteId,则抛出错误 if (!siteId) { throw new Error('siteId is required'); } // 获取默认数据源 const dataSource = this.dataSourceManager.getDataSource('default'); const now = new Date(); // 在事务中处理订单创建 return dataSource.transaction(async manager => { const orderRepo = manager.getRepository(Order); const orderSaleRepo = manager.getRepository(OrderSale); const productRepo = manager.getRepository(Product); // 保存订单信息 const order = await orderRepo.save({ siteId, externalOrderId: '-1', status: OrderStatus.PROCESSING, orderStatus: ErpOrderStatus.PROCESSING, currency: 'CAD', currency_symbol: '$', date_created: now, date_paid: now, total, customer_email, billing_phone, billing, shipping: billing, }); // 遍历销售项目并保存 for (const sale of sales) { const product = await productRepo.findOne({ where: { sku: sale.sku } }); const saleItem = { orderId: order.id, siteId: order.siteId, externalOrderItemId: '-1', productId: product.id, name: product.name, sku: sale.sku, quantity: sale.quantity, }; await orderSaleRepo.save(saleItem); } }); } /** * 获取待处理订单项统计 * 流程说明: * 1. 构建SQL查询,关联订单表和订单销售表 * 2. 按产品名称分组,统计每个产品的总数量和订单号列表 * 3. 只查询状态为"处理中"的订单 * 4. 执行总数查询 * 5. 添加分页,执行主查询 * 6. 返回待处理订单项统计和分页信息 * * 涉及实体: Order, OrderSale * * @param data 查询参数 * @returns 待处理订单项统计和分页信息 */ async pengdingItems(data: Record) { const { current = 1, pageSize = 10 } = data; const sql = ` SELECT os.name, SUM(os.quantity) AS quantity, JSON_ARRAYAGG(os.orderId) AS numbers FROM \`order\` o INNER JOIN order_sale os ON os.orderId = o.id WHERE o.status = 'processing' GROUP BY os.name LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize} `; const countSql = ` SELECT COUNT(*) AS total FROM ( SELECT 1 FROM \`order\` o INNER JOIN order_sale os ON os.orderId = o.id WHERE o.status = 'processing' GROUP BY os.name ) AS temp `; const [items, countResult] = await Promise.all([ this.orderModel.query(sql), this.orderModel.query(countSql), ]); const total = countResult[0]?.total || 0; return { items, total, current, pageSize, }; } /** * 更新订单销售项 * 流程说明: * 1. 获取默认数据源 * 2. 在事务中处理订单销售项更新 * 3. 查询订单信息 * 4. 删除该订单的所有销售项 * 5. 遍历新的销售项列表 * 6. 根据SKU查询产品信息 * 7. 保存新的订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) * 8. 处理事务错误 * * 涉及实体: Order, OrderSale, Product * * @param orderId 订单ID * @param sales 销售项列表 * @returns 更新成功返回true */ async updateOrderSales(orderId: number, sales: OrderSale[]) { try { const dataSource = this.dataSourceManager.getDataSource('default'); let transactionError = undefined; await dataSource.transaction(async manager => { const orderRepo = manager.getRepository(Order); const orderSaleRepo = manager.getRepository(OrderSale); const productRepo = manager.getRepository(Product); const order = await orderRepo.findOneBy({ id: orderId }); let product: Product; await orderSaleRepo.delete({ orderId }); for (const sale of sales) { product = await productRepo.findOneBy({ sku: sale.sku }); await orderSaleRepo.save({ orderId, siteId: order.siteId, productId: product.id, name: product.name, sku: sale.sku, quantity: sale.quantity, // externalOrderItemId: }); }; }).catch(error => { transactionError = error; }); if (transactionError !== undefined) { throw new Error(`更新物流信息错误:${transactionError.message}`); } return true; } catch (error) { throw new Error(`更新发货产品失败:${error.message}`); } } /** * 更新换货订单 * 流程说明: * 1. 该方法用于换货确认功能 * 2. 需要更新OrderSale和OrderItem数据 * 3. 当前方法暂未实现 * * 涉及实体: Order, OrderSale, OrderItem, Product * * @param orderId 订单ID * @param data 换货数据 * @returns 更新成功返回true */ //换货确认按钮改成调用这个方法 //换货功能更新OrderSale和Orderitem数据 async updateExchangeOrder(orderId: number, data: any) { throw new Error('暂未实现') // try { // const dataSource = this.dataSourceManager.getDataSource('default'); // let transactionError = undefined; // await dataSource.transaction(async manager => { // const orderRepo = manager.getRepository(Order); // const orderSaleRepo = manager.getRepository(OrderSale); // const orderItemRepo = manager.getRepository(OrderItem); // const productRepo = manager.getRepository(ProductV2); // const order = await orderRepo.findOneBy({ id: orderId }); // let product: ProductV2; // await orderSaleRepo.delete({ orderId }); // await orderItemRepo.delete({ orderId }); // for (const sale of data['sales']) { // product = await productRepo.findOneBy({ sku: sale['sku'] }); // await orderSaleRepo.save({ // orderId, // siteId: order.siteId, // productId: product.id, // name: product.name, // sku: sale['sku'], // quantity: sale['quantity'], // }); // }; // for (const item of data['items']) { // product = await productRepo.findOneBy({ sku: item['sku'] }); // await orderItemRepo.save({ // orderId, // siteId: order.siteId, // productId: product.id, // name: product.name, // externalOrderId: order.externalOrderId, // externalProductId: product.externalProductId, // sku: item['sku'], // quantity: item['quantity'], // }); // }; // //将是否换货状态改为true // await orderRepo.update( // order.id // , { // is_exchange: true // }); // //查询这个用户换过多少次货 // const counts = await orderRepo.countBy({ // is_editable: true, // customer_email: order.customer_email, // }); // //批量更新当前用户换货次数 // await orderRepo.update({ // customer_email: order.customer_email // }, { // exchange_frequency: counts // }); // }).catch(error => { // transactionError = error; // }); // if (transactionError !== undefined) { // throw new Error(`更新物流信息错误:${transactionError.message}`); // } // return true; // } catch (error) { // throw new Error(`更新发货产品失败:${error.message}`); // } } /** * 导出订单为CSV格式 * 流程说明: * 1. 在事务中处理订单导出 * 2. 根据订单ID列表查询订单信息(包含物流关联) * 3. 查询所有订单项 * 4. 按订单ID分组订单项 * 5. 构建导出数据,包含以下字段: * - 日期: 订单创建日期 * - 订单号: 外部订单号 * - 姓名地址: 收货人姓名和地址 * - 邮箱: 客户邮箱 * - 号码: 电话号码 * - 订单内容: 产品名称和数量 * - 盒数: 总盒数 * - 换盒数: 换货盒数(当前默认为0) * - 换货内容: 换货内容(当前默认为空) * - 快递号: 物流追踪号 * 6. 调用exportToCsv方法将数据转换为CSV格式 * 7. 返回CSV字符串内容 * * 涉及实体: Order, OrderItem, Shipment * * @param ids 订单ID列表 * @returns CSV格式字符串 */ // TODO async exportOrder(ids: number[]) { // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 interface ExportData { '日期': string; '订单号': string; '姓名地址': string; '邮箱': string; '号码': string; '订单内容': string; '盒数': number; '换盒数': number; '换货内容': string; '快递号': string; } try { // 过滤掉NaN和非数字值,只保留有效的数字ID const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0); const dataSource = this.dataSourceManager.getDataSource('default'); // 优化事务使用 return await dataSource.transaction(async manager => { // 准备查询条件 const whereCondition: any = {}; if (validIds.length > 0) { whereCondition.id = In(validIds); } // 获取订单、订单项和物流信息 const orders = await manager.getRepository(Order).find({ where: whereCondition, relations: ['shipment'] }); if (orders.length === 0) { throw new Error('未找到匹配的订单'); } // 获取所有订单ID const orderIds = orders.map(order => order.id); // 获取所有订单项 const orderItems = await manager.getRepository(OrderItem).find({ where: { orderId: In(orderIds) } }); // 按订单ID分组订单项 const orderItemsByOrderId = orderItems.reduce((acc, item) => { if (!acc[item.orderId]) { acc[item.orderId] = []; } acc[item.orderId].push(item); return acc; }, {} as Record); // 构建导出数据 const exportDataList: ExportData[] = orders.map(order => { // 获取订单的订单项 const items = orderItemsByOrderId[order.id] || []; // 计算总盒数 const boxCount = items.reduce((total, item) => total + item.quantity, 0); // 构建订单内容 const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; '); // 构建姓名地址 const shipping = order.shipping; const billing = order.billing; const firstName = shipping?.first_name || billing?.first_name || ''; const lastName = shipping?.last_name || billing?.last_name || ''; const name = `${firstName} ${lastName}`.trim() || ''; const address = shipping?.address_1 || billing?.address_1 || ''; const address2 = shipping?.address_2 || billing?.address_2 || ''; const city = shipping?.city || billing?.city || ''; const state = shipping?.state || billing?.state || ''; const postcode = shipping?.postcode || billing?.postcode || ''; const country = shipping?.country || billing?.country || ''; const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; // 获取电话号码 const phone = shipping?.phone || billing?.phone || ''; // 获取快递号 const trackingNumber = order.shipment?.tracking_id || ''; // 暂时没有换货相关数据,默认为0和空字符串 const exchangeBoxCount = 0; const exchangeContent = ''; return { '日期': order.date_created?.toISOString().split('T')[0] || '', '订单号': order.externalOrderId || '', '姓名地址': nameAddress, '邮箱': order.customer_email || '', '号码': phone, '订单内容': this.removeLastParenthesesContent(orderContent), '盒数': boxCount, '换盒数': exchangeBoxCount, '换货内容': exchangeContent, '快递号': trackingNumber }; }); // 返回CSV字符串内容给前端 const csvContent = await this.exportToCsv(exportDataList, { type: 'string' }); return csvContent; }); } catch (error) { throw new Error(`导出订单失败:${error.message}`); } } /** * 导出数据为CSV格式 * @param {any[]} data 数据数组 * @param {Object} options 配置选项 * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) * @param {boolean} [options.writeFile=false] 是否写入文件 * @returns {string|Buffer} 根据type返回字符串或Buffer */ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise { try { // 检查数据是否为空 if (!data || data.length === 0) { throw new Error('导出数据不能为空'); } const { type = 'string', fileName, writeFile = false } = options; // 生成表头 const headers = Object.keys(data[0]); let csvContent = headers.join(',') + '\n'; // 处理数据行 data.forEach(item => { const row = headers.map(key => { const value = item[key as keyof any]; // 处理特殊字符 if (typeof value === 'string') { // 转义双引号,将"替换为"" const escapedValue = value.replace(/"/g, '""'); // 如果包含逗号或换行符,需要用双引号包裹 if (escapedValue.includes(',') || escapedValue.includes('\n')) { return `"${escapedValue}"`; } return escapedValue; } // 处理日期类型 if (value instanceof Date) { return value.toISOString(); } // 处理undefined和null if (value === undefined || value === null) { return ''; } return String(value); }).join(','); csvContent += row + '\n'; }); // 如果需要写入文件 if (writeFile && fileName) { // 获取当前用户目录 const userHomeDir = os.homedir(); // 构建目标路径(下载目录) const downloadsDir = path.join(userHomeDir, 'Downloads'); // 确保下载目录存在 if (!fs.existsSync(downloadsDir)) { fs.mkdirSync(downloadsDir, { recursive: true }); } const filePath = path.join(downloadsDir, fileName); // 写入文件 fs.writeFileSync(filePath, csvContent, 'utf8'); console.log(`数据已成功导出至 ${filePath}`); return filePath; } // 根据类型返回不同结果 if (type === 'buffer') { return Buffer.from(csvContent, 'utf8'); } return csvContent; } catch (error) { console.error('导出CSV时出错:', error); throw new Error(`导出CSV文件失败: ${error.message}`); } } /** * 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身) * @param str 输入字符串 * @returns 删除后的字符串 */ removeLastParenthesesContent(str: string): string { if (!str || typeof str !== 'string') { return str; } // 辅助函数:删除指定位置的括号对及其内容 const removeParenthesesAt = (s: string, leftIndex: number): string => { if (leftIndex === -1) return s; let rightIndex = -1; let parenCount = 0; for (let i = leftIndex; i < s.length; i++) { const char = s[i]; if (char === '(') { parenCount++; } else if (char === ')') { parenCount--; if (parenCount === 0) { rightIndex = i; break; } } } if (rightIndex !== -1) { return s.substring(0, leftIndex) + s.substring(rightIndex + 1); } return s; }; // 1. 处理每个分号前面的括号对 let result = str; // 找出所有分号的位置 const semicolonIndices: number[] = []; for (let i = 0; i < result.length; i++) { if (result[i] === ';') { semicolonIndices.push(i); } } // 从后向前处理每个分号,避免位置变化影响后续处理 for (let i = semicolonIndices.length - 1; i >= 0; i--) { const semicolonIndex = semicolonIndices[i]; // 从分号位置向前查找最近的左括号 let lastLeftParenIndex = -1; for (let j = semicolonIndex - 1; j >= 0; j--) { if (result[j] === '(') { lastLeftParenIndex = j; break; } } // 如果找到左括号,删除该括号对及其内容 if (lastLeftParenIndex !== -1) { result = removeParenthesesAt(result, lastLeftParenIndex); } } // 2. 处理整个字符串的最后一个括号对 let lastLeftParenIndex = result.lastIndexOf('('); if (lastLeftParenIndex !== -1) { result = removeParenthesesAt(result, lastLeftParenIndex); } return result; } }