import { Config, 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 { WpProduct } from '../entity/wp_product.entity'; import { Product } from '../entity/product.entty'; import { OrderFee } from '../entity/order_fee.entity'; import { OrderRefund } from '../entity/order_refund.entity'; import { OrderRefundItem } from '../entity/order_retund_item.entity'; import { OrderCoupon } from '../entity/order_copon.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 { Variation } from '../entity/variation.entity'; import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; import { OrderDetailRes } from '../dto/reponse.dto'; import { OrderNote } from '../entity/order_note.entity'; import { User } from '../entity/user.entity'; import { WpSite } from '../interface'; import { ShipmentItem } from '../entity/shipment_item.entity'; import { UpdateStockDTO } from '../dto/stock.dto'; import { StockService } from './stock.service'; import { OrderSaleOriginal } from '../entity/order_item_original.entity'; @Provide() export class OrderService { @Config('wpSite') sites: WpSite[]; @Inject() wPService: WPService; @Inject() stockService: StockService; @InjectEntityModel(Order) orderModel: Repository; @InjectEntityModel(User) userModel: Repository; @InjectEntityModel(OrderItem) orderItemModel: Repository; @InjectEntityModel(OrderSale) orderSaleModel: Repository; @InjectEntityModel(OrderSaleOriginal) orderSaleOriginalModel: Repository; @InjectEntityModel(WpProduct) wpProductModel: Repository; @InjectEntityModel(Variation) variationModel: 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; async syncOrders(siteId: string) { const orders = await this.wPService.getOrders(siteId); // 调用 WooCommerce API 获取订单 for (const order of orders) { await this.syncSingleOrder(siteId, order); } } async syncOrderById(siteId: string, orderId: string) { const order = await this.wPService.getOrder(siteId, orderId); await this.syncSingleOrder(siteId, order, true); } async syncSingleOrder(siteId: string, order: any, forceUpdate = false) { let { line_items, shipping_lines, fee_lines, coupon_lines, refunds, ...orderData } = order; const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: order.id, siteId: siteId }, }); const orderId = (await this.saveOrder(siteId, orderData)).id; 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; } 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, }); 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.productSku = 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: string, order: Order): Promise { order.externalOrderId = String(order.id); order.siteId = siteId; delete order.id; order.device_type = order?.meta_data?.find( el => el.key === '_wc_order_attribution_device_type' )?.value || ''; order.source_type = order?.meta_data?.find( el => el.key === '_wc_order_attribution_source_type' )?.value || ''; order.utm_source = order?.meta_data?.find( el => el.key === '_wc_order_attribution_utm_source' )?.value || ''; order.customer_email = order?.billing?.email || order?.shipping?.email; order.billing_phone = order?.billing?.phone || order?.shipping?.phone; const entity = plainToClass(Order, order); const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: order.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, rate: 0, }); } 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; default: return ErpOrderStatus.PENDING; } } async saveOrderItems(params: { siteId: string; 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)) ); 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 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; let constitution; if (orderItem.externalVariationId === '0') { const product = await this.wpProductModel.findOne({ where: { sku: orderItem.sku }, }); if (!product) return; constitution = product?.constitution; } else { const variation = await this.variationModel.findOne({ where: { sku: orderItem.sku }, }); if (!variation) return; constitution = variation?.constitution; } if (!Array.isArray(constitution)) return; const orderSales: OrderSale[] = []; for (const item of constitution) { const baseProduct = await this.productModel.findOne({ where: { sku: item.sku }, }); const orderSaleItem: OrderSale = plainToClass(OrderSale, { orderId: orderItem.orderId, siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId, productId: baseProduct.id, name: baseProduct.name, quantity: item.quantity * orderItem.quantity, sku: item.sku, isPackage: orderItem.name.toLowerCase().includes('package'), }); orderSales.push(orderSaleItem); } await this.orderSaleModel.save(orderSales); } async saveOrderRefunds({ siteId, orderId, externalOrderId, refunds, }: { siteId: string; orderId: number; externalOrderId: string; refunds: Record[]; }) { for (const item of refunds) { const refund = await this.wPService.getOrderRefund( 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, }, 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.billing_phone as billing_phone, 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, 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); } if (payment_method) { sqlQuery += ` AND o.payment_method like "%${payment_method}%" `; totalQuery += ` AND o.payment_method like "%${payment_method}%" `; } const user = await this.userModel.findOneBy({id: userId}); if (user?.permissions?.includes('order-10-days')) { 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); } } 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, }) { 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}%` } ); } return await query.getRawMany(); } async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; 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) : []; // 分页查询 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 getOrderDetail(id: number): Promise { const order = await this.orderModel.findOne({ where: { id } }); const site = this.sites.find(site => site.id === order.siteId); 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 }); } } // update order_sale_origin if not exist try { const order_sale_origin_count = await this.orderSaleOriginalModel.countBy({ orderId: id }); if (order_sale_origin_count === 0) { sales.forEach(async sale => { const { id: saleId, ...saleData } = sale; await this.orderSaleOriginalModel.save(saleData); }); } } catch (error) { console.log('create order sale origin error: ', error.message); } return { ...order, siteName: site.siteName, email: site.email, items, sales, refundItems, notes, shipment, }; } 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, ]), }, }); return orders.map(order => { return { externalOrderId: order.externalOrderId, id: order.id, siteName: this.sites.find(site => site.id === order.siteId)?.siteName || '', }; }); } async cancelOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); const site = this.wPService.geSite(order.siteId); 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 = this.wPService.geSite(order.siteId); 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 = this.wPService.geSite(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 { sales, total, billing, customer_email, billing_phone } = data; 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 OrderSaleOriginalRepo = manager.getRepository(OrderSaleOriginal); const productRepo = manager.getRepository(Product); const order = await orderRepo.save({ siteId: '-1', 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: '-1', externalOrderItemId: '-1', productId: product.id, name: product.name, sku: sale.sku, quantity: sale.quantity, }; await orderSaleRepo.save(saleItem); await OrderSaleOriginalRepo.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}`); } } }