From c75c0a614f8e4c0fa520384630300ce2c200d725 Mon Sep 17 00:00:00 2001 From: zhuotianyuan Date: Tue, 13 Jan 2026 19:25:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(shopyy):=20=E4=BF=AE=E5=A4=8D=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E6=9F=A5=E8=AF=A2=E5=8F=82=E6=95=B0=E6=98=A0=E5=B0=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正shopyy服务中获取所有订单的参数映射逻辑,添加支付时间范围支持 统一处理日期格式转换,确保参数正确传递 同时清理合并冲突标记和冗余代码 --- src/adapter/shopyy.adapter.ts | 17 +- src/dto/api.dto.ts | 17 + src/service/freightwaves.service.ts | 6 +- src/service/order.service copy.ts | 2651 --------------------------- src/service/order.service.ts | 76 + src/service/shopyy.service.ts | 3 +- 6 files changed, 112 insertions(+), 2658 deletions(-) delete mode 100644 src/service/order.service copy.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index d50e5b2..7365fcd 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -23,7 +23,7 @@ import { UpdateReviewDTO, OrderPaymentStatus, } from '../dto/site-api.dto'; -import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; +import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto'; import { ShopyyAllProductQuery, ShopyyCustomer, @@ -40,6 +40,7 @@ import { OrderStatus, } from '../enums/base.enum'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; +import dayjs = require('dayjs'); export class ShopyyAdapter implements ISiteAdapter { shopyyFinancialStatusMap = { '200': '待支付', @@ -569,9 +570,21 @@ export class ShopyyAdapter implements ISiteAdapter { per_page, }; } + mapGetAllOrdersParams(params: UnifiedSearchParamsDTO) :ShopyyGetAllOrdersParams{ + const pay_at_min = dayjs(params.after || '').valueOf().toString(); + const pay_at_max = dayjs(params.before || '').valueOf().toString(); + + return { + page: params.page || 1, + per_page: params.per_page || 20, + pay_at_min: pay_at_min, + pay_at_max: pay_at_max, + } + } async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - const data = await this.shopyyService.getAllOrders(this.site.id, params); + const normalizedParams = this.mapGetAllOrdersParams(params); + const data = await this.shopyyService.getAllOrders(this.site.id, normalizedParams); return data.map(this.mapPlatformToUnifiedOrder.bind(this)); } diff --git a/src/dto/api.dto.ts b/src/dto/api.dto.ts index 6cd4c41..bdd7d50 100644 --- a/src/dto/api.dto.ts +++ b/src/dto/api.dto.ts @@ -52,6 +52,23 @@ export class UnifiedSearchParamsDTO> { orderBy?: Record | string; } +/** + * Shopyy获取所有订单参数DTO + */ +export class ShopyyGetAllOrdersParams { + @ApiProperty({ description: '页码', example: 1, required: false }) + page?: number; + + @ApiProperty({ description: '每页数量', example: 20, required: false }) + per_page?: number; + + @ApiProperty({ description: '支付时间范围开始', example: '2023-01-01T00:00:00Z', required: false }) + pay_at_min?: string; + + @ApiProperty({ description: '支付时间范围结束', example: '2023-01-01T23:59:59Z', required: false }) + pay_at_max?: string; +} + /** * 批量操作错误项 */ diff --git a/src/service/freightwaves.service.ts b/src/service/freightwaves.service.ts index 90c0d4a..bcd6ca5 100644 --- a/src/service/freightwaves.service.ts +++ b/src/service/freightwaves.service.ts @@ -172,7 +172,7 @@ export class FreightwavesService { private async sendRequest(url: string, data: any): Promise> { try { // 设置请求头 - 使用太平洋时间 (America/Los_Angeles) - const date = dayjs().tz('America/Los_Angeles').format('YYYY-MM-DD HH:mm:ss'); + const date = dayjs().tz('America/Los_Angeles').format('YYYY-mm-dd HH:mm:ss'); const headers = { 'Content-Type': 'application/json', 'requestDate': date, @@ -267,7 +267,7 @@ export class FreightwavesService { partner: this.config.partner, }; - const response = await this.sendRequest('/shipService/order/createOrder', requestData); + const response = await this.sendRequest('/shipService/order/createOrder?apipost_id=0422aa', requestData); return response.data; } @@ -423,7 +423,7 @@ export class FreightwavesService { // 设置必要的配置 this.setConfig({ appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt', - apiBaseUrl: 'https://tms.freightwaves.ca', + apiBaseUrl: 'https://console-mock.apipost.cn/mock/0', partner: '25072621035200000060' }); diff --git a/src/service/order.service copy.ts b/src/service/order.service copy.ts deleted file mode 100644 index db2cccd..0000000 --- a/src/service/order.service copy.ts +++ /dev/null @@ -1,2651 +0,0 @@ -import { Inject, Logger, Provide } from '@midwayjs/core'; -import { WPService } from './wp.service'; -import { Order } from '../entity/order.entity'; -import { In, Like, Repository } from 'typeorm'; -import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; -import { plainToClass } from 'class-transformer'; -import { OrderItem } from '../entity/order_item.entity'; - -import { OrderSale } from '../entity/order_sale.entity'; -import { Product } from '../entity/product.entity'; -import { OrderFee } from '../entity/order_fee.entity'; -import { OrderRefund } from '../entity/order_refund.entity'; -import { OrderRefundItem } from '../entity/order_refund_item.entity'; -import { OrderCoupon } from '../entity/order_coupon.entity'; -import { OrderShipping } from '../entity/order_shipping.entity'; -import { OrderFulfillment } from '../entity/order_fulfillment.entity'; -import { Shipment } from '../entity/shipment.entity'; -import { Customer } from '../entity/customer.entity'; -import { - ErpOrderStatus, - OrderStatus, - StockRecordOperationType, -} from '../enums/base.enum'; -import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; -import dayjs = require('dayjs'); -import { OrderDetailRes } from '../dto/reponse.dto'; -import { OrderNote } from '../entity/order_note.entity'; -import { User } from '../entity/user.entity'; -import { SiteService } from './site.service'; -import { ShipmentItem } from '../entity/shipment_item.entity'; -import { UpdateStockDTO } from '../dto/stock.dto'; -import { StockService } from './stock.service'; -import { OrderItemOriginal } from '../entity/order_item_original.entity'; -import { SiteApiService } from './site-api.service'; -import { SyncOperationResult } from '../dto/api.dto'; - -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { UnifiedOrderDTO } from '../dto/site-api.dto'; -import { CustomerService } from './customer.service'; -@Provide() -export class OrderService { - - @Inject() - wpService: WPService; - - @Inject() - stockService: StockService; - - @InjectEntityModel(Order) - orderModel: Repository; - - @InjectEntityModel(User) - userModel: Repository; - - @InjectEntityModel(OrderItem) - orderItemModel: Repository; - - @InjectEntityModel(OrderItem) - orderItemOriginalModel: Repository; - - @InjectEntityModel(OrderSale) - orderSaleModel: Repository; - - @InjectEntityModel(Product) - productModel: Repository; - - @InjectEntityModel(OrderFee) - orderFeeModel: Repository; - - @InjectEntityModel(OrderRefund) - orderRefundModel: Repository; - - @InjectEntityModel(OrderRefundItem) - orderRefundItemModel: Repository; - - @InjectEntityModel(OrderCoupon) - orderCouponModel: Repository; - - @InjectEntityModel(OrderShipping) - orderShippingModel: Repository; - - @InjectEntityModel(OrderFulfillment) - orderFulfillmentModel: Repository; - - @InjectEntityModel(Shipment) - shipmentModel: Repository; - - @InjectEntityModel(ShipmentItem) - shipmentItemModel: Repository; - - @InjectEntityModel(OrderNote) - orderNoteModel: Repository; - - @Inject() - dataSourceManager: TypeORMDataSourceManager; - - @InjectEntityModel(Customer) - customerModel: Repository; - - @Inject() - siteService: SiteService; - - @Inject() - siteApiService: SiteApiService; - - @Inject() - customerService: CustomerService; - - @Logger() - logger; // 注入 Logger 实例 - - /** - * 批量同步订单 - * 流程说明: - * 1. 调用 WooCommerce API 获取订单列表 - * 2. 遍历每个订单,检查数据库中是否已存在 - * 3. 调用 syncSingleOrder 同步单个订单 - * 4. 统计同步结果(创建数、更新数、错误数) - * - * 涉及实体: Order - * - * @param siteId 站点ID - * @param params 查询参数 - * @returns 同步操作结果 - */ - async syncOrders(siteId: number, params: Record = {}): Promise { - // 调用 WooCommerce API 获取订单 - const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); - - // 初始化同步结果对象 - const syncResult: SyncOperationResult = { - total: result.length, - processed: 0, - synced: 0, - created: 0, - updated: 0, - errors: [] - }; - - // 遍历每个订单进行同步 - 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++; - } - } - this.logger.debug('syncOrders result', syncResult) - return syncResult; - } - - /** - * 根据订单ID同步单个订单 - * 流程说明: - * 1. 调用 WooCommerce API 获取指定订单 - * 2. 检查数据库中是否已存在该订单 - * 3. 调用 syncSingleOrder 同步订单数据 - * 4. 统计同步结果 - * - * 涉及实体: Order - * - * @param siteId 站点ID - * @param orderId 订单ID - * @returns 同步操作结果 - */ - async syncOrderById(siteId: number, orderId: string): Promise { - const syncResult: SyncOperationResult = { - total: 1, - processed: 0, - synced: 0, - created: 0, - updated: 0, - errors: [] - }; - - try { - // 调用 WooCommerce API 获取订单 - const adapter = await this.siteApiService.getAdapter(siteId); - const order = await adapter.getOrder({ id: orderId }); - - // 检查订单是否已存在,以区分创建和更新 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId: String(order.id), siteId: siteId }, - }); - if (!existingOrder) { - console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status) - } - // 同步单个订单 - await this.syncSingleOrder(siteId, order, true); - - // 统计结果 - syncResult.processed = 1; - syncResult.synced = 1; - - if (existingOrder) { - syncResult.updated = 1; - } else { - syncResult.created = 1; - } - - return syncResult; - } catch (error) { - // 记录错误 - syncResult.errors.push({ - identifier: orderId, - error: error.message || '同步失败' - }); - syncResult.processed = 1; - - return syncResult; - } - } - /** - * 订单状态自动切换映射表 - * 用于将 WordPress 订单状态转换为系统内部状态 - */ - orderAutoNextStatusMap = { - [OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold - [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded - } - - /** - * 自动更新订单状态 - * 流程说明: - * 1. 检查订单状态是否需要转换 - * 2. 如果需要转换,先同步状态到 WooCommerce - * 3. 然后将订单状态切换到下一状态 - * - * 涉及实体: Order - * - * @param siteId 站点ID - * @param order 订单对象 - */ - async autoUpdateOrderStatus(siteId: number, order: any) { - // console.log('更新订单状态', order.status, '=>', this.orderAutoNextStatusMap[order.status]) - // 其他状态保持不变 - const originStatus = order.status; - // 如果有值就赋值 - if (!this.orderAutoNextStatusMap[originStatus]) { - return - } - try { - const site = await this.siteService.get(siteId); - // 仅处理 WooCommerce 站点 - if (site.type !== 'woocommerce') { - return - } - // 将订单状态同步到 WooCommerce,然后切换至下一状态 - await this.wpService.updateOrder(site, String(order.id), { status: order.status }); - order.status = this.orderAutoNextStatusMap[originStatus]; - } catch (error) { - console.error('更新订单状态失败,原因为:', error) - } - } - /** - * 同步单个订单 - * 流程说明: - * 1. 从订单数据中解构出各个子项(line_items, shipping_lines, fee_lines, coupon_lines, refunds, fulfillments) - * 2. 检查数据库中是否已存在该订单 - * 3. 自动更新订单状态(如果需要) - * 4. 如果订单状态为 AUTO_DRAFT,则跳过处理 - * 5. 如果订单从未完成变为完成状态,则更新库存并返回 - * 6. 如果订单不可编辑且不强制更新,则跳过处理 - * 7. 保存订单主数据 - * 8. 保存订单项(OrderItem) - * 9. 保存退款信息(OrderRefund, OrderRefundItem) - * 10. 保存费用信息(OrderFee) - * 11. 保存优惠券信息(OrderCoupon) - * 12. 保存配送信息(OrderShipping) - * 13. 保存履约信息(OrderFulfillment) - * - * 涉及实体: Order, OrderItem, OrderRefund, OrderRefundItem, OrderFee, OrderCoupon, OrderShipping, OrderFulfillment - * - * @param siteId 站点ID - * @param order 订单数据 - * @param forceUpdate 是否强制更新 - */ - async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { - // 从订单数据中解构出各个子项 - let { - line_items, - shipping_lines, - fee_lines, - coupon_lines, - refunds, - fulfillments, // 物流信息 - ...orderData - } = order; - // console.log('同步进单个订单', order) - // 如果订单状态为 AUTO_DRAFT,则跳过处理 - if (order.status === OrderStatus.AUTO_DRAFT) { - return; - } - // 检查数据库中是否已存在该订单 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId: order.id, siteId: siteId }, - }); - // 自动更新订单状态(如果需要) - await this.autoUpdateOrderStatus(siteId, order); - - if (existingOrder) { - // 矫正数据库中的订单数据 - const updateData: any = { status: order.status }; - if (this.canUpdateErpStatus(existingOrder.orderStatus)) { - updateData.orderStatus = this.mapOrderStatus(order.status); - } - // 更新 - await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData); - // 更新 fulfillments 数据 - await this.saveOrderFulfillments({ - siteId, - orderId: existingOrder.id, - externalOrderId: order.id, - fulfillments: fulfillments, - }); - } - const externalOrderId = order.id; - // 如果订单从未完成变为完成状态,则更新库存 - if ( - existingOrder && - existingOrder.orderStatus !== ErpOrderStatus.COMPLETED && - orderData.status === OrderStatus.COMPLETED - ) { - this.updateStock(existingOrder); - // 不再直接返回,继续执行后续的更新操作 - } - // 如果订单不可编辑且不强制更新,则跳过处理 - 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, - }); - // 保存配送信息 - await this.saveOrderShipping({ - siteId, - orderId, - externalOrderId, - shipping_lines, - }); - // 保存履约信息 - await this.saveOrderFulfillments({ - siteId, - orderId, - externalOrderId, - fulfillments: fulfillments, - }); - } - - /** - * 更新订单库存 - * 流程说明: - * 1. 查询订单的所有销售项(OrderSale) - * 2. 遍历每个销售项,创建库存更新记录 - * 3. 调用 StockService 更新库存(出库操作) - * - * 涉及实体: OrderSale, Stock - * - * @param existingOrder 已存在的订单 - */ - async updateStock(existingOrder: Order) { - const items = await this.orderSaleModel.find({ - where: { orderId: existingOrder.id }, - }); - if (!items) return; - const stockPointId = 2; - // ['YT', 'NT', 'BC', 'AB', 'SK'].some( - // v => - // v.toLowerCase() === - // ( - // existingOrder?.shipping?.state || existingOrder?.billing?.state - // ).toLowerCase() - // ) - // ? 3 - // : 2; - for (const item of items) { - const updateStock = new UpdateStockDTO(); - updateStock.stockPointId = stockPointId; - updateStock.sku = item.sku; - updateStock.quantityChange = item.quantity; - updateStock.operationType = StockRecordOperationType.OUT; - updateStock.operatorId = 1; - updateStock.note = `订单${existingOrder.externalOrderId} 出库`; - await this.stockService.updateStock(updateStock); - } - } - - /** - * 保存订单主数据 - * 流程说明: - * 1. 将外部订单ID转换为字符串 - * 2. 创建订单实体对象 - * 3. 检查数据库中是否已存在该订单 - * 4. 如果存在: - * - 检查是否可以更新 ERP 状态 - * - 如果可以,则更新订单状态并保存 - * 5. 如果不存在: - * - 映射订单状态 - * - 创建或更新客户信息 - * - 保存新订单 - * - * 涉及实体: Order, Customer - * - * @param siteId 站点ID - * @param order 订单数据 - * @returns 保存后的订单实体 - */ - async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise { - // 将外部订单ID转换为字符串 - const externalOrderId = String(order.id) - delete order.id - - // 创建订单实体对象 - const entity = plainToClass(Order, { ...order, externalOrderId, siteId }); - // 检查数据库中是否已存在该订单 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId, siteId: siteId }, - }); - // 如果订单已存在 - if (existingOrder) { - // 检查是否可以更新 ERP 状态 - if (this.canUpdateErpStatus(existingOrder.orderStatus)) { - entity.orderStatus = this.mapOrderStatus(entity.status); - } else { - // 如果不能更新 ERP 状态,则保留原有的 orderStatus - entity.orderStatus = existingOrder.orderStatus; - } - // 更新订单数据(包括 shipping、billing 等字段) - await this.orderModel.update(existingOrder.id, entity); - entity.id = existingOrder.id; - return entity; - } - // 如果订单不存在,则映射订单状态 - entity.orderStatus = this.mapOrderStatus(entity.status); - // 创建或更新客户信息 - await this.customerService.upsertCustomer({ - email: order.customer_email, - site_id: siteId, - origin_id: String(order.customer_id), - billing: order.billing, - shipping: order.shipping, - first_name: order?.billing?.first_name || order?.shipping?.first_name, - last_name: order?.billing?.last_name || order?.shipping?.last_name, - fullname: order?.billing?.fullname || order?.shipping?.fullname, - phone: order?.billing?.phone || order?.shipping?.phone, - - // tags:['fromOrder'] - }); - // const customer = await this.customerModel.findOne({ - // where: { email: order.customer_email }, - // }); - // if (!customer) { - // // 这里用 customer create - // await this.customerModel.save({ - // email: order.customer_email, - // site_id: siteId, - // origin_id: String(order.customer_id), - // billing: order.billing, - // shipping: order.shipping, - // phone: order?.billing?.phone || order?.shipping?.phone, - // }); - // } - // 保存新订单 - return await this.orderModel.save(entity); - } - - /** - * 检查是否可以更新 ERP 状态 - * 某些状态不允许被覆盖,如: AFTER_SALE_PROCESSING, PENDING_RESHIPMENT, PENDING_REFUND - * - * @param currentErpStatus 当前 ERP 状态 - * @returns 是否可以更新 - */ - canUpdateErpStatus(currentErpStatus: string): boolean { - const nonOverridableStatuses = [ - 'AFTER_SALE_PROCESSING', - 'PENDING_RESHIPMENT', - 'PENDING_REFUND', - ]; - // 如果当前 ERP 状态不可覆盖,则禁止更新 - return !nonOverridableStatuses.includes(currentErpStatus); - } - - /** - * 映射订单状态 - * 将 WooCommerce 订单状态转换为 ERP 订单状态 - * - * @param status WooCommerce 订单状态 - * @returns ERP 订单状态 - */ - mapOrderStatus(status: OrderStatus): ErpOrderStatus { - switch (status) { - case OrderStatus.PENDING: - return ErpOrderStatus.PENDING; - case OrderStatus.PROCESSING: - return ErpOrderStatus.PROCESSING; - case OrderStatus.COMPLETED: - return ErpOrderStatus.COMPLETED; - case OrderStatus.CANCEL: - return ErpOrderStatus.CANCEL; - case OrderStatus.REFUNDED: - return ErpOrderStatus.REFUNDED; - case OrderStatus.FAILED: - return ErpOrderStatus.FAILED; - case OrderStatus.RETURN_REQUESTED: - return ErpOrderStatus.RETURN_REQUESTED; - case OrderStatus.RETURN_APPROVED: - return ErpOrderStatus.RETURN_APPROVED; - case OrderStatus.RETURN_CANCELLED: - return ErpOrderStatus.RETURN_CANCELLED; - default: - return ErpOrderStatus.PENDING; - } - } - - /** - * 保存订单项 - * 流程说明: - * 1. 查询数据库中已存在的订单项 - * 2. 找出需要删除的订单项(在数据库中存在但在新数据中不存在) - * 3. 删除这些订单项及其对应的销售项(OrderSale) - * 4. 遍历新的订单项,设置相关字段 - * 5. 保存每个订单项 - * 6. 为每个订单项创建对应的销售项(OrderSale) - * - * 涉及实体: OrderItem, OrderSale - * - * @param params 订单项参数 - */ - async saveOrderItems(params: { - siteId: number; - orderId: number; - externalOrderId: string; - orderItems: Record[]; - }) { - // 查询数据库中已存在的订单项 - const { siteId, orderId, externalOrderId, orderItems } = params; - const currentOrderItems = await this.orderItemModel.find({ - where: { siteId, externalOrderId: externalOrderId }, - }); - // 找出需要删除的订单项(在数据库中存在但在新数据中不存在) - const syncedOrderItemIds = new Set(orderItems.map(v => String(v.id))); - const orderItemToDelete = currentOrderItems.filter( - db => !syncedOrderItemIds.has(String(db.externalOrderItemId)) - ); - // 删除这些订单项及其对应的销售项(OrderSale) - if (orderItemToDelete.length > 0) { - const idsToDelete = orderItemToDelete.map(v => v.id); - await this.orderItemModel.delete(idsToDelete); - const itemIdsToDelete = orderItemToDelete.map(v => v.externalOrderItemId); - await this.orderSaleModel.delete({ - siteId, - externalOrderItemId: In(itemIdsToDelete), - }); - } - // 遍历新的订单项,设置相关字段并保存 - for (const orderItem of orderItems) { - orderItem.siteId = siteId; - orderItem.externalOrderId = externalOrderId; - orderItem.externalOrderItemId = String(orderItem.id); - orderItem.orderId = orderId; - orderItem.externalProductId = String(orderItem.product_id); - orderItem.externalVariationId = String(orderItem.variation_id); - delete orderItem.id; - const entity = plainToClass(OrderItem, orderItem); - // 保存订单项 - await this.saveOrderItem(entity); - // 为每个订单项创建对应的销售项(OrderSale) - await this.saveOrderSale(entity); - } - } - - /** - * 保存单个订单项 - * 流程说明: - * 1. 检查数据库中是否已存在该订单项 - * 2. 如果存在,则更新订单项 - * 3. 如果不存在,则创建新订单项 - * - * 涉及实体: OrderItem - * - * @param orderItem 订单项实体 - */ - async saveOrderItem(orderItem: OrderItem) { - const existingOrderItem = await this.orderItemModel.findOne({ - where: { - externalOrderId: orderItem.externalOrderId, - siteId: orderItem.siteId, - externalOrderItemId: orderItem.externalOrderItemId, - }, - }); - - if ( - existingOrderItem && - existingOrderItem.quantity === orderItem.quantity - ) { - return; - } - if (existingOrderItem) { - await this.orderItemModel.update(existingOrderItem.id, orderItem); - } else { - await this.orderItemModel.save(orderItem); - } - } - - /** - * 保存单个订单项(备用方法) - * 流程说明: - * 1. 检查数据库中是否已存在该订单项 - * 2. 如果数量相同,则跳过处理 - * 3. 如果存在但数量不同,则更新订单项 - * 4. 如果不存在,则创建新订单项 - * - * 注意: 该方法与 saveOrderItem 功能相同,可能是备用或待删除的方法 - * - * 涉及实体: OrderItem - * - * @param orderItem 订单项实体 - */ - async saveOrderItemsG(orderItem: OrderItem) { - const existingOrderItem = await this.orderItemModel.findOne({ - where: { - externalOrderId: orderItem.externalOrderId, - siteId: orderItem.siteId, - externalOrderItemId: orderItem.externalOrderItemId, - }, - }); - - if ( - existingOrderItem && - existingOrderItem.quantity === orderItem.quantity - ) { - return; - } - if (existingOrderItem) { - await this.orderItemModel.update(existingOrderItem.id, orderItem); - } else { - await this.orderItemModel.save(orderItem); - } - } - - /** - * 保存订单销售项 - * 流程说明: - * 1. 查询数据库中已存在的销售项 - * 2. 删除已存在的销售项 - * 3. 如果订单项没有 SKU,则跳过处理 - * 4. 查询产品信息(包含组件信息) - * 5. 如果产品有组件,则为每个组件创建销售项 - * 6. 如果产品没有组件,则直接创建销售项 - * 7. 保存所有销售项 - * - * 涉及实体: OrderSale, Product - * - * @param orderItem 订单项实体 - */ - 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); - } - } - - /** - * 保存订单退款信息 - * 流程说明: - * 1. 遍历退款列表,为每个退款项获取详细信息 - * 2. 设置退款相关字段(orderId、siteId、externalOrderId、externalRefundId) - * 3. 检查数据库中是否已存在该退款 - * 4. 如果存在,则更新退款信息 - * 5. 如果不存在,则创建新退款 - * 6. 保存退款项(OrderRefundItem) - * - * 涉及实体: OrderRefund, OrderRefundItem - * - * @param params 退款参数 - */ - async saveOrderRefunds({ - siteId, - orderId, - externalOrderId, - refunds, - }: { - siteId: number; - orderId: number; - externalOrderId: string; - refunds: Record[]; - }) { - for (const item of refunds) { - const refund = await this.wpService.getOrderRefund( - String(siteId), - externalOrderId, - item.id - ); - const refundItems = refund.line_items; - refund.orderId = orderId; - refund.siteId = siteId; - refund.externalOrderId = externalOrderId; - refund.externalRefundId = item.id; - delete refund.id; - const entity = plainToClass(OrderRefund, refund); - const existingOrderRefund = await this.orderRefundModel.findOne({ - where: { - siteId, - externalOrderId: externalOrderId, - externalRefundId: item.id, - }, - }); - let refundId; - if (existingOrderRefund) { - await this.orderRefundModel.update(existingOrderRefund.id, entity); - refundId = existingOrderRefund.id; - } else { - refundId = (await this.orderRefundModel.save(entity)).id; - } - this.saveOrderRefundItems({ - refundItems, - siteId, - refundId, - externalRefundId: item.id, - }); - } - } - - /** - * 保存订单退款项 - * 流程说明: - * 1. 遍历退款项列表 - * 2. 设置退款项相关字段(externalRefundItemId、externalProductId、externalVariationId) - * 3. 创建退款项实体 - * 4. 检查数据库中是否已存在该退款项 - * 5. 如果存在,则更新退款项 - * 6. 如果不存在,则创建新退款项 - * - * 涉及实体: OrderRefundItem - * - * @param params 退款项参数 - */ - async saveOrderRefundItems({ - refundItems, - siteId, - refundId, - externalRefundId, - }) { - for (const item of refundItems) { - item.externalRefundItemId = item.id; - item.externalProductId = item.product_id; - item.externalVariationId = item.variation_id; - delete item.id; - const refundItem = plainToClass(OrderRefundItem, { - refundId, - siteId, - externalRefundId, - ...item, - }); - const existingOrderRefundItem = await this.orderRefundItemModel.findOne({ - where: { - siteId, - externalRefundId, - externalRefundItemId: refundItem.externalRefundItemId, - }, - }); - if (existingOrderRefundItem) { - await this.orderRefundItemModel.update( - existingOrderRefundItem.id, - refundItem - ); - } else { - await this.orderRefundItemModel.save(refundItem); - } - } - } - - /** - * 保存订单费用信息 - * 流程说明: - * 1. 查询数据库中已存在的费用项 - * 2. 找出需要删除的费用项(在数据库中存在但在新数据中不存在) - * 3. 删除这些费用项 - * 4. 遍历新的费用项,设置相关字段 - * 5. 检查数据库中是否已存在该费用项 - * 6. 如果存在,则更新费用项 - * 7. 如果不存在,则创建新费用项 - * - * 涉及实体: OrderFee - * - * @param params 费用参数 - */ - async saveOrderFees({ siteId, orderId, externalOrderId, fee_lines }) { - const currentOrderFees = await this.orderFeeModel.find({ - where: { siteId, externalOrderId }, - }); - const syncedFeeIds = new Set(fee_lines.map(v => String(v.id))); - const toDeleteIds = currentOrderFees - .filter(db => !syncedFeeIds.has(db.externalOrderFeeId)) - .map(db => db.id); - if (toDeleteIds.length > 0) { - await this.orderFeeModel.delete(toDeleteIds); - } - for (const fee of fee_lines) { - fee.externalOrderFeeId = String(fee.id); - delete fee.id; - const entity = plainToClass(OrderFee, { - siteId, - orderId, - externalOrderId, - ...fee, - }); - const db = await this.orderFeeModel.findOne({ - where: { - siteId, - externalOrderId, - externalOrderFeeId: entity.externalOrderFeeId, - }, - }); - if (db) { - await this.orderFeeModel.update(db.id, entity); - } else { - await this.orderFeeModel.save(entity); - } - } - } - - /** - * 保存订单优惠券信息 - * 流程说明: - * 1. 遍历优惠券列表 - * 2. 设置优惠券相关字段(externalOrderCouponId、siteId、orderId、externalOrderId) - * 3. 创建优惠券实体 - * 4. 检查数据库中是否已存在该优惠券 - * 5. 如果存在,则更新优惠券 - * 6. 如果不存在,则创建新优惠券 - * - * 涉及实体: OrderCoupon - * - * @param params 优惠券参数 - */ - async saveOrderCoupons({ siteId, externalOrderId, orderId, coupon_lines }) { - for (const item of coupon_lines) { - item.externalOrderCouponId = item.id; - item.siteId = siteId; - item.orderId = orderId; - item.externalOrderId = externalOrderId; - delete item.id; - const coupon = plainToClass(OrderCoupon, item); - const existingOrderCoupon = await this.orderCouponModel.findOne({ - where: { - siteId, - externalOrderId, - externalOrderCouponId: coupon.externalOrderCouponId, - }, - }); - if (existingOrderCoupon) { - await this.orderCouponModel.update(existingOrderCoupon.id, coupon); - } else { - await this.orderCouponModel.save(coupon); - } - } - } - - /** - * 保存订单配送信息 - * 流程说明: - * 1. 遍历配送列表 - * 2. 设置配送相关字段(externalOrderShippingId、siteId、orderId、externalOrderId) - * 3. 创建配送实体 - * 4. 检查数据库中是否已存在该配送 - * 5. 如果存在,则更新配送 - * 6. 如果不存在,则创建新配送 - * - * 涉及实体: OrderShipping - * - * @param params 配送参数 - */ - async saveOrderShipping({ - siteId, - orderId, - externalOrderId, - shipping_lines, - }) { - for (const item of shipping_lines) { - item.externalOrderShippingId = item.id; - item.siteId = siteId; - item.orderId = orderId; - item.externalOrderId = externalOrderId; - delete item.id; - const shipping = plainToClass(OrderShipping, item); - const existingOrderShipping = await this.orderShippingModel.findOne({ - where: { - siteId, - externalOrderId, - externalOrderShippingId: shipping.externalOrderShippingId, - }, - }); - if (existingOrderShipping) { - await this.orderShippingModel.update( - existingOrderShipping.id, - shipping - ); - } else { - await this.orderShippingModel.save(shipping); - } - } - } - - /** - * 保存订单履约信息 - * 流程说明: - * 1. 检查履约列表是否存在,如果不存在则跳过 - * 2. 遍历履约列表 - * 3. 设置履约相关字段(externalFulfillmentId、siteId、orderId、externalOrderId) - * 4. 转换时间戳为日期格式 - * 5. 创建履约实体 - * 6. 检查数据库中是否已存在该履约 - * 7. 如果存在,则更新履约 - * 8. 如果不存在,则创建新履约 - * - * 涉及实体: OrderFulfillment - * - * @param params 履约参数 - */ - async saveOrderFulfillments({ - siteId, - orderId, - externalOrderId, - fulfillments, - }) { - // 如果履约列表不存在,则跳过处理 - if (!fulfillments || !Array.isArray(fulfillments) || fulfillments.length === 0) { - return; - } - - // 遍历履约列表 - for (const item of fulfillments) { - // 设置履约相关字段 - item.external_fulfillment_id = String(item.id); - item.site_id = siteId; - item.order_id = orderId; - item.external_order_id = String(externalOrderId); - - // 删除原始 ID - delete item.id; - - // 转换时间戳为日期格式 - if (item.date_created && typeof item.date_created === 'number') { - item.date_created = new Date(item.date_created * 1000); - } else if (item.date_created && typeof item.date_created === 'string') { - item.date_created = new Date(item.date_created); - } - - // 创建履约实体 - const fulfillment = plainToClass(OrderFulfillment, item); - - // 检查数据库中是否已存在该履约 - const existingOrderFulfillment = await this.orderFulfillmentModel.findOne({ - where: { - site_id: siteId, - external_order_id: externalOrderId, - external_fulfillment_id: fulfillment.external_fulfillment_id, - }, - }); - - // 如果存在,则更新履约 - if (existingOrderFulfillment) { - await this.orderFulfillmentModel.update( - existingOrderFulfillment.id, - fulfillment - ); - } else { - // 如果不存在,则创建新履约 - await this.orderFulfillmentModel.save(fulfillment); - } - } - } - - /** - * 获取订单列表 - * 流程说明: - * 1. 构建基础SQL查询,包含订单基本信息、客户统计、订阅信息、物流信息 - * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、状态、关键字等) - * 3. 检查用户权限,如果用户有"order-10-days"权限且未指定日期范围,则限制查询最近10天的订单 - * 4. 执行总数查询 - * 5. 添加分页和排序,执行主查询 - * 6. 返回订单列表和分页信息 - * - * 涉及实体: Order, Subscription, Shipment, Customer - * - * @param params 查询参数 - * @param userId 用户ID(用于权限检查) - * @returns 订单列表和分页信息 - */ - async getOrders({ - externalOrderId, - siteId, - startDate, - endDate, - status, - keyword, - current, - pageSize, - customer_email, - payment_method, - billing_phone, - isSubscriptionOnly = false, - }, userId = undefined) { - const parameters: any[] = []; - - // 基础查询 - let sqlQuery = ` - SELECT - o.id as id, - o.externalOrderId as externalOrderId, - o.siteId as siteId, - o.date_paid as date_paid, - o.total as total, - o.date_created as date_created, - o.customer_email as customer_email, - o.exchange_frequency as exchange_frequency, - o.transaction_id as transaction_id, - o.orderStatus as orderStatus, - o.customer_ip_address as customer_ip_address, - o.device_type as device_type, - o.source_type as source_type, - o.utm_source as utm_source, - o.customer_note as customer_note, - o.shipping as shipping, - o.billing as billing, - o.payment_method as payment_method, - cs.order_count as order_count, - cs.total_spent as total_spent, - CASE WHEN EXISTS ( - SELECT 1 FROM subscription s - WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId - ) THEN 1 ELSE 0 END AS isSubscription, - ( - SELECT COALESCE( - JSON_ARRAYAGG( - JSON_OBJECT( - 'id', s.id, - 'externalSubscriptionId', s.externalSubscriptionId, - 'status', s.status, - 'currency', s.currency, - 'total', s.total, - 'billing_period', s.billing_period, - 'billing_interval', s.billing_interval, - 'customer_id', s.customer_id, - 'customer_email', s.customer_email, - 'parent_id', s.parent_id, - 'start_date', s.start_date, - 'trial_end', s.trial_end, - 'next_payment_date', s.next_payment_date, - 'end_date', s.end_date, - 'line_items', s.line_items, - 'meta_data', s.meta_data - ) - ), - JSON_ARRAY() - ) - FROM subscription s - WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId - ) AS related, - COALESCE( - JSON_ARRAYAGG( - CASE WHEN s.id IS NOT NULL THEN JSON_OBJECT( - 'state', s.state, - 'tracking_provider', s.tracking_provider, - 'primary_tracking_number', s.primary_tracking_number - ) END - ), - JSON_ARRAY() - ) as shipmentList, - COALESCE( - JSON_ARRAYAGG( - CASE WHEN fulfillment.id IS NOT NULL THEN JSON_OBJECT( - 'id', fulfillment.id, - 'externalFulfillmentId', fulfillment.external_fulfillment_id, - 'orderId', fulfillment.order_id, - 'siteId', fulfillment.site_id, - 'status', fulfillment.status, - 'createdAt', fulfillment.created_at, - 'updatedAt', fulfillment.updated_at, - 'tracking_number', fulfillment.tracking_number, - 'shipping_provider', fulfillment.shipping_provider, - 'shipping_method', fulfillment.shipping_method, - 'items', fulfillment.items - ) END - ), - JSON_ARRAY() - ) as fulfillments - FROM \`order\` o - LEFT JOIN ( - SELECT - o.customer_email AS customer_email, - COUNT(o.id) AS order_count, - SUM(o.total) AS total_spent - FROM \`order\` o - WHERE o.status IN ('processing', 'completed') - GROUP BY o.customer_email - ) cs ON cs.customer_email = o.customer_email - LEFT JOIN order_shipment os ON os.order_id = o.id - LEFT JOIN shipment s ON s.id = os.shipment_id - LEFT JOIN order_fulfillment fulfillment ON fulfillment.order_id = o.id - WHERE 1=1 - `; - - // 计算总数 - let totalQuery = ` - SELECT COUNT(*) as total - FROM \`order\` o - LEFT JOIN ( - SELECT o.customer_email AS customer_email - FROM \`order\` o - WHERE o.status IN ('processing', 'completed') - GROUP BY o.customer_email - ) cs ON cs.customer_email = o.customer_email - WHERE 1=1 - `; - - // 动态添加过滤条件 - if (externalOrderId) { - sqlQuery += ` AND o.externalOrderId = ?`; - totalQuery += ` AND o.externalOrderId = ?`; - parameters.push(externalOrderId); - } - if (siteId) { - sqlQuery += ` AND o.siteId = ?`; - totalQuery += ` AND o.siteId = ?`; - parameters.push(siteId); - } - if (startDate) { - sqlQuery += ` AND o.date_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 }; - } - - /** - * 获取订单状态统计 - * 流程说明: - * 1. 构建查询,按订单状态分组统计订单数量 - * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、关键字等) - * 3. 支持关键字搜索,检查订单项名称是否包含关键字 - * 4. 支持订阅订单过滤 - * 5. 返回各状态的订单数量统计 - * - * 涉及实体: Order, OrderItem, Subscription - * - * @param params 查询参数 - * @returns 订单状态统计列表 - */ - async getOrderStatus({ - externalOrderId, - siteId, - startDate, - endDate, - keyword, - customer_email, - billing_phone, - isSubscriptionOnly = false, - }: any) { - const query = this.orderModel - .createQueryBuilder('order') - .select('order.orderStatus', 'status') - .addSelect('COUNT(*)', 'count') - .groupBy('order.orderStatus'); - - if (externalOrderId) { - query.andWhere('order.externalOrderId = :externalOrderId', { - externalOrderId, - }); - } - if (siteId) { - query.andWhere('order.siteId = :siteId', { siteId }); - } - if (startDate) { - query.andWhere('order.date_created >= :startDate', { startDate }); - } - if (endDate) { - query.andWhere('order.date_created <= :endDate', { endDate }); - } - if (customer_email) - query.andWhere('order.customer_email LIKE :customer_email', { - customer_email: `%${customer_email}%`, - }); - - // 🔥 关键字搜索:检查 order_item.name 是否包含 keyword - if (keyword) { - query.andWhere( - `EXISTS ( - SELECT 1 FROM order_item oi - WHERE oi.orderId = order.id - AND oi.name LIKE :keyword - )`, - { keyword: `%${keyword}%` } - ); - } - - if (isSubscriptionOnly) { - query.andWhere(`( - EXISTS ( - SELECT 1 FROM subscription s - WHERE s.siteId = order.siteId AND s.parent_id = order.externalOrderId - ) - )`); - } - - return await query.getRawMany(); - } - - /** - * 获取订单销售统计 - * 流程说明: - * 1. 查询总条数(按产品ID去重) - * 2. 分页查询产品基础信息(产品ID、名称、总数量、订单数) - * 3. 批量统计当前页产品的历史复购情况(第1次、第2次、第3次、>3次订单的数量和盒数) - * 4. 统计总量(时间段内的总数量、YOONE各规格数量、ZEX数量) - * 5. 支持按产品名称关键字过滤 - * 6. 支持排除套餐订单(exceptPackage) - * - * 涉及实体: OrderSale, Order - * - * @param params 查询参数 - * @returns 销售统计和分页信息 - */ - async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) { - const nameKeywords = name ? name.split(' ').filter(Boolean) : []; - const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); - const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'); - startDate = (startDate as any) || defaultStart as any; - endDate = (endDate as any) || defaultEnd as any; - const offset = (current - 1) * pageSize; - - // ------------------------- - // 1. 查询总条数 - // ------------------------- - const countParams: any[] = [startDate, endDate]; - let countSql = ` - SELECT COUNT(DISTINCT os.productId) AS totalCount - FROM order_sale os - INNER JOIN \`order\` o ON o.id = os.orderId - WHERE o.date_paid BETWEEN ? AND ? - AND o.status IN ('completed','processing') - `; - if (siteId) { - countSql += ' AND os.siteId = ?'; - countParams.push(siteId); - } - if (nameKeywords.length > 0) { - countSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; - countParams.push(...nameKeywords.map(w => `%${w}%`)); - } - const [countResult] = await this.orderSaleModel.query(countSql, countParams); - const totalCount = Number(countResult?.totalCount || 0); - - // ------------------------- - // 2. 分页查询 product 基础信息 - // ------------------------- - const itemParams: any[] = [startDate, endDate]; - let nameCondition = ''; - if (nameKeywords.length > 0) { - nameCondition = ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; - itemParams.push(...nameKeywords.map(w => `%${w}%`)); - } - - let itemSql = ` - SELECT os.productId, os.name, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders - FROM order_sale os - INNER JOIN \`order\` o ON o.id = os.orderId - WHERE o.date_paid BETWEEN ? AND ? - AND o.status IN ('completed','processing') - `; - if (siteId) { - itemSql += ' AND os.siteId = ?'; - itemParams.push(siteId); - } - if (exceptPackage) { - itemSql += ` - AND os.orderId IN ( - SELECT orderId - FROM order_sale - GROUP BY orderId - HAVING COUNT(*) = 1 - ) - `; - } - itemSql += nameCondition; - itemSql += ` - GROUP BY os.productId, os.name - ORDER BY totalQuantity DESC - LIMIT ? OFFSET ? - `; - itemParams.push(pageSize, offset); - const items = await this.orderSaleModel.query(itemSql, itemParams); - - // ------------------------- - // 3. 批量统计当前页 product 历史复购 - // ------------------------- - if (items.length > 0) { - const productIds = items.map(i => i.productId); - const pcParams: any[] = [...productIds, startDate, endDate]; - if (siteId) pcParams.push(siteId); - - let pcSql = ` - SELECT - os.productId, - SUM(CASE WHEN t.purchaseIndex = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex = 1 THEN os.orderId END) AS firstOrderCount, - SUM(CASE WHEN t.purchaseIndex = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex = 2 THEN os.orderId END) AS secondOrderCount, - SUM(CASE WHEN t.purchaseIndex = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex = 3 THEN os.orderId END) AS thirdOrderCount, - SUM(CASE WHEN t.purchaseIndex > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex > 3 THEN os.orderId END) AS moreThirdOrderCount - FROM order_sale os - INNER JOIN ( - SELECT o2.id AS orderId, - @idx := IF(@prev_email = o2.customer_email, @idx + 1, 1) AS purchaseIndex, - @prev_email := o2.customer_email - FROM \`order\` o2 - CROSS JOIN (SELECT @idx := 0, @prev_email := '') vars - WHERE o2.status IN ('completed','processing') - ORDER BY o2.customer_email, o2.date_paid - ) t ON t.orderId = os.orderId - WHERE os.productId IN (${productIds.map(() => '?').join(',')}) - AND os.orderId IN ( - SELECT id FROM \`order\` - WHERE date_paid BETWEEN ? AND ? - ${siteId ? 'AND siteId = ?' : ''} - ) - `; - if (exceptPackage) { - pcSql += ` - AND os.orderId IN ( - SELECT orderId - FROM order_sale - GROUP BY orderId - HAVING COUNT(*) = 1 - ) - `; - } - pcSql += ` - GROUP BY os.productId - `; - - console.log('------3.5-----', pcSql, pcParams, exceptPackage); - const pcResults = await this.orderSaleModel.query(pcSql, pcParams); - - const pcMap = new Map(); - pcResults.forEach(r => pcMap.set(r.productId, r)); - items.forEach(i => { - const r = pcMap.get(i.productId) || {}; - i.firstOrderYOONEBoxCount = Number(r.firstOrderYOONEBoxCount || 0); - i.firstOrderCount = Number(r.firstOrderCount || 0); - i.secondOrderYOONEBoxCount = Number(r.secondOrderYOONEBoxCount || 0); - i.secondOrderCount = Number(r.secondOrderCount || 0); - i.thirdOrderYOONEBoxCount = Number(r.thirdOrderYOONEBoxCount || 0); - i.thirdOrderCount = Number(r.thirdOrderCount || 0); - i.moreThirdOrderYOONEBoxCount = Number(r.moreThirdOrderYOONEBoxCount || 0); - i.moreThirdOrderCount = Number(r.moreThirdOrderCount || 0); - }); - } - - // ------------------------- - // 4. 总量统计(时间段 + siteId) - // ------------------------- - const totalParams: any[] = [startDate, endDate]; - const yooneParams: any[] = [startDate, endDate]; - let totalSql = ` - SELECT - SUM(os.quantity) AS totalQuantity - FROM order_sale os - INNER JOIN \`order\` o ON o.id = os.orderId - WHERE o.date_paid BETWEEN ? AND ? - AND o.status IN ('completed','processing') - `; - let yooneSql = ` - SELECT - SUM(CASE WHEN os.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, - }; - } - - - /** - * 获取订单项统计 - * 流程说明: - * 1. 使用CTE计算每个客户对每个产品的购买次数 - * 2. 查询订单项统计信息(外部产品ID、变体ID、名称、总数量、订单数) - * 3. 统计不同购买次数的订单数(第1次、第2次、第3次、>3次) - * 4. 支持按产品名称关键字过滤 - * 5. 支持分页查询 - * 6. 返回订单项统计和分页信息 - * - * 涉及实体: OrderItem, Order - * - * @param params 查询参数 - * @returns 订单项统计和分页信息 - */ - 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, - }; - } - - /** - * 获取订单项列表 - * 流程说明: - * 1. 构建SQL查询,关联订单表,获取订单项和订单信息 - * 2. 检查订单项是否为订阅项(通过meta_data中的特定key判断) - * 3. 根据参数动态添加过滤条件(日期范围、站点ID、产品名称、外部产品ID、变体ID) - * 4. 执行总数查询 - * 5. 添加分页和排序,执行主查询 - * 6. 返回订单项列表和分页信息 - * - * 涉及实体: OrderItem, Order - * - * @param params 查询参数 - * @returns 订单项列表和分页信息 - */ - async getOrderItemList({ - siteId, - startDate, - endDate, - current, - pageSize, - name, - externalProductId, - externalVariationId, - }: any) { - const params: any[] = []; - let sql = ` - SELECT - oi.*, - o.id AS orderId, - o.externalOrderId AS orderExternalOrderId, - o.date_created AS orderDateCreated, - o.customer_email AS orderCustomerEmail, - o.orderStatus AS orderStatus, - o.siteId AS orderSiteId, - CASE WHEN - JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"is_subscription"') - OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcs_bought_as_subscription"') - OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcsatt_scheme"') - OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_subscription"') - THEN 1 ELSE 0 END AS isSubscriptionItem - FROM order_item oi - INNER JOIN \`order\` o ON o.id = oi.orderId - WHERE 1=1 - `; - let countSql = ` - SELECT COUNT(*) AS total - FROM order_item oi - INNER JOIN \`order\` o ON o.id = oi.orderId - WHERE 1=1 - `; - const pushFilter = (cond: string, value: any) => { - sql += cond; countSql += cond; params.push(value); - }; - if (startDate) pushFilter(' AND o.date_created >= ?', startDate); - if (endDate) pushFilter(' AND o.date_created <= ?', endDate); - if (siteId) pushFilter(' AND oi.siteId = ?', siteId); - if (name) { - pushFilter(' AND oi.name LIKE ?', `%${name}%`); - } - if (externalProductId) pushFilter(' AND oi.externalProductId = ?', externalProductId); - if (externalVariationId) pushFilter(' AND oi.externalVariationId = ?', externalVariationId); - - sql += ' ORDER BY o.date_created DESC LIMIT ? OFFSET ?'; - const listParams = [...params, pageSize, (current - 1) * pageSize]; - - const items = await this.orderItemModel.query(sql, listParams); - const [countRow] = await this.orderItemModel.query(countSql, params); - const total = Number(countRow?.total || 0); - - return { items, total, current, pageSize }; - } - /** - * 获取订单详情 - * 流程说明: - * 1. 查询订单基本信息 - * 2. 查询站点信息 - * 3. 查询订单项 - * 4. 查询订单销售项 - * 5. 查询退款信息和退款项 - * 6. 查询订单备注(包含用户名) - * 7. 查询物流信息(包含物流项) - * 8. 查询关联的订阅和相关订单 - * 9. 返回完整的订单详情 - * - * 涉及实体: Order, Site, OrderItem, OrderSale, OrderRefund, OrderRefundItem, OrderNote, Shipment, ShipmentItem, Subscription - * - * @param id 订单ID - * @returns 订单详情 - */ - async getOrderDetail(id: number): Promise { - const order = await this.orderModel.findOne({ where: { id } }); - const site = await this.siteService.get(Number(order.siteId), true); - const items = await this.orderItemModel.find({ where: { orderId: id } }); - const sales = await this.orderSaleModel.find({ where: { orderId: id } }); - const refunds = await this.orderRefundModel.find({ - where: { orderId: id }, - }); - const refundItems = await this.orderRefundItemModel.find({ - where: { refundId: In(refunds.map(refund => refund.id)) }, - }); - const notes = await this.orderNoteModel - .createQueryBuilder('order_note') - .leftJoin(User, 'u', 'u.id=order_note.userId') - .select(['order_note.*', 'u.username as username']) - .where('order_note.orderId=:orderId', { orderId: id }) - .getRawMany(); - - const getShipmentSql = ` - SELECT - s.*, - CAST(CONCAT('[', GROUP_CONCAT(DISTINCT o.externalOrderId SEPARATOR ','), ']') AS JSON) AS orderIds - FROM - shipment s - JOIN - order_shipment os ON os.shipment_id = s.id - JOIN - \`order\` o ON os.order_id = o.id - WHERE - s.id IN ( - SELECT shipment_id FROM order_shipment WHERE order_id = ? - ) - GROUP BY - s.id; - `; - - const shipment = await this.shipmentModel.query(getShipmentSql, [id, id]); - if (shipment && shipment.length) { - for (const v of shipment) { - v.items = await this.shipmentItemModel.findBy({ shipment_id: v.id }); - } - } - - - - // 关联数据:订阅与相关订单(用于前端关联展示) - let relatedList: any[] = []; - try { - const related = await this.getRelatedByOrder(id); - const subs = Array.isArray(related?.subscriptions) ? related.subscriptions : []; - const ords = Array.isArray(related?.orders) ? related.orders : []; - const seen = new Set(); - const merge = [...subs, ...ords]; - for (const it of merge) { - const key = it?.externalSubscriptionId - ? `sub:${it.externalSubscriptionId}` - : it?.externalOrderId - ? `ord:${it.externalOrderId}` - : `id:${it?.id}`; - if (!seen.has(key)) { - seen.add(key); - relatedList.push(it); - } - } - } catch (error) { - // 关联查询失败不影响详情返回 - } - - return { - ...order, - name: site?.name, - // Site 实体无邮箱字段,这里返回空字符串保持兼容 - email: '', - items, - sales, - refundItems, - notes, - shipment, - related: relatedList, - }; - } - - /** - * 获取订单关联信息 - * 流程说明: - * 1. 查询订单基本信息 - * 2. 查询关联的订阅信息(通过parent_id关联) - * 3. 返回订单和订阅信息 - * - * 涉及实体: Order, Subscription - * - * @param orderId 订单ID - * @returns 订单和关联的订阅信息 - */ - async getRelatedByOrder(orderId: number) { - const order = await this.orderModel.findOne({ where: { id: orderId } }); - if (!order) throw new Error('订单不存在'); - const siteId = order.siteId; - const subSql = ` - SELECT * FROM subscription s - WHERE s.siteId = ? AND s.parent_id = ? - `; - const subscriptions = await this.orderModel.query(subSql, [siteId, order.externalOrderId]); - return { - order, - subscriptions, - orders: [], - }; - } - - /** - * 删除订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 删除订单配送信息 - * 3. 删除订单销售项 - * 4. 删除退款信息和退款项 - * 5. 删除订单项 - * 6. 删除订单费用 - * 7. 删除订单优惠券 - * 8. 删除订单主数据 - * - * 涉及实体: Order, OrderShipping, OrderSale, OrderRefund, OrderRefundItem, OrderItem, OrderFee, OrderCoupon - * - * @param id 订单ID - */ - async delOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error('订单不存在'); - await this.orderShippingModel.delete({ orderId: id }); - await this.orderSaleModel.delete({ orderId: id }); - const refunds = await this.orderRefundModel.find({ - where: { orderId: id }, - }); - if (refunds.length > 0) { - for (const refund of refunds) { - await this.orderRefundItemModel.delete({ refundId: refund.id }); - await this.orderRefundModel.delete({ id: refund.id }); - } - } - await this.orderItemModel.delete({ orderId: id }); - await this.orderFeeModel.delete({ orderId: id }); - await this.orderCouponModel.delete({ orderId: id }); - await this.orderModel.delete({ id }); - } - - /** - * 创建订单备注 - * 流程说明: - * 1. 接收用户ID和备注数据 - * 2. 创建订单备注实体,关联用户ID - * 3. 保存订单备注 - * - * 涉及实体: OrderNote - * - * @param userId 用户ID - * @param data 备注数据 - * @returns 保存后的订单备注 - */ - async createNote(userId: number, data: CreateOrderNoteDTO) { - return await this.orderNoteModel.save({ - ...data, - userId, - }); - } - - /** - * 根据订单号获取订单 - * 流程说明: - * 1. 根据订单号模糊查询订单(仅查询处理中和待补发的订单) - * 2. 批量获取订单涉及的站点名称 - * 3. 构建站点ID到站点名称的映射 - * 4. 返回订单列表,包含订单号、ID和站点名称 - * - * 涉及实体: Order, Site - * - * @param id 订单号 - * @returns 订单列表(包含订单号、ID和站点名称) - */ - async getOrderByNumber(id: string) { - const orders = await this.orderModel.find({ - where: { - externalOrderId: Like(id), - orderStatus: In([ - ErpOrderStatus.PROCESSING, - ErpOrderStatus.PENDING_RESHIPMENT, - ]), - }, - }); - // 批量获取订单涉及的站点名称,避免使用配置文件 - const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); - const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false); - const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name])); - return orders.map(order => ({ - externalOrderId: order.externalOrderId, - id: order.id, - name: siteMap.get(String(order.siteId)) || '', - })); - } - - /** - * 取消订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 查询订单所属站点 - * 3. 如果订单状态不是已取消,则调用WooCommerce API更新订单状态为已取消 - * 4. 更新本地订单状态为已取消 - * 5. 更新订单ERP状态为已取消 - * - * 涉及实体: Order, Site - * - * @param id 订单ID - */ - async cancelOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - const site = await this.siteService.get(Number(order.siteId), true); - if (order.status !== OrderStatus.CANCEL) { - await this.wpService.updateOrder(site, order.externalOrderId, { - status: OrderStatus.CANCEL, - }); - order.status = OrderStatus.CANCEL; - } - order.orderStatus = ErpOrderStatus.CANCEL; - await this.orderModel.save(order); - } - - /** - * 退款订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 查询订单所属站点 - * 3. 如果订单状态不是已退款,则调用WooCommerce API更新订单状态为已退款 - * 4. 更新本地订单状态为已退款 - * 5. 更新订单ERP状态为已退款 - * - * 涉及实体: Order, Site - * - * @param id 订单ID - */ - async refundOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - const site = await this.siteService.get(Number(order.siteId), true); - if (order.status !== OrderStatus.REFUNDED) { - await this.wpService.updateOrder(site, order.externalOrderId, { - status: OrderStatus.REFUNDED, - }); - order.status = OrderStatus.REFUNDED; - } - order.orderStatus = ErpOrderStatus.REFUNDED; - await this.orderModel.save(order); - } - - /** - * 完成订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 查询订单所属站点 - * 3. 如果订单状态不是已完成,则调用WooCommerce API更新订单状态为已完成 - * 4. 更新本地订单状态为已完成 - * 5. 更新订单ERP状态为已完成 - * - * 涉及实体: Order, Site - * - * @param id 订单ID - */ - async completedOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - const site = await this.siteService.get(order.siteId); - if (order.status !== OrderStatus.COMPLETED) { - await this.wpService.updateOrder(site, order.externalOrderId, { - status: OrderStatus.COMPLETED, - }); - order.status = OrderStatus.COMPLETED; - } - order.orderStatus = ErpOrderStatus.COMPLETED; - await this.orderModel.save(order); - } - - /** - * 更改订单状态 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 更新订单ERP状态 - * 3. 保存订单 - * - * 涉及实体: Order - * - * @param id 订单ID - * @param status ERP订单状态 - */ - async changeStatus(id: number, status: ErpOrderStatus) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - order.orderStatus = status; - await this.orderModel.save(order); - } - - - /** - * 创建订单 - * 流程说明: - * 1. 验证必需参数siteId是否存在 - * 2. 获取默认数据源 - * 3. 在事务中处理订单创建 - * 4. 保存订单基本信息(站点ID、外部订单号、状态、货币、日期、客户信息等) - * 5. 遍历销售项目列表 - * 6. 根据SKU查询产品信息 - * 7. 保存订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) - * - * 涉及实体: Order, OrderSale, Product - * - * @param data 订单数据 - * @returns 创建的订单 - */ - async createOrder(data: Record) { - // 从数据中解构出需要用的属性 - const { siteId, sales, total, billing, customer_email, billing_phone } = data; - // 如果没有 siteId,则抛出错误 - if (!siteId) { - throw new Error('siteId is required'); - } - // 获取默认数据源 - const dataSource = this.dataSourceManager.getDataSource('default'); - const now = new Date(); - // 在事务中处理订单创建 - return dataSource.transaction(async manager => { - const orderRepo = manager.getRepository(Order); - const orderSaleRepo = manager.getRepository(OrderSale); - const productRepo = manager.getRepository(Product); - // 保存订单信息 - const order = await orderRepo.save({ - siteId, - externalOrderId: '-1', - status: OrderStatus.PROCESSING, - orderStatus: ErpOrderStatus.PROCESSING, - currency: 'CAD', - currency_symbol: '$', - date_created: now, - date_paid: now, - total, - customer_email, - billing_phone, - billing, - shipping: billing, - }); - // 遍历销售项目并保存 - for (const sale of sales) { - const product = await productRepo.findOne({ where: { sku: sale.sku } }); - const saleItem = { - orderId: order.id, - siteId: order.siteId, - externalOrderItemId: '-1', - productId: product.id, - name: product.name, - sku: sale.sku, - quantity: sale.quantity, - }; - await orderSaleRepo.save(saleItem); - } - }); - } - - /** - * 获取待处理订单项统计 - * 流程说明: - * 1. 构建SQL查询,关联订单表和订单销售表 - * 2. 按产品名称分组,统计每个产品的总数量和订单号列表 - * 3. 只查询状态为"处理中"的订单 - * 4. 执行总数查询 - * 5. 添加分页,执行主查询 - * 6. 返回待处理订单项统计和分页信息 - * - * 涉及实体: Order, OrderSale - * - * @param data 查询参数 - * @returns 待处理订单项统计和分页信息 - */ - async pengdingItems(data: Record) { - const { current = 1, pageSize = 10 } = data; - const sql = ` - SELECT - os.name, - SUM(os.quantity) AS quantity, - JSON_ARRAYAGG(os.orderId) AS numbers - FROM \`order\` o - INNER JOIN order_sale os ON os.orderId = o.id - WHERE o.status = 'processing' - GROUP BY os.name - LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize} - `; - - const countSql = ` - SELECT COUNT(*) AS total FROM ( - SELECT 1 - FROM \`order\` o - INNER JOIN order_sale os ON os.orderId = o.id - WHERE o.status = 'processing' - GROUP BY os.name - ) AS temp - `; - const [items, countResult] = await Promise.all([ - this.orderModel.query(sql), - this.orderModel.query(countSql), - ]); - - const total = countResult[0]?.total || 0; - return { - items, - total, - current, - pageSize, - }; - } - - /** - * 更新订单销售项 - * 流程说明: - * 1. 获取默认数据源 - * 2. 在事务中处理订单销售项更新 - * 3. 查询订单信息 - * 4. 删除该订单的所有销售项 - * 5. 遍历新的销售项列表 - * 6. 根据SKU查询产品信息 - * 7. 保存新的订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) - * 8. 处理事务错误 - * - * 涉及实体: Order, OrderSale, Product - * - * @param orderId 订单ID - * @param sales 销售项列表 - * @returns 更新成功返回true - */ - async updateOrderSales(orderId: number, sales: OrderSale[]) { - try { - const dataSource = this.dataSourceManager.getDataSource('default'); - let transactionError = undefined; - - await dataSource.transaction(async manager => { - const orderRepo = manager.getRepository(Order); - const orderSaleRepo = manager.getRepository(OrderSale); - const productRepo = manager.getRepository(Product); - - const order = await orderRepo.findOneBy({ id: orderId }); - let product: Product; - await orderSaleRepo.delete({ orderId }); - for (const sale of sales) { - product = await productRepo.findOneBy({ sku: sale.sku }); - await orderSaleRepo.save({ - orderId, - siteId: order.siteId, - productId: product.id, - name: product.name, - sku: sale.sku, - quantity: sale.quantity, - // externalOrderItemId: - }); - }; - - - }).catch(error => { - transactionError = error; - }); - - if (transactionError !== undefined) { - throw new Error(`更新物流信息错误:${transactionError.message}`); - } - return true; - } catch (error) { - throw new Error(`更新发货产品失败:${error.message}`); - } - } - - /** - * 更新换货订单 - * 流程说明: - * 1. 该方法用于换货确认功能 - * 2. 需要更新OrderSale和OrderItem数据 - * 3. 当前方法暂未实现 - * - * 涉及实体: Order, OrderSale, OrderItem, Product - * - * @param orderId 订单ID - * @param data 换货数据 - * @returns 更新成功返回true - */ - //换货确认按钮改成调用这个方法 - //换货功能更新OrderSale和Orderitem数据 - async updateExchangeOrder(orderId: number, data: any) { - throw new Error('暂未实现') - // try { - // const dataSource = this.dataSourceManager.getDataSource('default'); - // let transactionError = undefined; - - // await dataSource.transaction(async manager => { - // const orderRepo = manager.getRepository(Order); - // const orderSaleRepo = manager.getRepository(OrderSale); - // const orderItemRepo = manager.getRepository(OrderItem); - - - // const productRepo = manager.getRepository(ProductV2); - - // const order = await orderRepo.findOneBy({ id: orderId }); - // let product: ProductV2; - - // await orderSaleRepo.delete({ orderId }); - // await orderItemRepo.delete({ orderId }); - // for (const sale of data['sales']) { - // product = await productRepo.findOneBy({ sku: sale['sku'] }); - // await orderSaleRepo.save({ - // orderId, - // siteId: order.siteId, - // productId: product.id, - // name: product.name, - // sku: sale['sku'], - // quantity: sale['quantity'], - // }); - // }; - - // for (const item of data['items']) { - // product = await productRepo.findOneBy({ sku: item['sku'] }); - - // await orderItemRepo.save({ - // orderId, - // siteId: order.siteId, - // productId: product.id, - // name: product.name, - // externalOrderId: order.externalOrderId, - // externalProductId: product.externalProductId, - - // sku: item['sku'], - // quantity: item['quantity'], - // }); - - // }; - - // //将是否换货状态改为true - // await orderRepo.update( - // order.id - // , { - // is_exchange: true - // }); - - // //查询这个用户换过多少次货 - // const counts = await orderRepo.countBy({ - // is_editable: true, - // customer_email: order.customer_email, - // }); - - // //批量更新当前用户换货次数 - // await orderRepo.update({ - // customer_email: order.customer_email - // }, { - // exchange_frequency: counts - // }); - - // }).catch(error => { - // transactionError = error; - // }); - - // if (transactionError !== undefined) { - // throw new Error(`更新物流信息错误:${transactionError.message}`); - // } - // return true; - // } catch (error) { - // throw new Error(`更新发货产品失败:${error.message}`); - // } - } - - /** - * 导出订单为CSV格式 - * 流程说明: - * 1. 在事务中处理订单导出 - * 2. 根据订单ID列表查询订单信息(包含物流关联) - * 3. 查询所有订单项 - * 4. 按订单ID分组订单项 - * 5. 构建导出数据,包含以下字段: - * - 日期: 订单创建日期 - * - 订单号: 外部订单号 - * - 姓名地址: 收货人姓名和地址 - * - 邮箱: 客户邮箱 - * - 号码: 电话号码 - * - 订单内容: 产品名称和数量 - * - 盒数: 总盒数 - * - 换盒数: 换货盒数(当前默认为0) - * - 换货内容: 换货内容(当前默认为空) - * - 快递号: 物流追踪号 - * 6. 调用exportToCsv方法将数据转换为CSV格式 - * 7. 返回CSV字符串内容 - * - * 涉及实体: Order, OrderItem, Shipment - * - * @param ids 订单ID列表 - * @returns CSV格式字符串 - */ - // TODO - async exportOrder(ids: number[]) { - // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 - interface ExportData { - '日期': string; - '订单号': string; - '姓名地址': string; - '邮箱': string; - '号码': string; - '订单内容': string; - '盒数': number; - '换盒数': number; - '换货内容': string; - '快递号': string; - } - - try { - - // 过滤掉NaN和非数字值,只保留有效的数字ID - const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0); - - const dataSource = this.dataSourceManager.getDataSource('default'); - - // 优化事务使用 - return await dataSource.transaction(async manager => { - // 准备查询条件 - const whereCondition: any = {}; - if (validIds.length > 0) { - whereCondition.id = In(validIds); - } - - // 获取订单、订单项和物流信息 - const orders = await manager.getRepository(Order).find({ - where: whereCondition, - relations: ['shipment'] - }); - - if (orders.length === 0) { - throw new Error('未找到匹配的订单'); - } - - // 获取所有订单ID - const orderIds = orders.map(order => order.id); - - // 获取所有订单项 - const orderItems = await manager.getRepository(OrderItem).find({ - where: { - orderId: In(orderIds) - } - }); - - // 按订单ID分组订单项 - const orderItemsByOrderId = orderItems.reduce((acc, item) => { - if (!acc[item.orderId]) { - acc[item.orderId] = []; - } - acc[item.orderId].push(item); - return acc; - }, {} as Record); - - // 构建导出数据 - const exportDataList: ExportData[] = orders.map(order => { - // 获取订单的订单项 - const items = orderItemsByOrderId[order.id] || []; - - // 计算总盒数 - const boxCount = items.reduce((total, item) => total + item.quantity, 0); - - // 构建订单内容 - const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; '); - - // 构建姓名地址 - const shipping = order.shipping; - const billing = order.billing; - const firstName = shipping?.first_name || billing?.first_name || ''; - const lastName = shipping?.last_name || billing?.last_name || ''; - const name = `${firstName} ${lastName}`.trim() || ''; - const address = shipping?.address_1 || billing?.address_1 || ''; - const address2 = shipping?.address_2 || billing?.address_2 || ''; - const city = shipping?.city || billing?.city || ''; - const state = shipping?.state || billing?.state || ''; - const postcode = shipping?.postcode || billing?.postcode || ''; - const country = shipping?.country || billing?.country || ''; - const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; - - // 获取电话号码 - const phone = shipping?.phone || billing?.phone || ''; - - // 获取快递号 - const trackingNumber = order.shipment?.tracking_id || ''; - - // 暂时没有换货相关数据,默认为0和空字符串 - const exchangeBoxCount = 0; - const exchangeContent = ''; - - return { - '日期': order.date_created?.toISOString().split('T')[0] || '', - '订单号': order.externalOrderId || '', - '姓名地址': nameAddress, - '邮箱': order.customer_email || '', - '号码': phone, - '订单内容': 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}`); - } - } - - - -} diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 282c093..586a5b4 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -2644,4 +2644,80 @@ export class OrderService { throw new Error(`导出CSV文件失败: ${error.message}`); } } + + /** + * 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身) + * @param str 输入字符串 + * @returns 删除后的字符串 + */ + removeLastParenthesesContent(str: string): string { + if (!str || typeof str !== 'string') { + return str; + } + + // 辅助函数:删除指定位置的括号对及其内容 + const removeParenthesesAt = (s: string, leftIndex: number): string => { + if (leftIndex === -1) return s; + + let rightIndex = -1; + let parenCount = 0; + + for (let i = leftIndex; i < s.length; i++) { + const char = s[i]; + if (char === '(') { + parenCount++; + } else if (char === ')') { + parenCount--; + if (parenCount === 0) { + rightIndex = i; + break; + } + } + } + + if (rightIndex !== -1) { + return s.substring(0, leftIndex) + s.substring(rightIndex + 1); + } + + return s; + }; + + // 1. 处理每个分号前面的括号对 + let result = str; + + // 找出所有分号的位置 + const semicolonIndices: number[] = []; + for (let i = 0; i < result.length; i++) { + if (result[i] === ';') { + semicolonIndices.push(i); + } + } + + // 从后向前处理每个分号,避免位置变化影响后续处理 + for (let i = semicolonIndices.length - 1; i >= 0; i--) { + const semicolonIndex = semicolonIndices[i]; + + // 从分号位置向前查找最近的左括号 + let lastLeftParenIndex = -1; + for (let j = semicolonIndex - 1; j >= 0; j--) { + if (result[j] === '(') { + lastLeftParenIndex = j; + break; + } + } + + // 如果找到左括号,删除该括号对及其内容 + if (lastLeftParenIndex !== -1) { + result = removeParenthesesAt(result, lastLeftParenIndex); + } + } + + // 2. 处理整个字符串的最后一个括号对 + let lastLeftParenIndex = result.lastIndexOf('('); + if (lastLeftParenIndex !== -1) { + result = removeParenthesesAt(result, lastLeftParenIndex); + } + + return result; + } } diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index 275e823..1e623f2 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -313,7 +313,6 @@ export class ShopyyService { const { items: firstPageItems, totalPages} = firstPage; - // const { page = 1, per_page = 100 } = params; // 如果只有一页数据,直接返回 if (totalPages <= 1) { return firstPageItems; @@ -334,7 +333,7 @@ export class ShopyyService { // 创建当前批次的并发请求 for (let i = 0; i < batchSize; i++) { const page = currentPage + i; - const pagePromise = this.getOrders(site, page, 100) + const pagePromise = this.getOrders(site, page, 100, params) .then(pageResult => pageResult.items) .catch(error => { console.error(`获取第 ${page} 页数据失败:`, error);