|
|
|
|
@ -39,6 +39,7 @@ import * as path from 'path';
|
|
|
|
|
import * as os from 'os';
|
|
|
|
|
import { UnifiedOrderDTO } from '../dto/site-api.dto';
|
|
|
|
|
import { CustomerService } from './customer.service';
|
|
|
|
|
import { ProductService } from './product.service';
|
|
|
|
|
@Provide()
|
|
|
|
|
export class OrderService {
|
|
|
|
|
|
|
|
|
|
@ -110,6 +111,8 @@ export class OrderService {
|
|
|
|
|
|
|
|
|
|
@Logger()
|
|
|
|
|
logger; // 注入 Logger 实例
|
|
|
|
|
@Inject()
|
|
|
|
|
productService: ProductService;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量同步订单
|
|
|
|
|
@ -138,7 +141,7 @@ export class OrderService {
|
|
|
|
|
updated: 0,
|
|
|
|
|
errors: []
|
|
|
|
|
};
|
|
|
|
|
console.log('开始进入循环同步订单', result.length, '个订单')
|
|
|
|
|
|
|
|
|
|
// 遍历每个订单进行同步
|
|
|
|
|
for (const order of result) {
|
|
|
|
|
try {
|
|
|
|
|
@ -146,8 +149,8 @@ export class OrderService {
|
|
|
|
|
const existingOrder = await this.orderModel.findOne({
|
|
|
|
|
where: { externalOrderId: String(order.id), siteId: siteId },
|
|
|
|
|
});
|
|
|
|
|
if(!existingOrder){
|
|
|
|
|
console.log("数据库中不存在",order.id, '订单状态:', order.status )
|
|
|
|
|
if (!existingOrder) {
|
|
|
|
|
console.log("数据库中不存在", order.id, '订单状态:', order.status)
|
|
|
|
|
}
|
|
|
|
|
// 同步单个订单
|
|
|
|
|
await this.syncSingleOrder(siteId, order);
|
|
|
|
|
@ -162,7 +165,6 @@ export class OrderService {
|
|
|
|
|
} else {
|
|
|
|
|
syncResult.created++;
|
|
|
|
|
}
|
|
|
|
|
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// 记录错误但不中断整个同步过程
|
|
|
|
|
syncResult.errors.push({
|
|
|
|
|
@ -172,8 +174,6 @@ export class OrderService {
|
|
|
|
|
syncResult.processed++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log('同步完成', syncResult.updated, 'created:', syncResult.created)
|
|
|
|
|
|
|
|
|
|
this.logger.debug('syncOrders result', syncResult)
|
|
|
|
|
return syncResult;
|
|
|
|
|
}
|
|
|
|
|
@ -211,8 +211,8 @@ export class OrderService {
|
|
|
|
|
const existingOrder = await this.orderModel.findOne({
|
|
|
|
|
where: { externalOrderId: String(order.id), siteId: siteId },
|
|
|
|
|
});
|
|
|
|
|
if(!existingOrder){
|
|
|
|
|
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status )
|
|
|
|
|
if (!existingOrder) {
|
|
|
|
|
console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
|
|
|
|
|
}
|
|
|
|
|
// 同步单个订单
|
|
|
|
|
await this.syncSingleOrder(siteId, order, true);
|
|
|
|
|
@ -271,7 +271,7 @@ export class OrderService {
|
|
|
|
|
try {
|
|
|
|
|
const site = await this.siteService.get(siteId);
|
|
|
|
|
// 仅处理 WooCommerce 站点
|
|
|
|
|
if(site.type !== 'woocommerce'){
|
|
|
|
|
if (site.type !== 'woocommerce') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// 将订单状态同步到 WooCommerce,然后切换至下一状态
|
|
|
|
|
@ -281,6 +281,11 @@ export class OrderService {
|
|
|
|
|
console.error('更新订单状态失败,原因为:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
|
|
|
|
|
return await this.orderModel.findOne({
|
|
|
|
|
where: { externalOrderId: String(externalOrderId), siteId },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 同步单个订单
|
|
|
|
|
* 流程说明:
|
|
|
|
|
@ -318,60 +323,41 @@ export class OrderService {
|
|
|
|
|
// console.log('同步进单个订单', order)
|
|
|
|
|
// 如果订单状态为 AUTO_DRAFT,则跳过处理
|
|
|
|
|
if (order.status === OrderStatus.AUTO_DRAFT) {
|
|
|
|
|
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 检查数据库中是否已存在该订单
|
|
|
|
|
const existingOrder = await this.orderModel.findOne({
|
|
|
|
|
where: { externalOrderId: String(order.id), siteId: siteId },
|
|
|
|
|
});
|
|
|
|
|
// 自动更新订单状态(如果需要)
|
|
|
|
|
// 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断
|
|
|
|
|
// if(!order.is_editable && !forceUpdate){
|
|
|
|
|
// this.logger.debug('订单不可编辑,跳过处理', siteId, order.id)
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
// 自动转换远程订单的状态(如果需要)
|
|
|
|
|
await this.autoUpdateOrderStatus(siteId, order);
|
|
|
|
|
|
|
|
|
|
if(existingOrder){
|
|
|
|
|
// 矫正数据库中的订单数据
|
|
|
|
|
const updateData: any = { status: order.status };
|
|
|
|
|
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
|
|
|
|
|
updateData.orderStatus = this.mapOrderStatus(order.status as any);
|
|
|
|
|
}
|
|
|
|
|
// 更新订单主数据
|
|
|
|
|
await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData);
|
|
|
|
|
// 更新 fulfillments 数据
|
|
|
|
|
await this.saveOrderFulfillments({
|
|
|
|
|
siteId,
|
|
|
|
|
orderId: existingOrder.id,
|
|
|
|
|
externalOrderId:order.id,
|
|
|
|
|
fulfillments: fulfillments,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const externalOrderId = String(order.id);
|
|
|
|
|
// 如果订单从未完成变为完成状态,则更新库存
|
|
|
|
|
// 这里的 saveOrder 已经包括了创建订单和更新订单
|
|
|
|
|
let orderRecord: Order = await this.saveOrder(siteId, orderData);
|
|
|
|
|
// 如果订单从未完成变为完成状态,则更新库存
|
|
|
|
|
if (
|
|
|
|
|
existingOrder &&
|
|
|
|
|
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED &&
|
|
|
|
|
orderRecord &&
|
|
|
|
|
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
|
|
|
|
|
orderData.status === OrderStatus.COMPLETED
|
|
|
|
|
) {
|
|
|
|
|
this.updateStock(existingOrder);
|
|
|
|
|
await this.updateStock(orderRecord);
|
|
|
|
|
// 不再直接返回,继续执行后续的更新操作
|
|
|
|
|
}
|
|
|
|
|
// 如果订单不可编辑且不强制更新,则跳过处理
|
|
|
|
|
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 保存订单主数据
|
|
|
|
|
const orderRecord = await this.saveOrder(siteId, orderData);
|
|
|
|
|
const externalOrderId = String(order.id);
|
|
|
|
|
const orderId = orderRecord.id;
|
|
|
|
|
// 保存订单项
|
|
|
|
|
await this.saveOrderItems({
|
|
|
|
|
siteId,
|
|
|
|
|
orderId,
|
|
|
|
|
externalOrderId: String(externalOrderId),
|
|
|
|
|
externalOrderId,
|
|
|
|
|
orderItems: line_items,
|
|
|
|
|
});
|
|
|
|
|
// 保存退款信息
|
|
|
|
|
await this.saveOrderRefunds({
|
|
|
|
|
siteId,
|
|
|
|
|
orderId,
|
|
|
|
|
externalOrderId ,
|
|
|
|
|
externalOrderId,
|
|
|
|
|
refunds,
|
|
|
|
|
});
|
|
|
|
|
// 保存费用信息
|
|
|
|
|
@ -462,13 +448,14 @@ export class OrderService {
|
|
|
|
|
* @param order 订单数据
|
|
|
|
|
* @returns 保存后的订单实体
|
|
|
|
|
*/
|
|
|
|
|
async saveOrder(siteId: number, order: Partial<UnifiedOrderDTO>): Promise<Order> {
|
|
|
|
|
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
|
|
|
|
|
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
|
|
|
|
|
// 将外部订单ID转换为字符串
|
|
|
|
|
const externalOrderId = String(order.id)
|
|
|
|
|
delete order.id
|
|
|
|
|
|
|
|
|
|
// 创建订单实体对象
|
|
|
|
|
const entity = plainToClass(Order, {...order, externalOrderId, siteId});
|
|
|
|
|
const entity = plainToClass(Order, { ...order, externalOrderId, siteId });
|
|
|
|
|
// 检查数据库中是否已存在该订单
|
|
|
|
|
const existingOrder = await this.orderModel.findOne({
|
|
|
|
|
where: { externalOrderId, siteId: siteId },
|
|
|
|
|
@ -711,6 +698,8 @@ export class OrderService {
|
|
|
|
|
*
|
|
|
|
|
* @param orderItem 订单项实体
|
|
|
|
|
*/
|
|
|
|
|
// TODO 这里存的是库存商品实际
|
|
|
|
|
// 所以叫做 orderInventoryItems 可能更合适
|
|
|
|
|
async saveOrderSale(orderItem: OrderItem) {
|
|
|
|
|
const currentOrderSale = await this.orderSaleModel.find({
|
|
|
|
|
where: {
|
|
|
|
|
@ -729,46 +718,47 @@ export class OrderService {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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({
|
|
|
|
|
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
|
|
|
|
|
return {
|
|
|
|
|
product: await this.productModel.findOne({
|
|
|
|
|
where: { 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);
|
|
|
|
|
}
|
|
|
|
|
relations: ['components', 'attributes'],
|
|
|
|
|
}),
|
|
|
|
|
quantity: comp.quantity * orderItem.quantity,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
|
|
|
|
})) : [{ product, quantity: orderItem.quantity }]
|
|
|
|
|
|
|
|
|
|
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
|
|
|
|
|
if (!componentDetail.product) return null
|
|
|
|
|
const attrsObj = this.productService.getAttributesObject(product.attributes)
|
|
|
|
|
const orderSale = plainToClass(OrderSale, {
|
|
|
|
|
orderId: orderItem.orderId,
|
|
|
|
|
siteId: orderItem.siteId,
|
|
|
|
|
externalOrderItemId: orderItem.externalOrderItemId,
|
|
|
|
|
productId: product.id,
|
|
|
|
|
name: product.name,
|
|
|
|
|
quantity: orderItem.quantity,
|
|
|
|
|
sku: product.sku,
|
|
|
|
|
isPackage: orderItem.name.toLowerCase().includes('package'),
|
|
|
|
|
productId: componentDetail.product.id,
|
|
|
|
|
name: componentDetail.product.name,
|
|
|
|
|
quantity: componentDetail.quantity * orderItem.quantity,
|
|
|
|
|
sku: componentDetail.product.sku,
|
|
|
|
|
isPackage: componentDetail.product.type === 'bundle',
|
|
|
|
|
isYoone: attrsObj?.['brand']?.name === 'yoone',
|
|
|
|
|
isZyn: attrsObj?.['brand']?.name === 'zyn',
|
|
|
|
|
isZex: attrsObj?.['brand']?.name === 'zex',
|
|
|
|
|
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
|
|
|
|
|
size: this.extractNumberFromString(attrsObj?.['strength']?.name) || null,
|
|
|
|
|
});
|
|
|
|
|
orderSales.push(orderSaleItem);
|
|
|
|
|
}
|
|
|
|
|
return orderSale
|
|
|
|
|
}).filter(v => v !== null)
|
|
|
|
|
|
|
|
|
|
if (orderSales.length > 0) {
|
|
|
|
|
await this.orderSaleModel.save(orderSales);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
extractNumberFromString(str: string): number {
|
|
|
|
|
if (!str) return 0;
|
|
|
|
|
|
|
|
|
|
const num = parseInt(str, 10);
|
|
|
|
|
return isNaN(num) ? 0 : num;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存订单退款信息
|
|
|
|
|
@ -1237,13 +1227,13 @@ export class OrderService {
|
|
|
|
|
parameters.push(siteId);
|
|
|
|
|
}
|
|
|
|
|
if (startDate) {
|
|
|
|
|
sqlQuery += ` AND o.date_paid >= ?`;
|
|
|
|
|
totalQuery += ` AND o.date_paid >= ?`;
|
|
|
|
|
sqlQuery += ` AND o.date_created >= ?`;
|
|
|
|
|
totalQuery += ` AND o.date_created >= ?`;
|
|
|
|
|
parameters.push(startDate);
|
|
|
|
|
}
|
|
|
|
|
if (endDate) {
|
|
|
|
|
sqlQuery += ` AND o.date_paid <= ?`;
|
|
|
|
|
totalQuery += ` AND o.date_paid <= ?`;
|
|
|
|
|
sqlQuery += ` AND o.date_created <= ?`;
|
|
|
|
|
totalQuery += ` AND o.date_created <= ?`;
|
|
|
|
|
parameters.push(endDate);
|
|
|
|
|
}
|
|
|
|
|
// 支付方式筛选(使用参数化,避免SQL注入)
|
|
|
|
|
@ -1331,7 +1321,7 @@ export class OrderService {
|
|
|
|
|
// 添加分页到主查询
|
|
|
|
|
sqlQuery += `
|
|
|
|
|
GROUP BY o.id
|
|
|
|
|
ORDER BY o.date_paid DESC
|
|
|
|
|
ORDER BY o.date_created DESC
|
|
|
|
|
LIMIT ? OFFSET ?
|
|
|
|
|
`;
|
|
|
|
|
parameters.push(pageSize, (current - 1) * pageSize);
|
|
|
|
|
@ -1429,7 +1419,7 @@ export class OrderService {
|
|
|
|
|
* @param params 查询参数
|
|
|
|
|
* @returns 销售统计和分页信息
|
|
|
|
|
*/
|
|
|
|
|
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
|
|
|
|
|
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) {
|
|
|
|
|
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
|
|
|
|
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
|
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
|
@ -1645,11 +1635,12 @@ export class OrderService {
|
|
|
|
|
* @returns 订单项统计和分页信息
|
|
|
|
|
*/
|
|
|
|
|
async getOrderItems({
|
|
|
|
|
current,
|
|
|
|
|
pageSize,
|
|
|
|
|
siteId,
|
|
|
|
|
startDate,
|
|
|
|
|
endDate,
|
|
|
|
|
current,
|
|
|
|
|
pageSize,
|
|
|
|
|
sku,
|
|
|
|
|
name,
|
|
|
|
|
}: QueryOrderSalesDTO) {
|
|
|
|
|
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
|
|
|
|
@ -1907,8 +1898,8 @@ export class OrderService {
|
|
|
|
|
const key = it?.externalSubscriptionId
|
|
|
|
|
? `sub:${it.externalSubscriptionId}`
|
|
|
|
|
: it?.externalOrderId
|
|
|
|
|
? `ord:${it.externalOrderId}`
|
|
|
|
|
: `id:${it?.id}`;
|
|
|
|
|
? `ord:${it.externalOrderId}`
|
|
|
|
|
: `id:${it?.id}`;
|
|
|
|
|
if (!seen.has(key)) {
|
|
|
|
|
seen.add(key);
|
|
|
|
|
relatedList.push(it);
|
|
|
|
|
@ -2202,14 +2193,14 @@ export class OrderService {
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
siteId: order.siteId,
|
|
|
|
|
externalOrderItemId: '-1',
|
|
|
|
|
productId: product.id,
|
|
|
|
|
name: product.name,
|
|
|
|
|
sku: sale.sku,
|
|
|
|
|
quantity: sale.quantity,
|
|
|
|
|
};
|
|
|
|
|
await orderSaleRepo.save(saleItem);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
@ -2342,83 +2333,83 @@ export class OrderService {
|
|
|
|
|
//换货功能更新OrderSale和Orderitem数据
|
|
|
|
|
async updateExchangeOrder(orderId: number, data: any) {
|
|
|
|
|
throw new Error('暂未实现')
|
|
|
|
|
// try {
|
|
|
|
|
// const dataSource = this.dataSourceManager.getDataSource('default');
|
|
|
|
|
// let transactionError = undefined;
|
|
|
|
|
// 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);
|
|
|
|
|
// 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 productRepo = manager.getRepository(ProductV2);
|
|
|
|
|
|
|
|
|
|
// const order = await orderRepo.findOneBy({ id: orderId });
|
|
|
|
|
// let product: 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'],
|
|
|
|
|
// });
|
|
|
|
|
// };
|
|
|
|
|
// 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'] });
|
|
|
|
|
// 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,
|
|
|
|
|
// 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'],
|
|
|
|
|
// });
|
|
|
|
|
// sku: item['sku'],
|
|
|
|
|
// quantity: item['quantity'],
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// };
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
// //将是否换货状态改为true
|
|
|
|
|
// await orderRepo.update(
|
|
|
|
|
// order.id
|
|
|
|
|
// , {
|
|
|
|
|
// is_exchange: true
|
|
|
|
|
// });
|
|
|
|
|
// //将是否换货状态改为true
|
|
|
|
|
// await orderRepo.update(
|
|
|
|
|
// order.id
|
|
|
|
|
// , {
|
|
|
|
|
// is_exchange: true
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// //查询这个用户换过多少次货
|
|
|
|
|
// const counts = await orderRepo.countBy({
|
|
|
|
|
// is_editable: true,
|
|
|
|
|
// customer_email: order.customer_email,
|
|
|
|
|
// });
|
|
|
|
|
// //查询这个用户换过多少次货
|
|
|
|
|
// const counts = await orderRepo.countBy({
|
|
|
|
|
// is_editable: true,
|
|
|
|
|
// customer_email: order.customer_email,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// //批量更新当前用户换货次数
|
|
|
|
|
// await orderRepo.update({
|
|
|
|
|
// customer_email: order.customer_email
|
|
|
|
|
// }, {
|
|
|
|
|
// exchange_frequency: counts
|
|
|
|
|
// });
|
|
|
|
|
// //批量更新当前用户换货次数
|
|
|
|
|
// await orderRepo.update({
|
|
|
|
|
// customer_email: order.customer_email
|
|
|
|
|
// }, {
|
|
|
|
|
// exchange_frequency: counts
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// }).catch(error => {
|
|
|
|
|
// transactionError = error;
|
|
|
|
|
// });
|
|
|
|
|
// }).catch(error => {
|
|
|
|
|
// transactionError = error;
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// if (transactionError !== undefined) {
|
|
|
|
|
// throw new Error(`更新物流信息错误:${transactionError.message}`);
|
|
|
|
|
// }
|
|
|
|
|
// return true;
|
|
|
|
|
// } catch (error) {
|
|
|
|
|
// throw new Error(`更新发货产品失败:${error.message}`);
|
|
|
|
|
// }
|
|
|
|
|
// if (transactionError !== undefined) {
|
|
|
|
|
// throw new Error(`更新物流信息错误:${transactionError.message}`);
|
|
|
|
|
// }
|
|
|
|
|
// return true;
|
|
|
|
|
// } catch (error) {
|
|
|
|
|
// throw new Error(`更新发货产品失败:${error.message}`);
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -2474,7 +2465,7 @@ export class OrderService {
|
|
|
|
|
return await dataSource.transaction(async manager => {
|
|
|
|
|
// 准备查询条件
|
|
|
|
|
const whereCondition: any = {};
|
|
|
|
|
if(validIds.length > 0){
|
|
|
|
|
if (validIds.length > 0) {
|
|
|
|
|
whereCondition.id = In(validIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -2548,7 +2539,7 @@ export class OrderService {
|
|
|
|
|
'姓名地址': nameAddress,
|
|
|
|
|
'邮箱': order.customer_email || '',
|
|
|
|
|
'号码': phone,
|
|
|
|
|
'订单内容': this.removeLastParenthesesContent(orderContent),
|
|
|
|
|
'订单内容': orderContent,
|
|
|
|
|
'盒数': boxCount,
|
|
|
|
|
'换盒数': exchangeBoxCount,
|
|
|
|
|
'换货内容': exchangeContent,
|
|
|
|
|
@ -2570,163 +2561,82 @@ export class OrderService {
|
|
|
|
|
* 导出数据为CSV格式
|
|
|
|
|
* @param {any[]} data 数据数组
|
|
|
|
|
* @param {Object} options 配置选项
|
|
|
|
|
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
|
|
|
|
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
|
|
|
|
* @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 });
|
|
|
|
|
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 filePath = path.join(downloadsDir, fileName);
|
|
|
|
|
const { type = 'string', fileName, writeFile = false } = options;
|
|
|
|
|
|
|
|
|
|
// 写入文件
|
|
|
|
|
fs.writeFileSync(filePath, csvContent, 'utf8');
|
|
|
|
|
// 生成表头
|
|
|
|
|
const headers = Object.keys(data[0]);
|
|
|
|
|
let csvContent = headers.join(',') + '\n';
|
|
|
|
|
|
|
|
|
|
console.log(`数据已成功导出至 ${filePath}`);
|
|
|
|
|
return filePath;
|
|
|
|
|
// 处理数据行
|
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据类型返回不同结果
|
|
|
|
|
if (type === 'buffer') {
|
|
|
|
|
return Buffer.from(csvContent, 'utf8');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return csvContent;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('导出CSV时出错:', error);
|
|
|
|
|
throw new Error(`导出CSV文件失败: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身)
|
|
|
|
|
* @param str 输入字符串
|
|
|
|
|
* @returns 删除后的字符串
|
|
|
|
|
*/
|
|
|
|
|
removeLastParenthesesContent(str: string): string {
|
|
|
|
|
if (!str || typeof str !== 'string') {
|
|
|
|
|
return str;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 辅助函数:删除指定位置的括号对及其内容
|
|
|
|
|
const removeParenthesesAt = (s: string, leftIndex: number): string => {
|
|
|
|
|
if (leftIndex === -1) return s;
|
|
|
|
|
|
|
|
|
|
let rightIndex = -1;
|
|
|
|
|
let parenCount = 0;
|
|
|
|
|
|
|
|
|
|
for (let i = leftIndex; i < s.length; i++) {
|
|
|
|
|
const char = s[i];
|
|
|
|
|
if (char === '(') {
|
|
|
|
|
parenCount++;
|
|
|
|
|
} else if (char === ')') {
|
|
|
|
|
parenCount--;
|
|
|
|
|
if (parenCount === 0) {
|
|
|
|
|
rightIndex = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rightIndex !== -1) {
|
|
|
|
|
return s.substring(0, leftIndex) + s.substring(rightIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 1. 处理每个分号前面的括号对
|
|
|
|
|
let result = str;
|
|
|
|
|
|
|
|
|
|
// 找出所有分号的位置
|
|
|
|
|
const semicolonIndices: number[] = [];
|
|
|
|
|
for (let i = 0; i < result.length; i++) {
|
|
|
|
|
if (result[i] === ';') {
|
|
|
|
|
semicolonIndices.push(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 从后向前处理每个分号,避免位置变化影响后续处理
|
|
|
|
|
for (let i = semicolonIndices.length - 1; i >= 0; i--) {
|
|
|
|
|
const semicolonIndex = semicolonIndices[i];
|
|
|
|
|
|
|
|
|
|
// 从分号位置向前查找最近的左括号
|
|
|
|
|
let lastLeftParenIndex = -1;
|
|
|
|
|
for (let j = semicolonIndex - 1; j >= 0; j--) {
|
|
|
|
|
if (result[j] === '(') {
|
|
|
|
|
lastLeftParenIndex = j;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果找到左括号,删除该括号对及其内容
|
|
|
|
|
if (lastLeftParenIndex !== -1) {
|
|
|
|
|
result = removeParenthesesAt(result, lastLeftParenIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 处理整个字符串的最后一个括号对
|
|
|
|
|
let lastLeftParenIndex = result.lastIndexOf('(');
|
|
|
|
|
if (lastLeftParenIndex !== -1) {
|
|
|
|
|
result = removeParenthesesAt(result, lastLeftParenIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|