import { Inject, 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 { 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'; @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(Shipment) shipmentModel: Repository; @InjectEntityModel(ShipmentItem) shipmentItemModel: Repository; @InjectEntityModel(OrderNote) orderNoteModel: Repository; @Inject() dataSourceManager: TypeORMDataSourceManager; @InjectEntityModel(Customer) customerModel: Repository; @Inject() siteService: SiteService; @Inject() siteApiService: SiteApiService; 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: [] }; // 遍历每个订单进行同步 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++; } } catch (error) { // 记录错误但不中断整个同步过程 syncResult.errors.push({ identifier: String(order.id), error: error.message || '同步失败' }); syncResult.processed++; } } return syncResult; } 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 order = await this.wpService.getOrder(siteId, 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; } } // 订单状态切换表 orderAutoNextStatusMap = { [OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded } // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换 async autoUpdateOrderStatus(siteId: number, order: any) { console.log('更新订单状态', order) // 其他状态保持不变 const originStatus = order.status; // 如果有值就赋值 if (!this.orderAutoNextStatusMap[originStatus]) { return } try { const site = await this.siteService.get(siteId); // 将订单状态同步到 WooCommerce,然后切换至下一状态 await this.wpService.updateOrder(site, String(order.id), { status: order.status }); order.status = this.orderAutoNextStatusMap[originStatus]; } catch (error) { console.error('更新订单状态失败,原因为:', error) } } // wordpress 发来, async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { let { line_items, shipping_lines, fee_lines, coupon_lines, refunds, ...orderData } = order; // console.log('同步进单个订单', order) const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: order.id, siteId: siteId }, }); // 矫正状态 await this.autoUpdateOrderStatus(siteId, order); if (order.status === OrderStatus.AUTO_DRAFT) { return; } // 更新订单 if (existingOrder) { await this.orderModel.update({ id: existingOrder.id }, { orderStatus: this.mapOrderStatus(order.status) }); } const site = await this.siteService.get(siteId,true); if(site.type === 'woocommerce'){ // 矫正数据库状态 await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, { orderStatus: order.status, }) } const externalOrderId = order.id; if ( existingOrder && existingOrder.orderStatus !== ErpOrderStatus.COMPLETED && orderData.status === OrderStatus.COMPLETED ) { this.updateStock(existingOrder); return; } if (existingOrder && !existingOrder.is_editable && !forceUpdate) { return; } const orderRecord = await this.saveOrder(siteId, orderData); const orderId = orderRecord.id; await this.saveOrderItems({ siteId, orderId, 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, }); console.log('同步进单个订单2') await this.saveOrderShipping({ siteId, orderId, externalOrderId, shipping_lines, }); } 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); } } async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise { const externalOrderId = String(order.id) delete order.id // order.billing_phone = order?.billing?.phone || order?.shipping?.phone; const entity = plainToClass(Order, {...order, externalOrderId, siteId}); const existingOrder = await this.orderModel.findOne({ where: { externalOrderId, siteId: siteId }, }); if (existingOrder) { if (this.canUpdateErpStatus(existingOrder.orderStatus)) { entity.orderStatus = this.mapOrderStatus(entity.status); } await this.orderModel.update(existingOrder.id, entity); entity.id = existingOrder.id; return entity; } entity.orderStatus = this.mapOrderStatus(entity.status); const customer = await this.customerModel.findOne({ where: { email: order.customer_email }, }); if (!customer) { await this.customerModel.save({ email: order.customer_email, site_id: siteId, origin_id: order.customer_id, }); } return await this.orderModel.save(entity); } canUpdateErpStatus(currentErpStatus: string): boolean { const nonOverridableStatuses = [ 'AFTER_SALE_PROCESSING', 'PENDING_RESHIPMENT', 'PENDING_REFUND', ]; // 如果当前 ERP 状态不可覆盖,则禁止更新 return !nonOverridableStatuses.includes(currentErpStatus); } 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; } } async saveOrderItems(params: { siteId: number; orderId: number; externalOrderId: string; orderItems: Record[]; }) { console.log('saveOrderItems params',params) 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)) ); 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); await this.saveOrderSale(entity); } } 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); } } 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); } } 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'], }); if (!product) return; const orderSales: OrderSale[] = []; if (product.components && product.components.length > 0) { for (const comp of product.components) { const baseProduct = await this.productModel.findOne({ where: { sku: comp.sku }, }); if (baseProduct) { const orderSaleItem: OrderSale = plainToClass(OrderSale, { orderId: orderItem.orderId, siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId, productId: baseProduct.id, name: baseProduct.name, quantity: comp.quantity * orderItem.quantity, sku: comp.sku, isPackage: orderItem.name.toLowerCase().includes('package'), }); orderSales.push(orderSaleItem); } } } else { const orderSaleItem: OrderSale = plainToClass(OrderSale, { orderId: orderItem.orderId, siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId, productId: product.id, name: product.name, quantity: orderItem.quantity, sku: product.sku, isPackage: orderItem.name.toLowerCase().includes('package'), }); orderSales.push(orderSaleItem); } if (orderSales.length > 0) { await this.orderSaleModel.save(orderSales); } } 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, }); } } 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); } } } 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); } } } 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); } } } 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); } } } 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 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 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_created >= ?`; totalQuery += ` AND o.date_created >= ?`; parameters.push(startDate); } if (endDate) { sqlQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_created <= ?`; 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_created DESC LIMIT ? OFFSET ? `; parameters.push(pageSize, (current - 1) * pageSize); // 执行查询 const orders = await this.orderModel.query(sqlQuery, parameters); return { items: orders, total, current, pageSize }; } 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(); } async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: 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.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity, SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity, SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity, SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity, SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew, SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity, SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity, SUM(CASE WHEN os.isZex = 1 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, }; } async getOrderItems({ siteId, startDate, endDate, current, pageSize, 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, }; } 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 }; } 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, }; } 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: [], }; } 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 }); } async createNote(userId: number, data: CreateOrderNoteDTO) { return await this.orderNoteModel.save({ ...data, userId, }); } 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)) || '', })); } 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); } 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); } 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); } 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); } 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); } }); } 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, }; } 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}`); } } //换货确认按钮改成调用这个方法 //换货功能更新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}`); // } } // TODO async exportOrder(ids: number[]) { // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 interface ExportData { '日期': string; '订单号': string; '姓名地址': string; '邮箱': string; '号码': string; '订单内容': string; '盒数': number; '换盒数': number; '换货内容': string; '快递号': string; } try { // 空值检查 const dataSource = this.dataSourceManager.getDataSource('default'); // 优化事务使用 return await dataSource.transaction(async manager => { // 准备查询条件 const whereCondition: any = {}; if (ids && ids.length > 0) { whereCondition.id = In(ids); } // 获取订单、订单项和物流信息 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, '订单内容': 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}`); } } }