API/src/service/order.service.ts

1965 lines
64 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { Inject, 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 { 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';
@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(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;
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: []
};
// 遍历每个订单进行同步
for (const order of result) {
try {
// 检查订单是否已存在,以区分创建和更新
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId },
});
if(!existingOrder){
console.log("数据库中不存在",order.id, '订单状态:', order.status )
}
// 同步单个订单
await this.syncSingleOrder(siteId, order);
// 统计结果
syncResult.processed++;
syncResult.synced++;
if (existingOrder) {
syncResult.updated++;
} else {
syncResult.created++;
}
} catch (error) {
// 记录错误但不中断整个同步过程
syncResult.errors.push({
identifier: String(order.id),
error: error.message || '同步失败'
});
syncResult.processed++;
}
}
return syncResult;
}
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 order = await this.wpService.getOrder(siteId, 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;
}
}
// 订单状态切换表
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.status, '=>', this.orderAutoNextStatusMap[order.status])
// 其他状态保持不变
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 site = await this.siteService.get(siteId,true);
if(site.type === 'woocommerce'){
// 矫正数据库状态
await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, {
orderStatus: 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,
});
console.log('同步进单个订单2')
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: UnifiedOrderDTO): Promise<Order> {
const externalOrderId = String(order.id)
delete order.id
// order.billing_phone = order?.billing?.phone || order?.shipping?.phone;
const entity = plainToClass(Order, {...order, externalOrderId, siteId});
const existingOrder = await this.orderModel.findOne({
where: { 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,
site_id: siteId,
origin_id: String(order.customer_id),
});
}
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: { siteSkus: Like(`%${orderItem.sku}%`) },
relations: ['components'],
});
if (!product) return;
const orderSales: OrderSale[] = [];
if (product.components && product.components.length > 0) {
for (const comp of product.components) {
const baseProduct = await this.productModel.findOne({
where: { sku: comp.sku },
});
if (baseProduct) {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: baseProduct.id,
name: baseProduct.name,
quantity: comp.quantity * orderItem.quantity,
sku: comp.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
}
} else {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: product.id,
name: product.name,
quantity: orderItem.quantity,
sku: product.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
if (orderSales.length > 0) {
await this.orderSaleModel.save(orderSales);
}
}
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}`);
// }
}
// TODO
async exportOrder(ids: number[]) {
// 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号
interface ExportData {
'日期': string;
'订单号': string;
'姓名地址': string;
'邮箱': string;
'号码': string;
'订单内容': string;
'盒数': number;
'换盒数': number;
'换货内容': string;
'快递号': string;
}
try {
// 空值检查
const dataSource = this.dataSourceManager.getDataSource('default');
// 优化事务使用
return await dataSource.transaction(async manager => {
// 准备查询条件
const whereCondition: any = {};
if (ids && ids.length > 0) {
whereCondition.id = In(ids);
}
// 获取订单、订单项和物流信息
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,
'订单内容': 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}`);
}
}
}