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

1306 lines
42 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 { Config, 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 { WpProduct } from '../entity/wp_product.entity';
import { Product } from '../entity/product.entty';
import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity';
import { OrderCoupon } from '../entity/order_copon.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 { OrderDetailRes } from '../dto/reponse.dto';
import { OrderNote } from '../entity/order_note.entity';
import { User } from '../entity/user.entity';
import { WpSite } from '../interface';
import { ShipmentItem } from '../entity/shipment_item.entity';
import { UpdateStockDTO } from '../dto/stock.dto';
import { StockService } from './stock.service';
import { OrderSaleOriginal } from '../entity/order_item_original.entity';
@Provide()
export class OrderService {
@Config('wpSite')
sites: WpSite[];
@Inject()
wPService: WPService;
@Inject()
stockService: StockService;
@InjectEntityModel(Order)
orderModel: Repository<Order>;
@InjectEntityModel(OrderItem)
orderItemModel: Repository<OrderItem>;
@InjectEntityModel(OrderSale)
orderSaleModel: Repository<OrderSale>;
@InjectEntityModel(OrderSaleOriginal)
orderSaleOriginalModel: Repository<OrderSaleOriginal>;
@InjectEntityModel(WpProduct)
wpProductModel: Repository<WpProduct>;
@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>;
async syncOrders(siteId: string) {
const orders = await this.wPService.getOrders(siteId); // 调用 WooCommerce API 获取订单
for (const order of orders) {
await this.syncSingleOrder(siteId, order);
}
}
async syncOrderById(siteId: string, orderId: string) {
const order = await this.wPService.getOrder(siteId, orderId);
await this.syncSingleOrder(siteId, order, true);
}
async syncSingleOrder(siteId: string, order: any, forceUpdate = false) {
let {
line_items,
shipping_lines,
fee_lines,
coupon_lines,
refunds,
...orderData
} = order;
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: order.id, siteId: siteId },
});
const orderId = (await this.saveOrder(siteId, orderData)).id;
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;
}
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.productSku = 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: string, 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;
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;
default:
return ErpOrderStatus.PENDING;
}
}
async saveOrderItems(params: {
siteId: string;
orderId: number;
externalOrderId: string;
orderItems: Record<string, any>[];
}) {
const { siteId, orderId, externalOrderId, orderItems } = params;
const currentOrderItems = await this.orderItemModel.find({
where: { siteId, externalOrderId: externalOrderId },
});
const syncedOrderItemIds = new Set(orderItems.map(v => String(v.id)));
const orderItemToDelete = currentOrderItems.filter(
db => !syncedOrderItemIds.has(String(db.externalOrderItemId))
);
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 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;
let constitution;
if (orderItem.externalVariationId === '0') {
const product = await this.wpProductModel.findOne({
where: { sku: orderItem.sku },
});
if (!product) return;
constitution = product?.constitution;
} else {
const variation = await this.variationModel.findOne({
where: { sku: orderItem.sku },
});
if (!variation) return;
constitution = variation?.constitution;
}
if (!Array.isArray(constitution)) return;
const orderSales: OrderSale[] = [];
for (const item of constitution) {
const baseProduct = await this.productModel.findOne({
where: { sku: item.sku },
});
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: baseProduct.id,
name: baseProduct.name,
quantity: item.quantity * orderItem.quantity,
sku: item.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
await this.orderSaleModel.save(orderSales);
}
async saveOrderRefunds({
siteId,
orderId,
externalOrderId,
refunds,
}: {
siteId: string;
orderId: number;
externalOrderId: string;
refunds: Record<string, any>[];
}) {
for (const item of refunds) {
const refund = await this.wPService.getOrderRefund(
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,
}) {
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.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,
cs.order_count as order_count,
cs.total_spent as total_spent,
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);
}
// 处理 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);
}
}
if (customer_email) {
sqlQuery += ` AND o.customer_email LIKE ?`;
totalQuery += ` AND o.customer_email LIKE ?`;
parameters.push(`%${customer_email}%`);
}
// 关键字搜索
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,
}) {
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}%` }
);
}
return await query.getRawMany();
}
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
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);
}
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);
const pcSql = `
SELECT
os.productId,
SUM(CASE WHEN t.purchaseIndex = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex = 1 THEN 1 END) AS firstOrderCount,
SUM(CASE WHEN t.purchaseIndex = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex = 2 THEN 1 END) AS secondOrderCount,
SUM(CASE WHEN t.purchaseIndex = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex = 3 THEN 1 END) AS thirdOrderCount,
SUM(CASE WHEN t.purchaseIndex > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex > 3 THEN 1 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 = ?' : ''}
)
GROUP BY os.productId
`;
const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
const pcMap = new Map<number, any>();
pcResults.forEach(r => pcMap.set(r.productId, r));
items.forEach(i => {
const r = pcMap.get(i.productId) || {};
i.firstOrderYOONEBoxCount = Number(r.firstOrderYOONEBoxCount || 0);
i.firstOrderCount = Number(r.firstOrderCount || 0);
i.secondOrderYOONEBoxCount = Number(r.secondOrderYOONEBoxCount || 0);
i.secondOrderCount = Number(r.secondOrderCount || 0);
i.thirdOrderYOONEBoxCount = Number(r.thirdOrderYOONEBoxCount || 0);
i.thirdOrderCount = Number(r.thirdOrderCount || 0);
i.moreThirdOrderYOONEBoxCount = Number(r.moreThirdOrderYOONEBoxCount || 0);
i.moreThirdOrderCount = Number(r.moreThirdOrderCount || 0);
});
}
// -------------------------
// 4. 总量统计(时间段 + siteId
// -------------------------
const totalParams: any[] = [startDate, endDate];
const yooneParams: any[] = [startDate, endDate];
let totalSql = `
SELECT
SUM(os.quantity) AS totalQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed','processing')
`;
let yooneSql = `
SELECT
SUM(CASE WHEN os.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) : [];
// 分页查询
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 getOrderDetail(id: number): Promise<OrderDetailRes> {
const order = await this.orderModel.findOne({ where: { id } });
const site = this.sites.find(site => site.id === order.siteId);
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 });
}
}
// update order_sale_origin if not exist
try {
const order_sale_origin_count = await this.orderSaleOriginalModel.countBy({ orderId: id });
if (order_sale_origin_count === 0) {
sales.forEach(async sale => {
const { id: saleId, ...saleData } = sale;
await this.orderSaleOriginalModel.save(saleData);
});
}
} catch (error) {
console.log('create order sale origin error: ', error.message);
}
return {
...order,
siteName: site.siteName,
email: site.email,
items,
sales,
refundItems,
notes,
shipment,
};
}
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,
]),
},
});
return orders.map(order => {
return {
externalOrderId: order.externalOrderId,
id: order.id,
siteName:
this.sites.find(site => site.id === order.siteId)?.siteName || '',
};
});
}
async cancelOrder(id: number) {
const order = await this.orderModel.findOne({ where: { id } });
if (!order) throw new Error(`订单 ${id}不存在`);
const site = this.wPService.geSite(order.siteId);
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 = this.wPService.geSite(order.siteId);
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 = this.wPService.geSite(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 { sales, total, billing, customer_email } = data;
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 OrderSaleOriginalRepo = manager.getRepository(OrderSaleOriginal);
const productRepo = manager.getRepository(Product);
const order = await orderRepo.save({
siteId: '-1',
externalOrderId: '-1',
status: OrderStatus.PROCESSING,
orderStatus: ErpOrderStatus.PROCESSING,
currency: 'CAD',
currency_symbol: '$',
date_created: now,
date_paid: now,
total,
customer_email,
billing,
shipping: billing,
});
for (const sale of sales) {
const product = await productRepo.findOne({ where: { sku: sale.sku } });
const saleItem = {
orderId: order.id,
siteId: '-1',
externalOrderItemId: '-1',
productId: product.id,
name: product.name,
sku: sale.sku,
quantity: sale.quantity,
};
await orderSaleRepo.save(saleItem);
await OrderSaleOriginalRepo.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
});
};
}).catch(error => {
transactionError = error;
});
if (transactionError !== undefined) {
throw new Error(`更新物流信息错误:${transactionError.message}`);
}
return true;
} catch (error) {
throw new Error(`更新发货产品失败:${error.message}`);
}
}
}