diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 2843547..e14e6d2 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -227,8 +227,10 @@ export class ShopyyAdapter implements ISiteAdapter { // ========== 订单映射方法 ========== mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { + // console.log(item) + if(!item) throw new Error('订单数据不能为空') // 提取账单和送货地址 如果不存在则为空对象 - const billing = (item as any).billing_address || {}; + const billing = (item).bill_address || {}; const shipping = (item as any).shipping_address || {}; // 构建账单地址对象 @@ -313,7 +315,7 @@ export class ShopyyAdapter implements ISiteAdapter { product_id: p.product_id, quantity: p.quantity, total: String(p.price ?? ''), - sku: p.sku || p.sku_code || '', + sku: p.sku_code || '', price: String(p.price ?? ''), }) ); @@ -440,7 +442,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // 更新账单地址 - params.billing_address = params.billing_address || {}; + params.billing_address = params?.billing_address || {}; if (data.billing.first_name !== undefined) { params.billing_address.first_name = data.billing.first_name; } diff --git a/src/dto/order.dto.ts b/src/dto/order.dto.ts index 34e35db..3c69770 100644 --- a/src/dto/order.dto.ts +++ b/src/dto/order.dto.ts @@ -98,13 +98,9 @@ export class QueryOrderDTO { } export class QueryOrderSalesDTO { - @ApiProperty() + @ApiProperty({ description: '是否为原产品还是库存产品' }) @Rule(RuleType.bool().default(false)) - isSource: boolean; - - @ApiProperty() - @Rule(RuleType.bool().default(false)) - exceptPackage: boolean; + isSource: boolean; @ApiProperty({ example: '1', description: '页码' }) @Rule(RuleType.number()) @@ -114,19 +110,31 @@ export class QueryOrderSalesDTO { @Rule(RuleType.number()) pageSize: number; - @ApiProperty() + @ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }',type: 'any', required: false }) + @Rule(RuleType.object().allow(null)) + orderBy?: Record; + // filter + @ApiProperty({ description: '是否排除套餐' }) + @Rule(RuleType.bool().default(false)) + exceptPackage: boolean; + + @ApiProperty({ description: '站点ID' }) @Rule(RuleType.number()) siteId: number; - @ApiProperty() + @ApiProperty({ description: '名称' }) @Rule(RuleType.string()) name: string; - @ApiProperty() + @ApiProperty({ description: 'SKU' }) + @Rule(RuleType.string()) + sku: string; + + @ApiProperty({ description: '开始日期' }) @Rule(RuleType.date()) startDate: Date; - @ApiProperty() + @ApiProperty({ description: '结束日期' }) @Rule(RuleType.date()) endDate: Date; } diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index b434fa8..37bd5e7 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -200,7 +200,7 @@ export interface ShopyyOrder { customer_email?: string; email?: string; // 地址字段 - billing_address?: { + bill_address?: { first_name?: string; last_name?: string; name?: string; diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts index 5c603dc..d4f1cbb 100644 --- a/src/entity/order.entity.ts +++ b/src/entity/order.entity.ts @@ -272,6 +272,14 @@ export class Order { @Expose() updatedAt: Date; + @ApiProperty({ type: 'json', nullable: true, description: '订单项列表' }) + @Expose() + orderItems?: any[]; + + @ApiProperty({ type: 'json', nullable: true, description: '销售项列表' }) + @Expose() + orderSales?: any[]; + // 在插入或更新前处理用户代理字符串 @BeforeInsert() @BeforeUpdate() diff --git a/src/entity/order_sale.entity.ts b/src/entity/order_sale.entity.ts index 552a96c..35b0e0b 100644 --- a/src/entity/order_sale.entity.ts +++ b/src/entity/order_sale.entity.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@midwayjs/swagger'; import { Exclude, Expose } from 'class-transformer'; import { - BeforeInsert, - BeforeUpdate, + // BeforeInsert, + // BeforeUpdate, Column, CreateDateColumn, Entity, @@ -22,22 +22,22 @@ export class OrderSale { @Expose() id?: number; - @ApiProperty() + @ApiProperty({ name:'原始订单ID' }) @Column() @Expose() orderId: number; // 订单 ID - @ApiProperty() - @Column({ nullable: true }) + @ApiProperty({ name:'站点' }) + @Column() @Expose() siteId: number; // 来源站点唯一标识 - @ApiProperty() + @ApiProperty({name: "原始订单 itemId"}) @Column({ nullable: true }) @Expose() externalOrderItemId: string; // WooCommerce 订单item ID - @ApiProperty() + @ApiProperty({name: "产品 ID"}) @Column() @Expose() productId: number; @@ -62,25 +62,35 @@ export class OrderSale { @Expose() isPackage: boolean; - @ApiProperty() - @Column({ default: false }) + @ApiProperty({ description: '品牌', type: 'string',nullable: true}) @Expose() - isYoone: boolean; + @Column({ nullable: true }) + brand?: string; - @ApiProperty() - @Column({ default: false }) + @ApiProperty({ description: '口味', type: 'string', nullable: true }) @Expose() - isZex: boolean; + @Column({ nullable: true }) + flavor?: string; - @ApiProperty({ nullable: true }) - @Column({ type: 'int', nullable: true }) + @ApiProperty({ description: '湿度', type: 'string', nullable: true }) @Expose() - size: number | null; + @Column({ nullable: true }) + humidity?: string; - @ApiProperty() - @Column({ default: false }) + @ApiProperty({ description: '尺寸', type: 'string', nullable: true }) @Expose() - isYooneNew: boolean; + @Column({ nullable: true }) + size?: string; + + @ApiProperty({name: '强度', nullable: true }) + @Column({ nullable: true }) + @Expose() + strength: string | null; + + @ApiProperty({ description: '版本', type: 'string', nullable: true }) + @Expose() + @Column({ nullable: true }) + version?: string; @ApiProperty({ example: '2022-12-12 11:11:11', @@ -97,25 +107,4 @@ export class OrderSale { @UpdateDateColumn() @Expose() updatedAt?: Date; - - // === 自动计算逻辑 === - @BeforeInsert() - @BeforeUpdate() - setFlags() { - if (!this.name) return; - const lower = this.name.toLowerCase(); - this.isYoone = lower.includes('yoone'); - this.isZex = lower.includes('zex'); - this.isYooneNew = this.isYoone && lower.includes('new'); - let size: number | null = null; - const sizes = [3, 6, 9, 12, 15, 18]; - for (const s of sizes) { - if (lower.includes(s.toString())) { - size = s; - break; - } - } - this.size = size; - } - } diff --git a/src/job/sync_shipment.job.ts b/src/job/sync_shipment.job.ts index 94707d9..f6497f3 100644 --- a/src/job/sync_shipment.job.ts +++ b/src/job/sync_shipment.job.ts @@ -75,17 +75,20 @@ export class SyncUniuniShipmentJob implements IJob{ '255': 'Gateway_To_Gateway_Transit' }; async onTick() { - try { const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); - shipments.forEach(shipment => { - this.logisticsService.updateShipmentState(shipment); - }); - } catch (error) { - this.logger.error(`更新运单状态失败 ${error.message}`); - } + const results = await Promise.all( + shipments.map(async shipment => { + return await this.logisticsService.updateShipmentState(shipment); + }) + ) + this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`); + return results } onComplete(result: any) { - + this.logger.info(`更新运单状态完成 ${result}`); + } + onError(error: any) { + this.logger.error(`更新运单状态失败 ${error.message}`); } } \ No newline at end of file diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index ce4fc70..b37c781 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -125,6 +125,10 @@ export class LogisticsService { try { const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number); console.log('updateShipmentState data:', data); + // huo + if(data.status === 'FAIL'){ + throw new Error('获取运单状态失败,原因为'+ data.ret_msg) + } shipment.state = data.data[0].state; if (shipment.state in [203, 215, 216, 230]) { // todo,写常数 shipment.finished = true; diff --git a/src/service/order.service copy.ts b/src/service/order.service copy.ts new file mode 100644 index 0000000..db2cccd --- /dev/null +++ b/src/service/order.service copy.ts @@ -0,0 +1,2651 @@ +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 24293eb..802aa76 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -39,6 +39,7 @@ import * as path from 'path'; import * as os from 'os'; import { UnifiedOrderDTO } from '../dto/site-api.dto'; import { CustomerService } from './customer.service'; +import { ProductService } from './product.service'; @Provide() export class OrderService { @@ -110,7 +111,9 @@ export class OrderService { @Logger() logger; // 注入 Logger 实例 - + @Inject() + productService: ProductService; + /** * 批量同步订单 * 流程说明: @@ -146,8 +149,8 @@ export class OrderService { const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: String(order.id), siteId: siteId }, }); - if(!existingOrder){ - console.log("数据库中不存在",order.id, '订单状态:', order.status ) + if (!existingOrder) { + console.log("数据库中不存在", order.id, '订单状态:', order.status) } // 同步单个订单 await this.syncSingleOrder(siteId, order); @@ -211,8 +214,8 @@ export class OrderService { const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: String(order.id), siteId: siteId }, }); - if(!existingOrder){ - console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status ) + if (!existingOrder) { + console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status) } // 同步单个订单 await this.syncSingleOrder(siteId, order, true); @@ -271,7 +274,7 @@ export class OrderService { try { const site = await this.siteService.get(siteId); // 仅处理 WooCommerce 站点 - if(site.type !== 'woocommerce'){ + if (site.type !== 'woocommerce') { return } // 将订单状态同步到 WooCommerce,然后切换至下一状态 @@ -281,6 +284,11 @@ export class OrderService { console.error('更新订单状态失败,原因为:', error) } } + async getOrderByExternalOrderId(siteId: number, externalOrderId: string) { + return await this.orderModel.findOne({ + where: { externalOrderId: String(externalOrderId), siteId }, + }); + } /** * 同步单个订单 * 流程说明: @@ -318,47 +326,28 @@ export class OrderService { // console.log('同步进单个订单', order) // 如果订单状态为 AUTO_DRAFT,则跳过处理 if (order.status === OrderStatus.AUTO_DRAFT) { + this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id) return; } - // 检查数据库中是否已存在该订单 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId: String(order.id), siteId: siteId }, - }); - // 自动更新订单状态(如果需要) + // 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断 + // if(!order.is_editable && !forceUpdate){ + // this.logger.debug('订单不可编辑,跳过处理', siteId, order.id) + // return; + // } + // 自动转换远程订单的状态(如果需要) await this.autoUpdateOrderStatus(siteId, order); - - if(existingOrder){ - // 矫正数据库中的订单数据 - const updateData: any = { status: order.status }; - if (this.canUpdateErpStatus(existingOrder.orderStatus)) { - updateData.orderStatus = this.mapOrderStatus(order.status as any); - } - // 更新订单主数据 - await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData); - // 更新 fulfillments 数据 - await this.saveOrderFulfillments({ - siteId, - orderId: existingOrder.id, - externalOrderId:order.id, - fulfillments: fulfillments, - }); - } - const externalOrderId = String(order.id); - // 如果订单从未完成变为完成状态,则更新库存 + // 这里的 saveOrder 已经包括了创建订单和更新订单 + let orderRecord: Order = await this.saveOrder(siteId, orderData); + // 如果订单从未完成变为完成状态,则更新库存 if ( - existingOrder && - existingOrder.orderStatus !== ErpOrderStatus.COMPLETED && + orderRecord && + orderRecord.orderStatus !== ErpOrderStatus.COMPLETED && orderData.status === OrderStatus.COMPLETED ) { - this.updateStock(existingOrder); + await this.updateStock(orderRecord); // 不再直接返回,继续执行后续的更新操作 } - // 如果订单不可编辑且不强制更新,则跳过处理 - if (existingOrder && !existingOrder.is_editable && !forceUpdate) { - return; - } - // 保存订单主数据 - const orderRecord = await this.saveOrder(siteId, orderData); + const externalOrderId = String(order.id); const orderId = orderRecord.id; // 保存订单项 await this.saveOrderItems({ @@ -462,13 +451,14 @@ export class OrderService { * @param order 订单数据 * @returns 保存后的订单实体 */ - async saveOrder(siteId: number, order: Partial): Promise { + // 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等 + async saveOrder(siteId: number, order: Omit): Promise { // 将外部订单ID转换为字符串 - const externalOrderId = String(order.id) + const externalOrderId = String(order.id) delete order.id - + // 创建订单实体对象 - const entity = plainToClass(Order, {...order, externalOrderId, siteId}); + const entity = plainToClass(Order, { ...order, externalOrderId, siteId }); // 检查数据库中是否已存在该订单 const existingOrder = await this.orderModel.findOne({ where: { externalOrderId, siteId: siteId }, @@ -711,6 +701,8 @@ export class OrderService { * * @param orderItem 订单项实体 */ + // TODO 这里存的是库存商品实际 + // 所以叫做 orderInventoryItems 可能更合适 async saveOrderSale(orderItem: OrderItem) { const currentOrderSale = await this.orderSaleModel.find({ where: { @@ -725,50 +717,53 @@ export class OrderService { // 从数据库查询产品,关联查询组件 const product = await this.productModel.findOne({ where: { siteSkus: Like(`%${orderItem.sku}%`) }, - relations: ['components'], + relations: ['components','attributes','attributes.dict'], }); 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({ + const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => { + return { + product: await this.productModel.findOne({ where: { sku: comp.sku }, - }); - 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); - } + relations: ['components', 'attributes','attributes.dict'], + }), + quantity: comp.quantity * orderItem.quantity, } - } else { - const orderSaleItem: OrderSale = plainToClass(OrderSale, { + })) : [{ product, quantity: orderItem.quantity }] + + const orderSales: OrderSale[] = componentDetails.map(componentDetail => { + if (!componentDetail.product) return null + const attrsObj = this.productService.getAttributesObject(product.attributes) + const orderSale = plainToClass(OrderSale, { orderId: orderItem.orderId, siteId: orderItem.siteId, externalOrderItemId: orderItem.externalOrderItemId, - productId: product.id, - name: product.name, - quantity: orderItem.quantity, - sku: product.sku, - isPackage: orderItem.name.toLowerCase().includes('package'), + productId: componentDetail.product.id, + name: componentDetail.product.name, + quantity: componentDetail.quantity * orderItem.quantity, + sku: componentDetail.product.sku, + // 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。 + isPackage: componentDetail.product.type === 'bundle', + isYoone: attrsObj?.['brand']?.name === 'yoone', + isZyn: attrsObj?.['brand']?.name === 'zyn', + isZex: attrsObj?.['brand']?.name === 'zex', + isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new', + strength: attrsObj?.['strength']?.name, }); - orderSales.push(orderSaleItem); - } - + return orderSale + }).filter(v => v !== null) + console.log("orderSales",orderSales) if (orderSales.length > 0) { await this.orderSaleModel.save(orderSales); } } + // // extract stren + // extractNumberFromString(str: string): number { + // if (!str) return 0; + + // const num = parseInt(str, 10); + // return isNaN(num) ? 0 : num; + // } /** * 保存订单退款信息 @@ -1429,7 +1424,7 @@ export class OrderService { * @param params 查询参数 * @returns 销售统计和分页信息 */ - async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { + 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'); @@ -1582,14 +1577,14 @@ export class OrderService { `; 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 + SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '3mg' THEN os.quantity ELSE 0 END) AS yoone3Quantity, + SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '6mg' THEN os.quantity ELSE 0 END) AS yoone6Quantity, + SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '9mg' THEN os.quantity ELSE 0 END) AS yoone9Quantity, + SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12Quantity, + SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew, + SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '15mg' THEN os.quantity ELSE 0 END) AS yoone15Quantity, + SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '18mg' THEN os.quantity ELSE 0 END) AS yoone18Quantity, + SUM(CASE WHEN os.brand = 'zex' THEN os.quantity ELSE 0 END) AS zexQuantity FROM order_sale os INNER JOIN \`order\` o ON o.id = os.orderId WHERE o.date_paid BETWEEN ? AND ? @@ -1645,11 +1640,12 @@ export class OrderService { * @returns 订单项统计和分页信息 */ async getOrderItems({ + current, + pageSize, siteId, startDate, endDate, - current, - pageSize, + sku, name, }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; @@ -1907,8 +1903,8 @@ export class OrderService { const key = it?.externalSubscriptionId ? `sub:${it.externalSubscriptionId}` : it?.externalOrderId - ? `ord:${it.externalOrderId}` - : `id:${it?.id}`; + ? `ord:${it.externalOrderId}` + : `id:${it?.id}`; if (!seen.has(key)) { seen.add(key); relatedList.push(it); @@ -2202,14 +2198,14 @@ export class OrderService { 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, - }; + orderId: order.id, + siteId: order.siteId, + externalOrderItemId: '-1', + productId: product.id, + name: product.name, + sku: sale.sku, + quantity: sale.quantity, + }; await orderSaleRepo.save(saleItem); } }); @@ -2342,83 +2338,83 @@ export class OrderService { //换货功能更新OrderSale和Orderitem数据 async updateExchangeOrder(orderId: number, data: any) { throw new Error('暂未实现') - // try { - // const dataSource = this.dataSourceManager.getDataSource('default'); - // let transactionError = undefined; + // 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); + // 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 productRepo = manager.getRepository(ProductV2); - // const order = await orderRepo.findOneBy({ id: orderId }); - // let product: 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'], - // }); - // }; + // 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'] }); + // 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, + // 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'], - // }); + // sku: item['sku'], + // quantity: item['quantity'], + // }); - // }; + // }; - // //将是否换货状态改为true - // await orderRepo.update( - // order.id - // , { - // is_exchange: true - // }); + // //将是否换货状态改为true + // await orderRepo.update( + // order.id + // , { + // is_exchange: true + // }); - // //查询这个用户换过多少次货 - // const counts = await orderRepo.countBy({ - // is_editable: true, - // customer_email: order.customer_email, - // }); + // //查询这个用户换过多少次货 + // const counts = await orderRepo.countBy({ + // is_editable: true, + // customer_email: order.customer_email, + // }); - // //批量更新当前用户换货次数 - // await orderRepo.update({ - // customer_email: order.customer_email - // }, { - // exchange_frequency: counts - // }); + // //批量更新当前用户换货次数 + // await orderRepo.update({ + // customer_email: order.customer_email + // }, { + // exchange_frequency: counts + // }); - // }).catch(error => { - // transactionError = error; - // }); + // }).catch(error => { + // transactionError = error; + // }); - // if (transactionError !== undefined) { - // throw new Error(`更新物流信息错误:${transactionError.message}`); - // } - // return true; - // } catch (error) { - // throw new Error(`更新发货产品失败:${error.message}`); - // } + // if (transactionError !== undefined) { + // throw new Error(`更新物流信息错误:${transactionError.message}`); + // } + // return true; + // } catch (error) { + // throw new Error(`更新发货产品失败:${error.message}`); + // } } /** @@ -2464,17 +2460,17 @@ export class OrderService { } 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){ + if (validIds.length > 0) { whereCondition.id = In(validIds); } @@ -2490,7 +2486,7 @@ export class OrderService { // 获取所有订单ID const orderIds = orders.map(order => order.id); - + // 获取所有订单项 const orderItems = await manager.getRepository(OrderItem).find({ where: { @@ -2511,13 +2507,13 @@ export class OrderService { 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; @@ -2531,10 +2527,10 @@ export class OrderService { 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 || ''; @@ -2570,84 +2566,86 @@ export class OrderService { * 导出数据为CSV格式 * @param {any[]} data 数据数组 * @param {Object} options 配置选项 - * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' - * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) + * @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 }); + async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise { + try { + // 检查数据是否为空 + if (!data || data.length === 0) { + throw new Error('导出数据不能为空'); } - - const filePath = path.join(downloadsDir, fileName); - - // 写入文件 - fs.writeFileSync(filePath, csvContent, 'utf8'); - - console.log(`数据已成功导出至 ${filePath}`); - return filePath; + + 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}`); } - - // 根据类型返回不同结果 - if (type === 'buffer') { - return Buffer.from(csvContent, 'utf8'); - } - - return csvContent; - } catch (error) { - console.error('导出CSV时出错:', error); - throw new Error(`导出CSV文件失败: ${error.message}`); } } +<<<<<<< HEAD /** * 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身) @@ -2730,3 +2728,5 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: } +======= +>>>>>>> 68574dbc7a74e0f8130f195eba4a28dd3887c485 diff --git a/src/service/product.service.ts b/src/service/product.service.ts index d518fca..78ffeb9 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1461,12 +1461,17 @@ export class ProductService { return { sku, name: val(rec.name), - nameCn: val(rec.nameCn), + nameCn: val(rec.nameCn), description: val(rec.description), price: num(rec.price), promotionPrice: num(rec.promotionPrice), type: val(rec.type), - siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined, + siteSkus: rec.siteSkus + ? String(rec.siteSkus) + .split(/[;,]/) // 支持英文分号或英文逗号分隔 + .map(s => s.trim()) + .filter(Boolean) + : undefined, category, // 添加分类字段 attributes: attributes.length > 0 ? attributes : undefined, @@ -1531,7 +1536,14 @@ export class ProductService { return dto; } - + getAttributesObject(attributes:DictItem[]){ + if(!attributes) return {} + const obj:any = {} + attributes.forEach(attr=>{ + obj[attr.dict.name] = attr + }) + return obj + } // 将单个产品转换为 CSV 行数组 transformProductToCsvRow( p: Product, diff --git a/src/service/statistics.service.ts b/src/service/statistics.service.ts index fd0f741..ffa557e 100644 --- a/src/service/statistics.service.ts +++ b/src/service/statistics.service.ts @@ -73,16 +73,16 @@ export class StatisticsService { order_sales_summary AS ( SELECT orderId, - SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, - SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, - SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity + SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity, + SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity, + SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity, + SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, + SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity FROM order_sale GROUP BY orderId ), @@ -269,16 +269,16 @@ export class StatisticsService { order_sales_summary AS ( SELECT orderId, - SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, - SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, - SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity + SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity, + SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity, + SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity, + SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, + SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity FROM order_sale GROUP BY orderId ), @@ -466,16 +466,16 @@ export class StatisticsService { order_sales_summary AS ( SELECT orderId, - SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, - SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, - SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, - SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity + SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity, + SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity, + SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity, + SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, + SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity, + SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity FROM order_sale GROUP BY orderId ),