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'; @Provide() export class OrderService { @Config('wpSite') sites: WpSite[]; @Inject() wPService: WPService; @Inject() stockService: StockService; @InjectEntityModel(Order) orderModel: Repository; @InjectEntityModel(OrderItem) orderItemModel: Repository; @InjectEntityModel(OrderSale) orderSaleModel: 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; 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, }); } 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, }) { 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.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, 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); } // 处理 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 (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, }) { 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 }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const parameters: any[] = [startDate, endDate]; // 主查询:带分页 let sqlQuery = ` WITH product_purchase_counts AS ( SELECT o.customer_email, os.productId, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') GROUP BY o.customer_email, os.productId ) SELECT os.productId AS productId, os.name AS name, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders, c.name AS categoryName, COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount, SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount, COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount, SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount, COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount, SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount, COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount, SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount FROM order_sale os INNER JOIN \`order\` o ON o.id = os.orderId INNER JOIN product p ON os.productId = p.id INNER JOIN category c ON p.categoryId = c.id INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId WHERE o.date_paid BETWEEN ? AND ? AND o.status IN ('completed', 'processing') `; if (siteId) { sqlQuery += ' AND os.siteId = ?'; parameters.push(siteId); } if (nameKeywords.length > 0) { sqlQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')'; parameters.push(...nameKeywords.map(word => `%${word}%`)); } sqlQuery += ` GROUP BY os.productId, os.name, c.name ORDER BY totalQuantity DESC LIMIT ? OFFSET ? `; parameters.push(pageSize, (current - 1) * pageSize); const items = await this.orderSaleModel.query(sqlQuery, parameters); // 总条数 const countParams: any[] = [startDate, endDate]; let totalCountQuery = ` 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) { totalCountQuery += ' AND os.siteId = ?'; countParams.push(siteId); } if (nameKeywords.length > 0) { totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')'; countParams.push(...nameKeywords.map(word => `%${word}%`)); } const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams); // 一次查询获取所有 yoone box 数量 const totalQuantityParams: any[] = [startDate, endDate]; let totalQuantityQuery = ` SELECT SUM(os.quantity) AS totalQuantity, SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%3%' THEN os.quantity ELSE 0 END) AS yoone3Quantity, SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%6%' THEN os.quantity ELSE 0 END) AS yoone6Quantity, SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%9%' THEN os.quantity ELSE 0 END) AS yoone9Quantity, SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' THEN os.quantity ELSE 0 END) AS yoone12Quantity, SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%15%' THEN os.quantity ELSE 0 END) AS yoone15Quantity 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) { totalQuantityQuery += ' AND os.siteId = ?'; totalQuantityParams.push(siteId); } if (nameKeywords.length > 0) { totalQuantityQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')'; totalQuantityParams.push(...nameKeywords.map(word => `%${word}%`)); } const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams); return { items, total: totalCountResult[0]?.totalCount || 0, totalQuantity: Number(totalQuantityResult.totalQuantity || 0), yoone3Quantity: Number(totalQuantityResult.yoone3Quantity || 0), yoone6Quantity: Number(totalQuantityResult.yoone6Quantity || 0), yoone9Quantity: Number(totalQuantityResult.yoone9Quantity || 0), yoone12Quantity: Number(totalQuantityResult.yoone12Quantity || 0), yoone15Quantity: Number(totalQuantityResult.yoone15Quantity || 0), current, pageSize, }; } // async getOrderSales({ // 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,os.productId, os.name, COUNT(DISTINCT o.id,os.productId) AS order_count // FROM \`order\` o // JOIN order_sale os ON o.id = os.orderId // WHERE o.status IN ('completed', 'processing') // GROUP BY o.customer_email, os.productId, os.name // ) // SELECT // os.productId AS productId, // os.name AS name, // SUM(os.quantity) AS totalQuantity, // COUNT(distinct os.orderId) AS totalOrders, // c.name AS categoryName, // COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount, // SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount, // COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount, // SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount, // COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount, // SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount, // COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount, // SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount // FROM order_sale os // INNER JOIN \`order\` o ON o.id = os.orderId // INNER JOIN product p ON os.productId = p.id // INNER JOIN category c ON p.categoryId = c.id // INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId // WHERE o.date_paid BETWEEN ? AND ? // AND o.status IN ('processing', 'completed') // `; // const parameters: any[] = [startDate, endDate]; // if (siteId) { // sqlQuery += ' AND os.siteId = ?'; // parameters.push(siteId); // } // if (nameKeywords.length > 0) { // sqlQuery += // ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND '); // parameters.push(...nameKeywords.map(word => `%${word}%`)); // } // sqlQuery += ` // GROUP BY os.productId, os.name, c.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 os.productId) AS totalCount // FROM order_sale os // INNER JOIN \`order\` o ON o.id = os.orderId // INNER JOIN product p ON os.productId = p.id // INNER JOIN category c ON p.categoryId = c.id // WHERE o.date_created BETWEEN ? AND ? // AND o.status IN ('processing', 'completed') // `; // const totalCountParameters: any[] = [startDate, endDate]; // if (siteId) { // totalCountQuery += ' AND os.siteId = ?'; // totalCountParameters.push(siteId); // } // if (nameKeywords.length > 0) { // totalCountQuery += // ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND '); // totalCountParameters.push(...nameKeywords.map(word => `%${word}%`)); // } // const totalCountResult = await this.orderSaleModel.query( // totalCountQuery, // totalCountParameters // ); // let totalQuantityQuery = ` // SELECT SUM(os.quantity) AS totalQuantity // FROM order_sale os // INNER JOIN \`order\` o ON o.id = os.orderId // INNER JOIN product p ON os.productId = p.id // INNER JOIN category c ON p.categoryId = c.id // WHERE o.date_created BETWEEN ? AND ? // AND o.status IN ('processing', 'completed') // `; // const totalQuantityParameters: any[] = [startDate, endDate]; // if (siteId) { // totalQuantityQuery += ' AND os.siteId = ?'; // totalQuantityParameters.push(siteId); // } // const yoone3QuantityQuery = // totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%3%"'; // const yoone6QuantityQuery = // totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%6%"'; // const yoone9QuantityQuery = // totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%9%"'; // const yoone12QuantityQuery = // totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%12%"'; // const yoone15QuantityQuery = // totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%15%"'; // const yooneParameters = [...totalQuantityParameters]; // if (nameKeywords.length > 0) { // totalQuantityQuery += // ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND '); // totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`)); // } // const totalQuantityResult = await this.orderSaleModel.query( // totalQuantityQuery, // totalQuantityParameters // ); // const yoone3QuantityResult = await this.orderSaleModel.query( // yoone3QuantityQuery, // yooneParameters // ); // const yoone6QuantityResult = await this.orderSaleModel.query( // yoone6QuantityQuery, // yooneParameters // ); // const yoone9QuantityResult = await this.orderSaleModel.query( // yoone9QuantityQuery, // yooneParameters // ); // const yoone12QuantityResult = await this.orderSaleModel.query( // yoone12QuantityQuery, // yooneParameters // ); // const yoone15QuantityResult = await this.orderSaleModel.query( // yoone15QuantityQuery, // yooneParameters // ); // return { // items, // total: totalCountResult[0]?.totalCount, // totalQuantity: Number( // totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) // ), // yoone3Quantity: Number( // yoone3QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) // ), // yoone6Quantity: Number( // yoone6QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) // ), // yoone9Quantity: Number( // yoone9QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) // ), // yoone12Quantity: Number( // yoone12QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) // ), // yoone15Quantity: Number( // yoone15QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 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 }); } } 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 } = 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 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, shipping: billing, }); for (const sale of sales) { const product = await productRepo.findOne({ where: { sku: sale.sku } }); await orderSaleRepo.save({ orderId: order.id, siteId: '-1', externalOrderItemId: '-1', productId: product.id, name: product.name, sku: sale.sku, quantity: sale.quantity, }); } }); } 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, }; } }