feat(订单): 添加订单配送方式、费用项和优惠券项支持

扩展订单DTO和适配器以支持配送方式、费用项和优惠券项数据
实现Shopyy平台getAllOrders方法并添加分页并发处理
优化订单状态自动更新逻辑,支持Shopyy平台状态映射
This commit is contained in:
zhuotianyuan 2025-12-26 19:56:10 +08:00
parent 823967a268
commit 361b05117c
8 changed files with 251 additions and 15 deletions

View File

@ -222,6 +222,9 @@ export class ShopyyAdapter implements ISiteAdapter {
billing_full_address: formatAddress(billingObj), billing_full_address: formatAddress(billingObj),
shipping_full_address: formatAddress(shippingObj), shipping_full_address: formatAddress(shippingObj),
payment_method: item.payment_method, payment_method: item.payment_method,
shipping_lines: item.shipping_lines,
fee_lines: item.fee_lines,
coupon_lines: item.coupon_lines,
refunds: [], refunds: [],
date_created: date_created:
typeof item.created_at === 'number' typeof item.created_at === 'number'
@ -238,6 +241,8 @@ export class ShopyyAdapter implements ISiteAdapter {
}; };
} }
private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO {
// 处理多地址结构 // 处理多地址结构
const addresses = item.addresses || []; const addresses = item.addresses || [];
@ -391,12 +396,12 @@ export class ShopyyAdapter implements ISiteAdapter {
} }
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> { async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
// Shopyy getAllOrders 暂未实现 const data = await this.shopyyService.getAllOrders(this.site.id,params);
throw new Error('Shopyy getAllOrders 暂未实现'); return data.map(this.mapOrder.bind(this));
} }
async getOrder(id: string | number): Promise<UnifiedOrderDTO> { async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
const data = await this.shopyyService.getOrder(String(this.site.id), String(id)); const data = await this.shopyyService.getOrder(this.site.id, String(id));
return this.mapOrder(data); return this.mapOrder(data);
} }

View File

@ -417,6 +417,9 @@ export class WooCommerceAdapter implements ISiteAdapter {
payment_method: item.payment_method_title, payment_method: item.payment_method_title,
date_created: item.date_created, date_created: item.date_created,
date_modified: item.date_modified, date_modified: item.date_modified,
shipping_lines: item.shipping_lines,
fee_lines: item.fee_lines,
coupon_lines: item.coupon_lines,
raw: item, raw: item,
}; };
} }

View File

@ -246,6 +246,61 @@ export interface ShopyyOrder {
updated_at?: number | string; updated_at?: number | string;
date_updated?: string; date_updated?: string;
last_modified?: string; last_modified?: string;
// 配送方式
shipping_lines?: Array<UnifiedShippingLineDTO>;
// 费用项
fee_lines?: Array<UnifiedFeeLineDTO>;
// 优惠券项
coupon_lines?: Array<UnifiedCouponLineDTO>;
}
export class UnifiedShippingLineDTO {
// 配送方式DTO用于承载统一配送方式数据
id?: string | number;
method_title?: string;
method_id?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}
export class UnifiedFeeLineDTO {
// 费用项DTO用于承载统一费用项数据
id?: string | number;
name?: string;
tax_class?: string;
tax_status?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}
export class UnifiedCouponLineDTO {
// 优惠券项DTO用于承载统一优惠券项数据
id?: string | number;
code?: string;
discount?: string;
discount_tax?: string;
meta_data?: any[];
} }
// 客户类型 // 客户类型

View File

