diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 297c4e7..33d1e61 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -15,9 +15,11 @@ import { CreateWebhookDTO, UpdateWebhookDTO, UnifiedAddressDTO, - UnifiedShippingLineDTO + UnifiedShippingLineDTO, + OrderFulfillmentStatus, + FulfillmentDTO } from '../dto/site-api.dto'; -import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; +import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { ShopyyCustomer, ShopyyOrder, @@ -29,7 +31,17 @@ import { OrderStatus, } from '../enums/base.enum'; export class ShopyyAdapter implements ISiteAdapter { - constructor(private site: any, private shopyyService: ShopyyService) { + shopyyFinancialStatusMap= { + '200': '待支付', + '210': "支付中", + '220':"部分支付", + '230':"已支付", + '240':"支付失败", + '250':"部分退款", + '260':"已退款", + '290':"已取消", + } + constructor(private site: any, private shopyyService: ShopyyService) { this.mapCustomer = this.mapCustomer.bind(this); this.mapProduct = this.mapProduct.bind(this); this.mapVariation = this.mapVariation.bind(this); @@ -135,6 +147,8 @@ export class ShopyyAdapter implements ISiteAdapter { shopyyOrderStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; [100]: OrderStatus.PENDING, // 100 未完成 转为 pending [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing + // 已发货 + [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled } @@ -187,15 +201,15 @@ export class ShopyyAdapter implements ISiteAdapter { item.shipping_country || '', }; - - // 构建送货地址对象 + + // 构建送货地址对象 const shipping_lines: UnifiedShippingLineDTO[] = [ { id: item.fulfillments?.[0]?.id || 0, method_title: item.payment_method || '', method_id: item.payment_method || '', total: Number(item.current_shipping_price).toExponential(2) || '0.00', - total_tax: '0.00', + total_tax: '0.00', taxes: [], meta_data: [], }, @@ -229,28 +243,32 @@ export class ShopyyAdapter implements ISiteAdapter { price: String(p.price ?? ''), }) ); - - const currencySymbols: Record = { - 'EUR': '€', - 'USD': '$', - 'GBP': '£', - 'JPY': '¥', - 'AUD': 'A$', - 'CAD': 'C$', - 'CHF': 'CHF', - 'CNY': '¥', - 'HKD': 'HK$', - 'NZD': 'NZ$', - 'SGD': 'S$' - // 可以根据需要添加更多货币代码和符号 - }; - // 映射订单状态,如果不存在则默认 pending - const status = this.shopyyOrderStatusMap[item.status?? item.order_status] || OrderStatus.PENDING; + // 货币符号 + const currencySymbols: Record = { + 'EUR': '€', + 'USD': '$', + 'GBP': '£', + 'JPY': '¥', + 'AUD': 'A$', + 'CAD': 'C$', + 'CHF': 'CHF', + 'CNY': '¥', + 'HKD': 'HK$', + 'NZD': 'NZ$', + 'SGD': 'S$' + // 可以根据需要添加更多货币代码和符号 + }; + // 映射订单状态,如果不存在则默认 pending + const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING; + const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status] + // 发货状态 + const fulfillment_status = this.shopyyFulfillmentStatusMap[item.fulfillment_status]; return { id: item.id || item.order_id, number: item.order_number || item.order_sn, status, + financial_status: finalcial_status, currency: item.currency_code || item.currency, total: String(item.total_price ?? item.total_amount ?? ''), customer_id: item.customer_id || item.user_id, @@ -271,11 +289,11 @@ export class ShopyyAdapter implements ISiteAdapter { customer_ip_address: item.ip || '', device_type: item.source_device || '', utm_source: item.utm_source || '', - source_type: 'shopyy', + source_type: 'shopyy', // FIXME date_paid: typeof item.pay_at === 'number' ? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString() : null, - + refunds: [], currency_symbol: (currencySymbols[item.currency] || '$') || '', date_created: @@ -289,10 +307,31 @@ export class ShopyyAdapter implements ISiteAdapter { : item.date_updated || item.last_modified || (typeof item.updated_at === 'string' ? item.updated_at : ''), + fulfillment_status, + fulfillments: item.fulfillments?.map?.((f) => ({ + id: f.id, + tracking_number: f.tracking_number || '', + shipping_provider: f.tracking_company || '', + shipping_method: f.tracking_company || '', + date_created: typeof f.created_at === 'number' + ? new Date(f.created_at * 1000).toISOString() + : f.created_at || '', + // status: f.payment_tracking_status + })) || [], raw: item, }; } - + shopyyFulfillmentStatusMap = { + // 未发货 + '300': OrderFulfillmentStatus.PENDING, + // 部分发货 + '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, + // 已发货 + '320': OrderFulfillmentStatus.FULFILLED, + // 已取消 + '330': OrderFulfillmentStatus.CANCELLED, + // 确认发货 + } private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { // 处理多地址结构 @@ -363,11 +402,11 @@ export class ShopyyAdapter implements ISiteAdapter { 'products/list', params ); - const { items=[], total, totalPages, page, per_page } = response; + const { items = [], total, totalPages, page, per_page } = response; const finalItems = items.map((item) => ({ - ...item, - permalink: `${this.site.websiteUrl}/products/${item.handle}`, - })).map(this.mapProduct.bind(this)) + ...item, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, + })).map(this.mapProduct.bind(this)) return { items: finalItems as UnifiedProductDTO[], total, @@ -427,22 +466,22 @@ export class ShopyyAdapter implements ISiteAdapter { ): Promise { return await this.shopyyService.batchProcessProducts(this.site, data); } - mapUnifiedOrderQueryToShopyyQuery(params:UnifiedSearchParamsDTO ){ - const {where={} as any, ...restParams} = params|| {} + mapUnifiedOrderQueryToShopyyQuery(params: UnifiedSearchParamsDTO) { + const { where = {} as any, ...restParams } = params || {} const statusMap = { 'pending': '100', // 100 未完成 'processing': '110', // 110 待处理 'completed': "180", // 180 已完成(确认收货) 'cancelled': '190', // 190 取消 } - const normalizedParams:any= { - ...restParams, + const normalizedParams: any = { + ...restParams, } - if(where){ + if (where) { normalizedParams.where = { ...where, } - if(where.status){ + if (where.status) { normalizedParams.where.status = statusMap[where.status]; } } @@ -469,9 +508,9 @@ export class ShopyyAdapter implements ISiteAdapter { } async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - const data = await this.shopyyService.getAllOrders(this.site.id,params); + const data = await this.shopyyService.getAllOrders(this.site.id, params); return data.map(this.mapOrder.bind(this)); - } + } async getOrder(id: string | number): Promise { const data = await this.shopyyService.getOrder(this.site.id, String(id)); @@ -543,7 +582,7 @@ export class ShopyyAdapter implements ISiteAdapter { // 调用 ShopyyService 的取消履行方法 const cancelShipData = { order_id: String(orderId), - fullfillment_id: data.shipment_id || '' + fulfillment_id: data.shipment_id || '' }; const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); @@ -574,18 +613,13 @@ export class ShopyyAdapter implements ISiteAdapter { * @param data 履行数据 * @returns 创建结果 */ - async createOrderFulfillment(orderId: string | number, data: { - tracking_number: string; - tracking_provider: string; - date_shipped?: string; - status_shipped?: string; - }): Promise { + async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { // 调用 Shopyy Service 的 createFulfillment 方法 const fulfillmentData = { tracking_number: data.tracking_number, - carrier_code: data.tracking_provider, - carrier_name: data.tracking_provider, - shipping_method: data.status_shipped || 'standard' + carrier_code: data.shipping_provider, + carrier_name: data.shipping_provider, + shipping_method: data.shipping_method || 'standard' }; return await this.shopyyService.createFulfillment(this.site, String(orderId), fulfillmentData); @@ -702,9 +736,9 @@ export class ShopyyAdapter implements ISiteAdapter { content: review.comment || review.content || '', rating: Number(review.score || review.rating || 0), status: String(review.status || 'approved'), - date_created: - typeof review.created_at === 'number' - ? new Date(review.created_at * 1000).toISOString() + date_created: + typeof review.created_at === 'number' + ? new Date(review.created_at * 1000).toISOString() : String(review.created_at || review.date_added || '') }; } @@ -751,7 +785,7 @@ export class ShopyyAdapter implements ISiteAdapter { id: item.id, name: item.webhook_name || `Webhook-${item.id}`, topic: item.event_code || '', - delivery_url: item.url|| '', + delivery_url: item.url || '', status: 'active', }; } @@ -791,12 +825,12 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteWebhook(this.site, id); } - async getLinks(): Promise> { + async getLinks(): Promise> { // ShopYY站点的管理后台链接通常基于apiUrl构建 const url = this.site.websiteUrl // 提取基础域名,去掉可能的路径部分 const baseUrl = url.replace(/\/api\/.*$/i, ''); - + const links = [ { title: '访问网站', url: baseUrl }, { title: '管理后台', url: `${baseUrl}/admin/` }, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index b39eff8..badf467 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -454,12 +454,13 @@ export class WooCommerceAdapter implements ISiteAdapter { // 包含账单地址与收货地址以及创建与更新时间 // 映射物流追踪信息,将后端格式转换为前端期望的格式 - const tracking = (item.trackings || []).map((track: any) => ({ - order_id: String(item.id), - tracking_provider: track.tracking_provider || '', + const fulfillments = (item.fulfillments || []).map((track: any) => ({ tracking_number: track.tracking_number || '', - date_shipped: track.date_shipped || '', - status_shipped: track.status_shipped || '', + shipping_provider: track.shipping_provider || '', + shipping_method: track.shipping_method || '', + status: track.status || '', + date_created: track.date_created || '', + items: track.items || [], })); return { @@ -496,7 +497,7 @@ export class WooCommerceAdapter implements ISiteAdapter { shipping_lines: item.shipping_lines, fee_lines: item.fee_lines, coupon_lines: item.coupon_lines, - tracking: tracking, + fulfillments, raw: item, }; } @@ -687,29 +688,29 @@ export class WooCommerceAdapter implements ISiteAdapter { await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); // 并行获取所有订单的履行信息 - const ordersWithTracking = await Promise.all( + const ordersWithFulfillments = await Promise.all( items.map(async (order: any) => { try { // 获取订单的履行信息 - const trackings = await this.getOrderFulfillments(order.id); + const fulfillments = await this.getOrderFulfillments(order.id); // 将履行信息添加到订单对象中 return { ...order, - trackings: trackings || [] + fulfillments: fulfillments || [] }; } catch (error) { // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 console.error(`获取订单 ${order.id} 的履行信息失败:`, error); return { ...order, - trackings: [] + fulfillments: [] }; } }) ); return { - items: ordersWithTracking.map(this.mapOrder), + items: ordersWithFulfillments.map(this.mapOrder), total, totalPages, page, @@ -1016,21 +1017,34 @@ export class WooCommerceAdapter implements ISiteAdapter { async createOrderFulfillment(orderId: string | number, data: { tracking_number: string; - tracking_provider: string; - date_shipped?: string; - status_shipped?: string; + shipping_provider: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; }): Promise { const shipmentData: any = { - tracking_provider: data.tracking_provider, + shipping_provider: data.shipping_provider, tracking_number: data.tracking_number, }; - if (data.date_shipped) { - shipmentData.date_shipped = data.date_shipped; + if (data.shipping_method) { + shipmentData.shipping_method = data.shipping_method; } - if (data.status_shipped) { - shipmentData.status_shipped = data.status_shipped; + if (data.status) { + shipmentData.status = data.status; + } + + if (data.date_created) { + shipmentData.date_created = data.date_created; + } + + if (data.items) { + shipmentData.items = data.items; } const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData); @@ -1039,9 +1053,14 @@ export class WooCommerceAdapter implements ISiteAdapter { async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { tracking_number?: string; - tracking_provider?: string; - date_shipped?: string; - status_shipped?: string; + shipping_provider?: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; }): Promise { return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); } diff --git a/src/config/config.default.ts b/src/config/config.default.ts index fe9e8bb..2d6fc59 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -16,6 +16,7 @@ import { OrderRefundItem } from '../entity/order_refund_item.entity'; import { OrderSale } from '../entity/order_sale.entity'; import { OrderItemOriginal } from '../entity/order_item_original.entity'; import { OrderShipping } from '../entity/order_shipping.entity'; +import { OrderFulfillment } from '../entity/order_fulfillment.entity'; import { Service } from '../entity/service.entity'; import { ShippingAddress } from '../entity/shipping_address.entity'; import { OrderNote } from '../entity/order_note.entity'; @@ -67,6 +68,7 @@ export default { ShipmentItem, Shipment, OrderShipping, + OrderFulfillment, Service, ShippingAddress, OrderNote, diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts index 842fcea..04ecf2f 100644 --- a/src/controller/order.controller.ts +++ b/src/controller/order.controller.ts @@ -9,6 +9,7 @@ import { Put, Query, } from '@midwayjs/core'; +import { SyncOperationResult } from '../dto/api.dto'; import { ApiOkResponse } from '@midwayjs/swagger'; import { BooleanRes, @@ -47,7 +48,7 @@ export class OrderController { } @ApiOkResponse({ - type: BooleanRes, + type: SyncOperationResult, }) @Post('/syncOrder/:siteId/order/:orderId') async syncOrderById( diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index f0c96c3..9f69b87 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -1017,7 +1017,7 @@ export class SiteApiController { body.orders.map(order => adapter.createOrderFulfillment(order.order_id, { tracking_number: order.tracking_number, - tracking_provider: order.shipping_provider, + shipping_provider: order.shipping_provider, items: order.items, }).catch(error => ({ order_id: order.order_id, @@ -1081,9 +1081,10 @@ export class SiteApiController { const adapter = await this.siteApiService.getAdapter(siteId); const data = await adapter.createOrderFulfillment(orderId, { tracking_number: body.tracking_number, - tracking_provider: body.tracking_provider, - date_shipped: body.date_shipped, - status_shipped: body.status_shipped, + shipping_provider: body.shipping_provider, + shipping_method: body.shipping_method, + status: body.status, + date_created: body.date_created, }); this.logger.info(`[Site API] 创建订单履约跟踪信息成功, siteId: ${siteId}, orderId: ${orderId}`); return successResponse(data); @@ -1107,9 +1108,10 @@ export class SiteApiController { const adapter = await this.siteApiService.getAdapter(siteId); const data = await adapter.updateOrderFulfillment(orderId, fulfillmentId, { tracking_number: body.tracking_number, - tracking_provider: body.tracking_provider, - date_shipped: body.date_shipped, - status_shipped: body.status_shipped, + shipping_provider: body.shipping_provider, + shipping_method: body.shipping_method, + status: body.status, + date_created: body.date_created, }); this.logger.info(`[Site API] 更新订单履约跟踪信息成功, siteId: ${siteId}, orderId: ${orderId}, fulfillmentId: ${fulfillmentId}`); return successResponse(data); diff --git a/src/dto/api.dto.ts b/src/dto/api.dto.ts index 6af71f4..558048b 100644 --- a/src/dto/api.dto.ts +++ b/src/dto/api.dto.ts @@ -81,7 +81,14 @@ export interface BatchOperationResult { /** * 同步操作结果接口 */ -export interface SyncOperationResult extends BatchOperationResult { +export class SyncOperationResult implements BatchOperationResult { + total: number; + processed: number; + created?: number; + updated?: number; + deleted?: number; + skipped?: number; + errors: BatchErrorItem[]; // 同步成功数量 synced: number; } diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index d48f2bb..2bcbca7 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -187,18 +187,39 @@ export interface ShopyyOrder { transaction_no?: string; }>; fulfillments?: Array<{ + // 物流回传状态 payment_tracking_status?: number; + // 备注 note?: string; + // 更新时间 updated_at?: number; + // 追踪接口编号 courier_code?: string; + // 物流公司 id courier_id?: number; + // 创建时间 created_at?: number; - tracking_number?: string; id?: number; + // 物流单号 + tracking_number?: string; + // 物流公司名称 tracking_company?: string; + // 物流回传结果 payment_tracking_result?: string; + // 物流回传时间 payment_tracking_at?: number; - products?: Array<{ order_product_id?: number; quantity?: number; updated_at?: number; created_at?: number; id?: number }>; + // 商品 + products?: Array<{ + // 订单商品表 id + order_product_id?: number; + // 数量 + quantity?: number; + // 更新时间 + updated_at?: number; + // 创建时间 + created_at?: number; + // 发货商品表 id + id?: number }>; }>; shipping_zone_plans?: Array<{ shipping_price?: number | string; @@ -425,9 +446,9 @@ export class ShopyPartFulfillmentDTO { // https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse export class ShopyyCancelFulfillmentDTO { order_id: string; - fullfillment_id: string; + fulfillment_id: string; } export class ShopyyBatchFulfillmentItemDTO { - fullfillments: ShopyPartFulfillmentDTO[] + fulfillments: ShopyPartFulfillmentDTO[] } diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index a34751c..a93f125 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -5,6 +5,18 @@ import { // export class UnifiedOrderWhere{ // [] // } +export enum OrderFulfillmentStatus { + // 未发货 + PENDING, + // 部分发货 + PARTIALLY_FULFILLED, + // 已发货 + FULFILLED, + // 已取消 + CANCELLED, + // 确认发货 + CONFIRMED, +} export class UnifiedTagDTO { // 标签DTO用于承载统一标签数据 @ApiProperty({ description: '标签ID' }) @@ -21,23 +33,6 @@ export class UnifiedCategoryDTO { @ApiProperty({ description: '分类名称' }) name: string; } -// 订单跟踪号 -export class UnifiedOrderTrackingDTO { - @ApiProperty({ description: '订单ID' }) - order_id: string; - - @ApiProperty({ description: '快递公司' }) - tracking_provider: string; - - @ApiProperty({ description: '运单跟踪号' }) - tracking_number: string; - - @ApiProperty({ description: '发货日期' }) - date_shipped: string; - - @ApiProperty({ description: '发货状态' }) - status_shipped: string; -} export class UnifiedImageDTO { // 图片DTO用于承载统一图片数据 @@ -320,6 +315,9 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '订单状态' }) status: string; + @ApiProperty({ description: '财务状态',nullable: true }) + financial_status?: string; + @ApiProperty({ description: '货币' }) currency: string; @@ -385,9 +383,6 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '优惠券项', type: () => [UnifiedCouponLineDTO], required: false }) coupon_lines?: UnifiedCouponLineDTO[]; - - @ApiProperty({ description: '物流追踪信息', type: () => [UnifiedOrderTrackingDTO], required: false }) - tracking?: UnifiedOrderTrackingDTO[]; @ApiProperty({ description: '支付时间', required: false }) date_paid?: string | null; @@ -403,6 +398,12 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '来源类型', required: false }) source_type?: string; + + @ApiProperty({ description: '订单状态', required: false }) + fulfillment_status?: OrderFulfillmentStatus; + // 物流信息 + @ApiProperty({ description: '物流信息', type: () => [FulfillmentDTO], required: false }) + fulfillments?: FulfillmentDTO[]; } export class UnifiedShippingLineDTO { @@ -798,6 +799,12 @@ export class FulfillmentDTO { @ApiProperty({ description: '发货方式', required: false }) shipping_method?: string; + @ApiProperty({ description: '状态', required: false }) + status?: string; + + @ApiProperty({ description: '创建时间', required: false }) + date_created?: string; + @ApiProperty({ description: '发货商品项', type: () => [FulfillmentItemDTO], required: false }) items?: FulfillmentItemDTO[]; } @@ -957,3 +964,24 @@ export class BatchFulfillmentsDTO { @ApiProperty({ description: '批量发货订单列表', type: () => [BatchFulfillmentItemDTO] }) orders: BatchFulfillmentItemDTO[]; } + +// 订单跟踪号 +export class UnifiedOrderTrackingDTO { + @ApiProperty({ description: '订单ID' }) + order_id: string; + + @ApiProperty({ description: '物流单号' }) + tracking_number: string; + + @ApiProperty({ description: '物流公司' }) + shipping_provider: string; + + @ApiProperty({ description: '发货方式' }) + shipping_method?: string; + + @ApiProperty({ description: '状态' }) + status?: string; + + @ApiProperty({ description: '创建时间' }) + date_created?: string; +} \ No newline at end of file diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index 58b3c32..3150a78 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -370,11 +370,16 @@ export interface WooOrder { date_modified?: string; date_modified_gmt?: string; // 物流追踪信息 - trackings?: Array<{ - tracking_provider?: string; + fulfillments?: Array<{ tracking_number?: string; - date_shipped?: string; - status_shipped?: string; + shipping_provider?: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id?: number; + quantity?: number; + }>; }>; } export interface WooOrderRefund { diff --git a/src/entity/order_fulfillment.entity.ts b/src/entity/order_fulfillment.entity.ts new file mode 100644 index 0000000..26d538c --- /dev/null +++ b/src/entity/order_fulfillment.entity.ts @@ -0,0 +1,86 @@ +import { ApiProperty } from '@midwayjs/swagger'; +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('order_fulfillment') +@Exclude() +export class OrderFulfillment { + @ApiProperty() + @PrimaryGeneratedColumn() + @Expose() + id: number; + + @ApiProperty() + @Column({ name: 'order_id', nullable: true }) + @Expose() + order_id: number; // 订单 ID + + @ApiProperty() + @Column({ name: 'site_id', nullable: true }) + @Expose() + site_id: number; // 站点ID + + @ApiProperty() + @Column({ name: 'external_order_id', nullable: true }) + @Expose() + external_order_id: string; // 外部订单 ID + + @ApiProperty() + @Column({ name: 'external_fulfillment_id', nullable: true }) + @Expose() + external_fulfillment_id: string; // 外部履约 ID + + @ApiProperty() + @Column({ name: 'tracking_number' }) + @Expose() + tracking_number: string; // 物流单号 + + @ApiProperty() + @Column({ name: 'shipping_provider', nullable: true }) + @Expose() + shipping_provider: string; // 物流公司 + + @ApiProperty() + @Column({ name: 'shipping_method', nullable: true }) + @Expose() + shipping_method: string; // 发货方式 + + @ApiProperty() + @Column({ nullable: true }) + @Expose() + status: string; // 状态 + + @ApiProperty() + @Column({ name: 'date_created', type: 'timestamp', nullable: true }) + @Expose() + date_created: Date; // 创建时间 + + @ApiProperty() + @Column({ type: 'json', nullable: true }) + @Expose() + items: any[]; // 发货商品项 + + @ApiProperty({ + example: '2022-12-12 11:11:11', + description: '创建时间', + required: true, + }) + @CreateDateColumn({ name: 'created_at' }) + @Expose() + created_at: Date; + + @ApiProperty({ + example: '2022-12-12 11:11:11', + description: '更新时间', + required: true, + }) + @UpdateDateColumn({ name: 'updated_at' }) + @Expose() + updated_at: Date; +} diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts index 316263a..a579a33 100644 --- a/src/entity/site.entity.ts +++ b/src/entity/site.entity.ts @@ -37,7 +37,7 @@ export class Site { @Column({ default: false }) isDisabled: boolean; - @ManyToMany(() => Area) + @ManyToMany(() => Area, { cascade: true }) @JoinTable({ name: 'site_areas_area', joinColumn: { diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 0283ad4..2e02cc3 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -238,10 +238,14 @@ export interface ISiteAdapter { */ createOrderFulfillment(orderId: string | number, data: { tracking_number: string; - tracking_provider: string; - date_shipped?: string; - status_shipped?: string; - items?: any[]; + shipping_provider: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; }): Promise; /** @@ -249,9 +253,14 @@ export interface ISiteAdapter { */ updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { tracking_number?: string; - tracking_provider?: string; - date_shipped?: string; - status_shipped?: string; + shipping_provider?: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; }): Promise; /** diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 998b260..02ab0a1 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -13,6 +13,7 @@ import { OrderRefund } from '../entity/order_refund.entity'; import { OrderRefundItem } from '../entity/order_refund_item.entity'; import { OrderCoupon } from '../entity/order_coupon.entity'; import { OrderShipping } from '../entity/order_shipping.entity'; +import { OrderFulfillment } from '../entity/order_fulfillment.entity'; import { Shipment } from '../entity/shipment.entity'; import { Customer } from '../entity/customer.entity'; import { @@ -80,6 +81,9 @@ export class OrderService { @InjectEntityModel(OrderShipping) orderShippingModel: Repository; + @InjectEntityModel(OrderFulfillment) + orderFulfillmentModel: Repository; + @InjectEntityModel(Shipment) shipmentModel: Repository; @@ -107,10 +111,25 @@ export class OrderService { @Logger() logger; // 注入 Logger 实例 + /** + * 批量同步订单 + * 流程说明: + * 1. 调用 WooCommerce API 获取订单列表 + * 2. 遍历每个订单,检查数据库中是否已存在 + * 3. 调用 syncSingleOrder 同步单个订单 + * 4. 统计同步结果(创建数、更新数、错误数) + * + * 涉及实体: Order + * + * @param siteId 站点ID + * @param params 查询参数 + * @returns 同步操作结果 + */ async syncOrders(siteId: number, params: Record = {}): Promise { // 调用 WooCommerce API 获取订单 const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); + // 初始化同步结果对象 const syncResult: SyncOperationResult = { total: result.length, processed: 0, @@ -137,6 +156,7 @@ export class OrderService { syncResult.processed++; syncResult.synced++; + // 根据订单是否存在,更新创建或更新计数 if (existingOrder) { syncResult.updated++; } else { @@ -155,6 +175,20 @@ export class OrderService { return syncResult; } + /** + * 根据订单ID同步单个订单 + * 流程说明: + * 1. 调用 WooCommerce API 获取指定订单 + * 2. 检查数据库中是否已存在该订单 + * 3. 调用 syncSingleOrder 同步订单数据 + * 4. 统计同步结果 + * + * 涉及实体: Order + * + * @param siteId 站点ID + * @param orderId 订单ID + * @returns 同步操作结果 + */ async syncOrderById(siteId: number, orderId: string): Promise { const syncResult: SyncOperationResult = { total: 1, @@ -167,7 +201,8 @@ export class OrderService { try { // 调用 WooCommerce API 获取订单 - const order = await this.wpService.getOrder(siteId, orderId); + const adapter = await this.siteApiService.getAdapter(siteId); + const order = await adapter.getOrder(orderId); // 检查订单是否已存在,以区分创建和更新 const existingOrder = await this.orderModel.findOne({ @@ -201,13 +236,27 @@ export class OrderService { return syncResult; } } - // 订单状态切换表 + /** + * 订单状态自动切换映射表 + * 用于将 WordPress 订单状态转换为系统内部状态 + */ orderAutoNextStatusMap = { [OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded } - // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换 + /** + * 自动更新订单状态 + * 流程说明: + * 1. 检查订单状态是否需要转换 + * 2. 如果需要转换,先同步状态到 WooCommerce + * 3. 然后将订单状态切换到下一状态 + * + * 涉及实体: Order + * + * @param siteId 站点ID + * @param order 订单对象 + */ async autoUpdateOrderStatus(siteId: number, order: any) { // console.log('更新订单状态', order.status, '=>', this.orderAutoNextStatusMap[order.status]) // 其他状态保持不变 @@ -218,6 +267,10 @@ export class OrderService { } try { const site = await this.siteService.get(siteId); + // 仅处理 WooCommerce 站点 + if(site.type !== 'woocommerce'){ + return + } // 将订单状态同步到 WooCommerce,然后切换至下一状态 await this.wpService.updateOrder(site, String(order.id), { status: order.status }); order.status = this.orderAutoNextStatusMap[originStatus]; @@ -225,83 +278,140 @@ export class OrderService { console.error('更新订单状态失败,原因为:', error) } } - // wordpress 发来, + /** + * 同步单个订单 + * 流程说明: + * 1. 从订单数据中解构出各个子项(line_items, shipping_lines, fee_lines, coupon_lines, refunds, fulfillments) + * 2. 检查数据库中是否已存在该订单 + * 3. 自动更新订单状态(如果需要) + * 4. 如果订单状态为 AUTO_DRAFT,则跳过处理 + * 5. 如果订单从未完成变为完成状态,则更新库存并返回 + * 6. 如果订单不可编辑且不强制更新,则跳过处理 + * 7. 保存订单主数据 + * 8. 保存订单项(OrderItem) + * 9. 保存退款信息(OrderRefund, OrderRefundItem) + * 10. 保存费用信息(OrderFee) + * 11. 保存优惠券信息(OrderCoupon) + * 12. 保存配送信息(OrderShipping) + * 13. 保存履约信息(OrderFulfillment) + * + * 涉及实体: Order, OrderItem, OrderRefund, OrderRefundItem, OrderFee, OrderCoupon, OrderShipping, OrderFulfillment + * + * @param siteId 站点ID + * @param order 订单数据 + * @param forceUpdate 是否强制更新 + */ async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { + // 从订单数据中解构出各个子项 let { line_items, shipping_lines, fee_lines, coupon_lines, refunds, + fulfillments, // 物流信息 ...orderData } = order; // console.log('同步进单个订单', order) + // 如果订单状态为 AUTO_DRAFT,则跳过处理 + if (order.status === OrderStatus.AUTO_DRAFT) { + return; + } + // 检查数据库中是否已存在该订单 const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: order.id, siteId: siteId }, }); - // 矫正状态 + // 自动更新订单状态(如果需要) await this.autoUpdateOrderStatus(siteId, order); - if (order.status === OrderStatus.AUTO_DRAFT) { - return; + + if(existingOrder){ + // 矫正数据库中的订单数据 + 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, + }); } - // 更新订单 - if (existingOrder) { - 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, - }) - } 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; } + // 保存订单主数据 const orderRecord = await this.saveOrder(siteId, orderData); const orderId = orderRecord.id; + // 保存订单项 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, }); - // console.log('同步进单个订单2') + // 保存配送信息 await this.saveOrderShipping({ siteId, orderId, externalOrderId, shipping_lines, }); + // 保存履约信息 + await this.saveOrderFulfillments({ + siteId, + orderId, + externalOrderId, + fulfillments: fulfillments, + }); } + /** + * 更新订单库存 + * 流程说明: + * 1. 查询订单的所有销售项(OrderSale) + * 2. 遍历每个销售项,创建库存更新记录 + * 3. 调用 StockService 更新库存(出库操作) + * + * 涉及实体: OrderSale, Stock + * + * @param existingOrder 已存在的订单 + */ async updateStock(existingOrder: Order) { const items = await this.orderSaleModel.find({ where: { orderId: existingOrder.id }, @@ -329,24 +439,54 @@ export class OrderService { } } + /** + * 保存订单主数据 + * 流程说明: + * 1. 将外部订单ID转换为字符串 + * 2. 创建订单实体对象 + * 3. 检查数据库中是否已存在该订单 + * 4. 如果存在: + * - 检查是否可以更新 ERP 状态 + * - 如果可以,则更新订单状态并保存 + * 5. 如果不存在: + * - 映射订单状态 + * - 创建或更新客户信息 + * - 保存新订单 + * + * 涉及实体: Order, Customer + * + * @param siteId 站点ID + * @param order 订单数据 + * @returns 保存后的订单实体 + */ async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise { + // 将外部订单ID转换为字符串 const externalOrderId = String(order.id) delete order.id - // order.billing_phone = order?.billing?.phone || order?.shipping?.phone; + // 创建订单实体对象 const entity = plainToClass(Order, {...order, externalOrderId, siteId}); + // 检查数据库中是否已存在该订单 const existingOrder = await this.orderModel.findOne({ where: { externalOrderId, siteId: siteId }, }); + // 如果订单已存在 if (existingOrder) { + // 检查是否可以更新 ERP 状态 if (this.canUpdateErpStatus(existingOrder.orderStatus)) { entity.orderStatus = this.mapOrderStatus(entity.status); + } else { + // 如果不能更新 ERP 状态,则保留原有的 orderStatus + entity.orderStatus = existingOrder.orderStatus; } + // 更新订单数据(包括 shipping、billing 等字段) await this.orderModel.update(existingOrder.id, entity); entity.id = existingOrder.id; return entity; } + // 如果订单不存在,则映射订单状态 entity.orderStatus = this.mapOrderStatus(entity.status); + // 创建或更新客户信息 await this.customerService.upsertCustomer({ email: order.customer_email, site_id: siteId, @@ -357,7 +497,7 @@ export class OrderService { last_name: order?.billing?.last_name || order?.shipping?.last_name, fullname: order?.billing?.fullname || order?.shipping?.fullname, phone: order?.billing?.phone || order?.shipping?.phone, - + // tags:['fromOrder'] }); // const customer = await this.customerModel.findOne({ @@ -374,9 +514,17 @@ export class OrderService { // phone: order?.billing?.phone || order?.shipping?.phone, // }); // } + // 保存新订单 return await this.orderModel.save(entity); } + /** + * 检查是否可以更新 ERP 状态 + * 某些状态不允许被覆盖,如: AFTER_SALE_PROCESSING, PENDING_RESHIPMENT, PENDING_REFUND + * + * @param currentErpStatus 当前 ERP 状态 + * @returns 是否可以更新 + */ canUpdateErpStatus(currentErpStatus: string): boolean { const nonOverridableStatuses = [ 'AFTER_SALE_PROCESSING', @@ -387,6 +535,13 @@ export class OrderService { return !nonOverridableStatuses.includes(currentErpStatus); } + /** + * 映射订单状态 + * 将 WooCommerce 订单状态转换为 ERP 订单状态 + * + * @param status WooCommerce 订单状态 + * @returns ERP 订单状态 + */ mapOrderStatus(status: OrderStatus): ErpOrderStatus { switch (status) { case OrderStatus.PENDING: @@ -412,21 +567,37 @@ export class OrderService { } } + /** + * 保存订单项 + * 流程说明: + * 1. 查询数据库中已存在的订单项 + * 2. 找出需要删除的订单项(在数据库中存在但在新数据中不存在) + * 3. 删除这些订单项及其对应的销售项(OrderSale) + * 4. 遍历新的订单项,设置相关字段 + * 5. 保存每个订单项 + * 6. 为每个订单项创建对应的销售项(OrderSale) + * + * 涉及实体: OrderItem, OrderSale + * + * @param params 订单项参数 + */ async saveOrderItems(params: { siteId: number; orderId: number; externalOrderId: string; orderItems: Record[]; }) { - // console.log('saveOrderItems params',params) + // 查询数据库中已存在的订单项 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)) ); + // 删除这些订单项及其对应的销售项(OrderSale) if (orderItemToDelete.length > 0) { const idsToDelete = orderItemToDelete.map(v => v.id); await this.orderItemModel.delete(idsToDelete); @@ -436,6 +607,7 @@ export class OrderService { externalOrderItemId: In(itemIdsToDelete), }); } + // 遍历新的订单项,设置相关字段并保存 for (const orderItem of orderItems) { orderItem.siteId = siteId; orderItem.externalOrderId = externalOrderId; @@ -445,11 +617,24 @@ export class OrderService { orderItem.externalVariationId = String(orderItem.variation_id); delete orderItem.id; const entity = plainToClass(OrderItem, orderItem); + // 保存订单项 await this.saveOrderItem(entity); + // 为每个订单项创建对应的销售项(OrderSale) await this.saveOrderSale(entity); } } + /** + * 保存单个订单项 + * 流程说明: + * 1. 检查数据库中是否已存在该订单项 + * 2. 如果存在,则更新订单项 + * 3. 如果不存在,则创建新订单项 + * + * 涉及实体: OrderItem + * + * @param orderItem 订单项实体 + */ async saveOrderItem(orderItem: OrderItem) { const existingOrderItem = await this.orderItemModel.findOne({ where: { @@ -472,6 +657,20 @@ export class OrderService { } } + /** + * 保存单个订单项(备用方法) + * 流程说明: + * 1. 检查数据库中是否已存在该订单项 + * 2. 如果数量相同,则跳过处理 + * 3. 如果存在但数量不同,则更新订单项 + * 4. 如果不存在,则创建新订单项 + * + * 注意: 该方法与 saveOrderItem 功能相同,可能是备用或待删除的方法 + * + * 涉及实体: OrderItem + * + * @param orderItem 订单项实体 + */ async saveOrderItemsG(orderItem: OrderItem) { const existingOrderItem = await this.orderItemModel.findOne({ where: { @@ -494,6 +693,21 @@ export class OrderService { } } + /** + * 保存订单销售项 + * 流程说明: + * 1. 查询数据库中已存在的销售项 + * 2. 删除已存在的销售项 + * 3. 如果订单项没有 SKU,则跳过处理 + * 4. 查询产品信息(包含组件信息) + * 5. 如果产品有组件,则为每个组件创建销售项 + * 6. 如果产品没有组件,则直接创建销售项 + * 7. 保存所有销售项 + * + * 涉及实体: OrderSale, Product + * + * @param orderItem 订单项实体 + */ async saveOrderSale(orderItem: OrderItem) { const currentOrderSale = await this.orderSaleModel.find({ where: { @@ -553,6 +767,20 @@ export class OrderService { } } + /** + * 保存订单退款信息 + * 流程说明: + * 1. 遍历退款列表,为每个退款项获取详细信息 + * 2. 设置退款相关字段(orderId、siteId、externalOrderId、externalRefundId) + * 3. 检查数据库中是否已存在该退款 + * 4. 如果存在,则更新退款信息 + * 5. 如果不存在,则创建新退款 + * 6. 保存退款项(OrderRefundItem) + * + * 涉及实体: OrderRefund, OrderRefundItem + * + * @param params 退款参数 + */ async saveOrderRefunds({ siteId, orderId, @@ -600,6 +828,20 @@ export class OrderService { } } + /** + * 保存订单退款项 + * 流程说明: + * 1. 遍历退款项列表 + * 2. 设置退款项相关字段(externalRefundItemId、externalProductId、externalVariationId) + * 3. 创建退款项实体 + * 4. 检查数据库中是否已存在该退款项 + * 5. 如果存在,则更新退款项 + * 6. 如果不存在,则创建新退款项 + * + * 涉及实体: OrderRefundItem + * + * @param params 退款项参数 + */ async saveOrderRefundItems({ refundItems, siteId, @@ -635,6 +877,21 @@ export class OrderService { } } + /** + * 保存订单费用信息 + * 流程说明: + * 1. 查询数据库中已存在的费用项 + * 2. 找出需要删除的费用项(在数据库中存在但在新数据中不存在) + * 3. 删除这些费用项 + * 4. 遍历新的费用项,设置相关字段 + * 5. 检查数据库中是否已存在该费用项 + * 6. 如果存在,则更新费用项 + * 7. 如果不存在,则创建新费用项 + * + * 涉及实体: OrderFee + * + * @param params 费用参数 + */ async saveOrderFees({ siteId, orderId, externalOrderId, fee_lines }) { const currentOrderFees = await this.orderFeeModel.find({ where: { siteId, externalOrderId }, @@ -670,6 +927,20 @@ export class OrderService { } } + /** + * 保存订单优惠券信息 + * 流程说明: + * 1. 遍历优惠券列表 + * 2. 设置优惠券相关字段(externalOrderCouponId、siteId、orderId、externalOrderId) + * 3. 创建优惠券实体 + * 4. 检查数据库中是否已存在该优惠券 + * 5. 如果存在,则更新优惠券 + * 6. 如果不存在,则创建新优惠券 + * + * 涉及实体: OrderCoupon + * + * @param params 优惠券参数 + */ async saveOrderCoupons({ siteId, externalOrderId, orderId, coupon_lines }) { for (const item of coupon_lines) { item.externalOrderCouponId = item.id; @@ -693,6 +964,20 @@ export class OrderService { } } + /** + * 保存订单配送信息 + * 流程说明: + * 1. 遍历配送列表 + * 2. 设置配送相关字段(externalOrderShippingId、siteId、orderId、externalOrderId) + * 3. 创建配送实体 + * 4. 检查数据库中是否已存在该配送 + * 5. 如果存在,则更新配送 + * 6. 如果不存在,则创建新配送 + * + * 涉及实体: OrderShipping + * + * @param params 配送参数 + */ async saveOrderShipping({ siteId, orderId, @@ -724,6 +1009,92 @@ export class OrderService { } } + /** + * 保存订单履约信息 + * 流程说明: + * 1. 检查履约列表是否存在,如果不存在则跳过 + * 2. 遍历履约列表 + * 3. 设置履约相关字段(externalFulfillmentId、siteId、orderId、externalOrderId) + * 4. 转换时间戳为日期格式 + * 5. 创建履约实体 + * 6. 检查数据库中是否已存在该履约 + * 7. 如果存在,则更新履约 + * 8. 如果不存在,则创建新履约 + * + * 涉及实体: OrderFulfillment + * + * @param params 履约参数 + */ + async saveOrderFulfillments({ + siteId, + orderId, + externalOrderId, + fulfillments, + }) { + // 如果履约列表不存在,则跳过处理 + if (!fulfillments || !Array.isArray(fulfillments) || fulfillments.length === 0) { + return; + } + + // 遍历履约列表 + for (const item of fulfillments) { + // 设置履约相关字段 + item.external_fulfillment_id = String(item.id); + item.site_id = siteId; + item.order_id = orderId; + item.external_order_id = String(externalOrderId); + + // 删除原始 ID + delete item.id; + + // 转换时间戳为日期格式 + if (item.date_created && typeof item.date_created === 'number') { + item.date_created = new Date(item.date_created * 1000); + } else if (item.date_created && typeof item.date_created === 'string') { + item.date_created = new Date(item.date_created); + } + + // 创建履约实体 + const fulfillment = plainToClass(OrderFulfillment, item); + + // 检查数据库中是否已存在该履约 + const existingOrderFulfillment = await this.orderFulfillmentModel.findOne({ + where: { + site_id: siteId, + external_order_id: externalOrderId, + external_fulfillment_id: fulfillment.external_fulfillment_id, + }, + }); + + // 如果存在,则更新履约 + if (existingOrderFulfillment) { + await this.orderFulfillmentModel.update( + existingOrderFulfillment.id, + fulfillment + ); + } else { + // 如果不存在,则创建新履约 + await this.orderFulfillmentModel.save(fulfillment); + } + } + } + + /** + * 获取订单列表 + * 流程说明: + * 1. 构建基础SQL查询,包含订单基本信息、客户统计、订阅信息、物流信息 + * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、状态、关键字等) + * 3. 检查用户权限,如果用户有"order-10-days"权限且未指定日期范围,则限制查询最近10天的订单 + * 4. 执行总数查询 + * 5. 添加分页和排序,执行主查询 + * 6. 返回订单列表和分页信息 + * + * 涉及实体: Order, Subscription, Shipment, Customer + * + * @param params 查询参数 + * @param userId 用户ID(用于权限检查) + * @returns 订单列表和分页信息 + */ async getOrders({ externalOrderId, siteId, @@ -803,7 +1174,25 @@ export class OrderService { ) END ), JSON_ARRAY() - ) as shipmentList + ) as shipmentList, + COALESCE( + JSON_ARRAYAGG( + CASE WHEN fulfillment.id IS NOT NULL THEN JSON_OBJECT( + 'id', fulfillment.id, + 'externalFulfillmentId', fulfillment.external_fulfillment_id, + 'orderId', fulfillment.order_id, + 'siteId', fulfillment.site_id, + 'status', fulfillment.status, + 'createdAt', fulfillment.created_at, + 'updatedAt', fulfillment.updated_at, + 'tracking_number', fulfillment.tracking_number, + 'shipping_provider', fulfillment.shipping_provider, + 'shipping_method', fulfillment.shipping_method, + 'items', fulfillment.items + ) END + ), + JSON_ARRAY() + ) as fulfillments FROM \`order\` o LEFT JOIN ( SELECT @@ -816,6 +1205,7 @@ export class OrderService { ) 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 + LEFT JOIN order_fulfillment fulfillment ON fulfillment.order_id = o.id WHERE 1=1 `; @@ -948,6 +1338,20 @@ export class OrderService { return { items: orders, total, current, pageSize }; } + /** + * 获取订单状态统计 + * 流程说明: + * 1. 构建查询,按订单状态分组统计订单数量 + * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、关键字等) + * 3. 支持关键字搜索,检查订单项名称是否包含关键字 + * 4. 支持订阅订单过滤 + * 5. 返回各状态的订单数量统计 + * + * 涉及实体: Order, OrderItem, Subscription + * + * @param params 查询参数 + * @returns 订单状态统计列表 + */ async getOrderStatus({ externalOrderId, siteId, @@ -1007,6 +1411,21 @@ export class OrderService { return await query.getRawMany(); } + /** + * 获取订单销售统计 + * 流程说明: + * 1. 查询总条数(按产品ID去重) + * 2. 分页查询产品基础信息(产品ID、名称、总数量、订单数) + * 3. 批量统计当前页产品的历史复购情况(第1次、第2次、第3次、>3次订单的数量和盒数) + * 4. 统计总量(时间段内的总数量、YOONE各规格数量、ZEX数量) + * 5. 支持按产品名称关键字过滤 + * 6. 支持排除套餐订单(exceptPackage) + * + * 涉及实体: OrderSale, Order + * + * @param params 查询参数 + * @returns 销售统计和分页信息 + */ async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); @@ -1207,6 +1626,21 @@ export class OrderService { } + /** + * 获取订单项统计 + * 流程说明: + * 1. 使用CTE计算每个客户对每个产品的购买次数 + * 2. 查询订单项统计信息(外部产品ID、变体ID、名称、总数量、订单数) + * 3. 统计不同购买次数的订单数(第1次、第2次、第3次、>3次) + * 4. 支持按产品名称关键字过滤 + * 5. 支持分页查询 + * 6. 返回订单项统计和分页信息 + * + * 涉及实体: OrderItem, Order + * + * @param params 查询参数 + * @returns 订单项统计和分页信息 + */ async getOrderItems({ siteId, startDate, @@ -1323,6 +1757,21 @@ export class OrderService { }; } + /** + * 获取订单项列表 + * 流程说明: + * 1. 构建SQL查询,关联订单表,获取订单项和订单信息 + * 2. 检查订单项是否为订阅项(通过meta_data中的特定key判断) + * 3. 根据参数动态添加过滤条件(日期范围、站点ID、产品名称、外部产品ID、变体ID) + * 4. 执行总数查询 + * 5. 添加分页和排序,执行主查询 + * 6. 返回订单项列表和分页信息 + * + * 涉及实体: OrderItem, Order + * + * @param params 查询参数 + * @returns 订单项列表和分页信息 + */ async getOrderItemList({ siteId, startDate, @@ -1380,6 +1829,24 @@ export class OrderService { return { items, total, current, pageSize }; } + /** + * 获取订单详情 + * 流程说明: + * 1. 查询订单基本信息 + * 2. 查询站点信息 + * 3. 查询订单项 + * 4. 查询订单销售项 + * 5. 查询退款信息和退款项 + * 6. 查询订单备注(包含用户名) + * 7. 查询物流信息(包含物流项) + * 8. 查询关联的订阅和相关订单 + * 9. 返回完整的订单详情 + * + * 涉及实体: Order, Site, OrderItem, OrderSale, OrderRefund, OrderRefundItem, OrderNote, Shipment, ShipmentItem, Subscription + * + * @param id 订单ID + * @returns 订单详情 + */ async getOrderDetail(id: number): Promise { const order = await this.orderModel.findOne({ where: { id } }); const site = await this.siteService.get(Number(order.siteId), true); @@ -1462,6 +1929,18 @@ export class OrderService { }; } + /** + * 获取订单关联信息 + * 流程说明: + * 1. 查询订单基本信息 + * 2. 查询关联的订阅信息(通过parent_id关联) + * 3. 返回订单和订阅信息 + * + * 涉及实体: Order, Subscription + * + * @param orderId 订单ID + * @returns 订单和关联的订阅信息 + */ async getRelatedByOrder(orderId: number) { const order = await this.orderModel.findOne({ where: { id: orderId } }); if (!order) throw new Error('订单不存在'); @@ -1478,6 +1957,22 @@ export class OrderService { }; } + /** + * 删除订单 + * 流程说明: + * 1. 查询订单是否存在 + * 2. 删除订单配送信息 + * 3. 删除订单销售项 + * 4. 删除退款信息和退款项 + * 5. 删除订单项 + * 6. 删除订单费用 + * 7. 删除订单优惠券 + * 8. 删除订单主数据 + * + * 涉及实体: Order, OrderShipping, OrderSale, OrderRefund, OrderRefundItem, OrderItem, OrderFee, OrderCoupon + * + * @param id 订单ID + */ async delOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error('订单不存在'); @@ -1498,6 +1993,19 @@ export class OrderService { await this.orderModel.delete({ id }); } + /** + * 创建订单备注 + * 流程说明: + * 1. 接收用户ID和备注数据 + * 2. 创建订单备注实体,关联用户ID + * 3. 保存订单备注 + * + * 涉及实体: OrderNote + * + * @param userId 用户ID + * @param data 备注数据 + * @returns 保存后的订单备注 + */ async createNote(userId: number, data: CreateOrderNoteDTO) { return await this.orderNoteModel.save({ ...data, @@ -1505,6 +2013,19 @@ export class OrderService { }); } + /** + * 根据订单号获取订单 + * 流程说明: + * 1. 根据订单号模糊查询订单(仅查询处理中和待补发的订单) + * 2. 批量获取订单涉及的站点名称 + * 3. 构建站点ID到站点名称的映射 + * 4. 返回订单列表,包含订单号、ID和站点名称 + * + * 涉及实体: Order, Site + * + * @param id 订单号 + * @returns 订单列表(包含订单号、ID和站点名称) + */ async getOrderByNumber(id: string) { const orders = await this.orderModel.find({ where: { @@ -1526,6 +2047,19 @@ export class OrderService { })); } + /** + * 取消订单 + * 流程说明: + * 1. 查询订单是否存在 + * 2. 查询订单所属站点 + * 3. 如果订单状态不是已取消,则调用WooCommerce API更新订单状态为已取消 + * 4. 更新本地订单状态为已取消 + * 5. 更新订单ERP状态为已取消 + * + * 涉及实体: Order, Site + * + * @param id 订单ID + */ async cancelOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); @@ -1540,6 +2074,19 @@ export class OrderService { await this.orderModel.save(order); } + /** + * 退款订单 + * 流程说明: + * 1. 查询订单是否存在 + * 2. 查询订单所属站点 + * 3. 如果订单状态不是已退款,则调用WooCommerce API更新订单状态为已退款 + * 4. 更新本地订单状态为已退款 + * 5. 更新订单ERP状态为已退款 + * + * 涉及实体: Order, Site + * + * @param id 订单ID + */ async refundOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); @@ -1554,6 +2101,19 @@ export class OrderService { await this.orderModel.save(order); } + /** + * 完成订单 + * 流程说明: + * 1. 查询订单是否存在 + * 2. 查询订单所属站点 + * 3. 如果订单状态不是已完成,则调用WooCommerce API更新订单状态为已完成 + * 4. 更新本地订单状态为已完成 + * 5. 更新订单ERP状态为已完成 + * + * 涉及实体: Order, Site + * + * @param id 订单ID + */ async completedOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); @@ -1568,6 +2128,18 @@ export class OrderService { await this.orderModel.save(order); } + /** + * 更改订单状态 + * 流程说明: + * 1. 查询订单是否存在 + * 2. 更新订单ERP状态 + * 3. 保存订单 + * + * 涉及实体: Order + * + * @param id 订单ID + * @param status ERP订单状态 + */ async changeStatus(id: number, status: ErpOrderStatus) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); @@ -1576,6 +2148,22 @@ export class OrderService { } + /** + * 创建订单 + * 流程说明: + * 1. 验证必需参数siteId是否存在 + * 2. 获取默认数据源 + * 3. 在事务中处理订单创建 + * 4. 保存订单基本信息(站点ID、外部订单号、状态、货币、日期、客户信息等) + * 5. 遍历销售项目列表 + * 6. 根据SKU查询产品信息 + * 7. 保存订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) + * + * 涉及实体: Order, OrderSale, Product + * + * @param data 订单数据 + * @returns 创建的订单 + */ async createOrder(data: Record) { // 从数据中解构出需要用的属性 const { siteId, sales, total, billing, customer_email, billing_phone } = data; @@ -1624,6 +2212,21 @@ export class OrderService { }); } + /** + * 获取待处理订单项统计 + * 流程说明: + * 1. 构建SQL查询,关联订单表和订单销售表 + * 2. 按产品名称分组,统计每个产品的总数量和订单号列表 + * 3. 只查询状态为"处理中"的订单 + * 4. 执行总数查询 + * 5. 添加分页,执行主查询 + * 6. 返回待处理订单项统计和分页信息 + * + * 涉及实体: Order, OrderSale + * + * @param data 查询参数 + * @returns 待处理订单项统计和分页信息 + */ async pengdingItems(data: Record) { const { current = 1, pageSize = 10 } = data; const sql = ` @@ -1661,6 +2264,24 @@ export class OrderService { }; } + /** + * 更新订单销售项 + * 流程说明: + * 1. 获取默认数据源 + * 2. 在事务中处理订单销售项更新 + * 3. 查询订单信息 + * 4. 删除该订单的所有销售项 + * 5. 遍历新的销售项列表 + * 6. 根据SKU查询产品信息 + * 7. 保存新的订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) + * 8. 处理事务错误 + * + * 涉及实体: Order, OrderSale, Product + * + * @param orderId 订单ID + * @param sales 销售项列表 + * @returns 更新成功返回true + */ async updateOrderSales(orderId: number, sales: OrderSale[]) { try { const dataSource = this.dataSourceManager.getDataSource('default'); @@ -1701,6 +2322,19 @@ export class OrderService { } } + /** + * 更新换货订单 + * 流程说明: + * 1. 该方法用于换货确认功能 + * 2. 需要更新OrderSale和OrderItem数据 + * 3. 当前方法暂未实现 + * + * 涉及实体: Order, OrderSale, OrderItem, Product + * + * @param orderId 订单ID + * @param data 换货数据 + * @returns 更新成功返回true + */ //换货确认按钮改成调用这个方法 //换货功能更新OrderSale和Orderitem数据 async updateExchangeOrder(orderId: number, data: any) { @@ -1784,6 +2418,32 @@ export class OrderService { // } } + /** + * 导出订单为CSV格式 + * 流程说明: + * 1. 在事务中处理订单导出 + * 2. 根据订单ID列表查询订单信息(包含物流关联) + * 3. 查询所有订单项 + * 4. 按订单ID分组订单项 + * 5. 构建导出数据,包含以下字段: + * - 日期: 订单创建日期 + * - 订单号: 外部订单号 + * - 姓名地址: 收货人姓名和地址 + * - 邮箱: 客户邮箱 + * - 号码: 电话号码 + * - 订单内容: 产品名称和数量 + * - 盒数: 总盒数 + * - 换盒数: 换货盒数(当前默认为0) + * - 换货内容: 换货内容(当前默认为空) + * - 快递号: 物流追踪号 + * 6. 调用exportToCsv方法将数据转换为CSV格式 + * 7. 返回CSV字符串内容 + * + * 涉及实体: Order, OrderItem, Shipment + * + * @param ids 订单ID列表 + * @returns CSV格式字符串 + */ // TODO async exportOrder(ids: number[]) { // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index e437f46..d083e2f 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -1025,7 +1025,7 @@ export class ShopyyService { */ async cancelFulfillment(site: any, data: { order_id: string; - fullfillment_id: string; + fulfillment_id: string; }): Promise { try { // ShopYY API: POST /orders/fulfillment/cancel diff --git a/src/service/site.service.ts b/src/service/site.service.ts index 165fb5c..7d9dd41 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -5,6 +5,7 @@ import { Site } from '../entity/site.entity'; import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto'; import { Area } from '../entity/area.entity'; import { StockPoint } from '../entity/stock_point.entity'; +import * as countries from 'i18n-iso-countries'; @Provide() @Scope(ScopeEnum.Singleton) @@ -25,12 +26,26 @@ export class SiteService { const newSite = new Site(); Object.assign(newSite, restData); - // 如果传入了区域代码,则查询并关联 Area 实体 + // 如果传入了区域代码,则查询或创建 Area 实体 if (areaCodes && areaCodes.length > 0) { - const areas = await this.areaModel.findBy({ + // 先查询数据库中已存在的 Area 实体 + const existingAreas = await this.areaModel.findBy({ code: In(areaCodes), }); - newSite.areas = areas; + const existingCodes = new Set(existingAreas.map(area => area.code)); + + // 为不存在的 Area 创建新实体 + const newAreas = areaCodes + .filter(code => !existingCodes.has(code)) + .map(areaCode => { + const area = new Area(); + area.code = areaCode; + area.name = countries.getName(areaCode, 'zh') || areaCode; // 使用 countries 获取中文名称,如果获取不到则使用 code + return area; + }); + + // 合并已存在和新创建的 Area 实体 + newSite.areas = [...existingAreas, ...newAreas]; } else { // 如果没有传入区域,则关联一个空数组,代表"全局" newSite.areas = []; @@ -74,11 +89,25 @@ export class SiteService { // 如果 DTO 中传入了 areas 字段(即使是空数组),也要更新关联关系 if (areaCodes !== undefined) { if (areaCodes.length > 0) { - // 如果区域代码数组不为空,则查找并更新关联 - const areas = await this.areaModel.findBy({ + // 先查询数据库中已存在的 Area 实体 + const existingAreas = await this.areaModel.findBy({ code: In(areaCodes), }); - siteToUpdate.areas = areas; + const existingCodes = new Set(existingAreas.map(area => area.code)); + + // 为不存在的 Area 创建新实体 + const newAreas = areaCodes + .filter(code => !existingCodes.has(code)) + .map(areaCode => { + const area = new Area(); + area.code = areaCode; + // name 使用 i18n-contries 获取 + area.name = countries.getName(areaCode, 'zh') || areaCode; // 使用 code 作为 name 的默认值 + return area; + }); + + // 合并已存在和新创建的 Area 实体 + siteToUpdate.areas = [...existingAreas, ...newAreas]; } else { // 如果传入空数组,则清空所有关联,代表"全局" siteToUpdate.areas = []; @@ -98,7 +127,7 @@ export class SiteService { } // 使用 save 方法保存实体及其更新后的关联关系 - await this.siteModel.save(siteToUpdate); + await this.siteModel.save(siteToUpdate); return true; } diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 24b4083..c7faa02 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -719,9 +719,14 @@ export class WPService implements IPlatformService { fulfillmentId: string, data: { tracking_number?: string; - tracking_provider?: string; - date_shipped?: string; - status_shipped?: string; + shipping_provider?: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; } ): Promise { const apiUrl = site.apiUrl; @@ -732,20 +737,28 @@ export class WPService implements IPlatformService { const fulfillmentData: any = {}; - if (data.tracking_provider !== undefined) { - fulfillmentData.tracking_provider = data.tracking_provider; + if (data.shipping_provider !== undefined) { + fulfillmentData.shipping_provider = data.shipping_provider; } if (data.tracking_number !== undefined) { fulfillmentData.tracking_number = data.tracking_number; } - if (data.date_shipped !== undefined) { - fulfillmentData.date_shipped = data.date_shipped; + if (data.shipping_method !== undefined) { + fulfillmentData.shipping_method = data.shipping_method; } - if (data.status_shipped !== undefined) { - fulfillmentData.status_shipped = data.status_shipped; + if (data.status !== undefined) { + fulfillmentData.status = data.status; + } + + if (data.date_created !== undefined) { + fulfillmentData.date_created = data.date_created; + } + + if (data.items !== undefined) { + fulfillmentData.items = data.items; } const config: AxiosRequestConfig = {