forked from yoone/API
1
0
Fork 0
API/src/service/order.service.ts

2732 lines
89 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import { ProductService } from './product.service';
@Provide()
export class OrderService {
@Inject()
wpService: WPService;
@Inject()
stockService: StockService;
@InjectEntityModel(Order)
orderModel: Repository<Order>;
@InjectEntityModel(User)
userModel: Repository<User>;
@InjectEntityModel(OrderItem)
orderItemModel: Repository<OrderItem>;
@InjectEntityModel(OrderItem)
orderItemOriginalModel: Repository<OrderItemOriginal>;
@InjectEntityModel(OrderSale)
orderSaleModel: Repository<OrderSale>;
@InjectEntityModel(Product)
productModel: Repository<Product>;
@InjectEntityModel(OrderFee)
orderFeeModel: Repository<OrderFee>;
@InjectEntityModel(OrderRefund)
orderRefundModel: Repository<OrderRefund>;
@InjectEntityModel(OrderRefundItem)
orderRefundItemModel: Repository<OrderRefundItem>;
@InjectEntityModel(OrderCoupon)
orderCouponModel: Repository<OrderCoupon>;
@InjectEntityModel(OrderShipping)
orderShippingModel: Repository<OrderShipping>;
@InjectEntityModel(OrderFulfillment)
orderFulfillmentModel: Repository<OrderFulfillment>;
@InjectEntityModel(Shipment)
shipmentModel: Repository<Shipment>;
@InjectEntityModel(ShipmentItem)
shipmentItemModel: Repository<ShipmentItem>;
@InjectEntityModel(OrderNote)
orderNoteModel: Repository<OrderNote>;
@Inject()
dataSourceManager: TypeORMDataSourceManager;
@InjectEntityModel(Customer)
customerModel: Repository<Customer>;
@Inject()
siteService: SiteService;
@Inject()
siteApiService: SiteApiService;
@Inject()
customerService: CustomerService;
@Logger()
logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/**
* 批量同步订单
* 流程说明:
* 1. 调用 WooCommerce API 获取订单列表
* 2. 遍历每个订单,检查数据库中是否已存在
* 3. 调用 syncSingleOrder 同步单个订单
* 4. 统计同步结果(创建数、更新数、错误数)
*
* 涉及实体: Order
*
* @param siteId 站点ID
* @param params 查询参数
* @returns 同步操作结果
*/
async syncOrders(siteId: number, params: Record<string, any> = {}): Promise<SyncOperationResult> {
// 调用 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: []
};
this.logger.info('开始进入循环同步订单', result.length, '个订单')
// 遍历每个订单进行同步
for (const order of result) {
try {
// 检查订单是否已存在,以区分创建和更新
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId },
});
if (!existingOrder) {
this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status)
}
// 同步单个订单
await this.syncSingleOrder(siteId, order);
// 统计结果
syncResult.processed++;
syncResult.synced++;
// 根据订单是否存在,更新创建或更新计数
if (existingOrder) {
syncResult.updated++;
} else {
syncResult.created++;
}
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
} catch (error) {
// 记录错误但不中断整个同步过程
syncResult.errors.push({
identifier: String(order.id),
error: error.message || '同步失败'
});
syncResult.processed++;
}
}
this.logger.info('同步完成', syncResult.updated, 'created:', syncResult.created)
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<SyncOperationResult> {
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) {
this.logger.debug("数据库不存在", 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)
}
}
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
return await this.orderModel.findOne({
where: { externalOrderId: String(externalOrderId), siteId },
});
}
/**
* 同步单个订单
* 流程说明:
* 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: UnifiedOrderDTO, 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) {
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
return;
}
// 检查数据库中是否已存在该订单
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(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 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 (
orderRecord &&
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
orderData.status === OrderStatus.COMPLETED
) {
await this.updateStock(orderRecord);
// 不再直接返回,继续执行后续的更新操作
}
const orderId = orderRecord.id;
// 保存订单项
await this.saveOrderItems({
siteId,
orderId,
externalOrderId: String(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 保存后的订单实体
*/
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
// 将外部订单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<string, any>[];
}) {
// 查询数据库中已存在的订单项
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 订单项实体
*/
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
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 productDetail = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name });
if (!productDetail || !productDetail.quantity) return;
const { product, quantity } = productDetail
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: { id: comp.productId },
}),
quantity: comp.quantity * orderItem.quantity,
}
})) : [{ product, 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,// 原始 itemId
parentProductId: product.id, // 父产品 ID 用于统计套餐 如果是单品则不记录
productId: componentDetail.product.id,
isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品
name: componentDetail.product.name,
quantity: componentDetail.quantity * orderItem.quantity,
sku: componentDetail.product.sku,
// 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。
brand: attrsObj?.['brand']?.name,
version: attrsObj?.['version']?.name,
strength: attrsObj?.['strength']?.name,
flavor: attrsObj?.['flavor']?.name,
humidity: attrsObj?.['humidity']?.name,
size: attrsObj?.['size']?.name,
category: componentDetail.product.category.name,
});
return orderSale
}).filter(v => v !== null)
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;
// }
/**
* 保存订单退款信息
* 流程说明:
* 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<string, any>[];
}) {
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_paid >= ?`;
totalQuery += ` AND o.date_paid >= ?`;
parameters.push(startDate);
}
if (endDate) {
sqlQuery += ` AND o.date_paid <= ?`;
totalQuery += ` AND o.date_paid <= ?`;
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_paid 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
`;
const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
const pcMap = new Map<number, any>();
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.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 ?
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({
current,
pageSize,
siteId,
startDate,
endDate,
sku,
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<OrderDetailRes> {
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<string>();
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<string, any>) {
// 从数据中解构出需要用的属性
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<string, any>) {
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<number, OrderItem[]>);
// 构建导出数据
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} 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<string | Buffer> {
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');
return filePath;
}
// 根据类型返回不同结果
if (type === 'buffer') {
return Buffer.from(csvContent, 'utf8');
}
return csvContent;
} catch (error) {
throw new Error(`导出CSV文件失败: ${error.message}`);
}
}
/**
* 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身)
* @param str 输入字符串
* @returns 删除后的字符串
*/
removeLastParenthesesContent(str: string): string {
if (!str || typeof str !== 'string') {
return str;
}
// 辅助函数:删除指定位置的括号对及其内容
const removeParenthesesAt = (s: string, leftIndex: number): string => {
if (leftIndex === -1) return s;
let rightIndex = -1;
let parenCount = 0;
for (let i = leftIndex; i < s.length; i++) {
const char = s[i];
if (char === '(') {
parenCount++;
} else if (char === ')') {
parenCount--;
if (parenCount === 0) {
rightIndex = i;
break;
}
}
}
if (rightIndex !== -1) {
return s.substring(0, leftIndex) + s.substring(rightIndex + 1);
}
return s;
};
// 1. 处理每个分号前面的括号对
let result = str;
// 找出所有分号的位置
const semicolonIndices: number[] = [];
for (let i = 0; i < result.length; i++) {
if (result[i] === ';') {
semicolonIndices.push(i);
}
}
// 从后向前处理每个分号,避免位置变化影响后续处理
for (let i = semicolonIndices.length - 1; i >= 0; i--) {
const semicolonIndex = semicolonIndices[i];
// 从分号位置向前查找最近的左括号
let lastLeftParenIndex = -1;
for (let j = semicolonIndex - 1; j >= 0; j--) {
if (result[j] === '(') {
lastLeftParenIndex = j;
break;
}
}
// 如果找到左括号,删除该括号对及其内容
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
}
// 2. 处理整个字符串的最后一个括号对
let lastLeftParenIndex = result.lastIndexOf('(');
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
return result;
}
}