@ -303,6 +303,86 @@ export class UnifiedOrderDTO {
@ApiProperty({ description: '原始数据', type: 'object', required: false }) @ApiProperty({ description: '原始数据', type: 'object', required: false })
raw?: Record<string, any>; raw?: Record<string, any>;
@ApiProperty({ description: '配送方式', type: () => [UnifiedShippingLineDTO], required: false })
shipping_lines?: UnifiedShippingLineDTO[];
@ApiProperty({ description: '费用项', type: () => [UnifiedFeeLineDTO], required: false })
fee_lines?: UnifiedFeeLineDTO[];
@ApiProperty({ description: '优惠券项', type: () => [UnifiedCouponLineDTO], required: false })
coupon_lines?: UnifiedCouponLineDTO[];
}
export class UnifiedShippingLineDTO {
// 配送方式DTO用于承载统一配送方式数据
@ApiProperty({ description: '配送方式ID' })
id?: string | number;
@ApiProperty({ description: '配送方式名称' })
method_title?: string;
@ApiProperty({ description: '配送方式实例ID' })
method_id?: string;
@ApiProperty({ description: '配送方式金额' })
total?: string;
@ApiProperty({ description: '配送方式税额' })
total_tax?: string;
@ApiProperty({ description: '配送方式税额详情' })
taxes?: any[];
@ApiProperty({ description: '配送方式元数据' })
meta_data?: any[];
}
export class UnifiedFeeLineDTO {
// 费用项DTO用于承载统一费用项数据
@ApiProperty({ description: '费用项ID' })
id?: string | number;
@ApiProperty({ description: '费用项名称' })
name?: string;
@ApiProperty({ description: '税率类' })
tax_class?: string;
@ApiProperty({ description: '税率状态' })
tax_status?: string;
@ApiProperty({ description: '总金额' })
total?: string;
@ApiProperty({ description: '总税额' })
total_tax?: string;
@ApiProperty({ description: '税额详情' })
taxes?: any[];
@ApiProperty({ description: '元数据' })
meta_data?: any[];
}
export class UnifiedCouponLineDTO {
// 优惠券项DTO用于承载统一优惠券项数据
@ApiProperty({ description: '优惠券项ID' })
id?: string | number;
@ApiProperty({ description: '优惠券项代码' })
code?: string;
@ApiProperty({ description: '优惠券项折扣' })
discount?: string;
@ApiProperty({ description: '优惠券项税额' })
discount_tax?: string;
@ApiProperty({ description: '优惠券项元数据' })
meta_data?: any[];
} }
export class UnifiedCustomerDTO { export class UnifiedCustomerDTO {

View File

@ -16,7 +16,7 @@ import { ProductStockComponent } from './product_stock_component.entity';
import { ProductSiteSku } from './product_site_sku.entity'; import { ProductSiteSku } from './product_site_sku.entity';
import { Category } from './category.entity'; import { Category } from './category.entity';
@Entity('product_v2') @Entity('product')
export class Product { export class Product {
@ApiProperty({ @ApiProperty({
example: '1', example: '1',

View File

@ -2,7 +2,7 @@ import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 't
import { Area } from './area.entity'; import { Area } from './area.entity';
import { StockPoint } from './stock_point.entity'; import { StockPoint } from './stock_point.entity';
@Entity('site_v2') @Entity('site')
export class Site { export class Site {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;

View File

@ -30,6 +30,7 @@ import { ShipmentItem } from '../entity/shipment_item.entity';
import { UpdateStockDTO } from '../dto/stock.dto'; import { UpdateStockDTO } from '../dto/stock.dto';
import { StockService } from './stock.service'; import { StockService } from './stock.service';
import { OrderItemOriginal } from '../entity/order_item_original.entity'; import { OrderItemOriginal } from '../entity/order_item_original.entity';
import { SiteApiService } from './site-api.service';
@Provide() @Provide()
export class OrderService { export class OrderService {
@ -91,12 +92,16 @@ export class OrderService {
@Inject() @Inject()
siteService: SiteService; siteService: SiteService;
@Inject()
siteApiService: SiteApiService;
async syncOrders(siteId: number, params: Record<string, any> = {}) { async syncOrders(siteId: number, params: Record<string, any> = {}) {
// 调用 WooCommerce API 获取订单 // 调用 WooCommerce API 获取订单
const orders = await this.wpService.getOrders(siteId, params); const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params);
let successCount = 0; let successCount = 0;
let failureCount = 0; let failureCount = 0;
for (const order of orders) { for (const order of result) {
try { try {
await this.syncSingleOrder(siteId, order); await this.syncSingleOrder(siteId, order);
successCount++; successCount++;
@ -129,6 +134,13 @@ export class OrderService {
[OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold [OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold
[OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded
} }
shopyyOrderAutoNextStatusMap = {//订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消;
[100]: OrderStatus.PENDING, // 100 未完成 转为 pending
[110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
}
// 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换 // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
async autoUpdateOrderStatus(siteId: number, order: any) { async autoUpdateOrderStatus(siteId: number, order: any) {
console.log('更新订单状态', order) console.log('更新订单状态', order)
@ -170,6 +182,35 @@ export class OrderService {
if (existingOrder) { if (existingOrder) {
await this.orderModel.update({ id: existingOrder.id }, { orderStatus: this.mapOrderStatus(order.status) }); await this.orderModel.update({ id: existingOrder.id }, { orderStatus: this.mapOrderStatus(order.status) });
} }
const site = await this.siteService.get(siteId,true);
if(site.type === 'woocommerce'){
// 矫正数据库状态
await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, {
orderStatus: order.status,
})
}else if(site.type === 'shopyy'){
const originStatus = orderData.status;
orderData.status= this.shopyyOrderAutoNextStatusMap[originStatus];
// 根据currency_code获取对应货币符号
const currencySymbols: Record<string, string> = {
'EUR': '€',
'USD': '$',
'GBP': '£',
'JPY': '¥',
'AUD': 'A$',
'CAD': 'C$',
'CHF': 'CHF',
'CNY': '¥',
'HKD': 'HK$',
'NZD': 'NZ$',
'SGD': 'S$'
// 可以根据需要添加更多货币代码和符号
};
if (orderData.currency) {
const currencyCode = orderData.currency.toUpperCase();
orderData.currency_symbol = currencySymbols[currencyCode] || '$';
}
}
const externalOrderId = order.id; const externalOrderId = order.id;
if ( if (
existingOrder && existingOrder &&

View File

@ -7,7 +7,6 @@ import { Site } from '../entity/site.entity';
import { UnifiedReviewDTO } from '../dto/site-api.dto'; import { UnifiedReviewDTO } from '../dto/site-api.dto';
import { ShopyyReview } from '../dto/shopyy.dto'; import { ShopyyReview } from '../dto/shopyy.dto';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
/** /**
* ShopYY平台服务实现 * ShopYY平台服务实现
*/ */
@ -306,14 +305,66 @@ export class ShopyyService {
}); });
return { return {
items: response.data || [], items: response.data?.list || [],
total: response.meta?.pagination?.total || 0, total: response?.data?.paginate?.total || 0,
totalPages: response.meta?.pagination?.total_pages || 0, totalPages: response?.data?.paginate?.pageTotal || 0,
page: response.meta?.pagination?.current_page || page, page: response?.data?.paginate?.current || page,
per_page: response.meta?.pagination?.per_page || pageSize per_page: response?.data?.paginate?.pagesize || pageSize
}; };
} }
async getAllOrders(site: any | number, params: Record<string, any> = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
const firstPage = await this.getOrders(site, 1, 100);
const { items: firstPageItems, totalPages} = firstPage;
// const { page = 1, per_page = 100 } = params;
// 如果只有一页数据,直接返回
if (totalPages <= 1) {
return firstPageItems;
}
// 限制最大页数,避免过多的并发请求
const actualMaxPages = Math.min(totalPages, maxPages);
// 收集所有页面数据,从第二页开始
const allItems = [...firstPageItems];
let currentPage = 2;
// 使用并发限制,避免一次性发起过多请求
while (currentPage <= actualMaxPages) {
const batchPromises: Promise<any[]>[] = [];
const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1);
// 创建当前批次的并发请求
for (let i = 0; i < batchSize; i++) {
const page = currentPage + i;
const pagePromise = this.getOrders(site, page, 100)
.then(pageResult => pageResult.items)
.catch(error => {
console.error(`获取第 ${page} 页数据失败:`, error);
return []; // 如果某页获取失败,返回空数组,不影响整体结果
});
batchPromises.push(pagePromise);
}
// 等待当前批次完成
const batchResults = await Promise.all(batchPromises);
// 合并当前批次的数据
for (const pageItems of batchResults) {
allItems.push(...pageItems);
}
// 移动到下一批次
currentPage += batchSize;
}
return allItems;
}
/** /**
* ShopYY订单详情 * ShopYY订单详情
* @param siteId ID * @param siteId ID
@ -838,4 +889,5 @@ export class ShopyyService {
throw new Error(`删除ShopYY webhook失败: ${errorMessage}`); throw new Error(`删除ShopYY webhook失败: ${errorMessage}`);
} }
} }
} }