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

1425 lines
47 KiB
TypeScript

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 parameters: any[] = [startDate, endDate];
// 主查询:带分页
let sqlQuery = `
WITH product_purchase_counts AS (
SELECT
o.customer_email,
os.productId,
COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
GROUP BY o.customer_email, os.productId
)
SELECT
os.productId AS productId,
os.name AS name,
SUM(os.quantity) AS totalQuantity,
COUNT(DISTINCT os.orderId) AS totalOrders,
c.name AS categoryName,
COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
INNER JOIN product p ON os.productId = p.id
INNER JOIN category c ON p.categoryId = c.id
INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing')
`;
if (siteId) {
sqlQuery += ' AND os.siteId = ?';
parameters.push(siteId);
}
if (nameKeywords.length > 0) {
sqlQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
parameters.push(...nameKeywords.map(word => `%${word}%`));
}
sqlQuery += `
GROUP BY os.productId, os.name, c.name
ORDER BY totalQuantity DESC
LIMIT ? OFFSET ?
`;
parameters.push(pageSize, (current - 1) * pageSize);
const items = await this.orderSaleModel.query(sqlQuery, parameters);
// 总条数
const countParams: any[] = [startDate, endDate];
let totalCountQuery = `
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) {
totalCountQuery += ' AND os.siteId = ?';
countParams.push(siteId);
}
if (nameKeywords.length > 0) {
totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
countParams.push(...nameKeywords.map(word => `%${word}%`));
}
const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams);
// 一次查询获取所有 yoone box 数量
const totalQuantityParams: any[] = [startDate, endDate];
let totalQuantityQuery = `
SELECT
SUM(os.quantity) AS totalQuantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%3%' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%6%' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%9%' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' AND os.name LIKE '%NEW%' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%15%' THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%18%' THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.name LIKE '%zex%' THEN os.quantity ELSE 0 END) AS zexQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing')
`;
if (siteId) {
totalQuantityQuery += ' AND os.siteId = ?';
totalQuantityParams.push(siteId);
}
if (nameKeywords.length > 0) {
totalQuantityQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
totalQuantityParams.push(...nameKeywords.map(word => `%${word}%`));
}
const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams);
return {
items,
total: totalCountResult[0]?.totalCount || 0,
totalQuantity: Number(totalQuantityResult.totalQuantity || 0),
yoone3Quantity: Number(totalQuantityResult.yoone3Quantity || 0),
yoone6Quantity: Number(totalQuantityResult.yoone6Quantity || 0),
yoone9Quantity: Number(totalQuantityResult.yoone9Quantity || 0),
yoone12Quantity: Number(totalQuantityResult.yoone12Quantity || 0),
yoone12QuantityNew: Number(totalQuantityResult.yoone12QuantityNew || 0),
yoone15Quantity: Number(totalQuantityResult.yoone15Quantity || 0),
yoone18Quantity: Number(totalQuantityResult.yoone18Quantity || 0),
zexQuantity: Number(totalQuantityResult.zexQuantity || 0),
current,
pageSize,
};
}
// async getOrderSales({
// 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,os.productId, os.name, COUNT(DISTINCT o.id,os.productId) AS order_count
// FROM \`order\` o
// JOIN order_sale os ON o.id = os.orderId
// WHERE o.status IN ('completed', 'processing')
// GROUP BY o.customer_email, os.productId, os.name
// )
// SELECT
// os.productId AS productId,
// os.name AS name,
// SUM(os.quantity) AS totalQuantity,
// COUNT(distinct os.orderId) AS totalOrders,
// c.name AS categoryName,
// COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
// SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
// COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
// SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
// COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
// SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
// COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
// SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
// FROM order_sale os
// INNER JOIN \`order\` o ON o.id = os.orderId
// INNER JOIN product p ON os.productId = p.id
// INNER JOIN category c ON p.categoryId = c.id
// INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
// WHERE o.date_paid BETWEEN ? AND ?
// AND o.status IN ('processing', 'completed')
// `;
// const parameters: any[] = [startDate, endDate];
// if (siteId) {
// sqlQuery += ' AND os.siteId = ?';
// parameters.push(siteId);
// }
// if (nameKeywords.length > 0) {
// sqlQuery +=
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
// parameters.push(...nameKeywords.map(word => `%${word}%`));
// }
// sqlQuery += `
// GROUP BY os.productId, os.name, c.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 os.productId) AS totalCount
// FROM order_sale os
// INNER JOIN \`order\` o ON o.id = os.orderId
// INNER JOIN product p ON os.productId = p.id
// INNER JOIN category c ON p.categoryId = c.id
// WHERE o.date_created BETWEEN ? AND ?
// AND o.status IN ('processing', 'completed')
// `;
// const totalCountParameters: any[] = [startDate, endDate];
// if (siteId) {
// totalCountQuery += ' AND os.siteId = ?';
// totalCountParameters.push(siteId);
// }
// if (nameKeywords.length > 0) {
// totalCountQuery +=
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
// totalCountParameters.push(...nameKeywords.map(word => `%${word}%`));
// }
// const totalCountResult = await this.orderSaleModel.query(
// totalCountQuery,
// totalCountParameters
// );
// let totalQuantityQuery = `
// SELECT SUM(os.quantity) AS totalQuantity
// FROM order_sale os
// INNER JOIN \`order\` o ON o.id = os.orderId
// INNER JOIN product p ON os.productId = p.id
// INNER JOIN category c ON p.categoryId = c.id
// WHERE o.date_created BETWEEN ? AND ?
// AND o.status IN ('processing', 'completed')
// `;
// const totalQuantityParameters: any[] = [startDate, endDate];
// if (siteId) {
// totalQuantityQuery += ' AND os.siteId = ?';
// totalQuantityParameters.push(siteId);
// }
// const yoone3QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%3%"';
// const yoone6QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%6%"';
// const yoone9QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%9%"';
// const yoone12QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%12%"';
// const yoone15QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%15%"';
// const yooneParameters = [...totalQuantityParameters];
// if (nameKeywords.length > 0) {
// totalQuantityQuery +=
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
// totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`));
// }
// const totalQuantityResult = await this.orderSaleModel.query(
// totalQuantityQuery,
// totalQuantityParameters
// );
// const yoone3QuantityResult = await this.orderSaleModel.query(
// yoone3QuantityQuery,
// yooneParameters
// );
// const yoone6QuantityResult = await this.orderSaleModel.query(
// yoone6QuantityQuery,
// yooneParameters
// );
// const yoone9QuantityResult = await this.orderSaleModel.query(
// yoone9QuantityQuery,
// yooneParameters
// );
// const yoone12QuantityResult = await this.orderSaleModel.query(
// yoone12QuantityQuery,
// yooneParameters
// );
// const yoone15QuantityResult = await this.orderSaleModel.query(
// yoone15QuantityQuery,
// yooneParameters
// );
// return {
// items,
// total: totalCountResult[0]?.totalCount,
// totalQuantity: Number(
// totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone3Quantity: Number(
// yoone3QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone6Quantity: Number(
// yoone6QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone9Quantity: Number(
// yoone9QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone12Quantity: Number(
// yoone12QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone15Quantity: Number(
// yoone15QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 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}`);
}
}
}