forked from yoone/API
1700 lines
55 KiB
TypeScript
1700 lines
55 KiB
TypeScript
import { Inject, Provide } from '@midwayjs/core';
|
|
import { WPService } from './wp.service';
|
|
import { Order } from '../entity/order.entity';
|
|
import { In, Like, Repository } from 'typeorm';
|
|
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
|
import { plainToClass } from 'class-transformer';
|
|
import { OrderItem } from '../entity/order_item.entity';
|
|
|
|
import { OrderSale } from '../entity/order_sale.entity';
|
|
import { 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 { Shipment } from '../entity/shipment.entity';
|
|
import { Customer } from '../entity/customer.entity';
|
|
import {
|
|
ErpOrderStatus,
|
|
OrderStatus,
|
|
StockRecordOperationType,
|
|
} from '../enums/base.enum';
|
|
import { Variation } from '../entity/variation.entity';
|
|
import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto';
|
|
import 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';
|
|
|
|
@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(Variation)
|
|
variationModel: Repository<Variation>;
|
|
|
|
@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(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;
|
|
|
|
async syncOrders(siteId: number, params: Record<string, any> = {}) {
|
|
// 调用 WooCommerce API 获取订单
|
|
const orders = await this.wpService.getOrders(siteId, params);
|
|
let successCount = 0;
|
|
let failureCount = 0;
|
|
for (const order of orders) {
|
|
try {
|
|
await this.syncSingleOrder(siteId, order);
|
|
successCount++;
|
|
} catch (error) {
|
|
console.error(`同步订单 ${order.id} 失败:`, error);
|
|
failureCount++;
|
|
}
|
|
}
|
|
return {
|
|
success: failureCount === 0,
|
|
successCount,
|
|
failureCount,
|
|
message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`,
|
|
};
|
|
}
|
|
|
|
async syncOrderById(siteId: number, orderId: string) {
|
|
try {
|
|
// 调用 WooCommerce API 获取订单
|
|
const order = await this.wpService.getOrder(siteId, orderId);
|
|
await this.syncSingleOrder(siteId, order, true);
|
|
return { success: true, message: '同步成功' };
|
|
} catch (error) {
|
|
console.error(`同步订单 ${orderId} 失败:`, error);
|
|
return { success: false, message: `同步失败: ${error.message}` };
|
|
}
|
|
}
|
|
// 订单状态切换表
|
|
orderAutoNextStatusMap = {
|
|
[OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold
|
|
[OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded
|
|
}
|
|
// 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
|
|
async autoUpdateOrderStatus(siteId: number, order: any) {
|
|
console.log('更新订单状态', order)
|
|
// 其他状态保持不变
|
|
const originStatus = order.status;
|
|
// 如果有值就赋值
|
|
if (!this.orderAutoNextStatusMap[originStatus]) {
|
|
return
|
|
}
|
|
try {
|
|
const site = await this.siteService.get(siteId);
|
|
// 将订单状态同步到 WooCommerce,然后切换至下一状态
|
|
await this.wpService.updateOrder(site, String(order.id), { status: order.status });
|
|
order.status = this.orderAutoNextStatusMap[originStatus];
|
|
} catch (error) {
|
|
console.error('更新订单状态失败,原因为:', error)
|
|
}
|
|
}
|
|
// wordpress 发来,
|
|
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) {
|
|
let {
|
|
line_items,
|
|
shipping_lines,
|
|
fee_lines,
|
|
coupon_lines,
|
|
refunds,
|
|
...orderData
|
|
} = order;
|
|
console.log('同步进单个订单', order)
|
|
const existingOrder = await this.orderModel.findOne({
|
|
where: { externalOrderId: order.id, siteId: siteId },
|
|
});
|
|
// 矫正状态
|
|
await this.autoUpdateOrderStatus(siteId, order);
|
|
if (order.status === OrderStatus.AUTO_DRAFT) {
|
|
return;
|
|
}
|
|
// 更新订单
|
|
if (existingOrder) {
|
|
await this.orderModel.update({ id: existingOrder.id }, { orderStatus: this.mapOrderStatus(order.status) });
|
|
}
|
|
const externalOrderId = order.id;
|
|
if (
|
|
existingOrder &&
|
|
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED &&
|
|
orderData.status === OrderStatus.COMPLETED
|
|
) {
|
|
this.updateStock(existingOrder);
|
|
return;
|
|
}
|
|
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
|
|
return;
|
|
}
|
|
const orderRecord = await this.saveOrder(siteId, orderData);
|
|
const orderId = orderRecord.id;
|
|
await this.saveOrderItems({
|
|
siteId,
|
|
orderId,
|
|
externalOrderId,
|
|
orderItems: line_items,
|
|
});
|
|
await this.saveOrderRefunds({
|
|
siteId,
|
|
orderId,
|
|
externalOrderId,
|
|
refunds,
|
|
});
|
|
await this.saveOrderFees({
|
|
siteId,
|
|
orderId,
|
|
externalOrderId,
|
|
fee_lines,
|
|
});
|
|
await this.saveOrderCoupons({
|
|
siteId,
|
|
orderId,
|
|
externalOrderId,
|
|
coupon_lines,
|
|
});
|
|
await this.saveOrderShipping({
|
|
siteId,
|
|
orderId,
|
|
externalOrderId,
|
|
shipping_lines,
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
async saveOrder(siteId: number, order: Order): Promise<Order> {
|
|
order.externalOrderId = String(order.id);
|
|
order.siteId = siteId;
|
|
delete order.id;
|
|
order.device_type =
|
|
order?.meta_data?.find(
|
|
el => el.key === '_wc_order_attribution_device_type'
|
|
)?.value || '';
|
|
order.source_type =
|
|
order?.meta_data?.find(
|
|
el => el.key === '_wc_order_attribution_source_type'
|
|
)?.value || '';
|
|
order.utm_source =
|
|
order?.meta_data?.find(
|
|
el => el.key === '_wc_order_attribution_utm_source'
|
|
)?.value || '';
|
|
order.customer_email = order?.billing?.email || order?.shipping?.email;
|
|
// order.billing_phone = order?.billing?.phone || order?.shipping?.phone;
|
|
const entity = plainToClass(Order, order);
|
|
const existingOrder = await this.orderModel.findOne({
|
|
where: { externalOrderId: order.externalOrderId, siteId: siteId },
|
|
});
|
|
if (existingOrder) {
|
|
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
|
|
entity.orderStatus = this.mapOrderStatus(entity.status);
|
|
}
|
|
await this.orderModel.update(existingOrder.id, entity);
|
|
entity.id = existingOrder.id;
|
|
return entity;
|
|
}
|
|
entity.orderStatus = this.mapOrderStatus(entity.status);
|
|
const customer = await this.customerModel.findOne({
|
|
where: { email: order.customer_email },
|
|
});
|
|
if (!customer) {
|
|
await this.customerModel.save({
|
|
email: order.customer_email,
|
|
rate: 0,
|
|
});
|
|
}
|
|
return await this.orderModel.save(entity);
|
|
}
|
|
|
|
canUpdateErpStatus(currentErpStatus: string): boolean {
|
|
const nonOverridableStatuses = [
|
|
'AFTER_SALE_PROCESSING',
|
|
'PENDING_RESHIPMENT',
|
|
'PENDING_REFUND',
|
|
];
|
|
// 如果当前 ERP 状态不可覆盖,则禁止更新
|
|
return !nonOverridableStatuses.includes(currentErpStatus);
|
|
}
|
|
|
|
mapOrderStatus(status: OrderStatus): ErpOrderStatus {
|
|
switch (status) {
|
|
case OrderStatus.PENDING:
|
|
return ErpOrderStatus.PENDING;
|
|
case OrderStatus.PROCESSING:
|
|
return ErpOrderStatus.PROCESSING;
|
|
case OrderStatus.COMPLETED:
|
|
return ErpOrderStatus.COMPLETED;
|
|
case OrderStatus.CANCEL:
|
|
return ErpOrderStatus.CANCEL;
|
|
case OrderStatus.REFUNDED:
|
|
return ErpOrderStatus.REFUNDED;
|
|
case OrderStatus.FAILED:
|
|
return ErpOrderStatus.FAILED;
|
|
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;
|
|
}
|
|
}
|
|
|
|
async saveOrderItems(params: {
|
|
siteId: number;
|
|
orderId: number;
|
|
externalOrderId: string;
|
|
orderItems: Record<string, any>[];
|
|
}) {
|
|
console.log('saveOrderItems params',params)
|
|
const { siteId, orderId, externalOrderId, orderItems } = params;
|
|
const currentOrderItems = await this.orderItemModel.find({
|
|
where: { siteId, externalOrderId: externalOrderId },
|
|
});
|
|
const syncedOrderItemIds = new Set(orderItems.map(v => String(v.id)));
|
|
const orderItemToDelete = currentOrderItems.filter(
|
|
db => !syncedOrderItemIds.has(String(db.externalOrderItemId))
|
|
);
|
|
if (orderItemToDelete.length > 0) {
|
|
const idsToDelete = orderItemToDelete.map(v => v.id);
|
|
await this.orderItemModel.delete(idsToDelete);
|
|
const itemIdsToDelete = orderItemToDelete.map(v => v.externalOrderItemId);
|
|
await this.orderSaleModel.delete({
|
|
siteId,
|
|
externalOrderItemId: In(itemIdsToDelete),
|
|
});
|
|
}
|
|
for (const orderItem of orderItems) {
|
|
orderItem.siteId = siteId;
|
|
orderItem.externalOrderId = externalOrderId;
|
|
orderItem.externalOrderItemId = String(orderItem.id);
|
|
orderItem.orderId = orderId;
|
|
orderItem.externalProductId = String(orderItem.product_id);
|
|
orderItem.externalVariationId = String(orderItem.variation_id);
|
|
delete orderItem.id;
|
|
const entity = plainToClass(OrderItem, orderItem);
|
|
await this.saveOrderItem(entity);
|
|
await this.saveOrderSale(entity);
|
|
}
|
|
}
|
|
|
|
async saveOrderItem(orderItem: OrderItem) {
|
|
const existingOrderItem = await this.orderItemModel.findOne({
|
|
where: {
|
|
externalOrderId: orderItem.externalOrderId,
|
|
siteId: orderItem.siteId,
|
|
externalOrderItemId: orderItem.externalOrderItemId,
|
|
},
|
|
});
|
|
|
|
if (
|
|
existingOrderItem &&
|
|
existingOrderItem.quantity === orderItem.quantity
|
|
) {
|
|
return;
|
|
}
|
|
if (existingOrderItem) {
|
|
await this.orderItemModel.update(existingOrderItem.id, orderItem);
|
|
} else {
|
|
await this.orderItemModel.save(orderItem);
|
|
}
|
|
}
|
|
|
|
async 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);
|
|
}
|
|
}
|
|
|
|
async saveOrderSale(orderItem: OrderItem) {
|
|
const currentOrderSale = await this.orderSaleModel.find({
|
|
where: {
|
|
siteId: orderItem.siteId,
|
|
externalOrderItemId: orderItem.externalOrderItemId,
|
|
},
|
|
});
|
|
if (currentOrderSale.length > 0) {
|
|
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
|
|
}
|
|
if (!orderItem.sku) return;
|
|
const product = await this.productModel.findOne({
|
|
where: { sku: orderItem.sku },
|
|
relations: ['components'],
|
|
});
|
|
|
|
if (!product) return;
|
|
|
|
const orderSales: OrderSale[] = [];
|
|
|
|
if (product.components && product.components.length > 0) {
|
|
for (const comp of product.components) {
|
|
const baseProduct = await this.productModel.findOne({
|
|
where: { sku: comp.sku },
|
|
});
|
|
if (baseProduct) {
|
|
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
|
orderId: orderItem.orderId,
|
|
siteId: orderItem.siteId,
|
|
externalOrderItemId: orderItem.externalOrderItemId,
|
|
productId: baseProduct.id,
|
|
name: baseProduct.name,
|
|
quantity: comp.quantity * orderItem.quantity,
|
|
sku: comp.sku,
|
|
isPackage: orderItem.name.toLowerCase().includes('package'),
|
|
});
|
|
orderSales.push(orderSaleItem);
|
|
}
|
|
}
|
|
} else {
|
|
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
|
orderId: orderItem.orderId,
|
|
siteId: orderItem.siteId,
|
|
externalOrderItemId: orderItem.externalOrderItemId,
|
|
productId: product.id,
|
|
name: product.name,
|
|
quantity: orderItem.quantity,
|
|
sku: product.sku,
|
|
isPackage: orderItem.name.toLowerCase().includes('package'),
|
|
});
|
|
orderSales.push(orderSaleItem);
|
|
}
|
|
|
|
if (orderSales.length > 0) {
|
|
await this.orderSaleModel.save(orderSales);
|
|
}
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
|
|
async saveOrderRefundItems({
|
|
refundItems,
|
|
siteId,
|
|
refundId,
|
|
externalRefundId,
|
|
}) {
|
|
for (const item of refundItems) {
|
|
item.externalRefundItemId = item.id;
|
|
item.externalProductId = item.product_id;
|
|
item.externalVariationId = item.variation_id;
|
|
delete item.id;
|
|
const refundItem = plainToClass(OrderRefundItem, {
|
|
refundId,
|
|
siteId,
|
|
externalRefundId,
|
|
...item,
|
|
});
|
|
const existingOrderRefundItem = await this.orderRefundItemModel.findOne({
|
|
where: {
|
|
siteId,
|
|
externalRefundId,
|
|
externalRefundItemId: refundItem.externalRefundItemId,
|
|
},
|
|
});
|
|
if (existingOrderRefundItem) {
|
|
await this.orderRefundItemModel.update(
|
|
existingOrderRefundItem.id,
|
|
refundItem
|
|
);
|
|
} else {
|
|
await this.orderRefundItemModel.save(refundItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
async saveOrderFees({ siteId, orderId, externalOrderId, fee_lines }) {
|
|
const currentOrderFees = await this.orderFeeModel.find({
|
|
where: { siteId, externalOrderId },
|
|
});
|
|
const syncedFeeIds = new Set(fee_lines.map(v => String(v.id)));
|
|
const toDeleteIds = currentOrderFees
|
|
.filter(db => !syncedFeeIds.has(db.externalOrderFeeId))
|
|
.map(db => db.id);
|
|
if (toDeleteIds.length > 0) {
|
|
await this.orderFeeModel.delete(toDeleteIds);
|
|
}
|
|
for (const fee of fee_lines) {
|
|
fee.externalOrderFeeId = String(fee.id);
|
|
delete fee.id;
|
|
const entity = plainToClass(OrderFee, {
|
|
siteId,
|
|
orderId,
|
|
externalOrderId,
|
|
...fee,
|
|
});
|
|
const db = await this.orderFeeModel.findOne({
|
|
where: {
|
|
siteId,
|
|
externalOrderId,
|
|
externalOrderFeeId: entity.externalOrderFeeId,
|
|
},
|
|
});
|
|
if (db) {
|
|
await this.orderFeeModel.update(db.id, entity);
|
|
} else {
|
|
await this.orderFeeModel.save(entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
async saveOrderCoupons({ siteId, externalOrderId, orderId, coupon_lines }) {
|
|
for (const item of coupon_lines) {
|
|
item.externalOrderCouponId = item.id;
|
|
item.siteId = siteId;
|
|
item.orderId = orderId;
|
|
item.externalOrderId = externalOrderId;
|
|
delete item.id;
|
|
const coupon = plainToClass(OrderCoupon, item);
|
|
const existingOrderCoupon = await this.orderCouponModel.findOne({
|
|
where: {
|
|
siteId,
|
|
externalOrderId,
|
|
externalOrderCouponId: coupon.externalOrderCouponId,
|
|
},
|
|
});
|
|
if (existingOrderCoupon) {
|
|
await this.orderCouponModel.update(existingOrderCoupon.id, coupon);
|
|
} else {
|
|
await this.orderCouponModel.save(coupon);
|
|
}
|
|
}
|
|
}
|
|
|
|
async saveOrderShipping({
|
|
siteId,
|
|
orderId,
|
|
externalOrderId,
|
|
shipping_lines,
|
|
}) {
|
|
for (const item of shipping_lines) {
|
|
item.externalOrderShippingId = item.id;
|
|
item.siteId = siteId;
|
|
item.orderId = orderId;
|
|
item.externalOrderId = externalOrderId;
|
|
delete item.id;
|
|
const shipping = plainToClass(OrderShipping, item);
|
|
const existingOrderShipping = await this.orderShippingModel.findOne({
|
|
where: {
|
|
siteId,
|
|
externalOrderId,
|
|
externalOrderShippingId: shipping.externalOrderShippingId,
|
|
},
|
|
});
|
|
if (existingOrderShipping) {
|
|
await this.orderShippingModel.update(
|
|
existingOrderShipping.id,
|
|
shipping
|
|
);
|
|
} else {
|
|
await this.orderShippingModel.save(shipping);
|
|
}
|
|
}
|
|
}
|
|
|
|
async getOrders({
|
|
externalOrderId,
|
|
siteId,
|
|
startDate,
|
|
endDate,
|
|
status,
|
|
keyword,
|
|
current,
|
|
pageSize,
|
|
customer_email,
|
|
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
|
|
FROM \`order\` o
|
|
LEFT JOIN (
|
|
SELECT
|
|
o.customer_email AS customer_email,
|
|
COUNT(o.id) AS order_count,
|
|
SUM(o.total) AS total_spent
|
|
FROM \`order\` o
|
|
WHERE o.status IN ('processing', 'completed')
|
|
GROUP BY o.customer_email
|
|
) cs ON cs.customer_email = o.customer_email
|
|
LEFT JOIN order_shipment os ON os.order_id = o.id
|
|
LEFT JOIN shipment s ON s.id = os.shipment_id
|
|
WHERE 1=1
|
|
`;
|
|
|
|
// 计算总数
|
|
let totalQuery = `
|
|
SELECT COUNT(*) as total
|
|
FROM \`order\` o
|
|
LEFT JOIN (
|
|
SELECT o.customer_email AS customer_email
|
|
FROM \`order\` o
|
|
WHERE o.status IN ('processing', 'completed')
|
|
GROUP BY o.customer_email
|
|
) cs ON cs.customer_email = o.customer_email
|
|
WHERE 1=1
|
|
`;
|
|
|
|
// 动态添加过滤条件
|
|
if (externalOrderId) {
|
|
sqlQuery += ` AND o.externalOrderId = ?`;
|
|
totalQuery += ` AND o.externalOrderId = ?`;
|
|
parameters.push(externalOrderId);
|
|
}
|
|
if (siteId) {
|
|
sqlQuery += ` AND o.siteId = ?`;
|
|
totalQuery += ` AND o.siteId = ?`;
|
|
parameters.push(siteId);
|
|
}
|
|
if (startDate) {
|
|
sqlQuery += ` AND o.date_created >= ?`;
|
|
totalQuery += ` AND o.date_created >= ?`;
|
|
parameters.push(startDate);
|
|
}
|
|
if (endDate) {
|
|
sqlQuery += ` AND o.date_created <= ?`;
|
|
totalQuery += ` AND o.date_created <= ?`;
|
|
parameters.push(endDate);
|
|
}
|
|
// 支付方式筛选(使用参数化,避免SQL注入)
|
|
if (payment_method) {
|
|
sqlQuery += ` AND o.payment_method LIKE ?`;
|
|
totalQuery += ` AND o.payment_method LIKE ?`;
|
|
parameters.push(`%${payment_method}%`);
|
|
}
|
|
const user = await this.userModel.findOneBy({ id: userId });
|
|
if (user?.permissions?.includes('order-10-days') && !startDate && !endDate) {
|
|
sqlQuery += ` AND o.date_created >= ?`;
|
|
totalQuery += ` AND o.date_created >= ?`;
|
|
const tenDaysAgo = new Date();
|
|
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);
|
|
parameters.push(tenDaysAgo.toISOString());
|
|
}
|
|
|
|
// 处理 status 参数
|
|
if (status) {
|
|
if (Array.isArray(status)) {
|
|
sqlQuery += ` AND o.orderStatus IN (${status
|
|
.map(() => '?')
|
|
.join(', ')})`;
|
|
totalQuery += ` AND o.orderStatus IN (${status
|
|
.map(() => '?')
|
|
.join(', ')})`;
|
|
parameters.push(...status);
|
|
} else {
|
|
sqlQuery += ` AND o.orderStatus = ?`;
|
|
totalQuery += ` AND o.orderStatus = ?`;
|
|
parameters.push(status);
|
|
}
|
|
}
|
|
|
|
// 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储)
|
|
if (isSubscriptionOnly) {
|
|
const subCond = `
|
|
AND (
|
|
EXISTS (
|
|
SELECT 1 FROM subscription s
|
|
WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId
|
|
)
|
|
|
|
)
|
|
`;
|
|
sqlQuery += subCond;
|
|
totalQuery += subCond;
|
|
}
|
|
|
|
if (customer_email) {
|
|
sqlQuery += ` AND o.customer_email LIKE ?`;
|
|
totalQuery += ` AND o.customer_email LIKE ?`;
|
|
parameters.push(`%${customer_email}%`);
|
|
}
|
|
|
|
if (billing_phone) {
|
|
sqlQuery += ` AND o.billing_phone LIKE ?`;
|
|
totalQuery += ` AND o.billing_phone LIKE ?`;
|
|
parameters.push(`%${billing_phone}%`);
|
|
}
|
|
|
|
// 关键字搜索
|
|
if (keyword) {
|
|
sqlQuery += `
|
|
AND EXISTS (
|
|
SELECT 1 FROM order_item oi
|
|
WHERE oi.orderId = o.id
|
|
AND oi.name LIKE ?
|
|
)
|
|
`;
|
|
totalQuery += `
|
|
AND EXISTS (
|
|
SELECT 1 FROM order_item oi
|
|
WHERE oi.orderId = o.id
|
|
AND oi.name LIKE ?
|
|
)
|
|
`;
|
|
parameters.push(`%${keyword}%`);
|
|
}
|
|
|
|
// 执行获取总数的查询
|
|
const totalResult = await this.orderModel.query(totalQuery, parameters);
|
|
const total = totalResult[0]?.total || 0;
|
|
|
|
// 添加分页到主查询
|
|
sqlQuery += `
|
|
GROUP BY o.id
|
|
ORDER BY o.date_created DESC
|
|
LIMIT ? OFFSET ?
|
|
`;
|
|
parameters.push(pageSize, (current - 1) * pageSize);
|
|
|
|
// 执行查询
|
|
const orders = await this.orderModel.query(sqlQuery, parameters);
|
|
return { items: orders, total, current, pageSize };
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: 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.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity,
|
|
SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity,
|
|
SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity,
|
|
SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity,
|
|
SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
|
|
SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity,
|
|
SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity,
|
|
SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity
|
|
FROM order_sale os
|
|
INNER JOIN \`order\` o ON o.id = os.orderId
|
|
WHERE o.date_paid BETWEEN ? AND ?
|
|
AND o.status IN ('completed','processing')
|
|
`;
|
|
|
|
if (siteId) {
|
|
totalSql += ' AND os.siteId = ?';
|
|
totalParams.push(siteId);
|
|
yooneSql += ' AND os.siteId = ?';
|
|
yooneParams.push(siteId);
|
|
}
|
|
|
|
if (nameKeywords.length > 0) {
|
|
totalSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')';
|
|
totalParams.push(...nameKeywords.map(w => `%${w}%`));
|
|
}
|
|
|
|
const [totalResult] = await this.orderSaleModel.query(totalSql, totalParams);
|
|
const [yooneResult] = await this.orderSaleModel.query(yooneSql, yooneParams);
|
|
|
|
return {
|
|
items,
|
|
total: totalCount, // ✅ 总条数
|
|
totalQuantity: Number(totalResult.totalQuantity || 0),
|
|
yoone3Quantity: Number(yooneResult.yoone3Quantity || 0),
|
|
yoone6Quantity: Number(yooneResult.yoone6Quantity || 0),
|
|
yoone9Quantity: Number(yooneResult.yoone9Quantity || 0),
|
|
yoone12Quantity: Number(yooneResult.yoone12Quantity || 0),
|
|
yoone12QuantityNew: Number(yooneResult.yoone12QuantityNew || 0),
|
|
yoone15Quantity: Number(yooneResult.yoone15Quantity || 0),
|
|
yoone18Quantity: Number(yooneResult.yoone18Quantity || 0),
|
|
zexQuantity: Number(yooneResult.zexQuantity || 0),
|
|
current,
|
|
pageSize,
|
|
};
|
|
}
|
|
|
|
|
|
async getOrderItems({
|
|
siteId,
|
|
startDate,
|
|
endDate,
|
|
current,
|
|
pageSize,
|
|
name,
|
|
}: QueryOrderSalesDTO) {
|
|
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
|
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
|
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
|
startDate = (startDate as any) || defaultStart as any;
|
|
endDate = (endDate as any) || defaultEnd as any;
|
|
// 分页查询
|
|
let sqlQuery = `
|
|
WITH product_purchase_counts AS (
|
|
SELECT o.customer_email,oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name, COUNT(DISTINCT o.id,oi.siteId,oi.externalProductId,oi.externalVariationId) AS order_count
|
|
FROM \`order\` o
|
|
JOIN order_item oi ON o.id = oi.orderId
|
|
WHERE o.status IN ('completed', 'processing')
|
|
GROUP BY o.customer_email, oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name
|
|
)
|
|
SELECT
|
|
oi.externalProductId AS externalProductId,
|
|
oi.externalVariationId AS externalVariationId,
|
|
oi.name AS name,
|
|
SUM(oi.quantity) AS totalQuantity,
|
|
COUNT(distinct oi.orderId) AS totalOrders,
|
|
COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
|
|
COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
|
|
COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
|
|
COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount
|
|
FROM order_item oi
|
|
INNER JOIN \`order\` o ON o.id = oi.orderId
|
|
INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.externalProductId = oi.externalProductId AND pc.externalVariationId = oi.externalVariationId
|
|
WHERE o.date_created BETWEEN ? AND ?
|
|
AND o.status IN ('processing', 'completed')
|
|
`;
|
|
const parameters: any[] = [startDate, endDate];
|
|
if (siteId) {
|
|
sqlQuery += ' AND oi.siteId = ?';
|
|
parameters.push(siteId);
|
|
}
|
|
if (nameKeywords.length > 0) {
|
|
sqlQuery +=
|
|
' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND ');
|
|
parameters.push(...nameKeywords.map(word => `%${word}%`));
|
|
}
|
|
sqlQuery += `
|
|
GROUP BY oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name
|
|
ORDER BY totalQuantity DESC
|
|
`;
|
|
sqlQuery += ' LIMIT ? OFFSET ?';
|
|
parameters.push(pageSize, (current - 1) * pageSize);
|
|
|
|
// 执行查询并传递参数
|
|
const items = await this.orderSaleModel.query(sqlQuery, parameters);
|
|
|
|
let totalCountQuery = `
|
|
SELECT COUNT(DISTINCT oi.siteId,oi.externalProductId,oi.externalVariationId) AS totalCount
|
|
FROM order_item oi
|
|
INNER JOIN \`order\` o ON o.id = oi.orderId
|
|
WHERE o.date_created BETWEEN ? AND ?
|
|
AND o.status IN ('processing', 'completed')
|
|
`;
|
|
const totalCountParameters: any[] = [startDate, endDate];
|
|
if (siteId) {
|
|
totalCountQuery += ' AND oi.siteId = ?';
|
|
totalCountParameters.push(siteId);
|
|
}
|
|
if (nameKeywords.length > 0) {
|
|
totalCountQuery +=
|
|
' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND ');
|
|
totalCountParameters.push(...nameKeywords.map(word => `%${word}%`));
|
|
}
|
|
|
|
const totalCountResult = await this.orderSaleModel.query(
|
|
totalCountQuery,
|
|
totalCountParameters
|
|
);
|
|
|
|
let totalQuantityQuery = `
|
|
SELECT SUM(oi.quantity) AS totalQuantity
|
|
FROM order_item oi
|
|
INNER JOIN \`order\` o ON o.id = oi.orderId
|
|
WHERE o.date_created BETWEEN ? AND ?
|
|
AND o.status IN ('processing', 'completed')
|
|
`;
|
|
|
|
const totalQuantityParameters: any[] = [startDate, endDate];
|
|
if (siteId) {
|
|
totalQuantityQuery += ' AND oi.siteId = ?';
|
|
totalQuantityParameters.push(siteId);
|
|
}
|
|
if (nameKeywords.length > 0) {
|
|
totalQuantityQuery +=
|
|
' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND ');
|
|
totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`));
|
|
}
|
|
|
|
const totalQuantityResult = await this.orderSaleModel.query(
|
|
totalQuantityQuery,
|
|
totalQuantityParameters
|
|
);
|
|
|
|
return {
|
|
items,
|
|
total: totalCountResult[0]?.totalCount,
|
|
totalQuantity: Number(
|
|
totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
|
),
|
|
current,
|
|
pageSize,
|
|
};
|
|
}
|
|
|
|
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 };
|
|
}
|
|
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,
|
|
};
|
|
}
|
|
|
|
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: [],
|
|
};
|
|
}
|
|
|
|
async delOrder(id: number) {
|
|
const order = await this.orderModel.findOne({ where: { id } });
|
|
if (!order) throw new Error('订单不存在');
|
|
await this.orderShippingModel.delete({ orderId: id });
|
|
await this.orderSaleModel.delete({ orderId: id });
|
|
const refunds = await this.orderRefundModel.find({
|
|
where: { orderId: id },
|
|
});
|
|
if (refunds.length > 0) {
|
|
for (const refund of refunds) {
|
|
await this.orderRefundItemModel.delete({ refundId: refund.id });
|
|
await this.orderRefundModel.delete({ id: refund.id });
|
|
}
|
|
}
|
|
await this.orderItemModel.delete({ orderId: id });
|
|
await this.orderFeeModel.delete({ orderId: id });
|
|
await this.orderCouponModel.delete({ orderId: id });
|
|
await this.orderModel.delete({ id });
|
|
}
|
|
|
|
async createNote(userId: number, data: CreateOrderNoteDTO) {
|
|
return await this.orderNoteModel.save({
|
|
...data,
|
|
userId,
|
|
});
|
|
}
|
|
|
|
async getOrderByNumber(id: string) {
|
|
const orders = await this.orderModel.find({
|
|
where: {
|
|
externalOrderId: Like(id),
|
|
orderStatus: In([
|
|
ErpOrderStatus.PROCESSING,
|
|
ErpOrderStatus.PENDING_RESHIPMENT,
|
|
]),
|
|
},
|
|
});
|
|
// 批量获取订单涉及的站点名称,避免使用配置文件
|
|
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)) || '',
|
|
}));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async changeStatus(id: number, status: ErpOrderStatus) {
|
|
const order = await this.orderModel.findOne({ where: { id } });
|
|
if (!order) throw new Error(`订单 ${id}不存在`);
|
|
order.orderStatus = status;
|
|
await this.orderModel.save(order);
|
|
}
|
|
|
|
|
|
async createOrder(data: Record<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);
|
|
}
|
|
});
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
//换货确认按钮改成调用这个方法
|
|
//换货功能更新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}`);
|
|
// }
|
|
}
|
|
|
|
}
|