2741 lines
90 KiB
TypeScript
2741 lines
90 KiB
TypeScript
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: []
|
||
};
|
||
console.log('开始进入循环同步订单', result.length, '个订单')
|
||
console.log('开始进入循环同步订单', result.length, '个订单')
|
||
// 遍历每个订单进行同步
|
||
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++;
|
||
}
|
||
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
|
||
} catch (error) {
|
||
// 记录错误但不中断整个同步过程
|
||
syncResult.errors.push({
|
||
identifier: String(order.id),
|
||
error: error.message || '同步失败'
|
||
});
|
||
syncResult.processed++;
|
||
}
|
||
}
|
||
console.log('同步完成', syncResult.updated, 'created:', syncResult.created)
|
||
|
||
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<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) {
|
||
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)
|
||
}
|
||
}
|
||
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)
|
||
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;
|
||
// }
|
||
|
||
/**
|
||
* 保存订单退款信息
|
||
* 流程说明:
|
||
* 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
|
||
`;
|
||
|
||
console.log('------3.5-----', pcSql, pcParams, exceptPackage);
|
||
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} (${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,
|
||
'订单内容': this.removeLastParenthesesContent(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');
|
||
|
||
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}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身)
|
||
* @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;
|
||
}
|
||
}
|