zksu
/
API
forked from yoone/API
1
0
Fork 0

feat(订单): 增强订单相关功能及数据模型

- 在订单实体中添加orderItems和orderSales字段
- 优化QueryOrderSalesDTO,增加排序字段和更多描述信息
- 重构saveOrderSale方法,使用产品属性自动设置品牌和强度
- 在订单查询中返回关联的orderItems和orderSales数据
- 添加getAttributesObject方法处理产品属性
This commit is contained in:
tikkhun 2026-01-10 11:15:24 +08:00
parent c9f9310a29
commit 17435b1381
6 changed files with 2974 additions and 264 deletions

View File

@ -98,13 +98,9 @@ export class QueryOrderDTO {
} }
export class QueryOrderSalesDTO { export class QueryOrderSalesDTO {
@ApiProperty() @ApiProperty({ description: '是否为原产品还是库存产品' })
@Rule(RuleType.bool().default(false)) @Rule(RuleType.bool().default(false))
isSource: boolean; isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' }) @ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number()) @Rule(RuleType.number())
@ -114,19 +110,31 @@ export class QueryOrderSalesDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
pageSize: number; pageSize: number;
@ApiProperty() @ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }',type: 'any', required: false })
@Rule(RuleType.object().allow(null))
orderBy?: Record<string, 'asc' | 'desc'>;
// filter
@ApiProperty({ description: '是否排除套餐' })
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ description: '站点ID' })
@Rule(RuleType.number()) @Rule(RuleType.number())
siteId: number; siteId: number;
@ApiProperty() @ApiProperty({ description: '名称' })
@Rule(RuleType.string()) @Rule(RuleType.string())
name: string; name: string;
@ApiProperty() @ApiProperty({ description: 'SKU' })
@Rule(RuleType.string())
sku: string;
@ApiProperty({ description: '开始日期' })
@Rule(RuleType.date()) @Rule(RuleType.date())
startDate: Date; startDate: Date;
@ApiProperty() @ApiProperty({ description: '结束日期' })
@Rule(RuleType.date()) @Rule(RuleType.date())
endDate: Date; endDate: Date;
} }

View File

@ -272,6 +272,14 @@ export class Order {
@Expose() @Expose()
updatedAt: Date; updatedAt: Date;
@ApiProperty({ type: 'json', nullable: true, description: '订单项列表' })
@Expose()
orderItems?: any[];
@ApiProperty({ type: 'json', nullable: true, description: '销售项列表' })
@Expose()
orderSales?: any[];
// 在插入或更新前处理用户代理字符串 // 在插入或更新前处理用户代理字符串
@BeforeInsert() @BeforeInsert()
@BeforeUpdate() @BeforeUpdate()

View File

@ -75,7 +75,7 @@ export class OrderSale {
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
@Column({ type: 'int', nullable: true }) @Column({ type: 'int', nullable: true })
@Expose() @Expose()
size: number | null; size: number | null; // 其实是 strength
@ApiProperty() @ApiProperty()
@Column({ default: false }) @Column({ default: false })
@ -98,24 +98,24 @@ export class OrderSale {
@Expose() @Expose()
updatedAt?: Date; updatedAt?: Date;
// === 自动计算逻辑 === // // === 自动计算逻辑 ===
@BeforeInsert() // @BeforeInsert()
@BeforeUpdate() // @BeforeUpdate()
setFlags() { // setFlags() {
if (!this.name) return; // if (!this.name) return;
const lower = this.name.toLowerCase(); // const lower = this.name.toLowerCase();
this.isYoone = lower.includes('yoone'); // this.isYoone = lower.includes('yoone');
this.isZex = lower.includes('zex'); // this.isZex = lower.includes('zex');
this.isYooneNew = this.isYoone && lower.includes('new'); // this.isYooneNew = this.isYoone && lower.includes('new');
let size: number | null = null; // let size: number | null = null;
const sizes = [3, 6, 9, 12, 15, 18]; // const sizes = [3, 6, 9, 12, 15, 18];
for (const s of sizes) { // for (const s of sizes) {
if (lower.includes(s.toString())) { // if (lower.includes(s.toString())) {
size = s; // size = s;
break; // break;
} // }
} // }
this.size = size; // this.size = size;
} // }
} }

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,8 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import { UnifiedOrderDTO } from '../dto/site-api.dto'; import { UnifiedOrderDTO } from '../dto/site-api.dto';
import { CustomerService } from './customer.service'; import { CustomerService } from './customer.service';
import { DictItem } from '../entity/dict_item.entity';
import { ProductService } from './product.service';
@Provide() @Provide()
export class OrderService { export class OrderService {
@ -110,7 +112,9 @@ export class OrderService {
@Logger() @Logger()
logger; // 注入 Logger 实例 logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/** /**
* *
* : * :
@ -146,8 +150,8 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId }, where: { externalOrderId: String(order.id), siteId: siteId },
}); });
if(!existingOrder){ if (!existingOrder) {
console.log("数据库中不存在",order.id, '订单状态:', order.status ) console.log("数据库中不存在", order.id, '订单状态:', order.status)
} }
// 同步单个订单 // 同步单个订单
await this.syncSingleOrder(siteId, order); await this.syncSingleOrder(siteId, order);
@ -208,8 +212,8 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId }, where: { externalOrderId: String(order.id), siteId: siteId },
}); });
if(!existingOrder){ if (!existingOrder) {
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status ) console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
} }
// 同步单个订单 // 同步单个订单
await this.syncSingleOrder(siteId, order, true); await this.syncSingleOrder(siteId, order, true);
@ -268,7 +272,7 @@ export class OrderService {
try { try {
const site = await this.siteService.get(siteId); const site = await this.siteService.get(siteId);
// 仅处理 WooCommerce 站点 // 仅处理 WooCommerce 站点
if(site.type !== 'woocommerce'){ if (site.type !== 'woocommerce') {
return return
} }
// 将订单状态同步到 WooCommerce,然后切换至下一状态 // 将订单状态同步到 WooCommerce,然后切换至下一状态
@ -278,6 +282,11 @@ export class OrderService {
console.error('更新订单状态失败,原因为:', error) console.error('更新订单状态失败,原因为:', error)
} }
} }
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
return await this.orderModel.findOne({
where: { externalOrderId: String(externalOrderId), siteId },
});
}
/** /**
* *
* : * :
@ -301,7 +310,7 @@ export class OrderService {
* @param order * @param order
* @param forceUpdate * @param forceUpdate
*/ */
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) {
// 从订单数据中解构出各个子项 // 从订单数据中解构出各个子项
let { let {
line_items, line_items,
@ -315,47 +324,27 @@ export class OrderService {
// console.log('同步进单个订单', order) // console.log('同步进单个订单', order)
// 如果订单状态为 AUTO_DRAFT,则跳过处理 // 如果订单状态为 AUTO_DRAFT,则跳过处理
if (order.status === OrderStatus.AUTO_DRAFT) { if (order.status === OrderStatus.AUTO_DRAFT) {
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
return; return;
} }
// 检查数据库中是否已存在该订单 // if(!order.is_editable && !forceUpdate){
const existingOrder = await this.orderModel.findOne({ // this.logger.debug('订单不可编辑,跳过处理', siteId, order.id)
where: { externalOrderId: order.id, siteId: siteId }, // return;
}); // }
// 自动更新订单状态(如果需要) // 自动转换远程订单的状态(如果需要)
await this.autoUpdateOrderStatus(siteId, order); await this.autoUpdateOrderStatus(siteId, order);
// 这里的 saveOrder 已经包括了创建订单和更新订单
if(existingOrder){ let orderRecord: Order = await this.saveOrder(siteId, orderData);
// 矫正数据库中的订单数据 // 如果订单从未完成变为完成状态,则更新库存
const updateData: any = { status: order.status };
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
updateData.orderStatus = this.mapOrderStatus(order.status);
}
// 更新
await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData);
// 更新 fulfillments 数据
await this.saveOrderFulfillments({
siteId,
orderId: existingOrder.id,
externalOrderId:order.id,
fulfillments: fulfillments,
});
}
const externalOrderId = order.id;
// 如果订单从未完成变为完成状态,则更新库存
if ( if (
existingOrder && orderRecord &&
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED && orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
orderData.status === OrderStatus.COMPLETED orderData.status === OrderStatus.COMPLETED
) { ) {
this.updateStock(existingOrder); await this.updateStock(orderRecord);
// 不再直接返回,继续执行后续的更新操作 // 不再直接返回,继续执行后续的更新操作
} }
// 如果订单不可编辑且不强制更新,则跳过处理 const externalOrderId = String(order.id);
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
return;
}
// 保存订单主数据
const orderRecord = await this.saveOrder(siteId, orderData);
const orderId = orderRecord.id; const orderId = orderRecord.id;
// 保存订单项 // 保存订单项
await this.saveOrderItems({ await this.saveOrderItems({
@ -459,13 +448,14 @@ export class OrderService {
* @param order * @param order
* @returns * @returns
*/ */
async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise<Order> { // 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items'|'refunds'>): Promise<Order> {
// 将外部订单ID转换为字符串 // 将外部订单ID转换为字符串
const externalOrderId = String(order.id) const externalOrderId = String(order.id)
delete 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({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId, siteId: siteId }, where: { externalOrderId, siteId: siteId },
@ -708,6 +698,8 @@ export class OrderService {
* *
* @param orderItem * @param orderItem
*/ */
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
async saveOrderSale(orderItem: OrderItem) { async saveOrderSale(orderItem: OrderItem) {
const currentOrderSale = await this.orderSaleModel.find({ const currentOrderSale = await this.orderSaleModel.find({
where: { where: {
@ -726,46 +718,44 @@ export class OrderService {
}); });
if (!product) return; if (!product) return;
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
const orderSales: OrderSale[] = []; return {
product: await this.productModel.findOne({
if (product.components && product.components.length > 0) {
for (const comp of product.components) {
const baseProduct = await this.productModel.findOne({
where: { sku: comp.sku }, where: { sku: comp.sku },
}); }),
if (baseProduct) { quantity: comp.quantity * orderItem.quantity,
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: baseProduct.id,
name: baseProduct.name,
quantity: comp.quantity * orderItem.quantity,
sku: comp.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
} }
} else { })) : [{ product, quantity: orderItem.quantity }];
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
const attrsObj = this.productService.getAttributesObject(product.attributes)
const orderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId, orderId: orderItem.orderId,
siteId: orderItem.siteId, siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId, externalOrderItemId: orderItem.externalOrderItemId,
productId: product.id, productId: componentDetail.product.id,
name: product.name, name: componentDetail.product.name,
quantity: orderItem.quantity, quantity: componentDetail.quantity * orderItem.quantity,
sku: product.sku, sku: componentDetail.product.sku,
isPackage: orderItem.name.toLowerCase().includes('package'), isPackage: componentDetail.product.type === 'bundle',
isYoone: attrsObj?.['brand']?.name === 'yoone',
isZyn: attrsObj?.['brand']?.name === 'zyn',
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
size: this.extractNumberFromString(attrsObj?.['strength']?.name) || null,
}); });
orderSales.push(orderSaleItem); return orderSale
} })
if (orderSales.length > 0) { if (orderSales.length > 0) {
await this.orderSaleModel.save(orderSales); await this.orderSaleModel.save(orderSales);
} }
} }
extractNumberFromString(str: string): number {
if (!str) return 0;
const num = parseInt(str, 10);
return isNaN(num) ? 0 : num;
}
/** /**
* 退 * 退
@ -1192,7 +1182,53 @@ export class OrderService {
) END ) END
), ),
JSON_ARRAY() JSON_ARRAY()
) as fulfillments ) as fulfillments,
(
SELECT COALESCE(
JSON_ARRAYAGG(
JSON_OBJECT(
'id', oi.id,
'name', oi.name,
'orderId', oi.orderId,
'siteId', oi.siteId,
'externalOrderId', oi.externalOrderId,
'externalOrderItemId', oi.externalOrderItemId,
'externalProductId', oi.externalProductId,
'externalVariationId', oi.externalVariationId,
'quantity', oi.quantity,
'subtotal', oi.subtotal,
'subtotal_tax', oi.subtotal_tax,
'total', oi.total,
'total_tax', oi.total_tax,
'sku', oi.sku,
'price', oi.price
)
),
JSON_ARRAY()
)
FROM order_item oi
WHERE oi.orderId = o.id
) AS orderItems,
(
SELECT COALESCE(
JSON_ARRAYAGG(
JSON_OBJECT(
'id', os.id,
'orderId', os.orderId,
'siteId', os.siteId,
'externalOrderItemId', os.externalOrderItemId,
'productId', os.productId,
'name', os.name,
'sku', os.sku,
'quantity', os.quantity,
'isPackage', os.isPackage
)
),
JSON_ARRAY()
)
FROM order_sale os
WHERE os.orderId = o.id
) AS orderSales
FROM \`order\` o FROM \`order\` o
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
@ -1426,7 +1462,7 @@ export class OrderService {
* @param params * @param params
* @returns * @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 nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); 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'); const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
@ -1642,11 +1678,12 @@ export class OrderService {
* @returns * @returns
*/ */
async getOrderItems({ async getOrderItems({
current,
pageSize,
siteId, siteId,
startDate, startDate,
endDate, endDate,
current, sku,
pageSize,
name, name,
}: QueryOrderSalesDTO) { }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
@ -1904,8 +1941,8 @@ export class OrderService {
const key = it?.externalSubscriptionId const key = it?.externalSubscriptionId
? `sub:${it.externalSubscriptionId}` ? `sub:${it.externalSubscriptionId}`
: it?.externalOrderId : it?.externalOrderId
? `ord:${it.externalOrderId}` ? `ord:${it.externalOrderId}`
: `id:${it?.id}`; : `id:${it?.id}`;
if (!seen.has(key)) { if (!seen.has(key)) {
seen.add(key); seen.add(key);
relatedList.push(it); relatedList.push(it);
@ -2199,14 +2236,14 @@ export class OrderService {
for (const sale of sales) { for (const sale of sales) {
const product = await productRepo.findOne({ where: { sku: sale.sku } }); const product = await productRepo.findOne({ where: { sku: sale.sku } });
const saleItem = { const saleItem = {
orderId: order.id, orderId: order.id,
siteId: order.siteId, siteId: order.siteId,
externalOrderItemId: '-1', externalOrderItemId: '-1',
productId: product.id, productId: product.id,
name: product.name, name: product.name,
sku: sale.sku, sku: sale.sku,
quantity: sale.quantity, quantity: sale.quantity,
}; };
await orderSaleRepo.save(saleItem); await orderSaleRepo.save(saleItem);
} }
}); });
@ -2339,83 +2376,83 @@ export class OrderService {
//换货功能更新OrderSale和Orderitem数据 //换货功能更新OrderSale和Orderitem数据
async updateExchangeOrder(orderId: number, data: any) { async updateExchangeOrder(orderId: number, data: any) {
throw new Error('暂未实现') throw new Error('暂未实现')
// try { // try {
// const dataSource = this.dataSourceManager.getDataSource('default'); // const dataSource = this.dataSourceManager.getDataSource('default');
// let transactionError = undefined; // let transactionError = undefined;
// await dataSource.transaction(async manager => { // await dataSource.transaction(async manager => {
// const orderRepo = manager.getRepository(Order); // const orderRepo = manager.getRepository(Order);
// const orderSaleRepo = manager.getRepository(OrderSale); // const orderSaleRepo = manager.getRepository(OrderSale);
// const orderItemRepo = manager.getRepository(OrderItem); // const orderItemRepo = manager.getRepository(OrderItem);
// const productRepo = manager.getRepository(ProductV2); // const productRepo = manager.getRepository(ProductV2);
// const order = await orderRepo.findOneBy({ id: orderId }); // const order = await orderRepo.findOneBy({ id: orderId });
// let product: ProductV2; // let product: ProductV2;
// await orderSaleRepo.delete({ orderId }); // await orderSaleRepo.delete({ orderId });
// await orderItemRepo.delete({ orderId }); // await orderItemRepo.delete({ orderId });
// for (const sale of data['sales']) { // for (const sale of data['sales']) {
// product = await productRepo.findOneBy({ sku: sale['sku'] }); // product = await productRepo.findOneBy({ sku: sale['sku'] });
// await orderSaleRepo.save({ // await orderSaleRepo.save({
// orderId, // orderId,
// siteId: order.siteId, // siteId: order.siteId,
// productId: product.id, // productId: product.id,
// name: product.name, // name: product.name,
// sku: sale['sku'], // sku: sale['sku'],
// quantity: sale['quantity'], // quantity: sale['quantity'],
// }); // });
// }; // };
// for (const item of data['items']) { // for (const item of data['items']) {
// product = await productRepo.findOneBy({ sku: item['sku'] }); // product = await productRepo.findOneBy({ sku: item['sku'] });
// await orderItemRepo.save({ // await orderItemRepo.save({
// orderId, // orderId,
// siteId: order.siteId, // siteId: order.siteId,
// productId: product.id, // productId: product.id,
// name: product.name, // name: product.name,
// externalOrderId: order.externalOrderId, // externalOrderId: order.externalOrderId,
// externalProductId: product.externalProductId, // externalProductId: product.externalProductId,
// sku: item['sku'], // sku: item['sku'],
// quantity: item['quantity'], // quantity: item['quantity'],
// }); // });
// }; // };
// //将是否换货状态改为true // //将是否换货状态改为true
// await orderRepo.update( // await orderRepo.update(
// order.id // order.id
// , { // , {
// is_exchange: true // is_exchange: true
// }); // });
// //查询这个用户换过多少次货 // //查询这个用户换过多少次货
// const counts = await orderRepo.countBy({ // const counts = await orderRepo.countBy({
// is_editable: true, // is_editable: true,
// customer_email: order.customer_email, // customer_email: order.customer_email,
// }); // });
// //批量更新当前用户换货次数 // //批量更新当前用户换货次数
// await orderRepo.update({ // await orderRepo.update({
// customer_email: order.customer_email // customer_email: order.customer_email
// }, { // }, {
// exchange_frequency: counts // exchange_frequency: counts
// }); // });
// }).catch(error => { // }).catch(error => {
// transactionError = error; // transactionError = error;
// }); // });
// if (transactionError !== undefined) { // if (transactionError !== undefined) {
// throw new Error(`更新物流信息错误:${transactionError.message}`); // throw new Error(`更新物流信息错误:${transactionError.message}`);
// } // }
// return true; // return true;
// } catch (error) { // } catch (error) {
// throw new Error(`更新发货产品失败:${error.message}`); // throw new Error(`更新发货产品失败:${error.message}`);
// } // }
} }
/** /**
@ -2461,17 +2498,17 @@ export class OrderService {
} }
try { try {
// 过滤掉NaN和非数字值只保留有效的数字ID // 过滤掉NaN和非数字值只保留有效的数字ID
const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0); const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0);
const dataSource = this.dataSourceManager.getDataSource('default'); const dataSource = this.dataSourceManager.getDataSource('default');
// 优化事务使用 // 优化事务使用
return await dataSource.transaction(async manager => { return await dataSource.transaction(async manager => {
// 准备查询条件 // 准备查询条件
const whereCondition: any = {}; const whereCondition: any = {};
if(validIds.length > 0){ if (validIds.length > 0) {
whereCondition.id = In(validIds); whereCondition.id = In(validIds);
} }
@ -2487,7 +2524,7 @@ export class OrderService {
// 获取所有订单ID // 获取所有订单ID
const orderIds = orders.map(order => order.id); const orderIds = orders.map(order => order.id);
// 获取所有订单项 // 获取所有订单项
const orderItems = await manager.getRepository(OrderItem).find({ const orderItems = await manager.getRepository(OrderItem).find({
where: { where: {
@ -2508,13 +2545,13 @@ export class OrderService {
const exportDataList: ExportData[] = orders.map(order => { const exportDataList: ExportData[] = orders.map(order => {
// 获取订单的订单项 // 获取订单的订单项
const items = orderItemsByOrderId[order.id] || []; const items = orderItemsByOrderId[order.id] || [];
// 计算总盒数 // 计算总盒数
const boxCount = items.reduce((total, item) => total + item.quantity, 0); const boxCount = items.reduce((total, item) => total + item.quantity, 0);
// 构建订单内容 // 构建订单内容
const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; '); const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
// 构建姓名地址 // 构建姓名地址
const shipping = order.shipping; const shipping = order.shipping;
const billing = order.billing; const billing = order.billing;
@ -2528,10 +2565,10 @@ export class OrderService {
const postcode = shipping?.postcode || billing?.postcode || ''; const postcode = shipping?.postcode || billing?.postcode || '';
const country = shipping?.country || billing?.country || ''; const country = shipping?.country || billing?.country || '';
const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`;
// 获取电话号码 // 获取电话号码
const phone = shipping?.phone || billing?.phone || ''; const phone = shipping?.phone || billing?.phone || '';
// 获取快递号 // 获取快递号
const trackingNumber = order.shipment?.tracking_id || ''; const trackingNumber = order.shipment?.tracking_id || '';
@ -2567,84 +2604,84 @@ export class OrderService {
* CSV格式 * CSV格式
* @param {any[]} data * @param {any[]} data
* @param {Object} options * @param {Object} options
* @param {string} [options.type='string'] :'string' | 'buffer' * @param {string} [options.type='string'] :'string' | 'buffer'
* @param {string} [options.fileName] (使) * @param {string} [options.fileName] (使)
* @param {boolean} [options.writeFile=false] * @param {boolean} [options.writeFile=false]
* @returns {string|Buffer} type返回字符串或Buffer * @returns {string|Buffer} type返回字符串或Buffer
*/ */
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> { async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
try { try {
// 检查数据是否为空 // 检查数据是否为空
if (!data || data.length === 0) { if (!data || data.length === 0) {
throw new Error('导出数据不能为空'); throw new Error('导出数据不能为空');
}
const { type = 'string', fileName, writeFile = false } = options;
// 生成表头
const headers = Object.keys(data[0]);
let csvContent = headers.join(',') + '\n';
// 处理数据行
data.forEach(item => {
const row = headers.map(key => {
const value = item[key as keyof any];
// 处理特殊字符
if (typeof value === 'string') {
// 转义双引号,将"替换为""
const escapedValue = value.replace(/"/g, '""');
// 如果包含逗号或换行符,需要用双引号包裹
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
return `"${escapedValue}"`;
}
return escapedValue;
}
// 处理日期类型
if (value instanceof Date) {
return value.toISOString();
}
// 处理undefined和null
if (value === undefined || value === null) {
return '';
}
return String(value);
}).join(',');
csvContent += row + '\n';
});
// 如果需要写入文件
if (writeFile && fileName) {
// 获取当前用户目录
const userHomeDir = os.homedir();
// 构建目标路径(下载目录)
const downloadsDir = path.join(userHomeDir, 'Downloads');
// 确保下载目录存在
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
} }
const filePath = path.join(downloadsDir, fileName); 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}`);
} }
}

View File

@ -1461,7 +1461,7 @@ export class ProductService {
return { return {
sku, sku,
name: val(rec.name), name: val(rec.name),
nameCn: val(rec.nameCn), nameCn: val(rec.nameCn),
description: val(rec.description), description: val(rec.description),
price: num(rec.price), price: num(rec.price),
promotionPrice: num(rec.promotionPrice), promotionPrice: num(rec.promotionPrice),
@ -1536,7 +1536,13 @@ export class ProductService {
return dto; return dto;
} }
getAttributesObject(attributes:DictItem[]){
const obj:any = {}
attributes.forEach(attr=>{
obj[attr.dict.name] = attr
})
return obj
}
// 将单个产品转换为 CSV 行数组 // 将单个产品转换为 CSV 行数组
transformProductToCsvRow( transformProductToCsvRow(
p: Product, p: Product,