From 8f6727ae757903b3445d276834decbb53ba3b853 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 15:22:18 +0800 Subject: [PATCH 01/24] =?UTF-8?q?feat(=E8=AE=A2=E5=8D=95):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=8E=B7=E5=8F=96=E8=AE=A2=E5=8D=95=E6=80=BB=E6=95=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现订单总数统计接口,包括: 1. 在ISiteAdapter接口添加countOrders方法 2. 在WooCommerce和Shopyy适配器中实现该方法 3. 添加控制器端点暴露该功能 4. 优化订单查询参数映射逻辑 refactor(Shopyy): 重构搜索参数映射逻辑 将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性 --- src/adapter/shopyy.adapter.ts | 105 ++++++++++++++++-------- src/adapter/woocommerce.adapter.ts | 13 ++- src/controller/media.controller.ts | 79 ------------------ src/controller/site-api.controller.ts | 26 +++++- src/dto/shopyy.dto.ts | 40 +++++++++ src/entity/product.entity.ts | 3 - src/interface/site-adapter.interface.ts | 5 ++ src/service/shopyy.service.ts | 37 ++------- 8 files changed, 162 insertions(+), 146 deletions(-) delete mode 100644 src/controller/media.controller.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 33d1e61..0cca7ff 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -23,7 +23,9 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { ShopyyCustomer, ShopyyOrder, + ShopyyOrderQuery, ShopyyProduct, + ShopyyProductQuery, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; @@ -64,17 +66,30 @@ export class ShopyyAdapter implements ISiteAdapter { } private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { - const { search, page, per_page } = params; - const shopyyParams: any = { - page: page || 1, - limit: per_page || 10, - }; + return this.mapSearchParams(params) + } - if (search) { - shopyyParams.query = search; + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + private mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), + page, + limit, } - - return shopyyParams; + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; } private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { @@ -393,14 +408,19 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) + } async getProducts( params: UnifiedSearchParamsDTO ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); const response = await this.shopyyService.fetchResourcePaged( this.site, 'products/list', - params + requestParams ); const { items = [], total, totalPages, page, per_page } = response; const finalItems = items.map((item) => ({ @@ -431,7 +451,6 @@ export class ShopyyAdapter implements ISiteAdapter { const res = await this.shopyyService.createProduct(this.site, data); return this.mapProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { // Shopyy update returns boolean? // shopyyService.updateProduct returns boolean. @@ -466,38 +485,49 @@ export class ShopyyAdapter implements ISiteAdapter { ): Promise { return await this.shopyyService.batchProcessProducts(this.site, data); } - mapUnifiedOrderQueryToShopyyQuery(params: UnifiedSearchParamsDTO) { - const { where = {} as any, ...restParams } = params || {} + /** + * 将统一的订单查询参数转换为 ShopYY 订单查询参数 + * 包含状态映射等特殊处理 + */ + private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 首先使用通用参数转换 + const baseParams = this.mapSearchParams(params); + + // 订单状态映射 const statusMap = { 'pending': '100', // 100 未完成 'processing': '110', // 110 待处理 'completed': "180", // 180 已完成(确认收货) 'cancelled': '190', // 190 取消 - } - const normalizedParams: any = { - ...restParams, - } - if (where) { - normalizedParams.where = { - ...where, - } - if (where.status) { - normalizedParams.where.status = statusMap[where.status]; + }; + + // 如果有状态参数,进行特殊映射 + if (baseParams.status) { + const unifiedStatus = baseParams.status + if (statusMap[unifiedStatus]) { + baseParams.status = statusMap[unifiedStatus]; } } - return normalizedParams + + // 处理ID参数 + if (baseParams.id) { + baseParams.ids = baseParams.id; + delete baseParams.id; + } + + return baseParams; } async getOrders( params: UnifiedSearchParamsDTO ): Promise> { - const normalizedParams = this.mapUnifiedOrderQueryToShopyyQuery(params); - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'orders', + normalizedParams + ); return { items: items.map(this.mapOrder.bind(this)), total, @@ -507,6 +537,17 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; + } + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { const data = await this.shopyyService.getAllOrders(this.site.id, params); return data.map(this.mapOrder.bind(this)); @@ -669,7 +710,7 @@ export class ShopyyAdapter implements ISiteAdapter { const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, 'media', // Shopyy的媒体API端点可能需要调整 - requestParams + requestParams ); return { items: items.map(this.mapMedia.bind(this)), diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index badf467..9828a07 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -718,6 +718,18 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + async getOrder(id: string | number): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); @@ -1294,4 +1306,3 @@ export class WooCommerceAdapter implements ISiteAdapter { } } } - diff --git a/src/controller/media.controller.ts b/src/controller/media.controller.ts deleted file mode 100644 index cafd85f..0000000 --- a/src/controller/media.controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core'; -import { WPService } from '../service/wp.service'; -import { successResponse, errorResponse } from '../utils/response.util'; - -@Controller('/media') -export class MediaController { - @Inject() - wpService: WPService; - - @Get('/list') - async list( - @Query('siteId') siteId: number, - @Query('page') page: number = 1, - @Query('pageSize') pageSize: number = 20 - ) { - try { - if (!siteId) { - return errorResponse('siteId is required'); - } - const result = await this.wpService.getMedia(siteId, page, pageSize); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Post('/upload') - async upload(@Fields() fields, @Files() files) { - try { - const siteId = fields.siteId; - if (!siteId) { - return errorResponse('siteId is required'); - } - if (!files || files.length === 0) { - return errorResponse('file is required'); - } - const file = files[0]; - const result = await this.wpService.createMedia(siteId, file); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Post('/update/:id') - async update(@Param('id') id: number, @Body() body) { - try { - const siteId = body.siteId; - if (!siteId) { - return errorResponse('siteId is required'); - } - // 过滤出需要更新的字段 - const { title, caption, description, alt_text } = body; - const data: any = {}; - if (title !== undefined) data.title = title; - if (caption !== undefined) data.caption = caption; - if (description !== undefined) data.description = description; - if (alt_text !== undefined) data.alt_text = alt_text; - - const result = await this.wpService.updateMedia(siteId, id, data); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Del('/:id') - async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) { - try { - if (!siteId) { - return errorResponse('siteId is required'); - } - const result = await this.wpService.deleteMedia(siteId, id, force); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } -} diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 9f69b87..5013390 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -672,6 +672,26 @@ export class SiteApiController { } } + @Get('/:siteId/orders/count') + @ApiOkResponse({ type: Object }) + async countOrders( + @Param('siteId') siteId: number, + @Query() query: any + ) { + this.logger.info(`[Site API] 获取订单总数开始, siteId: ${siteId}`); + try { + + + const adapter = await this.siteApiService.getAdapter(siteId); + const total = await adapter.countOrders(query); + this.logger.info(`[Site API] 获取订单总数成功, siteId: ${siteId}, total: ${total}`); + return successResponse({ total }); + } catch (error) { + this.logger.error(`[Site API] 获取订单总数失败, siteId: ${siteId}, 错误信息: ${error.message}`); + return errorResponse(error.message); + } + } + @Get('/:siteId/customers/:customerId/orders') @ApiOkResponse({ type: UnifiedOrderPaginationDTO }) async getCustomerOrders( @@ -1050,13 +1070,13 @@ export class SiteApiController { } } - @Get('/:siteId/orders/:orderId/trackings') + @Get('/:siteId/orders/:orderId/fulfillments') @ApiOkResponse({ type: Object }) - async getOrderTrackings( + async getOrderFulfillments( @Param('siteId') siteId: number, @Param('orderId') orderId: string ) { - this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`); + this.logger.info(`[Site API] 获取订单履约信息开始, siteId: ${siteId}, orderId: ${orderId}`); try { const adapter = await this.siteApiService.getAdapter(siteId); const data = await adapter.getOrderFulfillments(orderId); diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index 2bcbca7..fc534d6 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -4,6 +4,10 @@ export interface ShopyyTag { id?: number; name?: string; } +export interface ShopyyProductQuery{ + page: string; + limit: string; +} // 产品类型 export interface ShopyyProduct { // 产品主键 @@ -83,6 +87,42 @@ export interface ShopyyVariant { position?: number | string; sku_code?: string; } +// +// 订单查询参数类型 +export interface ShopyyOrderQuery { + // 订单ID集合 多个ID用','联接 例:1,2,3 + ids?: string; + // 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + status?: string; + // 物流状态 300 未发货;310 部分发货;320 已发货;330(确认收货) + fulfillment_status?: string; + // 支付状态 200 待支付;210 支付中;220 部分支付;230 已支付;240 支付失败;250 部分退款;260 已退款 ;290 已取消; + financial_status?: string; + // 支付时间 下限值 + pay_at_min?: string; + // 支付时间 上限值 + pay_at_max?: string; + // 创建开始时间 + created_at_min?: number; + // 创建结束时间 + created_at_max?: number; + // 更新时间开始 + updated_at_min?: string; + // 更新时间结束 + updated_at_max?: string; + // 起始ID + since_id?: string; + // 页码 + page?: string; + // 每页条数 + limit?: string; + // 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间 + order_field?: string; + // 排序方式(默认desc) desc=降序 asc=升序 + order_by?: string; + // 订单列表类型 + group?: string; +} // 订单类型 export interface ShopyyOrder { diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index ea763ec..1d7b296 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -65,9 +65,6 @@ export class Product { @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) promotionPrice: number; - - - // 分类关联 @ManyToOne(() => Category, category => category.products) @JoinColumn({ name: 'categoryId' }) diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 2e02cc3..ea6db0b 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -40,6 +40,11 @@ export interface ISiteAdapter { */ getOrders(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取订单总数 + */ + countOrders(params: Record): Promise; + /** * 获取所有订单 */ diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index d083e2f..c111a03 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -1,3 +1,6 @@ +/** + * https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse + */ import { ILogger, Inject, Provide } from '@midwayjs/core'; import axios, { AxiosRequestConfig } from 'axios'; import * as fs from 'fs'; @@ -180,41 +183,19 @@ export class ShopyyService { * 通用分页获取资源 */ public async fetchResourcePaged(site: any, endpoint: string, params: Record = {}) { - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - const where = params.where && typeof params.where === 'object' ? params.where : {}; - let orderby: string | undefined = params.orderby; - let order: 'asc' | 'desc' | undefined = params.orderDir as any; - if (!orderby && params.order && typeof params.order === 'object') { - const entries = Object.entries(params.order as Record); - if (entries.length > 0) { - const [field, dir] = entries[0]; - orderby = field; - order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; - } - } - // 映射统一入参到平台入参 - const requestParams = { - ...where, - ...(params.search ? { search: params.search } : {}), - ...(params.status ? { status: params.status } : {}), - ...(orderby ? { orderby } : {}), - ...(order ? { order } : {}), - page, - limit - }; - this.logger.debug('ShopYY API请求分页参数:'+ JSON.stringify(requestParams)); - const response = await this.request(site, endpoint, 'GET', null, requestParams); + const response = await this.request(site, endpoint, 'GET', null, params); + return this.mapPageResponse(response,params); + } + mapPageResponse(response:any,query: Record){ if (response?.code !== 0) { throw new Error(response?.msg) } - return { items: (response.data.list || []) as T[], total: response.data?.paginate?.total || 0, totalPages: response.data?.paginate?.pageTotal || 0, - page: response.data?.paginate?.current || requestParams.page, - per_page: response.data?.paginate?.pagesize || requestParams.limit, + page: response.data?.paginate?.current || query.page, + per_page: response.data?.paginate?.pagesize || query.limit, }; } From e024d8752dadbd1a51399fd31690cffd6948be6f Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 18:10:00 +0800 Subject: [PATCH 02/24] =?UTF-8?q?refactor(interface):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E9=80=82=E9=85=8D=E5=99=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E6=8C=89=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组 移除废弃的 fulfillOrder 方法 新增多个数据映射方法以支持统一数据格式转换 --- src/adapter/shopyy.adapter.ts | 1326 +++++++++++++-------- src/adapter/woocommerce.adapter.ts | 1411 ++++++++++++----------- src/controller/site-api.controller.ts | 20 - src/interface/site-adapter.interface.ts | 463 ++++++-- src/transformer/database.transformer.ts | 1 + src/transformer/file.transformer.ts | 1 + src/transformer/shopyy.transformer.ts | 0 src/transformer/woocommerce.adpater.ts | 8 + 8 files changed, 1907 insertions(+), 1323 deletions(-) create mode 100644 src/transformer/database.transformer.ts create mode 100644 src/transformer/file.transformer.ts create mode 100644 src/transformer/shopyy.transformer.ts create mode 100644 src/transformer/woocommerce.adpater.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 0cca7ff..1cf00fd 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -17,7 +17,10 @@ import { UnifiedAddressDTO, UnifiedShippingLineDTO, OrderFulfillmentStatus, - FulfillmentDTO + FulfillmentDTO, + CreateReviewDTO, + CreateVariationDTO, + UpdateReviewDTO } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { @@ -32,6 +35,7 @@ import { import { OrderStatus, } from '../enums/base.enum'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class ShopyyAdapter implements ISiteAdapter { shopyyFinancialStatusMap= { '200': '待支付', @@ -44,130 +48,181 @@ export class ShopyyAdapter implements ISiteAdapter { '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); - this.mapOrder = this.mapOrder.bind(this); - this.mapMedia = this.mapMedia.bind(this); - // this.mapSubscription = this.mapSubscription.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedVariation = this.mapPlatformToUnifiedVariation.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); } - private mapMedia(item: any): UnifiedMediaDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { + // 处理多地址结构 + const addresses = item.addresses || []; + const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); + + // 尝试从地址列表中获取billing和shipping + // 如果没有明确区分,默认使用默认地址或第一个地址 + const billingAddress = defaultAddress; + const shippingAddress = defaultAddress; + + const billing = { + first_name: billingAddress.first_name || item.first_name || '', + last_name: billingAddress.last_name || item.last_name || '', + fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), + company: billingAddress.company || '', + email: item.email || '', + phone: billingAddress.phone || item.contact || '', + address_1: billingAddress.address1 || '', + address_2: billingAddress.address2 || '', + city: billingAddress.city || '', + state: billingAddress.province || '', + postcode: billingAddress.zip || '', + country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' + }; + + const shipping = { + first_name: shippingAddress.first_name || item.first_name || '', + last_name: shippingAddress.last_name || item.last_name || '', + fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), + company: shippingAddress.company || '', + address_1: shippingAddress.address1 || '', + address_2: shippingAddress.address2 || '', + city: shippingAddress.city || '', + state: shippingAddress.province || '', + postcode: shippingAddress.zip || '', + country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' + }; + + return { + id: item.id || item.customer_id, + orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), + total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), + first_name: item.first_name || item.firstname || '', + last_name: item.last_name || item.lastname || '', + fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), + email: item.email || item.customer_email || '', + phone: item.contact || billing.phone || item.phone || '', + billing, + shipping, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), + raw: item, + }; + } + + mapUnifiedToPlatformCustomer(data: Partial) { + return data + } + + async getCustomer(id: string | number): Promise { + const customer = await this.shopyyService.getCustomer(this.site, id); + return this.mapPlatformToUnifiedCustomer(customer); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const { items, total, totalPages, page, per_page } = + await this.shopyyService.fetchCustomersPaged(this.site, params); + return { + items: items.map(this.mapPlatformToUnifiedCustomer.bind(this)), + total, + totalPages, + page, + per_page + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllCustomers 暂未实现 + throw new Error('Shopyy getAllCustomers 暂未实现'); + } + + async createCustomer(data: Partial): Promise { + const createdCustomer = await this.shopyyService.createCustomer(this.site, data); + return this.mapPlatformToUnifiedCustomer(createdCustomer); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + return this.mapPlatformToUnifiedCustomer(updatedCustomer); + } + + async deleteCustomer(id: string | number): Promise { + return await this.shopyyService.deleteCustomer(this.site, id); + } + + batchProcessCustomers?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { // 映射媒体项目 return { - id: item.id, - date_created: item.created_at, - date_modified: item.updated_at, - source_url: item.src, - title: item.alt || '', + id: data.id, + date_created: data.created_at, + date_modified: data.updated_at, + source_url: data.src, + title: data.alt || '', media_type: '', // Shopyy API未提供,暂时留空 mime_type: '', // Shopyy API未提供,暂时留空 }; } - private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { + mapUnifiedToPlatformMedia(data: Partial) { + return data + } + + async getMedia( + params: UnifiedSearchParamsDTO + ): Promise> { + const requestParams = this.mapMediaSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'media', // Shopyy的媒体API端点可能需要调整 + requestParams + ); + return { + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllMedia 暂未实现 + throw new Error('Shopyy getAllMedia 暂未实现'); + } + + async createMedia(file: any): Promise { + const createdMedia = await this.shopyyService.createMedia(this.site, file); + return this.mapPlatformToUnifiedMedia(createdMedia); + } + + async updateMedia(id: string | number, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + return this.mapPlatformToUnifiedMedia(updatedMedia); + } + + async deleteMedia(id: string | number): Promise { + return await this.shopyyService.deleteMedia(this.site, id); + } + + mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { return this.mapSearchParams(params) } - /** - * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 - * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 - */ - private mapSearchParams(params: UnifiedSearchParamsDTO): any { - // 处理分页参数 - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - - // 处理 where 条件 - const query: any = { - ...(params.where || {}), - page, - limit, - } - if(params.orderBy){ - const [field, dir] = Object.entries(params.orderBy)[0]; - query.order_by = dir === 'desc' ? 'desc' : 'asc'; - query.order_field = field - } - return query; - } - - private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { - // 映射产品状态 - function mapProductStatus(status: number) { - return status === 1 ? 'publish' : 'draft'; - } - return { - id: item.id, - name: item.name || item.title, - type: String(item.product_type ?? ''), - status: mapProductStatus(item.status), - sku: item.variant?.sku || '', - regular_price: String(item.variant?.price ?? ''), - sale_price: String(item.special_price ?? ''), - price: String(item.price ?? ''), - stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: item.inventory_quantity, - images: (item.images || []).map((img: any) => ({ - id: img.id || 0, - src: img.src, - name: '', - alt: img.alt || '', - // 排序 - position: img.position || '', - })), - attributes: (item.options || []).map(option => ({ - id: option.id || 0, - name: option.option_name || '', - options: (option.values || []).map(value => value.option_value || ''), - })), - tags: (item.tags || []).map((t: any) => ({ - id: t.id || 0, - name: t.name || '', - })), - // shopyy叫做专辑 - categories: item.collections.map((c: any) => ({ - id: c.id || 0, - name: c.title || '', - })), - variations: item.variants?.map(this.mapVariation.bind(this)) || [], - permalink: item.permalink, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : String(item.created_at ?? ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : String(item.updated_at ?? ''), - raw: item, - }; - } - - private mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { - // 映射变体 - return { - id: variant.id, - name: variant.sku || '', - sku: variant.sku || '', - regular_price: String(variant.price ?? ''), - sale_price: String(variant.special_price ?? ''), - price: String(variant.price ?? ''), - stock_status: - variant.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: variant.inventory_quantity, - }; - } - - 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 - } - private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_address || {}; const shipping = (item as any).shipping_address || {}; @@ -336,99 +391,162 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } - shopyyFulfillmentStatusMap = { - // 未发货 - '300': OrderFulfillmentStatus.PENDING, - // 部分发货 - '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, - // 已发货 - '320': OrderFulfillmentStatus.FULFILLED, - // 已取消 - '330': OrderFulfillmentStatus.CANCELLED, - // 确认发货 + + mapUnifiedToPlatformOrder(data: Partial) { + return data } - private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { - // 处理多地址结构 - const addresses = item.addresses || []; - const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); - - // 尝试从地址列表中获取billing和shipping - // 如果没有明确区分,默认使用默认地址或第一个地址 - const billingAddress = defaultAddress; - const shippingAddress = defaultAddress; - - const billing = { - first_name: billingAddress.first_name || item.first_name || '', - last_name: billingAddress.last_name || item.last_name || '', - fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), - company: billingAddress.company || '', - email: item.email || '', - phone: billingAddress.phone || item.contact || '', - address_1: billingAddress.address1 || '', - address_2: billingAddress.address2 || '', - city: billingAddress.city || '', - state: billingAddress.province || '', - postcode: billingAddress.zip || '', - country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' - }; - - const shipping = { - first_name: shippingAddress.first_name || item.first_name || '', - last_name: shippingAddress.last_name || item.last_name || '', - fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), - company: shippingAddress.company || '', - address_1: shippingAddress.address1 || '', - address_2: shippingAddress.address2 || '', - city: shippingAddress.city || '', - state: shippingAddress.province || '', - postcode: shippingAddress.zip || '', - country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' - }; - - return { - id: item.id || item.customer_id, - orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), - total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), - first_name: item.first_name || item.firstname || '', - last_name: item.last_name || item.lastname || '', - fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), - email: item.email || item.customer_email || '', - phone: item.contact || billing.phone || item.phone || '', - billing, - shipping, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), - raw: item, - }; - } - mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { - return this.mapSearchParams(query) + mapCreateOrderParams(data: Partial): any { + return data } - async getProducts( + mapUpdateOrderParams(data: Partial): any { + // 构建 ShopYY 订单更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.status !== undefined) { + // 映射订单状态 + const statusMap = { + [OrderStatus.PENDING]: 100, // pending -> 100 未完成 + [OrderStatus.PROCESSING]: 110, // processing -> 110 待处理 + [OrderStatus.COMPLETED]: 180, // completed -> 180 已完成 + [OrderStatus.CANCEL]: 190 // cancel -> 190 取消 + }; + params.status = statusMap[data.status] || 100; + } + + if (data.payment_method !== undefined) { + params.payment_method = data.payment_method; + } + + if (data.billing) { + // 更新客户信息 + if (data.billing.first_name !== undefined) { + params.firstname = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.lastname = data.billing.last_name; + } + if (data.billing.email !== undefined) { + params.email = data.billing.email; + } + if (data.billing.phone !== undefined) { + params.phone = data.billing.phone; + } + + // 更新账单地址 + params.billing_address = params.billing_address || {}; + if (data.billing.first_name !== undefined) { + params.billing_address.first_name = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.billing_address.last_name = data.billing.last_name; + } + if (data.billing.company !== undefined) { + params.billing_address.company = data.billing.company; + } + if (data.billing.address_1 !== undefined) { + params.billing_address.address1 = data.billing.address_1; + } + if (data.billing.address_2 !== undefined) { + params.billing_address.address2 = data.billing.address_2; + } + if (data.billing.city !== undefined) { + params.billing_address.city = data.billing.city; + } + if (data.billing.state !== undefined) { + params.billing_address.province = data.billing.state; + } + if (data.billing.postcode !== undefined) { + params.billing_address.zip = data.billing.postcode; + } + if (data.billing.country !== undefined) { + params.billing_address.country_code = data.billing.country; + } + } + + if (data.shipping) { + // 更新送货地址 + params.shipping_address = params.shipping_address || {}; + if (data.shipping.first_name !== undefined) { + params.shipping_address.first_name = data.shipping.first_name; + } + if (data.shipping.last_name !== undefined) { + params.shipping_address.last_name = data.shipping.last_name; + } + if (data.shipping.company !== undefined) { + params.shipping_address.company = data.shipping.company; + } + if (data.shipping.address_1 !== undefined) { + params.shipping_address.address1 = data.shipping.address_1; + } + if (data.shipping.address_2 !== undefined) { + params.shipping_address.address2 = data.shipping.address_2; + } + if (data.shipping.city !== undefined) { + params.shipping_address.city = data.shipping.city; + } + if (data.shipping.state !== undefined) { + params.shipping_address.province = data.shipping.state; + } + if (data.shipping.postcode !== undefined) { + params.shipping_address.zip = data.shipping.postcode; + } + if (data.shipping.country !== undefined) { + params.shipping_address.country_code = data.shipping.country; + } + if (data.shipping.phone !== undefined) { + params.shipping_address.phone = data.shipping.phone; + } + } + + // 更新订单项 + if (data.line_items && data.line_items.length > 0) { + params.products = data.line_items.map((item: UnifiedOrderLineItemDTO) => ({ + product_id: item.product_id, + quantity: item.quantity, + // price: item.price || '0.00', + sku: item.sku || '', + })); + } + + // 更新物流信息 + if (data.shipping_lines && data.shipping_lines.length > 0) { + const shippingLine = data.shipping_lines[0]; + if (shippingLine.method_title !== undefined) { + params.shipping_method = shippingLine.method_title; + } + if (shippingLine.total !== undefined) { + params.shipping_price = shippingLine.total; + } + } + + // // 更新备注信息 + // if (data.note !== undefined) { + // params.note = data.note; + // } + + return params; + } + + async getOrder(id: string | number): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(id)); + return this.mapPlatformToUnifiedOrder(data); + } + + async getOrders( params: UnifiedSearchParamsDTO - ): Promise> { - // 转换搜索参数 - const requestParams = this.mapProductQuery(params); - const response = await this.shopyyService.fetchResourcePaged( + ): Promise> { + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, - 'products/list', - requestParams + 'orders', + normalizedParams ); - 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)) return { - items: finalItems as UnifiedProductDTO[], + items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), total, totalPages, page, @@ -436,34 +554,37 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + const data = await this.shopyyService.getAllOrders(this.site.id, params); + return data.map(this.mapPlatformToUnifiedOrder.bind(this)); } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapProduct(product); + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; } - async createProduct(data: Partial): Promise { - const res = await this.shopyyService.createProduct(this.site, data); - return this.mapProduct(res); - } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). - await this.shopyyService.updateProduct(this.site, String(id), data); - return true; + async createOrder(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateOrderParams(data); + const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); + return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateVariation(productId: string | number, variationId: string | number, data: any): Promise { - await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data); - return { ...data, id: variationId }; + async updateOrder(id: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateOrderParams(data); + return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + } + + async deleteOrder(id: string | number): Promise { + return await this.shopyyService.deleteOrder(this.site, id); } async getOrderNotes(orderId: string | number): Promise { @@ -474,22 +595,65 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.createOrderNote(this.site, orderId, data); } - async deleteProduct(id: string | number): Promise { - // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); - return true; + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + // 取消订单履行 + try { + // 调用 ShopyyService 的取消履行方法 + const cancelShipData = { + order_id: String(orderId), + fulfillment_id: data.shipment_id || '' + }; + const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); + + return { + success: result, + order_id: orderId, + shipment_id: data.shipment_id, + reason: data.reason, + cancelled_at: new Date().toISOString() + }; + } catch (error) { + throw new Error(`履行失败: ${error.message}`); + } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - return await this.shopyyService.batchProcessProducts(this.site, data); + async getOrderFulfillments(orderId: string | number): Promise { + return await this.shopyyService.getFulfillments(this.site, String(orderId)); } - /** - * 将统一的订单查询参数转换为 ShopYY 订单查询参数 - * 包含状态映射等特殊处理 - */ - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + + async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { + // 调用 Shopyy Service 的 createFulfillment 方法 + const fulfillmentData = { + tracking_number: data.tracking_number, + 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); + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: string; + tracking_provider?: string; + date_shipped?: string; + status_shipped?: string; + }): Promise { + return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + batchProcessOrders?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 首先使用通用参数转换 const baseParams = this.mapSearchParams(params); @@ -518,18 +682,205 @@ export class ShopyyAdapter implements ISiteAdapter { return baseParams; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - // 转换订单查询参数 - const normalizedParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + // 映射产品状态 + function mapProductStatus(status: number) { + return status === 1 ? 'publish' : 'draft'; + } return { - items: items.map(this.mapOrder.bind(this)), + id: item.id, + name: item.name || item.title, + type: String(item.product_type ?? ''), + status: mapProductStatus(item.status), + sku: item.variant?.sku || '', + regular_price: String(item.variant?.price ?? ''), + sale_price: String(item.special_price ?? ''), + price: String(item.price ?? ''), + stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: item.inventory_quantity, + images: (item.images || []).map((img: any) => ({ + id: img.id || 0, + src: img.src, + name: '', + alt: img.alt || '', + // 排序 + position: img.position || '', + })), + attributes: (item.options || []).map(option => ({ + id: option.id || 0, + name: option.option_name || '', + options: (option.values || []).map(value => value.option_value || ''), + })), + tags: (item.tags || []).map((t: any) => ({ + id: t.id || 0, + name: t.name || '', + })), + // shopyy叫做专辑 + categories: item.collections.map((c: any) => ({ + id: c.id || 0, + name: c.title || '', + })), + variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], + permalink: item.permalink, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : String(item.created_at ?? ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : String(item.updated_at ?? ''), + raw: item, + }; + } + + mapUnifiedToPlatformProduct(data: Partial) { + return data + } + + mapCreateProductParams(data: Partial): any { + // 构建 ShopYY 产品创建参数 + const params: any = { + name: data.name || '', + product_type: data.type || 1, // 默认简单产品 + status: this.mapStatus(data.status || 'publish'), + price: data.price || '0.00', + special_price: data.sale_price || '', + inventory_tracking: data.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: data.stock_quantity || 0, + }; + + // 添加变体信息 + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => ({ + sku: variation.sku || '', + price: variation.price || '0.00', + special_price: variation.sale_price || '', + inventory_tracking: variation.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: variation.stock_quantity || 0, + })); + } + + // 添加图片信息 + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息 + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息 + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + mapUpdateProductParams(data: Partial): any { + // 映射产品状态: publish -> 1, draft -> 0 + const mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 构建 ShopYY 产品更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.name !== undefined) params.name = data.name; + if (data.type !== undefined) params.product_type = data.type; + if (data.status !== undefined) params.status = mapStatus(data.status); + if (data.price !== undefined) params.price = data.price; + if (data.sale_price !== undefined) params.special_price = data.sale_price; + if (data.sku !== undefined) params.sku = data.sku; + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + // 添加变体信息(如果存在) + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => { + const variationParams: any = {}; + if (variation.id !== undefined) variationParams.id = variation.id; + if (variation.sku !== undefined) variationParams.sku = variation.sku; + if (variation.price !== undefined) variationParams.price = variation.price; + if (variation.sale_price !== undefined) variationParams.special_price = variation.sale_price; + if (variation.stock_quantity !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_quantity; + } + if (variation.stock_status !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_status === 'instock' ? (variation.stock_quantity || 1) : 0; + } + return variationParams; + }); + } + + // 添加图片信息(如果存在) + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + id: image.id, + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息(如果存在) + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息(如果存在) + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + async getProduct(id: string | number): Promise { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, id); + return this.mapPlatformToUnifiedProduct(product); + } + + async getProducts( + params: UnifiedSearchParamsDTO + ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); + const response = await this.shopyyService.fetchResourcePaged( + this.site, + 'products/list', + requestParams + ); + const { items = [], total, totalPages, page, per_page } = response; + const finalItems = items.map((item) => ({ + ...item, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, + })).map(this.mapPlatformToUnifiedProduct.bind(this)) + return { + items: finalItems as UnifiedProductDTO[], total, totalPages, page, @@ -537,207 +888,61 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams = { - where, - page: 1, - per_page: 1, - } - const data = await this.getOrders(searchParams); - return data.total || 0; + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllProducts 暂未实现 + throw new Error('Shopyy getAllProducts 暂未实现'); } - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - const data = await this.shopyyService.getAllOrders(this.site.id, params); - return data.map(this.mapOrder.bind(this)); + async createProduct(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateProductParams(data); + const res = await this.shopyyService.createProduct(this.site, requestParams); + return this.mapPlatformToUnifiedProduct(res); } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); - return this.mapOrder(data); + async updateProduct(id: string | number, data: Partial): Promise { + // Shopyy update returns boolean? + // shopyyService.updateProduct returns boolean. + // So I can't return the updated product. + // I have to fetch it again or return empty/input. + // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + // 使用映射方法转换参数 + const requestParams = this.mapUpdateProductParams(data); + await this.shopyyService.updateProduct(this.site, String(id), requestParams); + return true; } - async createOrder(data: Partial): Promise { - const createdOrder = await this.shopyyService.createOrder(this.site, data); - return this.mapOrder(createdOrder); + async deleteProduct(id: string | number): Promise { + // Use batch delete + await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + return true; } - async updateOrder(id: string | number, data: Partial): Promise { - return await this.shopyyService.updateOrder(this.site, String(id), data); + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + return await this.shopyyService.batchProcessProducts(this.site, data); } - async deleteOrder(id: string | number): Promise { - return await this.shopyyService.deleteOrder(this.site, id); + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) } - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - // 订单履行(发货) - try { - // 判断是否为部分发货(包含 items) - if (data.items && data.items.length > 0) { - // 部分发货 - const partShipData = { - order_number: String(orderId), - note: data.shipping_method || '', - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: '1', // 默认快递公司代码 - products: data.items.map(item => ({ - quantity: item.quantity, - order_product_id: String(item.order_item_id) - })) - }; - return await this.shopyyService.partFulfillOrder(this.site, partShipData); - } else { - // 批量发货(完整发货) - const batchShipData = { - order_number: String(orderId), - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: 1, // 默认快递公司代码 - note: data.shipping_method || '', - mode: null // 新增模式 - }; - return await this.shopyyService.batchFulfillOrders(this.site, batchShipData); - } - } catch (error) { - throw new Error(`履行失败: ${error.message}`); - } + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data } - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - // 取消订单履行 - try { - // 调用 ShopyyService 的取消履行方法 - const cancelShipData = { - order_id: String(orderId), - fulfillment_id: data.shipment_id || '' - }; - const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); - - return { - success: result, - order_id: orderId, - shipment_id: data.shipment_id, - reason: data.reason, - cancelled_at: new Date().toISOString() - }; - } catch (error) { - throw new Error(`取消履行失败: ${error.message}`); - } + mapUnifiedToPlatformReview(data: Partial) { + return data } - /** - * 获取订单履行信息 - * @param orderId 订单ID - * @returns 履行信息列表 - */ - async getOrderFulfillments(orderId: string | number): Promise { - return await this.shopyyService.getFulfillments(this.site, String(orderId)); + mapCreateReviewParams(data: CreateReviewDTO) { + return data } - /** - * 创建订单履行信息 - * @param orderId 订单ID - * @param data 履行数据 - * @returns 创建结果 - */ - async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { - // 调用 Shopyy Service 的 createFulfillment 方法 - const fulfillmentData = { - tracking_number: data.tracking_number, - 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); - } - - /** - * 更新订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @param data 更新数据 - * @returns 更新结果 - */ - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: string; - tracking_provider?: string; - date_shipped?: string; - status_shipped?: string; - }): Promise { - return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); - } - - /** - * 删除订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @returns 删除结果 - */ - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - throw new Error('Shopyy does not support subscriptions.'); - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllSubscriptions 暂未实现 - throw new Error('Shopyy getAllSubscriptions 暂未实现'); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapMediaSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'media', // Shopyy的媒体API端点可能需要调整 - requestParams - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllMedia 暂未实现 - throw new Error('Shopyy getAllMedia 暂未实现'); - } - - async createMedia(file: any): Promise { - const createdMedia = await this.shopyyService.createMedia(this.site, file); - return this.mapMedia(createdMedia); - } - - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); - return this.mapMedia(updatedMedia); - } - - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data } async getReviews( @@ -762,12 +967,21 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllReviews 暂未实现'); } - async getReview(id: string | number): Promise { - const review = await this.shopyyService.getReview(this.site, id); - return this.mapReview(review); + async createReview(data: any): Promise { + const createdReview = await this.shopyyService.createReview(this.site, data); + return this.mapReview(createdReview); } - private mapReview(review: any): UnifiedReviewDTO { + async updateReview(id: string | number, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, id, data); + return this.mapReview(updatedReview); + } + + async deleteReview(id: string | number): Promise { + return await this.shopyyService.deleteReview(this.site, id); + } + + mapReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -784,7 +998,7 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { + mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { const { search, page, per_page, where } = params; const shopyyParams: any = { page: page || 1, @@ -799,38 +1013,133 @@ export class ShopyyAdapter implements ISiteAdapter { shopyyParams.status = where.status; } - // if (product_id) { - // shopyyParams.product_id = product_id; - // } - return shopyyParams; } - async createReview(data: any): Promise { - const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + mapUnifiedToPlatformSubscription(data: Partial) { + return data } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async getSubscriptions( + params: UnifiedSearchParamsDTO + ): Promise> { + throw new Error('Shopyy does not support subscriptions.'); } - // Webhook相关方法 - private mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllSubscriptions 暂未实现 + throw new Error('Shopyy getAllSubscriptions 暂未实现'); + } + + // ========== 产品变体映射方法 ========== + mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { + // 映射变体 return { - id: item.id, - name: item.webhook_name || `Webhook-${item.id}`, - topic: item.event_code || '', - delivery_url: item.url || '', - status: 'active', + id: variant.id, + name: variant.sku || '', + sku: variant.sku || '', + regular_price: String(variant.price ?? ''), + sale_price: String(variant.special_price ?? ''), + price: String(variant.price ?? ''), + stock_status: + variant.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: variant.inventory_quantity, }; } + mapUnifiedToPlatformVariation(data: Partial) { + return data + } + + mapCreateVariationParams(data: CreateVariationDTO) { + return data + } + + mapUpdateVariationParams(data: Partial): any { + // 构建 ShopYY 变体更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.id !== undefined) { + params.id = data.id; + } + if (data.sku !== undefined) { + params.sku = data.sku; + } + if (data.price !== undefined) { + params.price = data.price; + } + if (data.sale_price !== undefined) { + params.special_price = data.sale_price; + } + + // 处理库存信息 + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + return params; + } + + async getVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy getVariation 暂未实现'); + } + + async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getVariations 暂未实现'); + } + + async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getAllVariations 暂未实现'); + } + + async createVariation(productId: string | number, data: any): Promise { + throw new Error('Shopyy createVariation 暂未实现'); + } + + async updateVariation(productId: string | number, variationId: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateVariationParams(data); + await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), requestParams); + return { ...data, id: variationId }; + } + + async deleteVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy deleteVariation 暂未实现'); + } + + // ========== Webhook映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data + } + + mapUnifiedToPlatformWebhook(data: Partial) { + return data + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data + } + + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data + } + + async getWebhook(id: string | number): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, id); + return this.mapWebhook(webhook); + } + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { @@ -847,11 +1156,6 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllWebhooks 暂未实现'); } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); - } - async createWebhook(data: CreateWebhookDTO): Promise { const createdWebhook = await this.shopyyService.createWebhook(this.site, data); return this.mapWebhook(createdWebhook); @@ -866,6 +1170,17 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteWebhook(this.site, id); } + mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + return { + id: item.id, + name: item.webhook_name || `Webhook-${item.id}`, + topic: item.event_code || '', + delivery_url: item.url || '', + status: 'active', + }; + } + + // ========== 站点/其他方法 ========== async getLinks(): Promise> { // ShopYY站点的管理后台链接通常基于apiUrl构建 const url = this.site.websiteUrl @@ -885,59 +1200,58 @@ export class ShopyyAdapter implements ISiteAdapter { return links; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchCustomersPaged(this.site, params); - return { - items: items.map(this.mapCustomer.bind(this)), - total, - totalPages, + // ========== 辅助方法 ========== + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), page, - per_page - }; + limit, + } + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllCustomers 暂未实现 - throw new Error('Shopyy getAllCustomers 暂未实现'); + // 映射产品状态: publish -> 1, draft -> 0 + mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 映射库存状态: instock -> 1, outofstock -> 0 + mapStockStatus = (stockStatus: string) => { + return stockStatus === 'instock' ? 1 : 0; + }; + + 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 } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); - return this.mapCustomer(customer); + shopyyFulfillmentStatusMap = { + // 未发货 + '300': OrderFulfillmentStatus.PENDING, + // 部分发货 + '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, + // 已发货 + '320': OrderFulfillmentStatus.FULFILLED, + // 已取消 + '330': OrderFulfillmentStatus.CANCELLED, + // 确认发货 } - - async createCustomer(data: Partial): Promise { - const createdCustomer = await this.shopyyService.createCustomer(this.site, data); - return this.mapCustomer(createdCustomer); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); - return this.mapCustomer(updatedCustomer); - } - - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); - } - - async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getVariations 暂未实现'); - } - - async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getAllVariations 暂未实现'); - } - - async getVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy getVariation 暂未实现'); - } - - async createVariation(productId: string | number, data: any): Promise { - throw new Error('Shopyy createVariation 暂未实现'); - } - - async deleteVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy deleteVariation 暂未实现'); - } -} +} \ No newline at end of file diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 9828a07..a5f2f0a 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -15,6 +15,8 @@ import { UpdateVariationDTO, UnifiedProductVariationDTO, UnifiedVariationPaginationDTO, + CreateReviewDTO, + UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { @@ -29,6 +31,7 @@ import { } from '../dto/woocommerce.dto'; import { Site } from '../entity/site.entity'; import { WPService } from '../service/wp.service'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 @@ -40,192 +43,236 @@ export class WooCommerceAdapter implements ISiteAdapter { this.mapOrder = this.mapOrder.bind(this); this.mapWebhook = this.mapWebhook.bind(this); } + batchProcessProducts?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + mapCreateVariationParams(data: CreateVariationDTO) { + throw new Error('Method not implemented.'); + } + mapUpdateVariationParams(data: UpdateVariationDTO) { + throw new Error('Method not implemented.'); + } - // 映射 WooCommerce webhook 到统一格式 - private mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { + return data; + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data; + } + + mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + // 将 WooCommerce 客户数据映射为统一客户DTO + // 包含基础信息地址信息与时间信息 return { - id: webhook.id.toString(), - name: webhook.name, - status: webhook.status, - topic: webhook.topic, - delivery_url: webhook.delivery_url, - secret: webhook.secret, - api_version: webhook.api_version, - date_created: webhook.date_created, - date_modified: webhook.date_modified, - // metadata: webhook.meta_data || [], + id: item.id, + avatar: item.avatar_url, + email: item.email, + orders: Number(item.orders ?? 0), + total_spend: Number(item.total_spent ?? 0), + first_name: item.first_name, + last_name: item.last_name, + username: item.username, + phone: item.billing?.phone || item.shipping?.phone, + billing: item.billing, + shipping: item.shipping, + date_created: item.date_created, + date_modified: item.date_modified, + raw: item, }; } - // 获取站点的 webhooks 列表 - async getWebhooks(params: UnifiedSearchParamsDTO): Promise { - try { - const result = await this.wpService.getWebhooks(this.site, params); - - return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; - } catch (error) { - throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取所有webhooks - async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { - try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); - } catch (error) { - throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { - try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 创建新的 webhook - async createWebhook(data: CreateWebhookDTO): Promise { - try { - const params = { - name: data.name, - status: 'active', // 默认状态为活跃 - topic: data.topic, - delivery_url: data.delivery_url, - secret: data.secret, - api_version: data.api_version || 'wp/v2', - }; - const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { - try { - const params = { - ...(data.name ? { name: data.name } : {}), - ...(data.status ? { status: data.status } : {}), - ...(data.topic ? { topic: data.topic } : {}), - ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), - ...(data.secret ? { secret: data.secret } : {}), - ...(data.api_version ? { api_version: data.api_version } : {}), - }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { - try { - await this.wpService.deleteWebhook(this.site, id); - return true; - } catch (error) { - throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - async getLinks(): Promise> { - const baseUrl = this.site.apiUrl; - const links = [ - { title: '访问网站', url: baseUrl }, - { title: '管理后台', url: `${baseUrl}/wp-admin/` }, - { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, - { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, - { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, - { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, - { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, - { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, - { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, - ]; - return links; - } - - createMedia(file: any): Promise { - throw new Error('Method not implemented.'); - } - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - - - - private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { + mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; + const mapped: any = { ...(params.search ? { search: params.search } : {}), - ...(where.status ? { status: where.status } : {}), page, per_page, }; + // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 + if (params.orderBy) { + // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } + if (typeof params.orderBy === 'string') { + const [field, direction = 'desc'] = params.orderBy.split(':'); + mapped.orderby = field; + mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; + } else if (typeof params.orderBy === 'object') { + const entries = Object.entries(params.orderBy); + if (entries.length > 0) { + const [field, direction] = entries[0]; + mapped.orderby = field; + mapped.order = direction === 'asc' ? 'asc' : 'desc'; + } + } + } + const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); - if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); - if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); - if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); - if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); - if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); - if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); - if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); - if (where.offset !== undefined) mapped.offset = Number(where.offset); - if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); - if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); - if (where.slug) mapped.slug = String(where.slug); - if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { - if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); - if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); - if (where.status) mapped.status = String(where.status); - } - if (where.type) mapped.type = String(where.type); - if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); - if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); - if (where.sku) mapped.sku = String(where.sku); - if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); - if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); - if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); - if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); - if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); - if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); - if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); - if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); - if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); - if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); - if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); - if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); - if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); + const toNumber = (value: any): number | undefined => { + if (value === undefined || value === null || value === '') return undefined; + const n = Number(value); + return Number.isFinite(n) ? n : undefined; + }; + + if (where.exclude) mapped.exclude = toArray(where.exclude); + if (where.include) mapped.include = toArray(where.include); + if (where.ids) mapped.include = toArray(where.ids); + if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); + + if (where.email) mapped.email = String(where.email); + const roleSource = where.role; + if (roleSource !== undefined) mapped.role = String(roleSource); return mapped; } - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 客户操作方法 + async getCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`customers/${id}`); + return this.mapCustomer(res.data); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapCustomerSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'customers', + requestParams + ); + return { + items: items.map((i: any) => this.mapCustomer(i)), + total, + totalPages, + page, + per_page, + + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有客户数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // 处理orderBy参数,转换为WooCommerce API需要的格式 + const requestParams = this.mapCustomerSearchParams(params || {}); + + const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); + return customers.map((customer: any) => this.mapCustomer(customer)); + } + + async createCustomer(data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('customers', data); + return this.mapCustomer(res.data); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.put(`customers/${id}`, data); + return this.mapCustomer(res.data); + } + + async deleteCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`customers/${id}`, { force: true }); + return true; + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { + return data; + } + mapUnifiedToPlatformMedia(data: Partial) { + return data; + } + + mapMedia(item: WpMedia): UnifiedMediaDTO { + // 将 WordPress 媒体数据映射为统一媒体DTO + // 兼容不同字段命名的时间信息 + return { + id: item.id, + title: + typeof item.title === 'string' + ? item.title + : item.title?.rendered || '', + media_type: item.media_type, + mime_type: item.mime_type, + source_url: item.source_url, + date_created: item.date_created ?? item.date, + date_modified: item.date_modified ?? item.modified, + }; + } + + // 媒体操作方法 + async getMedia(params: UnifiedSearchParamsDTO): Promise> { + // 获取媒体列表并映射为统一媒体DTO集合 + const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( + this.site, + params + ); + return { + items: items.map(this.mapMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有媒体数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const media = await this.wpService.sdkGetAll(api, 'media', params); + return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + } + + createMedia(file: any): Promise { + throw new Error('Method not implemented.'); + } + + async updateMedia(id: string | number, data: any): Promise { + // 更新媒体信息 + return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + } + + async deleteMedia(id: string | number): Promise { + // 删除媒体资源 + await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + return true; + } + + async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { + // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 + const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); + return result as any; + } + + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { + return data; + } + mapUnifiedToPlatformOrder(data: Partial) { + return data; + } + + mapCreateOrderParams(data: Partial) { + return data; + } + mapUpdateOrderParams(data: Partial) { + return data; + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 计算分页参数 const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); @@ -293,60 +340,328 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - private mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { + private buildFullAddress(addr: any): string { + if (!addr) return ''; + const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); + return [ + name, + addr.company, + addr.address_1, + addr.address_2, + addr.city, + addr.state, + addr.postcode, + addr.country, + addr.phone + ].filter(Boolean).join(', '); + } + + mapOrder(item: WooOrder): UnifiedOrderDTO { + // 将 WooCommerce 订单数据映射为统一订单DTO + // 包含账单地址与收货地址以及创建与更新时间 + + // 映射物流追踪信息,将后端格式转换为前端期望的格式 + const fulfillments = (item.fulfillments || []).map((track: any) => ({ + tracking_number: track.tracking_number || '', + shipping_provider: track.shipping_provider || '', + shipping_method: track.shipping_method || '', + status: track.status || '', + date_created: track.date_created || '', + items: track.items || [], + })); + + return { + id: item.id, + number: item.number, + status: item.status, + currency: item.currency, + total: item.total, + customer_id: item.customer_id, + customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 + email: item.billing?.email || '', + customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), + refunds: item.refunds?.map?.(refund => ({ + id: refund.id, + reason: refund.reason, + total: refund.total, + })), + line_items: (item.line_items as any[]).map(li => ({ + ...li, + productId: li.product_id, + })), + customer_ip_address: item.customer_ip_address ?? '', + date_paid: item.date_paid ?? '', + utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', + device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', + source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', + billing: item.billing, + shipping: item.shipping, + billing_full_address: this.buildFullAddress(item.billing), + shipping_full_address: this.buildFullAddress(item.shipping), + payment_method: item.payment_method_title, + date_created: item.date_created, + date_modified: item.date_modified, + shipping_lines: item.shipping_lines, + fee_lines: item.fee_lines, + coupon_lines: item.coupon_lines, + fulfillments, + raw: item, + }; + } + + // 订单操作方法 + async getOrder(id: string | number): Promise { + // 获取单个订单详情 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${id}`); + return this.mapOrder(res.data); + } + + async getOrders(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + + // 并行获取所有订单的履行信息 + const ordersWithFulfillments = await Promise.all( + items.map(async (order: any) => { + try { + // 获取订单的履行信息 + const fulfillments = await this.getOrderFulfillments(order.id); + // 将履行信息添加到订单对象中 + return { + ...order, + fulfillments: fulfillments || [] + }; + } catch (error) { + // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 + console.error(`获取订单 ${order.id} 的履行信息失败:`, error); + return { + ...order, + fulfillments: [] + }; + } + }) + ); + + return { + items: ordersWithFulfillments.map(this.mapOrder), + total, + totalPages, + page, + per_page, + }; + } + + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订单数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const orders = await this.wpService.sdkGetAll(api, 'orders', params); + return orders.map((order: any) => this.mapOrder(order)); + } + + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + + async createOrder(data: Partial): Promise { + // 创建订单并返回统一订单DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('orders', data); + return this.mapOrder(res.data); + } + + async updateOrder(id: string | number, data: Partial): Promise { + // 更新订单并返回布尔结果 + return await this.wpService.updateOrder(this.site, String(id), data as any); + } + + async deleteOrder(id: string | number): Promise { + // 删除订单 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`orders/${id}`, { force: true }); + return true; + } + + async getOrderFulfillments(orderId: string | number): Promise { + return await this.wpService.getFulfillments(this.site, String(orderId)); + } + + async createOrderFulfillment(orderId: string | number, data: { + tracking_number: string; + shipping_provider: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; + }): Promise { + const shipmentData: any = { + shipping_provider: data.shipping_provider, + tracking_number: data.tracking_number, + }; + + if (data.shipping_method) { + shipmentData.shipping_method = data.shipping_method; + } + + 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); + return response.data; + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: 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); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + async getOrderNotes(orderId: string | number): Promise { + // 获取订单备注列表 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${orderId}/notes`); + return res.data; + } + + async createOrderNote(orderId: string | number, data: any): Promise { + // 创建订单备注 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post(`orders/${orderId}/notes`, data); + return res.data; + } + + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + throw new Error('暂未实现'); + // 取消订单履行 + // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // try { + // // 将订单状态改回处理中 + // await api.put(`orders/${orderId}`, { status: 'processing' }); + + // // 添加取消履行的备注 + // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; + // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); + + // return { + // success: true, + // order_id: orderId, + // shipment_id: data.shipment_id, + // reason: data.reason, + // cancelled_at: new Date().toISOString() + // }; + // } catch (error) { + // throw new Error(`取消履行失败: ${error.message}`); + // } + } + + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { + return data; + } + mapUnifiedToPlatformProduct(data: Partial) { + return data; + } + + mapCreateProductParams(data: Partial) { + return data; + } + mapUpdateProductParams(data: Partial) { + return data; + } + + mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; - const mapped: any = { ...(params.search ? { search: params.search } : {}), + ...(where.status ? { status: where.status } : {}), page, per_page, }; - // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 - if (params.orderBy) { - // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } - if (typeof params.orderBy === 'string') { - const [field, direction = 'desc'] = params.orderBy.split(':'); - mapped.orderby = field; - mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; - } else if (typeof params.orderBy === 'object') { - const entries = Object.entries(params.orderBy); - if (entries.length > 0) { - const [field, direction] = entries[0]; - mapped.orderby = field; - mapped.order = direction === 'asc' ? 'asc' : 'desc'; - } - } - } - const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - const toNumber = (value: any): number | undefined => { - if (value === undefined || value === null || value === '') return undefined; - const n = Number(value); - return Number.isFinite(n) ? n : undefined; - }; - - if (where.exclude) mapped.exclude = toArray(where.exclude); - if (where.include) mapped.include = toArray(where.include); - if (where.ids) mapped.include = toArray(where.ids); - if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); - - if (where.email) mapped.email = String(where.email); - const roleSource = where.role; - if (roleSource !== undefined) mapped.role = String(roleSource); + if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); + if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); + if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); + if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); + if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); + if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); + if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); + if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); + if (where.offset !== undefined) mapped.offset = Number(where.offset); + if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); + if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); + if (where.slug) mapped.slug = String(where.slug); + if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { + if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); + if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); + if (where.status) mapped.status = String(where.status); + } + if (where.type) mapped.type = String(where.type); + if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); + if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); + if (where.sku) mapped.sku = String(where.sku); + if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); + if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); + if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); + if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); + if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); + if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); + if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); + if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); + if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); + if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); + if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); + if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); + if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); return mapped; } - private mapProduct(item: WooProduct): UnifiedProductDTO { + mapProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -434,120 +749,41 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - private buildFullAddress(addr: any): string { - if (!addr) return ''; - const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); - return [ - name, - addr.company, - addr.address_1, - addr.address_2, - addr.city, - addr.state, - addr.postcode, - addr.country, - addr.phone - ].filter(Boolean).join(', '); - } - private mapOrder(item: WooOrder): UnifiedOrderDTO { - // 将 WooCommerce 订单数据映射为统一订单DTO - // 包含账单地址与收货地址以及创建与更新时间 - // 映射物流追踪信息,将后端格式转换为前端期望的格式 - const fulfillments = (item.fulfillments || []).map((track: any) => ({ - tracking_number: track.tracking_number || '', - shipping_provider: track.shipping_provider || '', - shipping_method: track.shipping_method || '', - status: track.status || '', - date_created: track.date_created || '', - items: track.items || [], - })); + // 产品操作方法 + async getProduct(id: string | number): Promise { + // 获取单个产品详情并映射为统一产品DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); + const product = res.data; - return { - id: item.id, - number: item.number, - status: item.status, - currency: item.currency, - total: item.total, - customer_id: item.customer_id, - customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 - email: item.billing?.email || '', - customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), - refunds: item.refunds?.map?.(refund => ({ - id: refund.id, - reason: refund.reason, - total: refund.total, - })), - line_items: (item.line_items as any[]).map(li => ({ - ...li, - productId: li.product_id, - })), - customer_ip_address: item.customer_ip_address ?? '', - date_paid: item.date_paid ?? '', - utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', - device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', - source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', - billing: item.billing, - shipping: item.shipping, - billing_full_address: this.buildFullAddress(item.billing), - shipping_full_address: this.buildFullAddress(item.shipping), - payment_method: item.payment_method_title, - date_created: item.date_created, - date_modified: item.date_modified, - shipping_lines: item.shipping_lines, - fee_lines: item.fee_lines, - coupon_lines: item.coupon_lines, - fulfillments, - raw: item, - }; + // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 + if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { + try { + // 批量获取该产品的所有变体数据 + const variations = await this.wpService.sdkGetAll( + api, + `products/${product.id}/variations` + ); + // 将完整的变体数据添加到产品对象中 + product.variations = variations; + } catch (error) { + // 如果获取变体失败,保持原有的 ID 数组 + console.error(`获取产品 ${product.id} 的变体数据失败:`, error); + } + } + + return this.mapProduct(product); } - private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { - // 将 WooCommerce 订阅数据映射为统一订阅DTO - // 若缺少创建时间则回退为开始时间 - return { - id: item.id, - status: item.status, - customer_id: item.customer_id, - billing_period: item.billing_period, - billing_interval: item.billing_interval, - date_created: item.date_created ?? item.start_date, - date_modified: item.date_modified, - start_date: item.start_date, - next_payment_date: item.next_payment_date, - line_items: item.line_items, - raw: item, - }; - } - - private mapMedia(item: WpMedia): UnifiedMediaDTO { - // 将 WordPress 媒体数据映射为统一媒体DTO - // 兼容不同字段命名的时间信息 - return { - id: item.id, - title: - typeof item.title === 'string' - ? item.title - : item.title?.rendered || '', - media_type: item.media_type, - mime_type: item.mime_type, - source_url: item.source_url, - date_created: item.date_created ?? item.date, - date_modified: item.date_modified ?? item.modified, - }; - } - - async getProducts( - params: UnifiedSearchParamsDTO - ): Promise> { + async getProducts(params: UnifiedSearchParamsDTO): Promise> { // 获取产品列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products', + requestParams + ); // 对于类型为 variable 的产品,需要加载完整的变体数据 const productsWithVariations = await Promise.all( @@ -611,31 +847,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return productsWithVariations.map((product: any) => this.mapProduct(product)); } - async getProduct(id: string | number): Promise { - // 获取单个产品详情并映射为统一产品DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products/${id}`); - const product = res.data; - - // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 - if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { - try { - // 批量获取该产品的所有变体数据 - const variations = await this.wpService.sdkGetAll( - api, - `products/${product.id}/variations` - ); - // 将完整的变体数据添加到产品对象中 - product.variations = variations; - } catch (error) { - // 如果获取变体失败,保持原有的 ID 数组 - console.error(`获取产品 ${product.id} 的变体数据失败:`, error); - } - } - - return this.mapProduct(product); - } - async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); @@ -645,21 +856,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async updateProduct(id: string | number, data: Partial): Promise { // 更新产品并返回统一产品DTO const res = await this.wpService.updateProduct(this.site, String(id), data as any); - return res - } - - async getOrderNotes(orderId: string | number): Promise { - // 获取订单备注列表 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${orderId}/notes`); - return res.data; - } - - async createOrderNote(orderId: string | number, data: any): Promise { - // 创建订单备注 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post(`orders/${orderId}/notes`, data); - return res.data; + return res; } async deleteProduct(id: string | number): Promise { @@ -673,212 +870,22 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - // 批量处理产品增删改 - return await this.wpService.batchProcessProducts(this.site, data); + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data; + } + mapUnifiedToPlatformReview(data: Partial) { + return data; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - - // 并行获取所有订单的履行信息 - const ordersWithFulfillments = await Promise.all( - items.map(async (order: any) => { - try { - // 获取订单的履行信息 - const fulfillments = await this.getOrderFulfillments(order.id); - // 将履行信息添加到订单对象中 - return { - ...order, - fulfillments: fulfillments || [] - }; - } catch (error) { - // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 - console.error(`获取订单 ${order.id} 的履行信息失败:`, error); - return { - ...order, - fulfillments: [] - }; - } - }) - ); - - return { - items: ordersWithFulfillments.map(this.mapOrder), - total, - totalPages, - page, - per_page, - }; + mapCreateReviewParams(data: CreateReviewDTO) { + return data; + } + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams: UnifiedSearchParamsDTO = { - where, - page: 1, - per_page: 1, - }; - const requestParams = this.mapOrderSearchParams(searchParams); - const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - return total || 0; - } - - async getOrder(id: string | number): Promise { - // 获取单个订单详情 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); - } - - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订单数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); - } - - async createOrder(data: Partial): Promise { - // 创建订单并返回统一订单DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('orders', data); - return this.mapOrder(res.data); - } - - async updateOrder(id: string | number, data: Partial): Promise { - // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); - } - - async deleteOrder(id: string | number): Promise { - // 删除订单 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); - return true; - } - - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - throw new Error('暂无实现') - // 订单履行(发货) - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 更新订单状态为已完成 - // await api.put(`orders/${orderId}`, { status: 'completed' }); - - // // 如果提供了物流信息,添加到订单备注 - // if (data.tracking_number || data.shipping_provider) { - // const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - // } - - // return { - // success: true, - // order_id: orderId, - // fulfillment_id: `fulfillment_${orderId}_${Date.now()}`, - // tracking_number: data.tracking_number, - // shipping_provider: data.shipping_provider, - // fulfilled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`履行失败: ${error.message}`); - // } - } - - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - throw new Error('暂未实现') - // 取消订单履行 - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 将订单状态改回处理中 - // await api.put(`orders/${orderId}`, { status: 'processing' }); - - // // 添加取消履行的备注 - // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - - // return { - // success: true, - // order_id: orderId, - // shipment_id: data.shipment_id, - // reason: data.reason, - // cancelled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`取消履行失败: ${error.message}`); - // } - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取订阅列表并映射为统一订阅DTO集合 - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'subscriptions', - params - ); - return { - items: items.map(this.mapSubscription), - total, - totalPages, - page, - per_page, - - }; - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订阅数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取媒体列表并映射为统一媒体DTO集合 - const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( - this.site, - params - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有媒体数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapMedia(mediaItem)); - } - - private mapReview(item: any): UnifiedReviewDTO & { raw: any } { + mapReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -893,17 +900,15 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getReviews( - params: UnifiedSearchParamsDTO - ): Promise { + // 评论操作方法 + async getReviews(params: UnifiedSearchParamsDTO): Promise { // 获取评论列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products/reviews', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products/reviews', + requestParams + ); return { items: items.map(this.mapReview.bind(this)), total, @@ -934,52 +939,42 @@ export class WooCommerceAdapter implements ISiteAdapter { return await this.wpService.deleteReview(this.site, id); } - async deleteMedia(id: string | number): Promise { - // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); - return true; + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data; + } + mapUnifiedToPlatformSubscription(data: Partial) { + return data; } - async updateMedia(id: string | number, data: any): Promise { - // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); - } - - async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { - // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 - const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); - return result as any; - } - - private mapCustomer(item: WooCustomer): UnifiedCustomerDTO { - // 将 WooCommerce 客户数据映射为统一客户DTO - // 包含基础信息地址信息与时间信息 + mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + // 将 WooCommerce 订阅数据映射为统一订阅DTO + // 若缺少创建时间则回退为开始时间 return { id: item.id, - avatar: item.avatar_url, - email: item.email, - orders: Number(item.orders ?? 0), - total_spend: Number(item.total_spent ?? 0), - first_name: item.first_name, - last_name: item.last_name, - username: item.username, - phone: item.billing?.phone || item.shipping?.phone, - billing: item.billing, - shipping: item.shipping, - date_created: item.date_created, + status: item.status, + customer_id: item.customer_id, + billing_period: item.billing_period, + billing_interval: item.billing_interval, + date_created: item.date_created ?? item.start_date, date_modified: item.date_modified, + start_date: item.start_date, + next_payment_date: item.next_payment_date, + line_items: item.line_items, raw: item, }; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const requestParams = this.mapCustomerSearchParams(params); + + // 订阅操作方法 + async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { + // 获取订阅列表并映射为统一订阅DTO集合 const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( this.site, - 'customers', - requestParams + 'subscriptions', + params ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map(this.mapSubscription), total, totalPages, page, @@ -988,101 +983,23 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有客户数据,不受分页限制 + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // 处理orderBy参数,转换为WooCommerce API需要的格式 - const requestParams = this.mapCustomerSearchParams(params || {}); - - const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); + return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); } - async getCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + // ========== 变体映射方法 ========== + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { + return data; } - - async createCustomer(data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('customers', data); - return this.mapCustomer(res.data); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); - } - - async deleteCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); - return true; - } - - async getOrderFulfillments(orderId: string | number): Promise { - return await this.wpService.getFulfillments(this.site, String(orderId)); - } - - async createOrderFulfillment(orderId: string | number, data: { - tracking_number: string; - shipping_provider: string; - shipping_method?: string; - status?: string; - date_created?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - const shipmentData: any = { - shipping_provider: data.shipping_provider, - tracking_number: data.tracking_number, - }; - - if (data.shipping_method) { - shipmentData.shipping_method = data.shipping_method; - } - - 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); - return response.data; - } - - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: 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); - } - - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + mapUnifiedToPlatformVariation(data: Partial) { + return data; } // 映射 WooCommerce 变体到统一格式 - private mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { + mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { // 将变体属性转换为统一格式 const mappedAttributes = variation.attributes && Array.isArray(variation.attributes) ? variation.attributes.map((attr: any) => ({ @@ -1131,6 +1048,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + // 变体操作方法 // 获取产品变体列表 async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { try { @@ -1305,4 +1223,145 @@ export class WooCommerceAdapter implements ISiteAdapter { throw new Error(`删除产品变体失败: ${error instanceof Error ? error.message : String(error)}`); } } + + // ========== 网络钩子映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data; + } + mapUnifiedToPlatformWebhook(data: Partial) { + return data; + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data; + } + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data; + } + + // 映射 WooCommerce webhook 到统一格式 + mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + return { + id: webhook.id.toString(), + name: webhook.name, + status: webhook.status, + topic: webhook.topic, + delivery_url: webhook.delivery_url, + secret: webhook.secret, + api_version: webhook.api_version, + date_created: webhook.date_created, + date_modified: webhook.date_modified, + // metadata: webhook.meta_data || [], + }; + } + + // 网络钩子操作方法 + // 获取站点的 webhooks 列表 + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { + try { + const result = await this.wpService.getWebhooks(this.site, params); + + return { + items: (result.items as WooWebhook[]).map(this.mapWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; + } catch (error) { + throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取所有webhooks + async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { + try { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); + return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + } catch (error) { + throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取单个 webhook 详情 + async getWebhook(id: string | number): Promise { + try { + const result = await this.wpService.getWebhook(this.site, id); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 创建新的 webhook + async createWebhook(data: CreateWebhookDTO): Promise { + try { + const params = { + name: data.name, + status: 'active', // 默认状态为活跃 + topic: data.topic, + delivery_url: data.delivery_url, + secret: data.secret, + api_version: data.api_version || 'wp/v2', + }; + const result = await this.wpService.createWebhook(this.site, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 更新现有的 webhook + async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + try { + const params = { + ...(data.name ? { name: data.name } : {}), + ...(data.status ? { status: data.status } : {}), + ...(data.topic ? { topic: data.topic } : {}), + ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), + ...(data.secret ? { secret: data.secret } : {}), + ...(data.api_version ? { api_version: data.api_version } : {}), + }; + const result = await this.wpService.updateWebhook(this.site, id, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 删除指定的 webhook + async deleteWebhook(id: string | number): Promise { + try { + await this.wpService.deleteWebhook(this.site, id); + return true; + } catch (error) { + throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // ========== 其他方法 ========== + async getLinks(): Promise> { + const baseUrl = this.site.apiUrl; + const links = [ + { title: '访问网站', url: baseUrl }, + { title: '管理后台', url: `${baseUrl}/wp-admin/` }, + { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, + { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, + { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, + { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, + { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, + { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, + { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, + ]; + return links; + } + + batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } + + batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 5013390..f4b1c83 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -7,7 +7,6 @@ import { CancelFulfillmentDTO, CreateReviewDTO, CreateWebhookDTO, - FulfillmentDTO, UnifiedCustomerDTO, UnifiedCustomerPaginationDTO, UnifiedMediaPaginationDTO, @@ -986,25 +985,6 @@ export class SiteApiController { } } - @Post('/:siteId/orders/:id/fulfill') - @ApiOkResponse({ type: Object }) - async fulfillOrder( - @Param('siteId') siteId: number, - @Param('id') id: string, - @Body() body: FulfillmentDTO - ) { - this.logger.info(`[Site API] 订单履约开始, siteId: ${siteId}, orderId: ${id}`); - try { - const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.fulfillOrder(id, body); - this.logger.info(`[Site API] 订单履约成功, siteId: ${siteId}, orderId: ${id}`); - return successResponse(data); - } catch (error) { - this.logger.error(`[Site API] 订单履约失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`); - return errorResponse(error.message); - } - } - @Post('/:siteId/orders/:id/cancel-fulfill') @ApiOkResponse({ type: Object }) async cancelFulfillment( diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index ea6db0b..630e5ed 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,51 +19,71 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { + // ========== 客户映射方法 ========== /** - * 获取产品列表 + * 将平台客户数据转换为统一客户数据格式 + * @param data 平台特定客户数据 + * @returns 统一客户数据格式 */ - getProducts(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO; /** - * 获取所有产品 + * 将统一客户数据格式转换为平台客户数据 + * @param data 统一客户数据格式 + * @returns 平台特定客户数据 */ - getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformCustomer(data: Partial): any; /** - * 获取单个产品 + * 获取单个客户 */ - getProduct(id: string | number): Promise; + getCustomer(id: string | number): Promise; /** - * 获取订单列表 + * 获取客户列表 */ - getOrders(params: UnifiedSearchParamsDTO): Promise>; + getCustomers(params: UnifiedSearchParamsDTO): Promise>; /** - * 获取订单总数 + * 获取所有客户 */ - countOrders(params: Record): Promise; + getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; /** - * 获取所有订单 + * 创建客户 */ - getAllOrders(params?: UnifiedSearchParamsDTO): Promise; + createCustomer(data: Partial): Promise; /** - * 获取单个订单 + * 更新客户 */ - getOrder(id: string | number): Promise; + updateCustomer(id: string | number, data: Partial): Promise; /** - * 获取订阅列表 + * 删除客户 */ - getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + deleteCustomer(id: string | number): Promise; /** - * 获取所有订阅 + * 批量处理客户 */ - getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + batchProcessCustomers?(data: BatchOperationDTO): Promise; + + // ========== 媒体映射方法 ========== + /** + * 将平台媒体数据转换为统一媒体数据格式 + * @param data 平台特定媒体数据 + * @returns 统一媒体数据格式 + */ + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO; + + /** + * 将统一媒体数据格式转换为平台媒体数据 + * @param data 统一媒体数据格式 + * @returns 平台特定媒体数据 + */ + mapUnifiedToPlatformMedia(data: Partial): any; /** * 获取媒体列表 @@ -80,75 +100,69 @@ export interface ISiteAdapter { */ createMedia(file: any): Promise; + // ========== 订单映射方法 ========== /** - * 获取评论列表 + * 将平台订单数据转换为统一订单数据格式 + * @param data 平台特定订单数据 + * @returns 统一订单数据格式 */ - getReviews(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO; /** - * 获取所有评论 + * 将统一订单数据格式转换为平台订单数据 + * @param data 统一订单数据格式 + * @returns 平台特定订单数据 */ - getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformOrder(data: Partial): any; /** - * 创建评论 + * 将统一订单创建参数转换为平台订单创建参数 + * @param data 统一订单创建参数 + * @returns 平台订单创建参数 */ - createReview(data: CreateReviewDTO): Promise; + mapCreateOrderParams(data: Partial): any; /** - * 更新评论 + * 将统一订单更新参数转换为平台订单更新参数 + * @param data 统一订单更新参数 + * @returns 平台订单更新参数 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + mapUpdateOrderParams(data: Partial): any; /** - * 删除评论 + * 获取单个订单 */ - deleteReview(id: number): Promise; + getOrder(id: string | number): Promise; /** - * 创建产品 + * 获取订单列表 */ - createProduct(data: Partial): Promise; + getOrders(params: UnifiedSearchParamsDTO): Promise>; /** - * 更新产品 + * 获取所有订单 */ - updateProduct(id: string | number, data: Partial): Promise; + getAllOrders(params?: UnifiedSearchParamsDTO): Promise; /** - * 删除产品 + * 获取订单总数 */ - deleteProduct(id: string | number): Promise; + countOrders(params: Record): Promise; /** - * 获取产品变体列表 + * 创建订单 */ - getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + createOrder(data: Partial): Promise; /** - * 获取所有产品变体 + * 更新订单 */ - getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + updateOrder(id: string | number, data: Partial): Promise; /** - * 获取单个产品变体 + * 删除订单 */ - getVariation(productId: string | number, variationId: string | number): Promise; - - /** - * 创建产品变体 - */ - createVariation(productId: string | number, data: CreateVariationDTO): Promise; - - /** - * 更新产品变体 - */ - updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; - - /** - * 删除产品变体 - */ - deleteVariation(productId: string | number, variationId: string | number): Promise; + deleteOrder(id: string | number): Promise; /** * 获取订单备注 @@ -160,71 +174,6 @@ export interface ISiteAdapter { */ createOrderNote(orderId: string | number, data: any): Promise; - batchProcessProducts?(data: BatchOperationDTO): Promise; - - createOrder(data: Partial): Promise; - updateOrder(id: string | number, data: Partial): Promise; - deleteOrder(id: string | number): Promise; - - batchProcessOrders?(data: BatchOperationDTO): Promise; - - getCustomers(params: UnifiedSearchParamsDTO): Promise>; - getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; - getCustomer(id: string | number): Promise; - createCustomer(data: Partial): Promise; - updateCustomer(id: string | number, data: Partial): Promise; - deleteCustomer(id: string | number): Promise; - - batchProcessCustomers?(data: BatchOperationDTO): Promise; - - /** - * 获取webhooks列表 - */ - getWebhooks(params: UnifiedSearchParamsDTO): Promise; - - /** - * 获取所有webhooks - */ - getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; - - /** - * 获取单个webhook - */ - getWebhook(id: string | number): Promise; - - /** - * 创建webhook - */ - createWebhook(data: CreateWebhookDTO): Promise; - - /** - * 更新webhook - */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; - - /** - * 删除webhook - */ - deleteWebhook(id: string | number): Promise; - - /** - * 获取站点链接列表 - */ - getLinks(): Promise>; - - /** - * 订单履行(发货) - */ - fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise; - /** * 取消订单履行 */ @@ -272,4 +221,276 @@ export interface ISiteAdapter { * 删除订单履行信息 */ deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise; + + /** + * 批量处理订单 + */ + batchProcessOrders?(data: BatchOperationDTO): Promise; + + // ========== 产品映射方法 ========== + /** + * 将平台产品数据转换为统一产品数据格式 + * @param data 平台特定产品数据 + * @returns 统一产品数据格式 + */ + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO; + + /** + * 将统一产品数据格式转换为平台产品数据 + * @param data 统一产品数据格式 + * @returns 平台特定产品数据 + */ + mapUnifiedToPlatformProduct(data: Partial): any; + + /** + * 将统一产品创建参数转换为平台产品创建参数 + * @param data 统一产品创建参数 + * @returns 平台产品创建参数 + */ + mapCreateProductParams(data: Partial): any; + + /** + * 将统一产品更新参数转换为平台产品更新参数 + * @param data 统一产品更新参数 + * @returns 平台产品更新参数 + */ + mapUpdateProductParams(data: Partial): any; + + /** + * 获取单个产品 + */ + getProduct(id: string | number): Promise; + + /** + * 获取产品列表 + */ + getProducts(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有产品 + */ + getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品 + */ + createProduct(data: Partial): Promise; + + /** + * 更新产品 + */ + updateProduct(id: string | number, data: Partial): Promise; + + /** + * 删除产品 + */ + deleteProduct(id: string | number): Promise; + + /** + * 批量处理产品 + */ + batchProcessProducts?(data: BatchOperationDTO): Promise; + + // ========== 评论映射方法 ========== + /** + * 将平台评论数据转换为统一评论数据格式 + * @param data 平台特定评论数据 + * @returns 统一评论数据格式 + */ + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO; + + /** + * 将统一评论数据格式转换为平台评论数据 + * @param data 统一评论数据格式 + * @returns 平台特定评论数据 + */ + mapUnifiedToPlatformReview(data: Partial): any; + + /** + * 将统一评论创建参数转换为平台评论创建参数 + * @param data 统一评论创建参数 + * @returns 平台评论创建参数 + */ + mapCreateReviewParams(data: CreateReviewDTO): any; + + /** + * 将统一评论更新参数转换为平台评论更新参数 + * @param data 统一评论更新参数 + * @returns 平台评论更新参数 + */ + mapUpdateReviewParams(data: UpdateReviewDTO): any; + + /** + * 获取评论列表 + */ + getReviews(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有评论 + */ + getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建评论 + */ + createReview(data: CreateReviewDTO): Promise; + + /** + * 更新评论 + */ + updateReview(id: number, data: UpdateReviewDTO): Promise; + + /** + * 删除评论 + */ + deleteReview(id: number): Promise; + + // ========== 订阅映射方法 ========== + /** + * 将平台订阅数据转换为统一订阅数据格式 + * @param data 平台特定订阅数据 + * @returns 统一订阅数据格式 + */ + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO; + + /** + * 将统一订阅数据格式转换为平台订阅数据 + * @param data 统一订阅数据格式 + * @returns 平台特定订阅数据 + */ + mapUnifiedToPlatformSubscription(data: Partial): any; + + /** + * 获取订阅列表 + */ + getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有订阅 + */ + getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + + // ========== 产品变体映射方法 ========== + /** + * 将平台产品变体数据转换为统一产品变体数据格式 + * @param data 平台特定产品变体数据 + * @returns 统一产品变体数据格式 + */ + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO; + + /** + * 将统一产品变体数据格式转换为平台产品变体数据 + * @param data 统一产品变体数据格式 + * @returns 平台特定产品变体数据 + */ + mapUnifiedToPlatformVariation(data: Partial): any; + + /** + * 将统一产品变体创建参数转换为平台产品变体创建参数 + * @param data 统一产品变体创建参数 + * @returns 平台产品变体创建参数 + */ + mapCreateVariationParams(data: CreateVariationDTO): any; + + /** + * 将统一产品变体更新参数转换为平台产品变体更新参数 + * @param data 统一产品变体更新参数 + * @returns 平台产品变体更新参数 + */ + mapUpdateVariationParams(data: UpdateVariationDTO): any; + + /** + * 获取单个产品变体 + */ + getVariation(productId: string | number, variationId: string | number): Promise; + + /** + * 获取产品变体列表 + */ + getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有产品变体 + */ + getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品变体 + */ + createVariation(productId: string | number, data: CreateVariationDTO): Promise; + + /** + * 更新产品变体 + */ + updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; + + /** + * 删除产品变体 + */ + deleteVariation(productId: string | number, variationId: string | number): Promise; + + // ========== Webhook映射方法 ========== + /** + * 将平台Webhook数据转换为统一Webhook数据格式 + * @param data 平台特定Webhook数据 + * @returns 统一Webhook数据格式 + */ + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO; + + /** + * 将统一Webhook数据格式转换为平台Webhook数据 + * @param data 统一Webhook数据格式 + * @returns 平台特定Webhook数据 + */ + mapUnifiedToPlatformWebhook(data: Partial): any; + + /** + * 将统一Webhook创建参数转换为平台Webhook创建参数 + * @param data 统一Webhook创建参数 + * @returns 平台Webhook创建参数 + */ + mapCreateWebhookParams(data: CreateWebhookDTO): any; + + /** + * 将统一Webhook更新参数转换为平台Webhook更新参数 + * @param data 统一Webhook更新参数 + * @returns 平台Webhook更新参数 + */ + mapUpdateWebhookParams(data: UpdateWebhookDTO): any; + + /** + * 获取单个webhook + */ + getWebhook(id: string | number): Promise; + + /** + * 获取webhooks列表 + */ + getWebhooks(params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有webhooks + */ + getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建webhook + */ + createWebhook(data: CreateWebhookDTO): Promise; + + /** + * 更新webhook + */ + updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + + /** + * 删除webhook + */ + deleteWebhook(id: string | number): Promise; + + // ========== 站点/其他方法 ========== + /** + * 获取站点链接列表 + */ + getLinks(): Promise>; } diff --git a/src/transformer/database.transformer.ts b/src/transformer/database.transformer.ts new file mode 100644 index 0000000..619ef04 --- /dev/null +++ b/src/transformer/database.transformer.ts @@ -0,0 +1 @@ +// 从 unified 到 数据库需要有个转换流程 diff --git a/src/transformer/file.transformer.ts b/src/transformer/file.transformer.ts new file mode 100644 index 0000000..b85ee23 --- /dev/null +++ b/src/transformer/file.transformer.ts @@ -0,0 +1 @@ +// 文件转换 diff --git a/src/transformer/shopyy.transformer.ts b/src/transformer/shopyy.transformer.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/transformer/woocommerce.adpater.ts b/src/transformer/woocommerce.adpater.ts new file mode 100644 index 0000000..e5b5c67 --- /dev/null +++ b/src/transformer/woocommerce.adpater.ts @@ -0,0 +1,8 @@ +import { UnifiedOrderDTO } from "../dto/site-api.dto"; + +export class ShipmentAdapter { + // 用于导出物流需要的数据 + mapFromOrder(order: UnifiedOrderDTO): any { + return order; + } +} \ No newline at end of file From 89d7d78cccae2a7ef0e810047bcad950dc70ff97 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 20:33:23 +0800 Subject: [PATCH 03/24] =?UTF-8?q?refactor(api):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=8F=82=E6=95=B0=E4=B8=BA=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=E5=B9=B6=E6=94=AF=E6=8C=81=E5=A4=9A=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构所有接口方法,将直接传递id参数改为接受where条件对象 支持通过id、sku、email等多条件查询实体 优化产品服务逻辑,支持通过sku直接查询产品 统一各适配器实现,确保接口一致性 --- src/adapter/shopyy.adapter.ts | 140 ++++++++++------ src/adapter/woocommerce.adapter.ts | 211 +++++++++++++----------- src/controller/site-api.controller.ts | 42 ++--- src/interface/site-adapter.interface.ts | 34 ++-- src/service/order.service.ts | 2 +- src/service/site-api.service.ts | 28 +--- src/service/wp.service.ts | 8 +- 7 files changed, 248 insertions(+), 217 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 1cf00fd..ffa66af 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -122,8 +122,11 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); + async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise { + if(!where.id && !where.email && !where.phone){ + throw new Error('必须传入 id 或 email 或 phone') + } + const customer = await this.shopyyService.getCustomer(this.site, where.id); return this.mapPlatformToUnifiedCustomer(customer); } @@ -149,13 +152,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedCustomer(createdCustomer); } - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + async updateCustomer(where: {id: string | number}, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data); return this.mapPlatformToUnifiedCustomer(updatedCustomer); } - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); + async deleteCustomer(where: {id: string | number}): Promise { + return await this.shopyyService.deleteCustomer(this.site, where.id); } batchProcessCustomers?(data: BatchOperationDTO): Promise { @@ -208,13 +211,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedMedia(createdMedia); } - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + async updateMedia(where: {id: string | number}, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data); return this.mapPlatformToUnifiedMedia(updatedMedia); } - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + async deleteMedia(where: {id: string | number}): Promise { + return await this.shopyyService.deleteMedia(this.site, where.id); } mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { @@ -530,8 +533,8 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); + async getOrder(where: {id: string | number}): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(where.id)); return this.mapPlatformToUnifiedOrder(data); } @@ -577,14 +580,14 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 使用映射方法转换参数 const requestParams = this.mapUpdateOrderParams(data); - return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams); } - async deleteOrder(id: string | number): Promise { - return await this.shopyyService.deleteOrder(this.site, id); + async deleteOrder(where: {id: string | number}): Promise { + return await this.shopyyService.deleteOrder(this.site, where.id); } async getOrderNotes(orderId: string | number): Promise { @@ -858,10 +861,18 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapPlatformToUnifiedProduct(product); + async getProduct(where: {id?: string | number, sku?: string}): Promise { + if(!where.id && !where.sku){ + throw new Error('必须传入 id 或 sku') + } + if (where.id) { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, where.id); + return this.mapPlatformToUnifiedProduct(product); + } else if (where.sku) { + // 通过sku获取产品 + return this.getProductBySku(where.sku); + } } async getProducts( @@ -900,24 +911,50 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } // 使用映射方法转换参数 const requestParams = this.mapUpdateProductParams(data); - await this.shopyyService.updateProduct(this.site, String(id), requestParams); + await this.shopyyService.updateProduct(this.site, productId, requestParams); return true; } - async deleteProduct(id: string | number): Promise { + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { + let productId: string | number; + if (where.id) { + productId = where.id; + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = product.id; + } else { + throw new Error('必须提供id或sku参数'); + } // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); return true; } + // 通过sku获取产品详情的私有方法 + private async getProductBySku(sku: string): Promise { + // 使用Shopyy API的搜索功能通过sku查询产品 + const response = await this.shopyyService.getProducts(this.site, 1, 100); + const product = response.items.find((item: any) => item.sku === sku); + if (!product) { + throw new Error(`未找到sku为${sku}的产品`); + } + return this.mapPlatformToUnifiedProduct(product); + } + async batchProcessProducts( data: { create?: any[]; update?: any[]; delete?: Array } ): Promise { @@ -929,9 +966,6 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data - } mapUnifiedToPlatformReview(data: Partial) { return data @@ -954,7 +988,7 @@ export class ShopyyAdapter implements ISiteAdapter { requestParams ); return { - items: items.map(this.mapReview), + items: items.map(this.mapPlatformToUnifiedReview), total, totalPages, page, @@ -969,19 +1003,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createReview(data: any): Promise { const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + return this.mapPlatformToUnifiedReview(createdReview); } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + async updateReview(where: {id: string | number}, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(updatedReview); } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async deleteReview(where: {id: string | number}): Promise { + return await this.shopyyService.deleteReview(this.site, where.id); } - mapReview(review: any): UnifiedReviewDTO { + mapPlatformToUnifiedReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -1119,9 +1153,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== Webhook映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data - } + mapUnifiedToPlatformWebhook(data: Partial) { return data @@ -1135,15 +1167,15 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); + async getWebhook(where: {id: string | number}): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(webhook); } async getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { - items: items.map(this.mapWebhook), + items: items.map(this.mapPlatformToUnifiedWebhook), total, totalPages, page, @@ -1158,19 +1190,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createWebhook(data: CreateWebhookDTO): Promise { const createdWebhook = await this.shopyyService.createWebhook(this.site, data); - return this.mapWebhook(createdWebhook); + return this.mapPlatformToUnifiedWebhook(createdWebhook); } - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { - const updatedWebhook = await this.shopyyService.updateWebhook(this.site, id, data); - return this.mapWebhook(updatedWebhook); + async updateWebhook(where: {id: string | number}, data: UpdateWebhookDTO): Promise { + const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data); + return this.mapPlatformToUnifiedWebhook(updatedWebhook); } - async deleteWebhook(id: string | number): Promise { - return await this.shopyyService.deleteWebhook(this.site, id); + async deleteWebhook(where: {id: string | number}): Promise { + return await this.shopyyService.deleteWebhook(this.site, where.id); } - mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { return { id: item.id, name: item.webhook_name || `Webhook-${item.id}`, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index a5f2f0a..0debb16 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -36,12 +36,15 @@ import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 constructor(private site: Site, private wpService: WPService) { - this.mapProduct = this.mapProduct.bind(this); - this.mapReview = this.mapReview.bind(this); - this.mapCustomer = this.mapCustomer.bind(this); - this.mapMedia = this.mapMedia.bind(this); - this.mapOrder = this.mapOrder.bind(this); - this.mapWebhook = this.mapWebhook.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedReview = this.mapPlatformToUnifiedReview.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedWebhook = this.mapPlatformToUnifiedWebhook.bind(this); + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data } batchProcessProducts?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); @@ -54,14 +57,8 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 客户映射方法 ========== - mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { - return data; - } - mapUnifiedToPlatformCustomer(data: Partial) { - return data; - } - mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + mapPlatformToUnifiedCustomer(item: WooCustomer): UnifiedCustomerDTO { // 将 WooCommerce 客户数据映射为统一客户DTO // 包含基础信息地址信息与时间信息 return { @@ -136,10 +133,10 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户操作方法 - async getCustomer(id: string | number): Promise { + async getCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + const res = await api.get(`customers/${where.id}`); + return this.mapPlatformToUnifiedCustomer(res.data); } async getCustomers(params: UnifiedSearchParamsDTO): Promise> { @@ -150,7 +147,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map((i: any) => this.mapPlatformToUnifiedCustomer(i)), total, totalPages, page, @@ -167,36 +164,33 @@ export class WooCommerceAdapter implements ISiteAdapter { const requestParams = this.mapCustomerSearchParams(params || {}); const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + return customers.map((customer: any) => this.mapPlatformToUnifiedCustomer(customer)); } async createCustomer(data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('customers', data); - return this.mapCustomer(res.data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async updateCustomer(id: string | number, data: Partial): Promise { + async updateCustomer(where: {id: string | number}, data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); + const res = await api.put(`customers/${where.id}`, data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async deleteCustomer(id: string | number): Promise { + async deleteCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); + await api.delete(`customers/${where.id}`, { force: true }); return true; } // ========== 媒体映射方法 ========== - mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { - return data; - } mapUnifiedToPlatformMedia(data: Partial) { return data; } - mapMedia(item: WpMedia): UnifiedMediaDTO { + mapPlatformToUnifiedMedia(item: WpMedia): UnifiedMediaDTO { // 将 WordPress 媒体数据映射为统一媒体DTO // 兼容不同字段命名的时间信息 return { @@ -221,7 +215,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapMedia.bind(this)), + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), total, totalPages, page, @@ -233,21 +227,21 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有媒体数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); } createMedia(file: any): Promise { throw new Error('Method not implemented.'); } - async updateMedia(id: string | number, data: any): Promise { + async updateMedia(where: {id: string | number}, data: any): Promise { // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + return await this.wpService.updateMedia(Number(this.site.id), Number(where.id), data); } - async deleteMedia(id: string | number): Promise { + async deleteMedia(where: {id: string | number}): Promise { // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + await this.wpService.deleteMedia(Number(this.site.id), Number(where.id), true); return true; } @@ -258,9 +252,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 订单映射方法 ========== - mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { - return data; - } mapUnifiedToPlatformOrder(data: Partial) { return data; } @@ -356,7 +347,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ].filter(Boolean).join(', '); } - mapOrder(item: WooOrder): UnifiedOrderDTO { + mapPlatformToUnifiedOrder(item: WooOrder): UnifiedOrderDTO { // 将 WooCommerce 订单数据映射为统一订单DTO // 包含账单地址与收货地址以及创建与更新时间 @@ -410,11 +401,11 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 订单操作方法 - async getOrder(id: string | number): Promise { + async getOrder(where: {id: string | number}): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); + const res = await api.get(`orders/${where.id}`); + return this.mapPlatformToUnifiedOrder(res.data); } async getOrders(params: UnifiedSearchParamsDTO): Promise> { @@ -444,7 +435,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: ordersWithFulfillments.map(this.mapOrder), + items: ordersWithFulfillments.map(this.mapPlatformToUnifiedOrder), total, totalPages, page, @@ -456,7 +447,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订单数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); + return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); } async countOrders(where: Record): Promise { @@ -475,18 +466,18 @@ export class WooCommerceAdapter implements ISiteAdapter { // 创建订单并返回统一订单DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); - return this.mapOrder(res.data); + return this.mapPlatformToUnifiedOrder(res.data); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); + return await this.wpService.updateOrder(this.site, String(where.id), data as any); } - async deleteOrder(id: string | number): Promise { + async deleteOrder(where: {id: string | number}): Promise { // 删除订单 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); + await api.delete(`orders/${where.id}`, { force: true }); return true; } @@ -591,9 +582,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { - return data; - } mapUnifiedToPlatformProduct(data: Partial) { return data; } @@ -661,7 +649,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapProduct(item: WooProduct): UnifiedProductDTO { + mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -749,11 +737,22 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - + async getProduct({id, sku}: {id?: string, sku?:string}){ + if(id) return this.getProductById(id); + if(sku) return this.getProductBySku(sku) + return this.getProductById(id || sku || ''); + } + async getProductBySku(sku: string){ + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products?sku=${sku}`); + const product = res.data[0]; + return this.mapPlatformToUnifiedProduct(product); + } // 产品操作方法 - async getProduct(id: string | number): Promise { + async getProductById(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); const product = res.data; @@ -773,7 +772,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - return this.mapProduct(product); + return this.mapPlatformToUnifiedProduct(product); } async getProducts(params: UnifiedSearchParamsDTO): Promise> { @@ -808,7 +807,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: productsWithVariations.map(this.mapProduct), + items: productsWithVariations.map(this.mapPlatformToUnifiedProduct), total, totalPages, page, @@ -844,26 +843,46 @@ export class WooCommerceAdapter implements ISiteAdapter { }) ); - return productsWithVariations.map((product: any) => this.mapProduct(product)); + return productsWithVariations.map((product: any) => this.mapPlatformToUnifiedProduct(product)); } async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); - return this.mapProduct(res); + return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { // 更新产品并返回统一产品DTO - const res = await this.wpService.updateProduct(this.site, String(id), data as any); + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } + const res = await this.wpService.updateProduct(this.site, productId, data as any); return res; } - async deleteProduct(id: string | number): Promise { + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { // 删除产品 + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } const api = (this.wpService as any).createApi(this.site, 'wc/v3'); try { - await api.delete(`products/${id}`, { force: true }); + await api.delete(`products/${productId}`, { force: true }); return true; } catch (e) { return false; @@ -871,9 +890,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data; - } + mapUnifiedToPlatformReview(data: Partial) { return data; } @@ -885,7 +902,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapReview(item: any): UnifiedReviewDTO & { raw: any } { + mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -910,7 +927,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map(this.mapReview.bind(this)), + items: items.map(this.mapPlatformToUnifiedReview.bind(this)), total, totalPages, page, @@ -922,32 +939,29 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有评论数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); - return reviews.map((review: any) => this.mapReview(review)); + return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); } async createReview(data: any): Promise { const res = await this.wpService.createReview(this.site, data); - return this.mapReview(res); + return this.mapPlatformToUnifiedReview(res); } - async updateReview(id: number, data: any): Promise { - const res = await this.wpService.updateReview(this.site, id, data); - return this.mapReview(res); + async updateReview(where: {id: number}, data: any): Promise { + const res = await this.wpService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(res); } - async deleteReview(id: number): Promise { - return await this.wpService.deleteReview(this.site, id); + async deleteReview(where: {id: number}): Promise { + return await this.wpService.deleteReview(this.site, where.id); } // ========== 订阅映射方法 ========== - mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { - return data; - } mapUnifiedToPlatformSubscription(data: Partial) { return data; } - mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + mapPlatformToUnifiedSubscription(item: WooSubscription): UnifiedSubscriptionDTO { // 将 WooCommerce 订阅数据映射为统一订阅DTO // 若缺少创建时间则回退为开始时间 return { @@ -974,7 +988,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapSubscription), + items: items.map(this.mapPlatformToUnifiedSubscription), total, totalPages, page, @@ -987,7 +1001,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); + return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); } // ========== 变体映射方法 ========== @@ -1225,9 +1239,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 网络钩子映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data; - } mapUnifiedToPlatformWebhook(data: Partial) { return data; } @@ -1240,7 +1251,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 映射 WooCommerce webhook 到统一格式 - mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(webhook: WooWebhook): UnifiedWebhookDTO { return { id: webhook.id.toString(), name: webhook.name, @@ -1262,12 +1273,12 @@ export class WooCommerceAdapter implements ISiteAdapter { const result = await this.wpService.getWebhooks(this.site, params); return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; + items: (result.items as WooWebhook[]).map(this.mapPlatformToUnifiedWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; } catch (error) { throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); } @@ -1278,17 +1289,17 @@ export class WooCommerceAdapter implements ISiteAdapter { try { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); } catch (error) { throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); } } // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { + async getWebhook(where: {id: string | number}): Promise { try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); } @@ -1306,14 +1317,14 @@ export class WooCommerceAdapter implements ISiteAdapter { api_version: data.api_version || 'wp/v2', }; const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + async updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise { try { const params = { ...(data.name ? { name: data.name } : {}), @@ -1323,17 +1334,17 @@ export class WooCommerceAdapter implements ISiteAdapter { ...(data.secret ? { secret: data.secret } : {}), ...(data.api_version ? { api_version: data.api_version } : {}), }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.updateWebhook(this.site, where.id, params); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { + async deleteWebhook(where: Partial>): Promise { try { - await this.wpService.deleteWebhook(this.site, id); + await this.wpService.deleteWebhook(this.site, where.id); return true; } catch (error) { throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index f4b1c83..00d8285 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -105,7 +105,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateReview(id, body); + const data = await adapter.updateReview({ id }, body); this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -123,7 +123,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.deleteReview(id); + const data = await adapter.deleteReview({ id }); this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -159,7 +159,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getWebhook(id); + const data = await adapter.getWebhook({ id }); this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -198,7 +198,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateWebhook(id, body); + const data = await adapter.updateWebhook({ id }, body); this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -216,7 +216,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.deleteWebhook(id); + const data = await adapter.deleteWebhook({ id }); this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -326,7 +326,7 @@ export class SiteApiController { if (site.type === 'woocommerce') { const page = query.page || 1; const perPage = (query.per_page) || 100; - const res = await this.siteApiService.wpService.getProducts(site, page, perPage); + const res = await this.siteApiService.wpService.getProducts(site, { page, per_page: perPage }); const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity']; const rows = (res.items || []).map((p: any) => [p.id, p.name, p.type, p.status, p.sku, p.regular_price, p.sale_price, p.stock_status, p.stock_quantity]); const toCsvValue = (val: any) => { @@ -359,7 +359,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getProduct(id); + const data = await adapter.getProduct({ id }); // 如果获取到商品数据,则增强ERP产品信息 if (data) { @@ -484,7 +484,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateProduct(id, body); + const data = await adapter.updateProduct({ id }, body); this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`); return successResponse(data); } catch (error) { @@ -539,7 +539,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const success = await adapter.deleteProduct(id); + const success = await adapter.deleteProduct({ id }); this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`); return successResponse(success); } catch (error) { @@ -584,7 +584,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const data = await adapter.updateProduct(id, item); + const data = await adapter.updateProduct({ id }, item); updated.push(data); } catch (e) { errors.push({ @@ -597,7 +597,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteProduct(id); + const ok = await adapter.deleteProduct({ id }); if (ok) deleted.push(id); else errors.push({ identifier: String(id), @@ -771,7 +771,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getOrder(id); + const data = await adapter.getOrder({ id }); this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(data); } catch (error) { @@ -843,7 +843,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const ok = await adapter.updateOrder(id, body); + const ok = await adapter.updateOrder({ id }, body); this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(ok); } catch (error) { @@ -861,7 +861,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const ok = await adapter.deleteOrder(id); + const ok = await adapter.deleteOrder({ id }); this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(ok); } catch (error) { @@ -901,7 +901,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const ok = await adapter.updateOrder(id, item); + const ok = await adapter.updateOrder({ id }, item); if (ok) updated.push(item); else errors.push({ identifier: String(item.id || 'unknown'), @@ -918,7 +918,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteOrder(id); + const ok = await adapter.deleteOrder({ id }); if (ok) deleted.push(id); else errors.push({ identifier: String(id), @@ -1435,7 +1435,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getCustomer(id); + const data = await adapter.getCustomer({ id }); this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(data); } catch (error) { @@ -1507,7 +1507,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateCustomer(id, body); + const data = await adapter.updateCustomer({ id }, body); this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(data); } catch (error) { @@ -1525,7 +1525,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const success = await adapter.deleteCustomer(id); + const success = await adapter.deleteCustomer({ id }); this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(success); } catch (error) { @@ -1561,7 +1561,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const data = await adapter.updateCustomer(id, item); + const data = await adapter.updateCustomer({ id }, item); updated.push(data); } catch (e) { failed.push({ action: 'update', item, error: (e as any).message }); @@ -1571,7 +1571,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteCustomer(id); + const ok = await adapter.deleteCustomer({ id }); if (ok) deleted.push(id); else failed.push({ action: 'delete', id, error: 'delete failed' }); } catch (e) { diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 630e5ed..f290517 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,7 +19,7 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { // ========== 客户映射方法 ========== /** * 将平台客户数据转换为统一客户数据格式 @@ -38,7 +38,7 @@ export interface ISiteAdapter { /** * 获取单个客户 */ - getCustomer(id: string | number): Promise; + getCustomer(where: Partial>): Promise; /** * 获取客户列表 @@ -58,12 +58,12 @@ export interface ISiteAdapter { /** * 更新客户 */ - updateCustomer(id: string | number, data: Partial): Promise; + updateCustomer(where: Partial>, data: Partial): Promise; /** * 删除客户 */ - deleteCustomer(id: string | number): Promise; + deleteCustomer(where: Partial>): Promise; /** * 批量处理客户 @@ -132,7 +132,7 @@ export interface ISiteAdapter { /** * 获取单个订单 */ - getOrder(id: string | number): Promise; + getOrder(where: Partial>): Promise; /** * 获取订单列表 @@ -147,7 +147,7 @@ export interface ISiteAdapter { /** * 获取订单总数 */ - countOrders(params: Record): Promise; + countOrders(params: Record): Promise; /** * 创建订单 @@ -157,12 +157,12 @@ export interface ISiteAdapter { /** * 更新订单 */ - updateOrder(id: string | number, data: Partial): Promise; + updateOrder(where: Partial>, data: Partial): Promise; /** * 删除订单 */ - deleteOrder(id: string | number): Promise; + deleteOrder(where: Partial>): Promise; /** * 获取订单备注 @@ -259,7 +259,7 @@ export interface ISiteAdapter { /** * 获取单个产品 */ - getProduct(id: string | number): Promise; + getProduct(where: Partial>): Promise; /** * 获取产品列表 @@ -279,12 +279,12 @@ export interface ISiteAdapter { /** * 更新产品 */ - updateProduct(id: string | number, data: Partial): Promise; + updateProduct(where: Partial>, data: Partial): Promise; /** * 删除产品 */ - deleteProduct(id: string | number): Promise; + deleteProduct(where: Partial>): Promise; /** * 批量处理产品 @@ -338,12 +338,12 @@ export interface ISiteAdapter { /** * 更新评论 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + updateReview(where: Partial>, data: UpdateReviewDTO): Promise; /** * 删除评论 */ - deleteReview(id: number): Promise; + deleteReview(where: Partial>): Promise; // ========== 订阅映射方法 ========== /** @@ -461,7 +461,7 @@ export interface ISiteAdapter { /** * 获取单个webhook */ - getWebhook(id: string | number): Promise; + getWebhook(where: Partial>): Promise; /** * 获取webhooks列表 @@ -481,16 +481,16 @@ export interface ISiteAdapter { /** * 更新webhook */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise; /** * 删除webhook */ - deleteWebhook(id: string | number): Promise; + deleteWebhook(where: Partial>): Promise; // ========== 站点/其他方法 ========== /** * 获取站点链接列表 */ - getLinks(): Promise>; + getLinks(): Promise>; } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 5c5e47f..1aa19bd 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -202,7 +202,7 @@ export class OrderService { try { // 调用 WooCommerce API 获取订单 const adapter = await this.siteApiService.getAdapter(siteId); - const order = await adapter.getOrder(orderId); + const order = await adapter.getOrder({ id: orderId }); // 检查订单是否已存在,以区分创建和更新 const existingOrder = await this.orderModel.findOne({ diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index c57a899..3aab4ac 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -110,36 +110,22 @@ export class SiteApiService { const adapter = await this.getAdapter(siteId); // 首先尝试查找产品 - if (product.id) { - try { - // 尝试获取产品以确认它是否存在 - const existingProduct = await adapter.getProduct(product.id); - if (existingProduct) { - // 产品存在,执行更新 - return await adapter.updateProduct(product.id, product); - } - } catch (error) { - // 如果获取产品失败,可能是因为产品不存在,继续执行创建逻辑 - console.log(`产品 ${product.id} 不存在,将创建新产品:`, error.message); - } - } else if (product.sku) { + if (product.sku) { // 如果没有提供ID但提供了SKU,尝试通过SKU查找产品 try { // 尝试搜索具有相同SKU的产品 - const searchResult = await adapter.getProducts({ where: { sku: product.sku } }); - if (searchResult.items && searchResult.items.length > 0) { - const existingProduct = searchResult.items[0]; + const existingProduct = await adapter.getProduct( { sku: product.sku }); + if (existingProduct) { // 找到现有产品,更新它 - return await adapter.updateProduct(existingProduct.id, product); + return await adapter.updateProduct({ id: existingProduct.id }, product); } + // 产品不存在,执行创建 + return await adapter.createProduct(product); } catch (error) { // 搜索失败,继续执行创建逻辑 console.log(`通过SKU搜索产品失败:`, error.message); } } - - // 产品不存在,执行创建 - return await adapter.createProduct(product); } /** @@ -197,7 +183,7 @@ export class SiteApiService { */ async getProductFromSite(siteId: number, productId: string | number): Promise { const adapter = await this.getAdapter(siteId); - return await adapter.getProduct(productId); + return await adapter.getProduct({ id: productId }); } /** diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index c7faa02..8c8bc89 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -240,9 +240,11 @@ export class WPService implements IPlatformService { return allData; } - async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise { + async getProducts(site: any, params: Record = {}): Promise { const api = this.createApi(site, 'wc/v3'); - return await this.sdkGetPage(api, 'products', { page, per_page: pageSize }); + const page = params.page ?? 1; + const per_page = params.per_page ?? params.pageSize ?? 100; + return await this.sdkGetPage(api, 'products', { ...params, page, per_page }); } async getProduct(site: any, id: number): Promise { @@ -254,7 +256,7 @@ export class WPService implements IPlatformService { // 导出 WooCommerce 产品为特殊CSV(平台特性) async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise { - const list = await this.getProducts(site, page, pageSize); + const list = await this.getProducts(site, { page, per_page: pageSize }); const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity']; const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); From 5e55b85107c2cae00ef39f83ee50492e39464b74 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 15:22:18 +0800 Subject: [PATCH 04/24] =?UTF-8?q?feat(=E8=AE=A2=E5=8D=95):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=8E=B7=E5=8F=96=E8=AE=A2=E5=8D=95=E6=80=BB=E6=95=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现订单总数统计接口,包括: 1. 在ISiteAdapter接口添加countOrders方法 2. 在WooCommerce和Shopyy适配器中实现该方法 3. 添加控制器端点暴露该功能 4. 优化订单查询参数映射逻辑 refactor(Shopyy): 重构搜索参数映射逻辑 将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性 --- src/adapter/shopyy.adapter.ts | 105 ++++++++++++++++-------- src/adapter/woocommerce.adapter.ts | 13 ++- src/controller/media.controller.ts | 79 ------------------ src/controller/site-api.controller.ts | 26 +++++- src/dto/shopyy.dto.ts | 40 +++++++++ src/entity/product.entity.ts | 3 - src/interface/site-adapter.interface.ts | 5 ++ src/service/shopyy.service.ts | 37 ++------- 8 files changed, 162 insertions(+), 146 deletions(-) delete mode 100644 src/controller/media.controller.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 33d1e61..0cca7ff 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -23,7 +23,9 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { ShopyyCustomer, ShopyyOrder, + ShopyyOrderQuery, ShopyyProduct, + ShopyyProductQuery, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; @@ -64,17 +66,30 @@ export class ShopyyAdapter implements ISiteAdapter { } private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { - const { search, page, per_page } = params; - const shopyyParams: any = { - page: page || 1, - limit: per_page || 10, - }; + return this.mapSearchParams(params) + } - if (search) { - shopyyParams.query = search; + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + private mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), + page, + limit, } - - return shopyyParams; + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; } private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { @@ -393,14 +408,19 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) + } async getProducts( params: UnifiedSearchParamsDTO ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); const response = await this.shopyyService.fetchResourcePaged( this.site, 'products/list', - params + requestParams ); const { items = [], total, totalPages, page, per_page } = response; const finalItems = items.map((item) => ({ @@ -431,7 +451,6 @@ export class ShopyyAdapter implements ISiteAdapter { const res = await this.shopyyService.createProduct(this.site, data); return this.mapProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { // Shopyy update returns boolean? // shopyyService.updateProduct returns boolean. @@ -466,38 +485,49 @@ export class ShopyyAdapter implements ISiteAdapter { ): Promise { return await this.shopyyService.batchProcessProducts(this.site, data); } - mapUnifiedOrderQueryToShopyyQuery(params: UnifiedSearchParamsDTO) { - const { where = {} as any, ...restParams } = params || {} + /** + * 将统一的订单查询参数转换为 ShopYY 订单查询参数 + * 包含状态映射等特殊处理 + */ + private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 首先使用通用参数转换 + const baseParams = this.mapSearchParams(params); + + // 订单状态映射 const statusMap = { 'pending': '100', // 100 未完成 'processing': '110', // 110 待处理 'completed': "180", // 180 已完成(确认收货) 'cancelled': '190', // 190 取消 - } - const normalizedParams: any = { - ...restParams, - } - if (where) { - normalizedParams.where = { - ...where, - } - if (where.status) { - normalizedParams.where.status = statusMap[where.status]; + }; + + // 如果有状态参数,进行特殊映射 + if (baseParams.status) { + const unifiedStatus = baseParams.status + if (statusMap[unifiedStatus]) { + baseParams.status = statusMap[unifiedStatus]; } } - return normalizedParams + + // 处理ID参数 + if (baseParams.id) { + baseParams.ids = baseParams.id; + delete baseParams.id; + } + + return baseParams; } async getOrders( params: UnifiedSearchParamsDTO ): Promise> { - const normalizedParams = this.mapUnifiedOrderQueryToShopyyQuery(params); - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'orders', + normalizedParams + ); return { items: items.map(this.mapOrder.bind(this)), total, @@ -507,6 +537,17 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; + } + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { const data = await this.shopyyService.getAllOrders(this.site.id, params); return data.map(this.mapOrder.bind(this)); @@ -669,7 +710,7 @@ export class ShopyyAdapter implements ISiteAdapter { const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, 'media', // Shopyy的媒体API端点可能需要调整 - requestParams + requestParams ); return { items: items.map(this.mapMedia.bind(this)), diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index badf467..9828a07 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -718,6 +718,18 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + async getOrder(id: string | number): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); @@ -1294,4 +1306,3 @@ export class WooCommerceAdapter implements ISiteAdapter { } } } - diff --git a/src/controller/media.controller.ts b/src/controller/media.controller.ts deleted file mode 100644 index cafd85f..0000000 --- a/src/controller/media.controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core'; -import { WPService } from '../service/wp.service'; -import { successResponse, errorResponse } from '../utils/response.util'; - -@Controller('/media') -export class MediaController { - @Inject() - wpService: WPService; - - @Get('/list') - async list( - @Query('siteId') siteId: number, - @Query('page') page: number = 1, - @Query('pageSize') pageSize: number = 20 - ) { - try { - if (!siteId) { - return errorResponse('siteId is required'); - } - const result = await this.wpService.getMedia(siteId, page, pageSize); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Post('/upload') - async upload(@Fields() fields, @Files() files) { - try { - const siteId = fields.siteId; - if (!siteId) { - return errorResponse('siteId is required'); - } - if (!files || files.length === 0) { - return errorResponse('file is required'); - } - const file = files[0]; - const result = await this.wpService.createMedia(siteId, file); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Post('/update/:id') - async update(@Param('id') id: number, @Body() body) { - try { - const siteId = body.siteId; - if (!siteId) { - return errorResponse('siteId is required'); - } - // 过滤出需要更新的字段 - const { title, caption, description, alt_text } = body; - const data: any = {}; - if (title !== undefined) data.title = title; - if (caption !== undefined) data.caption = caption; - if (description !== undefined) data.description = description; - if (alt_text !== undefined) data.alt_text = alt_text; - - const result = await this.wpService.updateMedia(siteId, id, data); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Del('/:id') - async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) { - try { - if (!siteId) { - return errorResponse('siteId is required'); - } - const result = await this.wpService.deleteMedia(siteId, id, force); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } -} diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 9f69b87..5013390 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -672,6 +672,26 @@ export class SiteApiController { } } + @Get('/:siteId/orders/count') + @ApiOkResponse({ type: Object }) + async countOrders( + @Param('siteId') siteId: number, + @Query() query: any + ) { + this.logger.info(`[Site API] 获取订单总数开始, siteId: ${siteId}`); + try { + + + const adapter = await this.siteApiService.getAdapter(siteId); + const total = await adapter.countOrders(query); + this.logger.info(`[Site API] 获取订单总数成功, siteId: ${siteId}, total: ${total}`); + return successResponse({ total }); + } catch (error) { + this.logger.error(`[Site API] 获取订单总数失败, siteId: ${siteId}, 错误信息: ${error.message}`); + return errorResponse(error.message); + } + } + @Get('/:siteId/customers/:customerId/orders') @ApiOkResponse({ type: UnifiedOrderPaginationDTO }) async getCustomerOrders( @@ -1050,13 +1070,13 @@ export class SiteApiController { } } - @Get('/:siteId/orders/:orderId/trackings') + @Get('/:siteId/orders/:orderId/fulfillments') @ApiOkResponse({ type: Object }) - async getOrderTrackings( + async getOrderFulfillments( @Param('siteId') siteId: number, @Param('orderId') orderId: string ) { - this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`); + this.logger.info(`[Site API] 获取订单履约信息开始, siteId: ${siteId}, orderId: ${orderId}`); try { const adapter = await this.siteApiService.getAdapter(siteId); const data = await adapter.getOrderFulfillments(orderId); diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index 2bcbca7..fc534d6 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -4,6 +4,10 @@ export interface ShopyyTag { id?: number; name?: string; } +export interface ShopyyProductQuery{ + page: string; + limit: string; +} // 产品类型 export interface ShopyyProduct { // 产品主键 @@ -83,6 +87,42 @@ export interface ShopyyVariant { position?: number | string; sku_code?: string; } +// +// 订单查询参数类型 +export interface ShopyyOrderQuery { + // 订单ID集合 多个ID用','联接 例:1,2,3 + ids?: string; + // 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + status?: string; + // 物流状态 300 未发货;310 部分发货;320 已发货;330(确认收货) + fulfillment_status?: string; + // 支付状态 200 待支付;210 支付中;220 部分支付;230 已支付;240 支付失败;250 部分退款;260 已退款 ;290 已取消; + financial_status?: string; + // 支付时间 下限值 + pay_at_min?: string; + // 支付时间 上限值 + pay_at_max?: string; + // 创建开始时间 + created_at_min?: number; + // 创建结束时间 + created_at_max?: number; + // 更新时间开始 + updated_at_min?: string; + // 更新时间结束 + updated_at_max?: string; + // 起始ID + since_id?: string; + // 页码 + page?: string; + // 每页条数 + limit?: string; + // 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间 + order_field?: string; + // 排序方式(默认desc) desc=降序 asc=升序 + order_by?: string; + // 订单列表类型 + group?: string; +} // 订单类型 export interface ShopyyOrder { diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index ea763ec..1d7b296 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -65,9 +65,6 @@ export class Product { @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) promotionPrice: number; - - - // 分类关联 @ManyToOne(() => Category, category => category.products) @JoinColumn({ name: 'categoryId' }) diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 2e02cc3..ea6db0b 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -40,6 +40,11 @@ export interface ISiteAdapter { */ getOrders(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取订单总数 + */ + countOrders(params: Record): Promise; + /** * 获取所有订单 */ diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index d083e2f..c111a03 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -1,3 +1,6 @@ +/** + * https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse + */ import { ILogger, Inject, Provide } from '@midwayjs/core'; import axios, { AxiosRequestConfig } from 'axios'; import * as fs from 'fs'; @@ -180,41 +183,19 @@ export class ShopyyService { * 通用分页获取资源 */ public async fetchResourcePaged(site: any, endpoint: string, params: Record = {}) { - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - const where = params.where && typeof params.where === 'object' ? params.where : {}; - let orderby: string | undefined = params.orderby; - let order: 'asc' | 'desc' | undefined = params.orderDir as any; - if (!orderby && params.order && typeof params.order === 'object') { - const entries = Object.entries(params.order as Record); - if (entries.length > 0) { - const [field, dir] = entries[0]; - orderby = field; - order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; - } - } - // 映射统一入参到平台入参 - const requestParams = { - ...where, - ...(params.search ? { search: params.search } : {}), - ...(params.status ? { status: params.status } : {}), - ...(orderby ? { orderby } : {}), - ...(order ? { order } : {}), - page, - limit - }; - this.logger.debug('ShopYY API请求分页参数:'+ JSON.stringify(requestParams)); - const response = await this.request(site, endpoint, 'GET', null, requestParams); + const response = await this.request(site, endpoint, 'GET', null, params); + return this.mapPageResponse(response,params); + } + mapPageResponse(response:any,query: Record){ if (response?.code !== 0) { throw new Error(response?.msg) } - return { items: (response.data.list || []) as T[], total: response.data?.paginate?.total || 0, totalPages: response.data?.paginate?.pageTotal || 0, - page: response.data?.paginate?.current || requestParams.page, - per_page: response.data?.paginate?.pagesize || requestParams.limit, + page: response.data?.paginate?.current || query.page, + per_page: response.data?.paginate?.pagesize || query.limit, }; } From 6311451d611355e3e6d07472c1f23dda70dc1cfa Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 18:10:00 +0800 Subject: [PATCH 05/24] =?UTF-8?q?refactor(interface):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E9=80=82=E9=85=8D=E5=99=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E6=8C=89=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组 移除废弃的 fulfillOrder 方法 新增多个数据映射方法以支持统一数据格式转换 --- src/adapter/shopyy.adapter.ts | 1326 +++++++++++++-------- src/adapter/woocommerce.adapter.ts | 1411 ++++++++++++----------- src/controller/site-api.controller.ts | 20 - src/interface/site-adapter.interface.ts | 463 ++++++-- src/transformer/database.transformer.ts | 1 + src/transformer/file.transformer.ts | 1 + src/transformer/shopyy.transformer.ts | 0 src/transformer/woocommerce.adpater.ts | 8 + 8 files changed, 1907 insertions(+), 1323 deletions(-) create mode 100644 src/transformer/database.transformer.ts create mode 100644 src/transformer/file.transformer.ts create mode 100644 src/transformer/shopyy.transformer.ts create mode 100644 src/transformer/woocommerce.adpater.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 0cca7ff..1cf00fd 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -17,7 +17,10 @@ import { UnifiedAddressDTO, UnifiedShippingLineDTO, OrderFulfillmentStatus, - FulfillmentDTO + FulfillmentDTO, + CreateReviewDTO, + CreateVariationDTO, + UpdateReviewDTO } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { @@ -32,6 +35,7 @@ import { import { OrderStatus, } from '../enums/base.enum'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class ShopyyAdapter implements ISiteAdapter { shopyyFinancialStatusMap= { '200': '待支付', @@ -44,130 +48,181 @@ export class ShopyyAdapter implements ISiteAdapter { '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); - this.mapOrder = this.mapOrder.bind(this); - this.mapMedia = this.mapMedia.bind(this); - // this.mapSubscription = this.mapSubscription.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedVariation = this.mapPlatformToUnifiedVariation.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); } - private mapMedia(item: any): UnifiedMediaDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { + // 处理多地址结构 + const addresses = item.addresses || []; + const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); + + // 尝试从地址列表中获取billing和shipping + // 如果没有明确区分,默认使用默认地址或第一个地址 + const billingAddress = defaultAddress; + const shippingAddress = defaultAddress; + + const billing = { + first_name: billingAddress.first_name || item.first_name || '', + last_name: billingAddress.last_name || item.last_name || '', + fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), + company: billingAddress.company || '', + email: item.email || '', + phone: billingAddress.phone || item.contact || '', + address_1: billingAddress.address1 || '', + address_2: billingAddress.address2 || '', + city: billingAddress.city || '', + state: billingAddress.province || '', + postcode: billingAddress.zip || '', + country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' + }; + + const shipping = { + first_name: shippingAddress.first_name || item.first_name || '', + last_name: shippingAddress.last_name || item.last_name || '', + fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), + company: shippingAddress.company || '', + address_1: shippingAddress.address1 || '', + address_2: shippingAddress.address2 || '', + city: shippingAddress.city || '', + state: shippingAddress.province || '', + postcode: shippingAddress.zip || '', + country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' + }; + + return { + id: item.id || item.customer_id, + orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), + total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), + first_name: item.first_name || item.firstname || '', + last_name: item.last_name || item.lastname || '', + fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), + email: item.email || item.customer_email || '', + phone: item.contact || billing.phone || item.phone || '', + billing, + shipping, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), + raw: item, + }; + } + + mapUnifiedToPlatformCustomer(data: Partial) { + return data + } + + async getCustomer(id: string | number): Promise { + const customer = await this.shopyyService.getCustomer(this.site, id); + return this.mapPlatformToUnifiedCustomer(customer); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const { items, total, totalPages, page, per_page } = + await this.shopyyService.fetchCustomersPaged(this.site, params); + return { + items: items.map(this.mapPlatformToUnifiedCustomer.bind(this)), + total, + totalPages, + page, + per_page + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllCustomers 暂未实现 + throw new Error('Shopyy getAllCustomers 暂未实现'); + } + + async createCustomer(data: Partial): Promise { + const createdCustomer = await this.shopyyService.createCustomer(this.site, data); + return this.mapPlatformToUnifiedCustomer(createdCustomer); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + return this.mapPlatformToUnifiedCustomer(updatedCustomer); + } + + async deleteCustomer(id: string | number): Promise { + return await this.shopyyService.deleteCustomer(this.site, id); + } + + batchProcessCustomers?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { // 映射媒体项目 return { - id: item.id, - date_created: item.created_at, - date_modified: item.updated_at, - source_url: item.src, - title: item.alt || '', + id: data.id, + date_created: data.created_at, + date_modified: data.updated_at, + source_url: data.src, + title: data.alt || '', media_type: '', // Shopyy API未提供,暂时留空 mime_type: '', // Shopyy API未提供,暂时留空 }; } - private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { + mapUnifiedToPlatformMedia(data: Partial) { + return data + } + + async getMedia( + params: UnifiedSearchParamsDTO + ): Promise> { + const requestParams = this.mapMediaSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'media', // Shopyy的媒体API端点可能需要调整 + requestParams + ); + return { + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllMedia 暂未实现 + throw new Error('Shopyy getAllMedia 暂未实现'); + } + + async createMedia(file: any): Promise { + const createdMedia = await this.shopyyService.createMedia(this.site, file); + return this.mapPlatformToUnifiedMedia(createdMedia); + } + + async updateMedia(id: string | number, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + return this.mapPlatformToUnifiedMedia(updatedMedia); + } + + async deleteMedia(id: string | number): Promise { + return await this.shopyyService.deleteMedia(this.site, id); + } + + mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { return this.mapSearchParams(params) } - /** - * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 - * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 - */ - private mapSearchParams(params: UnifiedSearchParamsDTO): any { - // 处理分页参数 - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - - // 处理 where 条件 - const query: any = { - ...(params.where || {}), - page, - limit, - } - if(params.orderBy){ - const [field, dir] = Object.entries(params.orderBy)[0]; - query.order_by = dir === 'desc' ? 'desc' : 'asc'; - query.order_field = field - } - return query; - } - - private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { - // 映射产品状态 - function mapProductStatus(status: number) { - return status === 1 ? 'publish' : 'draft'; - } - return { - id: item.id, - name: item.name || item.title, - type: String(item.product_type ?? ''), - status: mapProductStatus(item.status), - sku: item.variant?.sku || '', - regular_price: String(item.variant?.price ?? ''), - sale_price: String(item.special_price ?? ''), - price: String(item.price ?? ''), - stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: item.inventory_quantity, - images: (item.images || []).map((img: any) => ({ - id: img.id || 0, - src: img.src, - name: '', - alt: img.alt || '', - // 排序 - position: img.position || '', - })), - attributes: (item.options || []).map(option => ({ - id: option.id || 0, - name: option.option_name || '', - options: (option.values || []).map(value => value.option_value || ''), - })), - tags: (item.tags || []).map((t: any) => ({ - id: t.id || 0, - name: t.name || '', - })), - // shopyy叫做专辑 - categories: item.collections.map((c: any) => ({ - id: c.id || 0, - name: c.title || '', - })), - variations: item.variants?.map(this.mapVariation.bind(this)) || [], - permalink: item.permalink, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : String(item.created_at ?? ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : String(item.updated_at ?? ''), - raw: item, - }; - } - - private mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { - // 映射变体 - return { - id: variant.id, - name: variant.sku || '', - sku: variant.sku || '', - regular_price: String(variant.price ?? ''), - sale_price: String(variant.special_price ?? ''), - price: String(variant.price ?? ''), - stock_status: - variant.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: variant.inventory_quantity, - }; - } - - 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 - } - private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_address || {}; const shipping = (item as any).shipping_address || {}; @@ -336,99 +391,162 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } - shopyyFulfillmentStatusMap = { - // 未发货 - '300': OrderFulfillmentStatus.PENDING, - // 部分发货 - '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, - // 已发货 - '320': OrderFulfillmentStatus.FULFILLED, - // 已取消 - '330': OrderFulfillmentStatus.CANCELLED, - // 确认发货 + + mapUnifiedToPlatformOrder(data: Partial) { + return data } - private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { - // 处理多地址结构 - const addresses = item.addresses || []; - const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); - - // 尝试从地址列表中获取billing和shipping - // 如果没有明确区分,默认使用默认地址或第一个地址 - const billingAddress = defaultAddress; - const shippingAddress = defaultAddress; - - const billing = { - first_name: billingAddress.first_name || item.first_name || '', - last_name: billingAddress.last_name || item.last_name || '', - fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), - company: billingAddress.company || '', - email: item.email || '', - phone: billingAddress.phone || item.contact || '', - address_1: billingAddress.address1 || '', - address_2: billingAddress.address2 || '', - city: billingAddress.city || '', - state: billingAddress.province || '', - postcode: billingAddress.zip || '', - country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' - }; - - const shipping = { - first_name: shippingAddress.first_name || item.first_name || '', - last_name: shippingAddress.last_name || item.last_name || '', - fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), - company: shippingAddress.company || '', - address_1: shippingAddress.address1 || '', - address_2: shippingAddress.address2 || '', - city: shippingAddress.city || '', - state: shippingAddress.province || '', - postcode: shippingAddress.zip || '', - country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' - }; - - return { - id: item.id || item.customer_id, - orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), - total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), - first_name: item.first_name || item.firstname || '', - last_name: item.last_name || item.lastname || '', - fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), - email: item.email || item.customer_email || '', - phone: item.contact || billing.phone || item.phone || '', - billing, - shipping, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), - raw: item, - }; - } - mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { - return this.mapSearchParams(query) + mapCreateOrderParams(data: Partial): any { + return data } - async getProducts( + mapUpdateOrderParams(data: Partial): any { + // 构建 ShopYY 订单更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.status !== undefined) { + // 映射订单状态 + const statusMap = { + [OrderStatus.PENDING]: 100, // pending -> 100 未完成 + [OrderStatus.PROCESSING]: 110, // processing -> 110 待处理 + [OrderStatus.COMPLETED]: 180, // completed -> 180 已完成 + [OrderStatus.CANCEL]: 190 // cancel -> 190 取消 + }; + params.status = statusMap[data.status] || 100; + } + + if (data.payment_method !== undefined) { + params.payment_method = data.payment_method; + } + + if (data.billing) { + // 更新客户信息 + if (data.billing.first_name !== undefined) { + params.firstname = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.lastname = data.billing.last_name; + } + if (data.billing.email !== undefined) { + params.email = data.billing.email; + } + if (data.billing.phone !== undefined) { + params.phone = data.billing.phone; + } + + // 更新账单地址 + params.billing_address = params.billing_address || {}; + if (data.billing.first_name !== undefined) { + params.billing_address.first_name = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.billing_address.last_name = data.billing.last_name; + } + if (data.billing.company !== undefined) { + params.billing_address.company = data.billing.company; + } + if (data.billing.address_1 !== undefined) { + params.billing_address.address1 = data.billing.address_1; + } + if (data.billing.address_2 !== undefined) { + params.billing_address.address2 = data.billing.address_2; + } + if (data.billing.city !== undefined) { + params.billing_address.city = data.billing.city; + } + if (data.billing.state !== undefined) { + params.billing_address.province = data.billing.state; + } + if (data.billing.postcode !== undefined) { + params.billing_address.zip = data.billing.postcode; + } + if (data.billing.country !== undefined) { + params.billing_address.country_code = data.billing.country; + } + } + + if (data.shipping) { + // 更新送货地址 + params.shipping_address = params.shipping_address || {}; + if (data.shipping.first_name !== undefined) { + params.shipping_address.first_name = data.shipping.first_name; + } + if (data.shipping.last_name !== undefined) { + params.shipping_address.last_name = data.shipping.last_name; + } + if (data.shipping.company !== undefined) { + params.shipping_address.company = data.shipping.company; + } + if (data.shipping.address_1 !== undefined) { + params.shipping_address.address1 = data.shipping.address_1; + } + if (data.shipping.address_2 !== undefined) { + params.shipping_address.address2 = data.shipping.address_2; + } + if (data.shipping.city !== undefined) { + params.shipping_address.city = data.shipping.city; + } + if (data.shipping.state !== undefined) { + params.shipping_address.province = data.shipping.state; + } + if (data.shipping.postcode !== undefined) { + params.shipping_address.zip = data.shipping.postcode; + } + if (data.shipping.country !== undefined) { + params.shipping_address.country_code = data.shipping.country; + } + if (data.shipping.phone !== undefined) { + params.shipping_address.phone = data.shipping.phone; + } + } + + // 更新订单项 + if (data.line_items && data.line_items.length > 0) { + params.products = data.line_items.map((item: UnifiedOrderLineItemDTO) => ({ + product_id: item.product_id, + quantity: item.quantity, + // price: item.price || '0.00', + sku: item.sku || '', + })); + } + + // 更新物流信息 + if (data.shipping_lines && data.shipping_lines.length > 0) { + const shippingLine = data.shipping_lines[0]; + if (shippingLine.method_title !== undefined) { + params.shipping_method = shippingLine.method_title; + } + if (shippingLine.total !== undefined) { + params.shipping_price = shippingLine.total; + } + } + + // // 更新备注信息 + // if (data.note !== undefined) { + // params.note = data.note; + // } + + return params; + } + + async getOrder(id: string | number): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(id)); + return this.mapPlatformToUnifiedOrder(data); + } + + async getOrders( params: UnifiedSearchParamsDTO - ): Promise> { - // 转换搜索参数 - const requestParams = this.mapProductQuery(params); - const response = await this.shopyyService.fetchResourcePaged( + ): Promise> { + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, - 'products/list', - requestParams + 'orders', + normalizedParams ); - 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)) return { - items: finalItems as UnifiedProductDTO[], + items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), total, totalPages, page, @@ -436,34 +554,37 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + const data = await this.shopyyService.getAllOrders(this.site.id, params); + return data.map(this.mapPlatformToUnifiedOrder.bind(this)); } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapProduct(product); + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; } - async createProduct(data: Partial): Promise { - const res = await this.shopyyService.createProduct(this.site, data); - return this.mapProduct(res); - } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). - await this.shopyyService.updateProduct(this.site, String(id), data); - return true; + async createOrder(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateOrderParams(data); + const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); + return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateVariation(productId: string | number, variationId: string | number, data: any): Promise { - await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data); - return { ...data, id: variationId }; + async updateOrder(id: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateOrderParams(data); + return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + } + + async deleteOrder(id: string | number): Promise { + return await this.shopyyService.deleteOrder(this.site, id); } async getOrderNotes(orderId: string | number): Promise { @@ -474,22 +595,65 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.createOrderNote(this.site, orderId, data); } - async deleteProduct(id: string | number): Promise { - // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); - return true; + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + // 取消订单履行 + try { + // 调用 ShopyyService 的取消履行方法 + const cancelShipData = { + order_id: String(orderId), + fulfillment_id: data.shipment_id || '' + }; + const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); + + return { + success: result, + order_id: orderId, + shipment_id: data.shipment_id, + reason: data.reason, + cancelled_at: new Date().toISOString() + }; + } catch (error) { + throw new Error(`履行失败: ${error.message}`); + } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - return await this.shopyyService.batchProcessProducts(this.site, data); + async getOrderFulfillments(orderId: string | number): Promise { + return await this.shopyyService.getFulfillments(this.site, String(orderId)); } - /** - * 将统一的订单查询参数转换为 ShopYY 订单查询参数 - * 包含状态映射等特殊处理 - */ - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + + async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { + // 调用 Shopyy Service 的 createFulfillment 方法 + const fulfillmentData = { + tracking_number: data.tracking_number, + 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); + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: string; + tracking_provider?: string; + date_shipped?: string; + status_shipped?: string; + }): Promise { + return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + batchProcessOrders?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 首先使用通用参数转换 const baseParams = this.mapSearchParams(params); @@ -518,18 +682,205 @@ export class ShopyyAdapter implements ISiteAdapter { return baseParams; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - // 转换订单查询参数 - const normalizedParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + // 映射产品状态 + function mapProductStatus(status: number) { + return status === 1 ? 'publish' : 'draft'; + } return { - items: items.map(this.mapOrder.bind(this)), + id: item.id, + name: item.name || item.title, + type: String(item.product_type ?? ''), + status: mapProductStatus(item.status), + sku: item.variant?.sku || '', + regular_price: String(item.variant?.price ?? ''), + sale_price: String(item.special_price ?? ''), + price: String(item.price ?? ''), + stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: item.inventory_quantity, + images: (item.images || []).map((img: any) => ({ + id: img.id || 0, + src: img.src, + name: '', + alt: img.alt || '', + // 排序 + position: img.position || '', + })), + attributes: (item.options || []).map(option => ({ + id: option.id || 0, + name: option.option_name || '', + options: (option.values || []).map(value => value.option_value || ''), + })), + tags: (item.tags || []).map((t: any) => ({ + id: t.id || 0, + name: t.name || '', + })), + // shopyy叫做专辑 + categories: item.collections.map((c: any) => ({ + id: c.id || 0, + name: c.title || '', + })), + variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], + permalink: item.permalink, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : String(item.created_at ?? ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : String(item.updated_at ?? ''), + raw: item, + }; + } + + mapUnifiedToPlatformProduct(data: Partial) { + return data + } + + mapCreateProductParams(data: Partial): any { + // 构建 ShopYY 产品创建参数 + const params: any = { + name: data.name || '', + product_type: data.type || 1, // 默认简单产品 + status: this.mapStatus(data.status || 'publish'), + price: data.price || '0.00', + special_price: data.sale_price || '', + inventory_tracking: data.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: data.stock_quantity || 0, + }; + + // 添加变体信息 + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => ({ + sku: variation.sku || '', + price: variation.price || '0.00', + special_price: variation.sale_price || '', + inventory_tracking: variation.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: variation.stock_quantity || 0, + })); + } + + // 添加图片信息 + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息 + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息 + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + mapUpdateProductParams(data: Partial): any { + // 映射产品状态: publish -> 1, draft -> 0 + const mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 构建 ShopYY 产品更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.name !== undefined) params.name = data.name; + if (data.type !== undefined) params.product_type = data.type; + if (data.status !== undefined) params.status = mapStatus(data.status); + if (data.price !== undefined) params.price = data.price; + if (data.sale_price !== undefined) params.special_price = data.sale_price; + if (data.sku !== undefined) params.sku = data.sku; + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + // 添加变体信息(如果存在) + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => { + const variationParams: any = {}; + if (variation.id !== undefined) variationParams.id = variation.id; + if (variation.sku !== undefined) variationParams.sku = variation.sku; + if (variation.price !== undefined) variationParams.price = variation.price; + if (variation.sale_price !== undefined) variationParams.special_price = variation.sale_price; + if (variation.stock_quantity !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_quantity; + } + if (variation.stock_status !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_status === 'instock' ? (variation.stock_quantity || 1) : 0; + } + return variationParams; + }); + } + + // 添加图片信息(如果存在) + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + id: image.id, + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息(如果存在) + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息(如果存在) + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + async getProduct(id: string | number): Promise { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, id); + return this.mapPlatformToUnifiedProduct(product); + } + + async getProducts( + params: UnifiedSearchParamsDTO + ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); + const response = await this.shopyyService.fetchResourcePaged( + this.site, + 'products/list', + requestParams + ); + const { items = [], total, totalPages, page, per_page } = response; + const finalItems = items.map((item) => ({ + ...item, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, + })).map(this.mapPlatformToUnifiedProduct.bind(this)) + return { + items: finalItems as UnifiedProductDTO[], total, totalPages, page, @@ -537,207 +888,61 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams = { - where, - page: 1, - per_page: 1, - } - const data = await this.getOrders(searchParams); - return data.total || 0; + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllProducts 暂未实现 + throw new Error('Shopyy getAllProducts 暂未实现'); } - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - const data = await this.shopyyService.getAllOrders(this.site.id, params); - return data.map(this.mapOrder.bind(this)); + async createProduct(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateProductParams(data); + const res = await this.shopyyService.createProduct(this.site, requestParams); + return this.mapPlatformToUnifiedProduct(res); } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); - return this.mapOrder(data); + async updateProduct(id: string | number, data: Partial): Promise { + // Shopyy update returns boolean? + // shopyyService.updateProduct returns boolean. + // So I can't return the updated product. + // I have to fetch it again or return empty/input. + // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + // 使用映射方法转换参数 + const requestParams = this.mapUpdateProductParams(data); + await this.shopyyService.updateProduct(this.site, String(id), requestParams); + return true; } - async createOrder(data: Partial): Promise { - const createdOrder = await this.shopyyService.createOrder(this.site, data); - return this.mapOrder(createdOrder); + async deleteProduct(id: string | number): Promise { + // Use batch delete + await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + return true; } - async updateOrder(id: string | number, data: Partial): Promise { - return await this.shopyyService.updateOrder(this.site, String(id), data); + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + return await this.shopyyService.batchProcessProducts(this.site, data); } - async deleteOrder(id: string | number): Promise { - return await this.shopyyService.deleteOrder(this.site, id); + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) } - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - // 订单履行(发货) - try { - // 判断是否为部分发货(包含 items) - if (data.items && data.items.length > 0) { - // 部分发货 - const partShipData = { - order_number: String(orderId), - note: data.shipping_method || '', - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: '1', // 默认快递公司代码 - products: data.items.map(item => ({ - quantity: item.quantity, - order_product_id: String(item.order_item_id) - })) - }; - return await this.shopyyService.partFulfillOrder(this.site, partShipData); - } else { - // 批量发货(完整发货) - const batchShipData = { - order_number: String(orderId), - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: 1, // 默认快递公司代码 - note: data.shipping_method || '', - mode: null // 新增模式 - }; - return await this.shopyyService.batchFulfillOrders(this.site, batchShipData); - } - } catch (error) { - throw new Error(`履行失败: ${error.message}`); - } + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data } - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - // 取消订单履行 - try { - // 调用 ShopyyService 的取消履行方法 - const cancelShipData = { - order_id: String(orderId), - fulfillment_id: data.shipment_id || '' - }; - const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); - - return { - success: result, - order_id: orderId, - shipment_id: data.shipment_id, - reason: data.reason, - cancelled_at: new Date().toISOString() - }; - } catch (error) { - throw new Error(`取消履行失败: ${error.message}`); - } + mapUnifiedToPlatformReview(data: Partial) { + return data } - /** - * 获取订单履行信息 - * @param orderId 订单ID - * @returns 履行信息列表 - */ - async getOrderFulfillments(orderId: string | number): Promise { - return await this.shopyyService.getFulfillments(this.site, String(orderId)); + mapCreateReviewParams(data: CreateReviewDTO) { + return data } - /** - * 创建订单履行信息 - * @param orderId 订单ID - * @param data 履行数据 - * @returns 创建结果 - */ - async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { - // 调用 Shopyy Service 的 createFulfillment 方法 - const fulfillmentData = { - tracking_number: data.tracking_number, - 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); - } - - /** - * 更新订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @param data 更新数据 - * @returns 更新结果 - */ - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: string; - tracking_provider?: string; - date_shipped?: string; - status_shipped?: string; - }): Promise { - return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); - } - - /** - * 删除订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @returns 删除结果 - */ - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - throw new Error('Shopyy does not support subscriptions.'); - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllSubscriptions 暂未实现 - throw new Error('Shopyy getAllSubscriptions 暂未实现'); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapMediaSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'media', // Shopyy的媒体API端点可能需要调整 - requestParams - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllMedia 暂未实现 - throw new Error('Shopyy getAllMedia 暂未实现'); - } - - async createMedia(file: any): Promise { - const createdMedia = await this.shopyyService.createMedia(this.site, file); - return this.mapMedia(createdMedia); - } - - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); - return this.mapMedia(updatedMedia); - } - - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data } async getReviews( @@ -762,12 +967,21 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllReviews 暂未实现'); } - async getReview(id: string | number): Promise { - const review = await this.shopyyService.getReview(this.site, id); - return this.mapReview(review); + async createReview(data: any): Promise { + const createdReview = await this.shopyyService.createReview(this.site, data); + return this.mapReview(createdReview); } - private mapReview(review: any): UnifiedReviewDTO { + async updateReview(id: string | number, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, id, data); + return this.mapReview(updatedReview); + } + + async deleteReview(id: string | number): Promise { + return await this.shopyyService.deleteReview(this.site, id); + } + + mapReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -784,7 +998,7 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { + mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { const { search, page, per_page, where } = params; const shopyyParams: any = { page: page || 1, @@ -799,38 +1013,133 @@ export class ShopyyAdapter implements ISiteAdapter { shopyyParams.status = where.status; } - // if (product_id) { - // shopyyParams.product_id = product_id; - // } - return shopyyParams; } - async createReview(data: any): Promise { - const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + mapUnifiedToPlatformSubscription(data: Partial) { + return data } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async getSubscriptions( + params: UnifiedSearchParamsDTO + ): Promise> { + throw new Error('Shopyy does not support subscriptions.'); } - // Webhook相关方法 - private mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllSubscriptions 暂未实现 + throw new Error('Shopyy getAllSubscriptions 暂未实现'); + } + + // ========== 产品变体映射方法 ========== + mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { + // 映射变体 return { - id: item.id, - name: item.webhook_name || `Webhook-${item.id}`, - topic: item.event_code || '', - delivery_url: item.url || '', - status: 'active', + id: variant.id, + name: variant.sku || '', + sku: variant.sku || '', + regular_price: String(variant.price ?? ''), + sale_price: String(variant.special_price ?? ''), + price: String(variant.price ?? ''), + stock_status: + variant.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: variant.inventory_quantity, }; } + mapUnifiedToPlatformVariation(data: Partial) { + return data + } + + mapCreateVariationParams(data: CreateVariationDTO) { + return data + } + + mapUpdateVariationParams(data: Partial): any { + // 构建 ShopYY 变体更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.id !== undefined) { + params.id = data.id; + } + if (data.sku !== undefined) { + params.sku = data.sku; + } + if (data.price !== undefined) { + params.price = data.price; + } + if (data.sale_price !== undefined) { + params.special_price = data.sale_price; + } + + // 处理库存信息 + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + return params; + } + + async getVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy getVariation 暂未实现'); + } + + async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getVariations 暂未实现'); + } + + async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getAllVariations 暂未实现'); + } + + async createVariation(productId: string | number, data: any): Promise { + throw new Error('Shopyy createVariation 暂未实现'); + } + + async updateVariation(productId: string | number, variationId: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateVariationParams(data); + await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), requestParams); + return { ...data, id: variationId }; + } + + async deleteVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy deleteVariation 暂未实现'); + } + + // ========== Webhook映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data + } + + mapUnifiedToPlatformWebhook(data: Partial) { + return data + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data + } + + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data + } + + async getWebhook(id: string | number): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, id); + return this.mapWebhook(webhook); + } + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { @@ -847,11 +1156,6 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllWebhooks 暂未实现'); } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); - } - async createWebhook(data: CreateWebhookDTO): Promise { const createdWebhook = await this.shopyyService.createWebhook(this.site, data); return this.mapWebhook(createdWebhook); @@ -866,6 +1170,17 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteWebhook(this.site, id); } + mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + return { + id: item.id, + name: item.webhook_name || `Webhook-${item.id}`, + topic: item.event_code || '', + delivery_url: item.url || '', + status: 'active', + }; + } + + // ========== 站点/其他方法 ========== async getLinks(): Promise> { // ShopYY站点的管理后台链接通常基于apiUrl构建 const url = this.site.websiteUrl @@ -885,59 +1200,58 @@ export class ShopyyAdapter implements ISiteAdapter { return links; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchCustomersPaged(this.site, params); - return { - items: items.map(this.mapCustomer.bind(this)), - total, - totalPages, + // ========== 辅助方法 ========== + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), page, - per_page - }; + limit, + } + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllCustomers 暂未实现 - throw new Error('Shopyy getAllCustomers 暂未实现'); + // 映射产品状态: publish -> 1, draft -> 0 + mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 映射库存状态: instock -> 1, outofstock -> 0 + mapStockStatus = (stockStatus: string) => { + return stockStatus === 'instock' ? 1 : 0; + }; + + 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 } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); - return this.mapCustomer(customer); + shopyyFulfillmentStatusMap = { + // 未发货 + '300': OrderFulfillmentStatus.PENDING, + // 部分发货 + '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, + // 已发货 + '320': OrderFulfillmentStatus.FULFILLED, + // 已取消 + '330': OrderFulfillmentStatus.CANCELLED, + // 确认发货 } - - async createCustomer(data: Partial): Promise { - const createdCustomer = await this.shopyyService.createCustomer(this.site, data); - return this.mapCustomer(createdCustomer); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); - return this.mapCustomer(updatedCustomer); - } - - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); - } - - async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getVariations 暂未实现'); - } - - async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getAllVariations 暂未实现'); - } - - async getVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy getVariation 暂未实现'); - } - - async createVariation(productId: string | number, data: any): Promise { - throw new Error('Shopyy createVariation 暂未实现'); - } - - async deleteVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy deleteVariation 暂未实现'); - } -} +} \ No newline at end of file diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 9828a07..a5f2f0a 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -15,6 +15,8 @@ import { UpdateVariationDTO, UnifiedProductVariationDTO, UnifiedVariationPaginationDTO, + CreateReviewDTO, + UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { @@ -29,6 +31,7 @@ import { } from '../dto/woocommerce.dto'; import { Site } from '../entity/site.entity'; import { WPService } from '../service/wp.service'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 @@ -40,192 +43,236 @@ export class WooCommerceAdapter implements ISiteAdapter { this.mapOrder = this.mapOrder.bind(this); this.mapWebhook = this.mapWebhook.bind(this); } + batchProcessProducts?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + mapCreateVariationParams(data: CreateVariationDTO) { + throw new Error('Method not implemented.'); + } + mapUpdateVariationParams(data: UpdateVariationDTO) { + throw new Error('Method not implemented.'); + } - // 映射 WooCommerce webhook 到统一格式 - private mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { + return data; + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data; + } + + mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + // 将 WooCommerce 客户数据映射为统一客户DTO + // 包含基础信息地址信息与时间信息 return { - id: webhook.id.toString(), - name: webhook.name, - status: webhook.status, - topic: webhook.topic, - delivery_url: webhook.delivery_url, - secret: webhook.secret, - api_version: webhook.api_version, - date_created: webhook.date_created, - date_modified: webhook.date_modified, - // metadata: webhook.meta_data || [], + id: item.id, + avatar: item.avatar_url, + email: item.email, + orders: Number(item.orders ?? 0), + total_spend: Number(item.total_spent ?? 0), + first_name: item.first_name, + last_name: item.last_name, + username: item.username, + phone: item.billing?.phone || item.shipping?.phone, + billing: item.billing, + shipping: item.shipping, + date_created: item.date_created, + date_modified: item.date_modified, + raw: item, }; } - // 获取站点的 webhooks 列表 - async getWebhooks(params: UnifiedSearchParamsDTO): Promise { - try { - const result = await this.wpService.getWebhooks(this.site, params); - - return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; - } catch (error) { - throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取所有webhooks - async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { - try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); - } catch (error) { - throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { - try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 创建新的 webhook - async createWebhook(data: CreateWebhookDTO): Promise { - try { - const params = { - name: data.name, - status: 'active', // 默认状态为活跃 - topic: data.topic, - delivery_url: data.delivery_url, - secret: data.secret, - api_version: data.api_version || 'wp/v2', - }; - const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { - try { - const params = { - ...(data.name ? { name: data.name } : {}), - ...(data.status ? { status: data.status } : {}), - ...(data.topic ? { topic: data.topic } : {}), - ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), - ...(data.secret ? { secret: data.secret } : {}), - ...(data.api_version ? { api_version: data.api_version } : {}), - }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { - try { - await this.wpService.deleteWebhook(this.site, id); - return true; - } catch (error) { - throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - async getLinks(): Promise> { - const baseUrl = this.site.apiUrl; - const links = [ - { title: '访问网站', url: baseUrl }, - { title: '管理后台', url: `${baseUrl}/wp-admin/` }, - { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, - { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, - { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, - { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, - { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, - { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, - { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, - ]; - return links; - } - - createMedia(file: any): Promise { - throw new Error('Method not implemented.'); - } - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - - - - private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { + mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; + const mapped: any = { ...(params.search ? { search: params.search } : {}), - ...(where.status ? { status: where.status } : {}), page, per_page, }; + // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 + if (params.orderBy) { + // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } + if (typeof params.orderBy === 'string') { + const [field, direction = 'desc'] = params.orderBy.split(':'); + mapped.orderby = field; + mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; + } else if (typeof params.orderBy === 'object') { + const entries = Object.entries(params.orderBy); + if (entries.length > 0) { + const [field, direction] = entries[0]; + mapped.orderby = field; + mapped.order = direction === 'asc' ? 'asc' : 'desc'; + } + } + } + const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); - if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); - if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); - if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); - if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); - if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); - if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); - if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); - if (where.offset !== undefined) mapped.offset = Number(where.offset); - if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); - if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); - if (where.slug) mapped.slug = String(where.slug); - if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { - if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); - if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); - if (where.status) mapped.status = String(where.status); - } - if (where.type) mapped.type = String(where.type); - if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); - if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); - if (where.sku) mapped.sku = String(where.sku); - if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); - if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); - if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); - if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); - if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); - if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); - if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); - if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); - if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); - if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); - if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); - if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); - if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); + const toNumber = (value: any): number | undefined => { + if (value === undefined || value === null || value === '') return undefined; + const n = Number(value); + return Number.isFinite(n) ? n : undefined; + }; + + if (where.exclude) mapped.exclude = toArray(where.exclude); + if (where.include) mapped.include = toArray(where.include); + if (where.ids) mapped.include = toArray(where.ids); + if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); + + if (where.email) mapped.email = String(where.email); + const roleSource = where.role; + if (roleSource !== undefined) mapped.role = String(roleSource); return mapped; } - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 客户操作方法 + async getCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`customers/${id}`); + return this.mapCustomer(res.data); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapCustomerSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'customers', + requestParams + ); + return { + items: items.map((i: any) => this.mapCustomer(i)), + total, + totalPages, + page, + per_page, + + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有客户数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // 处理orderBy参数,转换为WooCommerce API需要的格式 + const requestParams = this.mapCustomerSearchParams(params || {}); + + const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); + return customers.map((customer: any) => this.mapCustomer(customer)); + } + + async createCustomer(data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('customers', data); + return this.mapCustomer(res.data); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.put(`customers/${id}`, data); + return this.mapCustomer(res.data); + } + + async deleteCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`customers/${id}`, { force: true }); + return true; + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { + return data; + } + mapUnifiedToPlatformMedia(data: Partial) { + return data; + } + + mapMedia(item: WpMedia): UnifiedMediaDTO { + // 将 WordPress 媒体数据映射为统一媒体DTO + // 兼容不同字段命名的时间信息 + return { + id: item.id, + title: + typeof item.title === 'string' + ? item.title + : item.title?.rendered || '', + media_type: item.media_type, + mime_type: item.mime_type, + source_url: item.source_url, + date_created: item.date_created ?? item.date, + date_modified: item.date_modified ?? item.modified, + }; + } + + // 媒体操作方法 + async getMedia(params: UnifiedSearchParamsDTO): Promise> { + // 获取媒体列表并映射为统一媒体DTO集合 + const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( + this.site, + params + ); + return { + items: items.map(this.mapMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有媒体数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const media = await this.wpService.sdkGetAll(api, 'media', params); + return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + } + + createMedia(file: any): Promise { + throw new Error('Method not implemented.'); + } + + async updateMedia(id: string | number, data: any): Promise { + // 更新媒体信息 + return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + } + + async deleteMedia(id: string | number): Promise { + // 删除媒体资源 + await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + return true; + } + + async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { + // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 + const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); + return result as any; + } + + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { + return data; + } + mapUnifiedToPlatformOrder(data: Partial) { + return data; + } + + mapCreateOrderParams(data: Partial) { + return data; + } + mapUpdateOrderParams(data: Partial) { + return data; + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 计算分页参数 const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); @@ -293,60 +340,328 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - private mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { + private buildFullAddress(addr: any): string { + if (!addr) return ''; + const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); + return [ + name, + addr.company, + addr.address_1, + addr.address_2, + addr.city, + addr.state, + addr.postcode, + addr.country, + addr.phone + ].filter(Boolean).join(', '); + } + + mapOrder(item: WooOrder): UnifiedOrderDTO { + // 将 WooCommerce 订单数据映射为统一订单DTO + // 包含账单地址与收货地址以及创建与更新时间 + + // 映射物流追踪信息,将后端格式转换为前端期望的格式 + const fulfillments = (item.fulfillments || []).map((track: any) => ({ + tracking_number: track.tracking_number || '', + shipping_provider: track.shipping_provider || '', + shipping_method: track.shipping_method || '', + status: track.status || '', + date_created: track.date_created || '', + items: track.items || [], + })); + + return { + id: item.id, + number: item.number, + status: item.status, + currency: item.currency, + total: item.total, + customer_id: item.customer_id, + customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 + email: item.billing?.email || '', + customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), + refunds: item.refunds?.map?.(refund => ({ + id: refund.id, + reason: refund.reason, + total: refund.total, + })), + line_items: (item.line_items as any[]).map(li => ({ + ...li, + productId: li.product_id, + })), + customer_ip_address: item.customer_ip_address ?? '', + date_paid: item.date_paid ?? '', + utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', + device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', + source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', + billing: item.billing, + shipping: item.shipping, + billing_full_address: this.buildFullAddress(item.billing), + shipping_full_address: this.buildFullAddress(item.shipping), + payment_method: item.payment_method_title, + date_created: item.date_created, + date_modified: item.date_modified, + shipping_lines: item.shipping_lines, + fee_lines: item.fee_lines, + coupon_lines: item.coupon_lines, + fulfillments, + raw: item, + }; + } + + // 订单操作方法 + async getOrder(id: string | number): Promise { + // 获取单个订单详情 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${id}`); + return this.mapOrder(res.data); + } + + async getOrders(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + + // 并行获取所有订单的履行信息 + const ordersWithFulfillments = await Promise.all( + items.map(async (order: any) => { + try { + // 获取订单的履行信息 + const fulfillments = await this.getOrderFulfillments(order.id); + // 将履行信息添加到订单对象中 + return { + ...order, + fulfillments: fulfillments || [] + }; + } catch (error) { + // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 + console.error(`获取订单 ${order.id} 的履行信息失败:`, error); + return { + ...order, + fulfillments: [] + }; + } + }) + ); + + return { + items: ordersWithFulfillments.map(this.mapOrder), + total, + totalPages, + page, + per_page, + }; + } + + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订单数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const orders = await this.wpService.sdkGetAll(api, 'orders', params); + return orders.map((order: any) => this.mapOrder(order)); + } + + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + + async createOrder(data: Partial): Promise { + // 创建订单并返回统一订单DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('orders', data); + return this.mapOrder(res.data); + } + + async updateOrder(id: string | number, data: Partial): Promise { + // 更新订单并返回布尔结果 + return await this.wpService.updateOrder(this.site, String(id), data as any); + } + + async deleteOrder(id: string | number): Promise { + // 删除订单 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`orders/${id}`, { force: true }); + return true; + } + + async getOrderFulfillments(orderId: string | number): Promise { + return await this.wpService.getFulfillments(this.site, String(orderId)); + } + + async createOrderFulfillment(orderId: string | number, data: { + tracking_number: string; + shipping_provider: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; + }): Promise { + const shipmentData: any = { + shipping_provider: data.shipping_provider, + tracking_number: data.tracking_number, + }; + + if (data.shipping_method) { + shipmentData.shipping_method = data.shipping_method; + } + + 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); + return response.data; + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: 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); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + async getOrderNotes(orderId: string | number): Promise { + // 获取订单备注列表 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${orderId}/notes`); + return res.data; + } + + async createOrderNote(orderId: string | number, data: any): Promise { + // 创建订单备注 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post(`orders/${orderId}/notes`, data); + return res.data; + } + + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + throw new Error('暂未实现'); + // 取消订单履行 + // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // try { + // // 将订单状态改回处理中 + // await api.put(`orders/${orderId}`, { status: 'processing' }); + + // // 添加取消履行的备注 + // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; + // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); + + // return { + // success: true, + // order_id: orderId, + // shipment_id: data.shipment_id, + // reason: data.reason, + // cancelled_at: new Date().toISOString() + // }; + // } catch (error) { + // throw new Error(`取消履行失败: ${error.message}`); + // } + } + + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { + return data; + } + mapUnifiedToPlatformProduct(data: Partial) { + return data; + } + + mapCreateProductParams(data: Partial) { + return data; + } + mapUpdateProductParams(data: Partial) { + return data; + } + + mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; - const mapped: any = { ...(params.search ? { search: params.search } : {}), + ...(where.status ? { status: where.status } : {}), page, per_page, }; - // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 - if (params.orderBy) { - // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } - if (typeof params.orderBy === 'string') { - const [field, direction = 'desc'] = params.orderBy.split(':'); - mapped.orderby = field; - mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; - } else if (typeof params.orderBy === 'object') { - const entries = Object.entries(params.orderBy); - if (entries.length > 0) { - const [field, direction] = entries[0]; - mapped.orderby = field; - mapped.order = direction === 'asc' ? 'asc' : 'desc'; - } - } - } - const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - const toNumber = (value: any): number | undefined => { - if (value === undefined || value === null || value === '') return undefined; - const n = Number(value); - return Number.isFinite(n) ? n : undefined; - }; - - if (where.exclude) mapped.exclude = toArray(where.exclude); - if (where.include) mapped.include = toArray(where.include); - if (where.ids) mapped.include = toArray(where.ids); - if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); - - if (where.email) mapped.email = String(where.email); - const roleSource = where.role; - if (roleSource !== undefined) mapped.role = String(roleSource); + if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); + if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); + if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); + if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); + if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); + if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); + if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); + if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); + if (where.offset !== undefined) mapped.offset = Number(where.offset); + if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); + if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); + if (where.slug) mapped.slug = String(where.slug); + if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { + if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); + if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); + if (where.status) mapped.status = String(where.status); + } + if (where.type) mapped.type = String(where.type); + if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); + if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); + if (where.sku) mapped.sku = String(where.sku); + if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); + if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); + if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); + if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); + if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); + if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); + if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); + if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); + if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); + if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); + if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); + if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); + if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); return mapped; } - private mapProduct(item: WooProduct): UnifiedProductDTO { + mapProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -434,120 +749,41 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - private buildFullAddress(addr: any): string { - if (!addr) return ''; - const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); - return [ - name, - addr.company, - addr.address_1, - addr.address_2, - addr.city, - addr.state, - addr.postcode, - addr.country, - addr.phone - ].filter(Boolean).join(', '); - } - private mapOrder(item: WooOrder): UnifiedOrderDTO { - // 将 WooCommerce 订单数据映射为统一订单DTO - // 包含账单地址与收货地址以及创建与更新时间 - // 映射物流追踪信息,将后端格式转换为前端期望的格式 - const fulfillments = (item.fulfillments || []).map((track: any) => ({ - tracking_number: track.tracking_number || '', - shipping_provider: track.shipping_provider || '', - shipping_method: track.shipping_method || '', - status: track.status || '', - date_created: track.date_created || '', - items: track.items || [], - })); + // 产品操作方法 + async getProduct(id: string | number): Promise { + // 获取单个产品详情并映射为统一产品DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); + const product = res.data; - return { - id: item.id, - number: item.number, - status: item.status, - currency: item.currency, - total: item.total, - customer_id: item.customer_id, - customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 - email: item.billing?.email || '', - customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), - refunds: item.refunds?.map?.(refund => ({ - id: refund.id, - reason: refund.reason, - total: refund.total, - })), - line_items: (item.line_items as any[]).map(li => ({ - ...li, - productId: li.product_id, - })), - customer_ip_address: item.customer_ip_address ?? '', - date_paid: item.date_paid ?? '', - utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', - device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', - source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', - billing: item.billing, - shipping: item.shipping, - billing_full_address: this.buildFullAddress(item.billing), - shipping_full_address: this.buildFullAddress(item.shipping), - payment_method: item.payment_method_title, - date_created: item.date_created, - date_modified: item.date_modified, - shipping_lines: item.shipping_lines, - fee_lines: item.fee_lines, - coupon_lines: item.coupon_lines, - fulfillments, - raw: item, - }; + // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 + if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { + try { + // 批量获取该产品的所有变体数据 + const variations = await this.wpService.sdkGetAll( + api, + `products/${product.id}/variations` + ); + // 将完整的变体数据添加到产品对象中 + product.variations = variations; + } catch (error) { + // 如果获取变体失败,保持原有的 ID 数组 + console.error(`获取产品 ${product.id} 的变体数据失败:`, error); + } + } + + return this.mapProduct(product); } - private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { - // 将 WooCommerce 订阅数据映射为统一订阅DTO - // 若缺少创建时间则回退为开始时间 - return { - id: item.id, - status: item.status, - customer_id: item.customer_id, - billing_period: item.billing_period, - billing_interval: item.billing_interval, - date_created: item.date_created ?? item.start_date, - date_modified: item.date_modified, - start_date: item.start_date, - next_payment_date: item.next_payment_date, - line_items: item.line_items, - raw: item, - }; - } - - private mapMedia(item: WpMedia): UnifiedMediaDTO { - // 将 WordPress 媒体数据映射为统一媒体DTO - // 兼容不同字段命名的时间信息 - return { - id: item.id, - title: - typeof item.title === 'string' - ? item.title - : item.title?.rendered || '', - media_type: item.media_type, - mime_type: item.mime_type, - source_url: item.source_url, - date_created: item.date_created ?? item.date, - date_modified: item.date_modified ?? item.modified, - }; - } - - async getProducts( - params: UnifiedSearchParamsDTO - ): Promise> { + async getProducts(params: UnifiedSearchParamsDTO): Promise> { // 获取产品列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products', + requestParams + ); // 对于类型为 variable 的产品,需要加载完整的变体数据 const productsWithVariations = await Promise.all( @@ -611,31 +847,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return productsWithVariations.map((product: any) => this.mapProduct(product)); } - async getProduct(id: string | number): Promise { - // 获取单个产品详情并映射为统一产品DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products/${id}`); - const product = res.data; - - // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 - if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { - try { - // 批量获取该产品的所有变体数据 - const variations = await this.wpService.sdkGetAll( - api, - `products/${product.id}/variations` - ); - // 将完整的变体数据添加到产品对象中 - product.variations = variations; - } catch (error) { - // 如果获取变体失败,保持原有的 ID 数组 - console.error(`获取产品 ${product.id} 的变体数据失败:`, error); - } - } - - return this.mapProduct(product); - } - async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); @@ -645,21 +856,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async updateProduct(id: string | number, data: Partial): Promise { // 更新产品并返回统一产品DTO const res = await this.wpService.updateProduct(this.site, String(id), data as any); - return res - } - - async getOrderNotes(orderId: string | number): Promise { - // 获取订单备注列表 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${orderId}/notes`); - return res.data; - } - - async createOrderNote(orderId: string | number, data: any): Promise { - // 创建订单备注 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post(`orders/${orderId}/notes`, data); - return res.data; + return res; } async deleteProduct(id: string | number): Promise { @@ -673,212 +870,22 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - // 批量处理产品增删改 - return await this.wpService.batchProcessProducts(this.site, data); + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data; + } + mapUnifiedToPlatformReview(data: Partial) { + return data; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - - // 并行获取所有订单的履行信息 - const ordersWithFulfillments = await Promise.all( - items.map(async (order: any) => { - try { - // 获取订单的履行信息 - const fulfillments = await this.getOrderFulfillments(order.id); - // 将履行信息添加到订单对象中 - return { - ...order, - fulfillments: fulfillments || [] - }; - } catch (error) { - // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 - console.error(`获取订单 ${order.id} 的履行信息失败:`, error); - return { - ...order, - fulfillments: [] - }; - } - }) - ); - - return { - items: ordersWithFulfillments.map(this.mapOrder), - total, - totalPages, - page, - per_page, - }; + mapCreateReviewParams(data: CreateReviewDTO) { + return data; + } + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams: UnifiedSearchParamsDTO = { - where, - page: 1, - per_page: 1, - }; - const requestParams = this.mapOrderSearchParams(searchParams); - const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - return total || 0; - } - - async getOrder(id: string | number): Promise { - // 获取单个订单详情 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); - } - - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订单数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); - } - - async createOrder(data: Partial): Promise { - // 创建订单并返回统一订单DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('orders', data); - return this.mapOrder(res.data); - } - - async updateOrder(id: string | number, data: Partial): Promise { - // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); - } - - async deleteOrder(id: string | number): Promise { - // 删除订单 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); - return true; - } - - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - throw new Error('暂无实现') - // 订单履行(发货) - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 更新订单状态为已完成 - // await api.put(`orders/${orderId}`, { status: 'completed' }); - - // // 如果提供了物流信息,添加到订单备注 - // if (data.tracking_number || data.shipping_provider) { - // const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - // } - - // return { - // success: true, - // order_id: orderId, - // fulfillment_id: `fulfillment_${orderId}_${Date.now()}`, - // tracking_number: data.tracking_number, - // shipping_provider: data.shipping_provider, - // fulfilled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`履行失败: ${error.message}`); - // } - } - - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - throw new Error('暂未实现') - // 取消订单履行 - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 将订单状态改回处理中 - // await api.put(`orders/${orderId}`, { status: 'processing' }); - - // // 添加取消履行的备注 - // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - - // return { - // success: true, - // order_id: orderId, - // shipment_id: data.shipment_id, - // reason: data.reason, - // cancelled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`取消履行失败: ${error.message}`); - // } - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取订阅列表并映射为统一订阅DTO集合 - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'subscriptions', - params - ); - return { - items: items.map(this.mapSubscription), - total, - totalPages, - page, - per_page, - - }; - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订阅数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取媒体列表并映射为统一媒体DTO集合 - const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( - this.site, - params - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有媒体数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapMedia(mediaItem)); - } - - private mapReview(item: any): UnifiedReviewDTO & { raw: any } { + mapReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -893,17 +900,15 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getReviews( - params: UnifiedSearchParamsDTO - ): Promise { + // 评论操作方法 + async getReviews(params: UnifiedSearchParamsDTO): Promise { // 获取评论列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products/reviews', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products/reviews', + requestParams + ); return { items: items.map(this.mapReview.bind(this)), total, @@ -934,52 +939,42 @@ export class WooCommerceAdapter implements ISiteAdapter { return await this.wpService.deleteReview(this.site, id); } - async deleteMedia(id: string | number): Promise { - // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); - return true; + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data; + } + mapUnifiedToPlatformSubscription(data: Partial) { + return data; } - async updateMedia(id: string | number, data: any): Promise { - // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); - } - - async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { - // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 - const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); - return result as any; - } - - private mapCustomer(item: WooCustomer): UnifiedCustomerDTO { - // 将 WooCommerce 客户数据映射为统一客户DTO - // 包含基础信息地址信息与时间信息 + mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + // 将 WooCommerce 订阅数据映射为统一订阅DTO + // 若缺少创建时间则回退为开始时间 return { id: item.id, - avatar: item.avatar_url, - email: item.email, - orders: Number(item.orders ?? 0), - total_spend: Number(item.total_spent ?? 0), - first_name: item.first_name, - last_name: item.last_name, - username: item.username, - phone: item.billing?.phone || item.shipping?.phone, - billing: item.billing, - shipping: item.shipping, - date_created: item.date_created, + status: item.status, + customer_id: item.customer_id, + billing_period: item.billing_period, + billing_interval: item.billing_interval, + date_created: item.date_created ?? item.start_date, date_modified: item.date_modified, + start_date: item.start_date, + next_payment_date: item.next_payment_date, + line_items: item.line_items, raw: item, }; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const requestParams = this.mapCustomerSearchParams(params); + + // 订阅操作方法 + async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { + // 获取订阅列表并映射为统一订阅DTO集合 const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( this.site, - 'customers', - requestParams + 'subscriptions', + params ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map(this.mapSubscription), total, totalPages, page, @@ -988,101 +983,23 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有客户数据,不受分页限制 + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // 处理orderBy参数,转换为WooCommerce API需要的格式 - const requestParams = this.mapCustomerSearchParams(params || {}); - - const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); + return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); } - async getCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + // ========== 变体映射方法 ========== + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { + return data; } - - async createCustomer(data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('customers', data); - return this.mapCustomer(res.data); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); - } - - async deleteCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); - return true; - } - - async getOrderFulfillments(orderId: string | number): Promise { - return await this.wpService.getFulfillments(this.site, String(orderId)); - } - - async createOrderFulfillment(orderId: string | number, data: { - tracking_number: string; - shipping_provider: string; - shipping_method?: string; - status?: string; - date_created?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - const shipmentData: any = { - shipping_provider: data.shipping_provider, - tracking_number: data.tracking_number, - }; - - if (data.shipping_method) { - shipmentData.shipping_method = data.shipping_method; - } - - 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); - return response.data; - } - - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: 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); - } - - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + mapUnifiedToPlatformVariation(data: Partial) { + return data; } // 映射 WooCommerce 变体到统一格式 - private mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { + mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { // 将变体属性转换为统一格式 const mappedAttributes = variation.attributes && Array.isArray(variation.attributes) ? variation.attributes.map((attr: any) => ({ @@ -1131,6 +1048,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + // 变体操作方法 // 获取产品变体列表 async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { try { @@ -1305,4 +1223,145 @@ export class WooCommerceAdapter implements ISiteAdapter { throw new Error(`删除产品变体失败: ${error instanceof Error ? error.message : String(error)}`); } } + + // ========== 网络钩子映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data; + } + mapUnifiedToPlatformWebhook(data: Partial) { + return data; + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data; + } + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data; + } + + // 映射 WooCommerce webhook 到统一格式 + mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + return { + id: webhook.id.toString(), + name: webhook.name, + status: webhook.status, + topic: webhook.topic, + delivery_url: webhook.delivery_url, + secret: webhook.secret, + api_version: webhook.api_version, + date_created: webhook.date_created, + date_modified: webhook.date_modified, + // metadata: webhook.meta_data || [], + }; + } + + // 网络钩子操作方法 + // 获取站点的 webhooks 列表 + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { + try { + const result = await this.wpService.getWebhooks(this.site, params); + + return { + items: (result.items as WooWebhook[]).map(this.mapWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; + } catch (error) { + throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取所有webhooks + async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { + try { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); + return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + } catch (error) { + throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取单个 webhook 详情 + async getWebhook(id: string | number): Promise { + try { + const result = await this.wpService.getWebhook(this.site, id); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 创建新的 webhook + async createWebhook(data: CreateWebhookDTO): Promise { + try { + const params = { + name: data.name, + status: 'active', // 默认状态为活跃 + topic: data.topic, + delivery_url: data.delivery_url, + secret: data.secret, + api_version: data.api_version || 'wp/v2', + }; + const result = await this.wpService.createWebhook(this.site, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 更新现有的 webhook + async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + try { + const params = { + ...(data.name ? { name: data.name } : {}), + ...(data.status ? { status: data.status } : {}), + ...(data.topic ? { topic: data.topic } : {}), + ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), + ...(data.secret ? { secret: data.secret } : {}), + ...(data.api_version ? { api_version: data.api_version } : {}), + }; + const result = await this.wpService.updateWebhook(this.site, id, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 删除指定的 webhook + async deleteWebhook(id: string | number): Promise { + try { + await this.wpService.deleteWebhook(this.site, id); + return true; + } catch (error) { + throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // ========== 其他方法 ========== + async getLinks(): Promise> { + const baseUrl = this.site.apiUrl; + const links = [ + { title: '访问网站', url: baseUrl }, + { title: '管理后台', url: `${baseUrl}/wp-admin/` }, + { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, + { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, + { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, + { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, + { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, + { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, + { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, + ]; + return links; + } + + batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } + + batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 5013390..f4b1c83 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -7,7 +7,6 @@ import { CancelFulfillmentDTO, CreateReviewDTO, CreateWebhookDTO, - FulfillmentDTO, UnifiedCustomerDTO, UnifiedCustomerPaginationDTO, UnifiedMediaPaginationDTO, @@ -986,25 +985,6 @@ export class SiteApiController { } } - @Post('/:siteId/orders/:id/fulfill') - @ApiOkResponse({ type: Object }) - async fulfillOrder( - @Param('siteId') siteId: number, - @Param('id') id: string, - @Body() body: FulfillmentDTO - ) { - this.logger.info(`[Site API] 订单履约开始, siteId: ${siteId}, orderId: ${id}`); - try { - const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.fulfillOrder(id, body); - this.logger.info(`[Site API] 订单履约成功, siteId: ${siteId}, orderId: ${id}`); - return successResponse(data); - } catch (error) { - this.logger.error(`[Site API] 订单履约失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`); - return errorResponse(error.message); - } - } - @Post('/:siteId/orders/:id/cancel-fulfill') @ApiOkResponse({ type: Object }) async cancelFulfillment( diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index ea6db0b..630e5ed 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,51 +19,71 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { + // ========== 客户映射方法 ========== /** - * 获取产品列表 + * 将平台客户数据转换为统一客户数据格式 + * @param data 平台特定客户数据 + * @returns 统一客户数据格式 */ - getProducts(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO; /** - * 获取所有产品 + * 将统一客户数据格式转换为平台客户数据 + * @param data 统一客户数据格式 + * @returns 平台特定客户数据 */ - getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformCustomer(data: Partial): any; /** - * 获取单个产品 + * 获取单个客户 */ - getProduct(id: string | number): Promise; + getCustomer(id: string | number): Promise; /** - * 获取订单列表 + * 获取客户列表 */ - getOrders(params: UnifiedSearchParamsDTO): Promise>; + getCustomers(params: UnifiedSearchParamsDTO): Promise>; /** - * 获取订单总数 + * 获取所有客户 */ - countOrders(params: Record): Promise; + getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; /** - * 获取所有订单 + * 创建客户 */ - getAllOrders(params?: UnifiedSearchParamsDTO): Promise; + createCustomer(data: Partial): Promise; /** - * 获取单个订单 + * 更新客户 */ - getOrder(id: string | number): Promise; + updateCustomer(id: string | number, data: Partial): Promise; /** - * 获取订阅列表 + * 删除客户 */ - getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + deleteCustomer(id: string | number): Promise; /** - * 获取所有订阅 + * 批量处理客户 */ - getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + batchProcessCustomers?(data: BatchOperationDTO): Promise; + + // ========== 媒体映射方法 ========== + /** + * 将平台媒体数据转换为统一媒体数据格式 + * @param data 平台特定媒体数据 + * @returns 统一媒体数据格式 + */ + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO; + + /** + * 将统一媒体数据格式转换为平台媒体数据 + * @param data 统一媒体数据格式 + * @returns 平台特定媒体数据 + */ + mapUnifiedToPlatformMedia(data: Partial): any; /** * 获取媒体列表 @@ -80,75 +100,69 @@ export interface ISiteAdapter { */ createMedia(file: any): Promise; + // ========== 订单映射方法 ========== /** - * 获取评论列表 + * 将平台订单数据转换为统一订单数据格式 + * @param data 平台特定订单数据 + * @returns 统一订单数据格式 */ - getReviews(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO; /** - * 获取所有评论 + * 将统一订单数据格式转换为平台订单数据 + * @param data 统一订单数据格式 + * @returns 平台特定订单数据 */ - getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformOrder(data: Partial): any; /** - * 创建评论 + * 将统一订单创建参数转换为平台订单创建参数 + * @param data 统一订单创建参数 + * @returns 平台订单创建参数 */ - createReview(data: CreateReviewDTO): Promise; + mapCreateOrderParams(data: Partial): any; /** - * 更新评论 + * 将统一订单更新参数转换为平台订单更新参数 + * @param data 统一订单更新参数 + * @returns 平台订单更新参数 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + mapUpdateOrderParams(data: Partial): any; /** - * 删除评论 + * 获取单个订单 */ - deleteReview(id: number): Promise; + getOrder(id: string | number): Promise; /** - * 创建产品 + * 获取订单列表 */ - createProduct(data: Partial): Promise; + getOrders(params: UnifiedSearchParamsDTO): Promise>; /** - * 更新产品 + * 获取所有订单 */ - updateProduct(id: string | number, data: Partial): Promise; + getAllOrders(params?: UnifiedSearchParamsDTO): Promise; /** - * 删除产品 + * 获取订单总数 */ - deleteProduct(id: string | number): Promise; + countOrders(params: Record): Promise; /** - * 获取产品变体列表 + * 创建订单 */ - getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + createOrder(data: Partial): Promise; /** - * 获取所有产品变体 + * 更新订单 */ - getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + updateOrder(id: string | number, data: Partial): Promise; /** - * 获取单个产品变体 + * 删除订单 */ - getVariation(productId: string | number, variationId: string | number): Promise; - - /** - * 创建产品变体 - */ - createVariation(productId: string | number, data: CreateVariationDTO): Promise; - - /** - * 更新产品变体 - */ - updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; - - /** - * 删除产品变体 - */ - deleteVariation(productId: string | number, variationId: string | number): Promise; + deleteOrder(id: string | number): Promise; /** * 获取订单备注 @@ -160,71 +174,6 @@ export interface ISiteAdapter { */ createOrderNote(orderId: string | number, data: any): Promise; - batchProcessProducts?(data: BatchOperationDTO): Promise; - - createOrder(data: Partial): Promise; - updateOrder(id: string | number, data: Partial): Promise; - deleteOrder(id: string | number): Promise; - - batchProcessOrders?(data: BatchOperationDTO): Promise; - - getCustomers(params: UnifiedSearchParamsDTO): Promise>; - getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; - getCustomer(id: string | number): Promise; - createCustomer(data: Partial): Promise; - updateCustomer(id: string | number, data: Partial): Promise; - deleteCustomer(id: string | number): Promise; - - batchProcessCustomers?(data: BatchOperationDTO): Promise; - - /** - * 获取webhooks列表 - */ - getWebhooks(params: UnifiedSearchParamsDTO): Promise; - - /** - * 获取所有webhooks - */ - getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; - - /** - * 获取单个webhook - */ - getWebhook(id: string | number): Promise; - - /** - * 创建webhook - */ - createWebhook(data: CreateWebhookDTO): Promise; - - /** - * 更新webhook - */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; - - /** - * 删除webhook - */ - deleteWebhook(id: string | number): Promise; - - /** - * 获取站点链接列表 - */ - getLinks(): Promise>; - - /** - * 订单履行(发货) - */ - fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise; - /** * 取消订单履行 */ @@ -272,4 +221,276 @@ export interface ISiteAdapter { * 删除订单履行信息 */ deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise; + + /** + * 批量处理订单 + */ + batchProcessOrders?(data: BatchOperationDTO): Promise; + + // ========== 产品映射方法 ========== + /** + * 将平台产品数据转换为统一产品数据格式 + * @param data 平台特定产品数据 + * @returns 统一产品数据格式 + */ + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO; + + /** + * 将统一产品数据格式转换为平台产品数据 + * @param data 统一产品数据格式 + * @returns 平台特定产品数据 + */ + mapUnifiedToPlatformProduct(data: Partial): any; + + /** + * 将统一产品创建参数转换为平台产品创建参数 + * @param data 统一产品创建参数 + * @returns 平台产品创建参数 + */ + mapCreateProductParams(data: Partial): any; + + /** + * 将统一产品更新参数转换为平台产品更新参数 + * @param data 统一产品更新参数 + * @returns 平台产品更新参数 + */ + mapUpdateProductParams(data: Partial): any; + + /** + * 获取单个产品 + */ + getProduct(id: string | number): Promise; + + /** + * 获取产品列表 + */ + getProducts(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有产品 + */ + getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品 + */ + createProduct(data: Partial): Promise; + + /** + * 更新产品 + */ + updateProduct(id: string | number, data: Partial): Promise; + + /** + * 删除产品 + */ + deleteProduct(id: string | number): Promise; + + /** + * 批量处理产品 + */ + batchProcessProducts?(data: BatchOperationDTO): Promise; + + // ========== 评论映射方法 ========== + /** + * 将平台评论数据转换为统一评论数据格式 + * @param data 平台特定评论数据 + * @returns 统一评论数据格式 + */ + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO; + + /** + * 将统一评论数据格式转换为平台评论数据 + * @param data 统一评论数据格式 + * @returns 平台特定评论数据 + */ + mapUnifiedToPlatformReview(data: Partial): any; + + /** + * 将统一评论创建参数转换为平台评论创建参数 + * @param data 统一评论创建参数 + * @returns 平台评论创建参数 + */ + mapCreateReviewParams(data: CreateReviewDTO): any; + + /** + * 将统一评论更新参数转换为平台评论更新参数 + * @param data 统一评论更新参数 + * @returns 平台评论更新参数 + */ + mapUpdateReviewParams(data: UpdateReviewDTO): any; + + /** + * 获取评论列表 + */ + getReviews(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有评论 + */ + getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建评论 + */ + createReview(data: CreateReviewDTO): Promise; + + /** + * 更新评论 + */ + updateReview(id: number, data: UpdateReviewDTO): Promise; + + /** + * 删除评论 + */ + deleteReview(id: number): Promise; + + // ========== 订阅映射方法 ========== + /** + * 将平台订阅数据转换为统一订阅数据格式 + * @param data 平台特定订阅数据 + * @returns 统一订阅数据格式 + */ + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO; + + /** + * 将统一订阅数据格式转换为平台订阅数据 + * @param data 统一订阅数据格式 + * @returns 平台特定订阅数据 + */ + mapUnifiedToPlatformSubscription(data: Partial): any; + + /** + * 获取订阅列表 + */ + getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有订阅 + */ + getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + + // ========== 产品变体映射方法 ========== + /** + * 将平台产品变体数据转换为统一产品变体数据格式 + * @param data 平台特定产品变体数据 + * @returns 统一产品变体数据格式 + */ + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO; + + /** + * 将统一产品变体数据格式转换为平台产品变体数据 + * @param data 统一产品变体数据格式 + * @returns 平台特定产品变体数据 + */ + mapUnifiedToPlatformVariation(data: Partial): any; + + /** + * 将统一产品变体创建参数转换为平台产品变体创建参数 + * @param data 统一产品变体创建参数 + * @returns 平台产品变体创建参数 + */ + mapCreateVariationParams(data: CreateVariationDTO): any; + + /** + * 将统一产品变体更新参数转换为平台产品变体更新参数 + * @param data 统一产品变体更新参数 + * @returns 平台产品变体更新参数 + */ + mapUpdateVariationParams(data: UpdateVariationDTO): any; + + /** + * 获取单个产品变体 + */ + getVariation(productId: string | number, variationId: string | number): Promise; + + /** + * 获取产品变体列表 + */ + getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有产品变体 + */ + getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品变体 + */ + createVariation(productId: string | number, data: CreateVariationDTO): Promise; + + /** + * 更新产品变体 + */ + updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; + + /** + * 删除产品变体 + */ + deleteVariation(productId: string | number, variationId: string | number): Promise; + + // ========== Webhook映射方法 ========== + /** + * 将平台Webhook数据转换为统一Webhook数据格式 + * @param data 平台特定Webhook数据 + * @returns 统一Webhook数据格式 + */ + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO; + + /** + * 将统一Webhook数据格式转换为平台Webhook数据 + * @param data 统一Webhook数据格式 + * @returns 平台特定Webhook数据 + */ + mapUnifiedToPlatformWebhook(data: Partial): any; + + /** + * 将统一Webhook创建参数转换为平台Webhook创建参数 + * @param data 统一Webhook创建参数 + * @returns 平台Webhook创建参数 + */ + mapCreateWebhookParams(data: CreateWebhookDTO): any; + + /** + * 将统一Webhook更新参数转换为平台Webhook更新参数 + * @param data 统一Webhook更新参数 + * @returns 平台Webhook更新参数 + */ + mapUpdateWebhookParams(data: UpdateWebhookDTO): any; + + /** + * 获取单个webhook + */ + getWebhook(id: string | number): Promise; + + /** + * 获取webhooks列表 + */ + getWebhooks(params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有webhooks + */ + getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建webhook + */ + createWebhook(data: CreateWebhookDTO): Promise; + + /** + * 更新webhook + */ + updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + + /** + * 删除webhook + */ + deleteWebhook(id: string | number): Promise; + + // ========== 站点/其他方法 ========== + /** + * 获取站点链接列表 + */ + getLinks(): Promise>; } diff --git a/src/transformer/database.transformer.ts b/src/transformer/database.transformer.ts new file mode 100644 index 0000000..619ef04 --- /dev/null +++ b/src/transformer/database.transformer.ts @@ -0,0 +1 @@ +// 从 unified 到 数据库需要有个转换流程 diff --git a/src/transformer/file.transformer.ts b/src/transformer/file.transformer.ts new file mode 100644 index 0000000..b85ee23 --- /dev/null +++ b/src/transformer/file.transformer.ts @@ -0,0 +1 @@ +// 文件转换 diff --git a/src/transformer/shopyy.transformer.ts b/src/transformer/shopyy.transformer.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/transformer/woocommerce.adpater.ts b/src/transformer/woocommerce.adpater.ts new file mode 100644 index 0000000..e5b5c67 --- /dev/null +++ b/src/transformer/woocommerce.adpater.ts @@ -0,0 +1,8 @@ +import { UnifiedOrderDTO } from "../dto/site-api.dto"; + +export class ShipmentAdapter { + // 用于导出物流需要的数据 + mapFromOrder(order: UnifiedOrderDTO): any { + return order; + } +} \ No newline at end of file From 9e90d5f9cf36dde91252979e461c45f093474374 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 20:33:23 +0800 Subject: [PATCH 06/24] =?UTF-8?q?refactor(api):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=8F=82=E6=95=B0=E4=B8=BA=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=E5=B9=B6=E6=94=AF=E6=8C=81=E5=A4=9A=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构所有接口方法,将直接传递id参数改为接受where条件对象 支持通过id、sku、email等多条件查询实体 优化产品服务逻辑,支持通过sku直接查询产品 统一各适配器实现,确保接口一致性 --- src/adapter/shopyy.adapter.ts | 140 ++++++++++------ src/adapter/woocommerce.adapter.ts | 211 +++++++++++++----------- src/controller/site-api.controller.ts | 42 ++--- src/interface/site-adapter.interface.ts | 34 ++-- src/service/order.service.ts | 2 +- src/service/site-api.service.ts | 28 +--- src/service/wp.service.ts | 8 +- 7 files changed, 248 insertions(+), 217 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 1cf00fd..ffa66af 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -122,8 +122,11 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); + async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise { + if(!where.id && !where.email && !where.phone){ + throw new Error('必须传入 id 或 email 或 phone') + } + const customer = await this.shopyyService.getCustomer(this.site, where.id); return this.mapPlatformToUnifiedCustomer(customer); } @@ -149,13 +152,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedCustomer(createdCustomer); } - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + async updateCustomer(where: {id: string | number}, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data); return this.mapPlatformToUnifiedCustomer(updatedCustomer); } - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); + async deleteCustomer(where: {id: string | number}): Promise { + return await this.shopyyService.deleteCustomer(this.site, where.id); } batchProcessCustomers?(data: BatchOperationDTO): Promise { @@ -208,13 +211,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedMedia(createdMedia); } - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + async updateMedia(where: {id: string | number}, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data); return this.mapPlatformToUnifiedMedia(updatedMedia); } - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + async deleteMedia(where: {id: string | number}): Promise { + return await this.shopyyService.deleteMedia(this.site, where.id); } mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { @@ -530,8 +533,8 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); + async getOrder(where: {id: string | number}): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(where.id)); return this.mapPlatformToUnifiedOrder(data); } @@ -577,14 +580,14 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 使用映射方法转换参数 const requestParams = this.mapUpdateOrderParams(data); - return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams); } - async deleteOrder(id: string | number): Promise { - return await this.shopyyService.deleteOrder(this.site, id); + async deleteOrder(where: {id: string | number}): Promise { + return await this.shopyyService.deleteOrder(this.site, where.id); } async getOrderNotes(orderId: string | number): Promise { @@ -858,10 +861,18 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapPlatformToUnifiedProduct(product); + async getProduct(where: {id?: string | number, sku?: string}): Promise { + if(!where.id && !where.sku){ + throw new Error('必须传入 id 或 sku') + } + if (where.id) { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, where.id); + return this.mapPlatformToUnifiedProduct(product); + } else if (where.sku) { + // 通过sku获取产品 + return this.getProductBySku(where.sku); + } } async getProducts( @@ -900,24 +911,50 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } // 使用映射方法转换参数 const requestParams = this.mapUpdateProductParams(data); - await this.shopyyService.updateProduct(this.site, String(id), requestParams); + await this.shopyyService.updateProduct(this.site, productId, requestParams); return true; } - async deleteProduct(id: string | number): Promise { + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { + let productId: string | number; + if (where.id) { + productId = where.id; + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = product.id; + } else { + throw new Error('必须提供id或sku参数'); + } // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); return true; } + // 通过sku获取产品详情的私有方法 + private async getProductBySku(sku: string): Promise { + // 使用Shopyy API的搜索功能通过sku查询产品 + const response = await this.shopyyService.getProducts(this.site, 1, 100); + const product = response.items.find((item: any) => item.sku === sku); + if (!product) { + throw new Error(`未找到sku为${sku}的产品`); + } + return this.mapPlatformToUnifiedProduct(product); + } + async batchProcessProducts( data: { create?: any[]; update?: any[]; delete?: Array } ): Promise { @@ -929,9 +966,6 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data - } mapUnifiedToPlatformReview(data: Partial) { return data @@ -954,7 +988,7 @@ export class ShopyyAdapter implements ISiteAdapter { requestParams ); return { - items: items.map(this.mapReview), + items: items.map(this.mapPlatformToUnifiedReview), total, totalPages, page, @@ -969,19 +1003,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createReview(data: any): Promise { const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + return this.mapPlatformToUnifiedReview(createdReview); } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + async updateReview(where: {id: string | number}, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(updatedReview); } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async deleteReview(where: {id: string | number}): Promise { + return await this.shopyyService.deleteReview(this.site, where.id); } - mapReview(review: any): UnifiedReviewDTO { + mapPlatformToUnifiedReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -1119,9 +1153,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== Webhook映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data - } + mapUnifiedToPlatformWebhook(data: Partial) { return data @@ -1135,15 +1167,15 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); + async getWebhook(where: {id: string | number}): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(webhook); } async getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { - items: items.map(this.mapWebhook), + items: items.map(this.mapPlatformToUnifiedWebhook), total, totalPages, page, @@ -1158,19 +1190,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createWebhook(data: CreateWebhookDTO): Promise { const createdWebhook = await this.shopyyService.createWebhook(this.site, data); - return this.mapWebhook(createdWebhook); + return this.mapPlatformToUnifiedWebhook(createdWebhook); } - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { - const updatedWebhook = await this.shopyyService.updateWebhook(this.site, id, data); - return this.mapWebhook(updatedWebhook); + async updateWebhook(where: {id: string | number}, data: UpdateWebhookDTO): Promise { + const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data); + return this.mapPlatformToUnifiedWebhook(updatedWebhook); } - async deleteWebhook(id: string | number): Promise { - return await this.shopyyService.deleteWebhook(this.site, id); + async deleteWebhook(where: {id: string | number}): Promise { + return await this.shopyyService.deleteWebhook(this.site, where.id); } - mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { return { id: item.id, name: item.webhook_name || `Webhook-${item.id}`, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index a5f2f0a..0debb16 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -36,12 +36,15 @@ import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 constructor(private site: Site, private wpService: WPService) { - this.mapProduct = this.mapProduct.bind(this); - this.mapReview = this.mapReview.bind(this); - this.mapCustomer = this.mapCustomer.bind(this); - this.mapMedia = this.mapMedia.bind(this); - this.mapOrder = this.mapOrder.bind(this); - this.mapWebhook = this.mapWebhook.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedReview = this.mapPlatformToUnifiedReview.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedWebhook = this.mapPlatformToUnifiedWebhook.bind(this); + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data } batchProcessProducts?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); @@ -54,14 +57,8 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 客户映射方法 ========== - mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { - return data; - } - mapUnifiedToPlatformCustomer(data: Partial) { - return data; - } - mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + mapPlatformToUnifiedCustomer(item: WooCustomer): UnifiedCustomerDTO { // 将 WooCommerce 客户数据映射为统一客户DTO // 包含基础信息地址信息与时间信息 return { @@ -136,10 +133,10 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户操作方法 - async getCustomer(id: string | number): Promise { + async getCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + const res = await api.get(`customers/${where.id}`); + return this.mapPlatformToUnifiedCustomer(res.data); } async getCustomers(params: UnifiedSearchParamsDTO): Promise> { @@ -150,7 +147,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map((i: any) => this.mapPlatformToUnifiedCustomer(i)), total, totalPages, page, @@ -167,36 +164,33 @@ export class WooCommerceAdapter implements ISiteAdapter { const requestParams = this.mapCustomerSearchParams(params || {}); const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + return customers.map((customer: any) => this.mapPlatformToUnifiedCustomer(customer)); } async createCustomer(data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('customers', data); - return this.mapCustomer(res.data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async updateCustomer(id: string | number, data: Partial): Promise { + async updateCustomer(where: {id: string | number}, data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); + const res = await api.put(`customers/${where.id}`, data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async deleteCustomer(id: string | number): Promise { + async deleteCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); + await api.delete(`customers/${where.id}`, { force: true }); return true; } // ========== 媒体映射方法 ========== - mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { - return data; - } mapUnifiedToPlatformMedia(data: Partial) { return data; } - mapMedia(item: WpMedia): UnifiedMediaDTO { + mapPlatformToUnifiedMedia(item: WpMedia): UnifiedMediaDTO { // 将 WordPress 媒体数据映射为统一媒体DTO // 兼容不同字段命名的时间信息 return { @@ -221,7 +215,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapMedia.bind(this)), + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), total, totalPages, page, @@ -233,21 +227,21 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有媒体数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); } createMedia(file: any): Promise { throw new Error('Method not implemented.'); } - async updateMedia(id: string | number, data: any): Promise { + async updateMedia(where: {id: string | number}, data: any): Promise { // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + return await this.wpService.updateMedia(Number(this.site.id), Number(where.id), data); } - async deleteMedia(id: string | number): Promise { + async deleteMedia(where: {id: string | number}): Promise { // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + await this.wpService.deleteMedia(Number(this.site.id), Number(where.id), true); return true; } @@ -258,9 +252,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 订单映射方法 ========== - mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { - return data; - } mapUnifiedToPlatformOrder(data: Partial) { return data; } @@ -356,7 +347,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ].filter(Boolean).join(', '); } - mapOrder(item: WooOrder): UnifiedOrderDTO { + mapPlatformToUnifiedOrder(item: WooOrder): UnifiedOrderDTO { // 将 WooCommerce 订单数据映射为统一订单DTO // 包含账单地址与收货地址以及创建与更新时间 @@ -410,11 +401,11 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 订单操作方法 - async getOrder(id: string | number): Promise { + async getOrder(where: {id: string | number}): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); + const res = await api.get(`orders/${where.id}`); + return this.mapPlatformToUnifiedOrder(res.data); } async getOrders(params: UnifiedSearchParamsDTO): Promise> { @@ -444,7 +435,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: ordersWithFulfillments.map(this.mapOrder), + items: ordersWithFulfillments.map(this.mapPlatformToUnifiedOrder), total, totalPages, page, @@ -456,7 +447,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订单数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); + return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); } async countOrders(where: Record): Promise { @@ -475,18 +466,18 @@ export class WooCommerceAdapter implements ISiteAdapter { // 创建订单并返回统一订单DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); - return this.mapOrder(res.data); + return this.mapPlatformToUnifiedOrder(res.data); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); + return await this.wpService.updateOrder(this.site, String(where.id), data as any); } - async deleteOrder(id: string | number): Promise { + async deleteOrder(where: {id: string | number}): Promise { // 删除订单 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); + await api.delete(`orders/${where.id}`, { force: true }); return true; } @@ -591,9 +582,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { - return data; - } mapUnifiedToPlatformProduct(data: Partial) { return data; } @@ -661,7 +649,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapProduct(item: WooProduct): UnifiedProductDTO { + mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -749,11 +737,22 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - + async getProduct({id, sku}: {id?: string, sku?:string}){ + if(id) return this.getProductById(id); + if(sku) return this.getProductBySku(sku) + return this.getProductById(id || sku || ''); + } + async getProductBySku(sku: string){ + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products?sku=${sku}`); + const product = res.data[0]; + return this.mapPlatformToUnifiedProduct(product); + } // 产品操作方法 - async getProduct(id: string | number): Promise { + async getProductById(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); const product = res.data; @@ -773,7 +772,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - return this.mapProduct(product); + return this.mapPlatformToUnifiedProduct(product); } async getProducts(params: UnifiedSearchParamsDTO): Promise> { @@ -808,7 +807,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: productsWithVariations.map(this.mapProduct), + items: productsWithVariations.map(this.mapPlatformToUnifiedProduct), total, totalPages, page, @@ -844,26 +843,46 @@ export class WooCommerceAdapter implements ISiteAdapter { }) ); - return productsWithVariations.map((product: any) => this.mapProduct(product)); + return productsWithVariations.map((product: any) => this.mapPlatformToUnifiedProduct(product)); } async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); - return this.mapProduct(res); + return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { // 更新产品并返回统一产品DTO - const res = await this.wpService.updateProduct(this.site, String(id), data as any); + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } + const res = await this.wpService.updateProduct(this.site, productId, data as any); return res; } - async deleteProduct(id: string | number): Promise { + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { // 删除产品 + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } const api = (this.wpService as any).createApi(this.site, 'wc/v3'); try { - await api.delete(`products/${id}`, { force: true }); + await api.delete(`products/${productId}`, { force: true }); return true; } catch (e) { return false; @@ -871,9 +890,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data; - } + mapUnifiedToPlatformReview(data: Partial) { return data; } @@ -885,7 +902,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapReview(item: any): UnifiedReviewDTO & { raw: any } { + mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -910,7 +927,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map(this.mapReview.bind(this)), + items: items.map(this.mapPlatformToUnifiedReview.bind(this)), total, totalPages, page, @@ -922,32 +939,29 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有评论数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); - return reviews.map((review: any) => this.mapReview(review)); + return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); } async createReview(data: any): Promise { const res = await this.wpService.createReview(this.site, data); - return this.mapReview(res); + return this.mapPlatformToUnifiedReview(res); } - async updateReview(id: number, data: any): Promise { - const res = await this.wpService.updateReview(this.site, id, data); - return this.mapReview(res); + async updateReview(where: {id: number}, data: any): Promise { + const res = await this.wpService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(res); } - async deleteReview(id: number): Promise { - return await this.wpService.deleteReview(this.site, id); + async deleteReview(where: {id: number}): Promise { + return await this.wpService.deleteReview(this.site, where.id); } // ========== 订阅映射方法 ========== - mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { - return data; - } mapUnifiedToPlatformSubscription(data: Partial) { return data; } - mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + mapPlatformToUnifiedSubscription(item: WooSubscription): UnifiedSubscriptionDTO { // 将 WooCommerce 订阅数据映射为统一订阅DTO // 若缺少创建时间则回退为开始时间 return { @@ -974,7 +988,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapSubscription), + items: items.map(this.mapPlatformToUnifiedSubscription), total, totalPages, page, @@ -987,7 +1001,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); + return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); } // ========== 变体映射方法 ========== @@ -1225,9 +1239,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 网络钩子映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data; - } mapUnifiedToPlatformWebhook(data: Partial) { return data; } @@ -1240,7 +1251,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 映射 WooCommerce webhook 到统一格式 - mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(webhook: WooWebhook): UnifiedWebhookDTO { return { id: webhook.id.toString(), name: webhook.name, @@ -1262,12 +1273,12 @@ export class WooCommerceAdapter implements ISiteAdapter { const result = await this.wpService.getWebhooks(this.site, params); return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; + items: (result.items as WooWebhook[]).map(this.mapPlatformToUnifiedWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; } catch (error) { throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); } @@ -1278,17 +1289,17 @@ export class WooCommerceAdapter implements ISiteAdapter { try { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); } catch (error) { throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); } } // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { + async getWebhook(where: {id: string | number}): Promise { try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); } @@ -1306,14 +1317,14 @@ export class WooCommerceAdapter implements ISiteAdapter { api_version: data.api_version || 'wp/v2', }; const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + async updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise { try { const params = { ...(data.name ? { name: data.name } : {}), @@ -1323,17 +1334,17 @@ export class WooCommerceAdapter implements ISiteAdapter { ...(data.secret ? { secret: data.secret } : {}), ...(data.api_version ? { api_version: data.api_version } : {}), }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.updateWebhook(this.site, where.id, params); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { + async deleteWebhook(where: Partial>): Promise { try { - await this.wpService.deleteWebhook(this.site, id); + await this.wpService.deleteWebhook(this.site, where.id); return true; } catch (error) { throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index f4b1c83..00d8285 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -105,7 +105,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateReview(id, body); + const data = await adapter.updateReview({ id }, body); this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -123,7 +123,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.deleteReview(id); + const data = await adapter.deleteReview({ id }); this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -159,7 +159,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getWebhook(id); + const data = await adapter.getWebhook({ id }); this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -198,7 +198,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateWebhook(id, body); + const data = await adapter.updateWebhook({ id }, body); this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -216,7 +216,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.deleteWebhook(id); + const data = await adapter.deleteWebhook({ id }); this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -326,7 +326,7 @@ export class SiteApiController { if (site.type === 'woocommerce') { const page = query.page || 1; const perPage = (query.per_page) || 100; - const res = await this.siteApiService.wpService.getProducts(site, page, perPage); + const res = await this.siteApiService.wpService.getProducts(site, { page, per_page: perPage }); const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity']; const rows = (res.items || []).map((p: any) => [p.id, p.name, p.type, p.status, p.sku, p.regular_price, p.sale_price, p.stock_status, p.stock_quantity]); const toCsvValue = (val: any) => { @@ -359,7 +359,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getProduct(id); + const data = await adapter.getProduct({ id }); // 如果获取到商品数据,则增强ERP产品信息 if (data) { @@ -484,7 +484,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateProduct(id, body); + const data = await adapter.updateProduct({ id }, body); this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`); return successResponse(data); } catch (error) { @@ -539,7 +539,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const success = await adapter.deleteProduct(id); + const success = await adapter.deleteProduct({ id }); this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`); return successResponse(success); } catch (error) { @@ -584,7 +584,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const data = await adapter.updateProduct(id, item); + const data = await adapter.updateProduct({ id }, item); updated.push(data); } catch (e) { errors.push({ @@ -597,7 +597,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteProduct(id); + const ok = await adapter.deleteProduct({ id }); if (ok) deleted.push(id); else errors.push({ identifier: String(id), @@ -771,7 +771,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getOrder(id); + const data = await adapter.getOrder({ id }); this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(data); } catch (error) { @@ -843,7 +843,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const ok = await adapter.updateOrder(id, body); + const ok = await adapter.updateOrder({ id }, body); this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(ok); } catch (error) { @@ -861,7 +861,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const ok = await adapter.deleteOrder(id); + const ok = await adapter.deleteOrder({ id }); this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(ok); } catch (error) { @@ -901,7 +901,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const ok = await adapter.updateOrder(id, item); + const ok = await adapter.updateOrder({ id }, item); if (ok) updated.push(item); else errors.push({ identifier: String(item.id || 'unknown'), @@ -918,7 +918,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteOrder(id); + const ok = await adapter.deleteOrder({ id }); if (ok) deleted.push(id); else errors.push({ identifier: String(id), @@ -1435,7 +1435,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getCustomer(id); + const data = await adapter.getCustomer({ id }); this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(data); } catch (error) { @@ -1507,7 +1507,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateCustomer(id, body); + const data = await adapter.updateCustomer({ id }, body); this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(data); } catch (error) { @@ -1525,7 +1525,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const success = await adapter.deleteCustomer(id); + const success = await adapter.deleteCustomer({ id }); this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(success); } catch (error) { @@ -1561,7 +1561,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const data = await adapter.updateCustomer(id, item); + const data = await adapter.updateCustomer({ id }, item); updated.push(data); } catch (e) { failed.push({ action: 'update', item, error: (e as any).message }); @@ -1571,7 +1571,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteCustomer(id); + const ok = await adapter.deleteCustomer({ id }); if (ok) deleted.push(id); else failed.push({ action: 'delete', id, error: 'delete failed' }); } catch (e) { diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 630e5ed..f290517 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,7 +19,7 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { // ========== 客户映射方法 ========== /** * 将平台客户数据转换为统一客户数据格式 @@ -38,7 +38,7 @@ export interface ISiteAdapter { /** * 获取单个客户 */ - getCustomer(id: string | number): Promise; + getCustomer(where: Partial>): Promise; /** * 获取客户列表 @@ -58,12 +58,12 @@ export interface ISiteAdapter { /** * 更新客户 */ - updateCustomer(id: string | number, data: Partial): Promise; + updateCustomer(where: Partial>, data: Partial): Promise; /** * 删除客户 */ - deleteCustomer(id: string | number): Promise; + deleteCustomer(where: Partial>): Promise; /** * 批量处理客户 @@ -132,7 +132,7 @@ export interface ISiteAdapter { /** * 获取单个订单 */ - getOrder(id: string | number): Promise; + getOrder(where: Partial>): Promise; /** * 获取订单列表 @@ -147,7 +147,7 @@ export interface ISiteAdapter { /** * 获取订单总数 */ - countOrders(params: Record): Promise; + countOrders(params: Record): Promise; /** * 创建订单 @@ -157,12 +157,12 @@ export interface ISiteAdapter { /** * 更新订单 */ - updateOrder(id: string | number, data: Partial): Promise; + updateOrder(where: Partial>, data: Partial): Promise; /** * 删除订单 */ - deleteOrder(id: string | number): Promise; + deleteOrder(where: Partial>): Promise; /** * 获取订单备注 @@ -259,7 +259,7 @@ export interface ISiteAdapter { /** * 获取单个产品 */ - getProduct(id: string | number): Promise; + getProduct(where: Partial>): Promise; /** * 获取产品列表 @@ -279,12 +279,12 @@ export interface ISiteAdapter { /** * 更新产品 */ - updateProduct(id: string | number, data: Partial): Promise; + updateProduct(where: Partial>, data: Partial): Promise; /** * 删除产品 */ - deleteProduct(id: string | number): Promise; + deleteProduct(where: Partial>): Promise; /** * 批量处理产品 @@ -338,12 +338,12 @@ export interface ISiteAdapter { /** * 更新评论 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + updateReview(where: Partial>, data: UpdateReviewDTO): Promise; /** * 删除评论 */ - deleteReview(id: number): Promise; + deleteReview(where: Partial>): Promise; // ========== 订阅映射方法 ========== /** @@ -461,7 +461,7 @@ export interface ISiteAdapter { /** * 获取单个webhook */ - getWebhook(id: string | number): Promise; + getWebhook(where: Partial>): Promise; /** * 获取webhooks列表 @@ -481,16 +481,16 @@ export interface ISiteAdapter { /** * 更新webhook */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise; /** * 删除webhook */ - deleteWebhook(id: string | number): Promise; + deleteWebhook(where: Partial>): Promise; // ========== 站点/其他方法 ========== /** * 获取站点链接列表 */ - getLinks(): Promise>; + getLinks(): Promise>; } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 5c5e47f..1aa19bd 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -202,7 +202,7 @@ export class OrderService { try { // 调用 WooCommerce API 获取订单 const adapter = await this.siteApiService.getAdapter(siteId); - const order = await adapter.getOrder(orderId); + const order = await adapter.getOrder({ id: orderId }); // 检查订单是否已存在,以区分创建和更新 const existingOrder = await this.orderModel.findOne({ diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index c57a899..3aab4ac 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -110,36 +110,22 @@ export class SiteApiService { const adapter = await this.getAdapter(siteId); // 首先尝试查找产品 - if (product.id) { - try { - // 尝试获取产品以确认它是否存在 - const existingProduct = await adapter.getProduct(product.id); - if (existingProduct) { - // 产品存在,执行更新 - return await adapter.updateProduct(product.id, product); - } - } catch (error) { - // 如果获取产品失败,可能是因为产品不存在,继续执行创建逻辑 - console.log(`产品 ${product.id} 不存在,将创建新产品:`, error.message); - } - } else if (product.sku) { + if (product.sku) { // 如果没有提供ID但提供了SKU,尝试通过SKU查找产品 try { // 尝试搜索具有相同SKU的产品 - const searchResult = await adapter.getProducts({ where: { sku: product.sku } }); - if (searchResult.items && searchResult.items.length > 0) { - const existingProduct = searchResult.items[0]; + const existingProduct = await adapter.getProduct( { sku: product.sku }); + if (existingProduct) { // 找到现有产品,更新它 - return await adapter.updateProduct(existingProduct.id, product); + return await adapter.updateProduct({ id: existingProduct.id }, product); } + // 产品不存在,执行创建 + return await adapter.createProduct(product); } catch (error) { // 搜索失败,继续执行创建逻辑 console.log(`通过SKU搜索产品失败:`, error.message); } } - - // 产品不存在,执行创建 - return await adapter.createProduct(product); } /** @@ -197,7 +183,7 @@ export class SiteApiService { */ async getProductFromSite(siteId: number, productId: string | number): Promise { const adapter = await this.getAdapter(siteId); - return await adapter.getProduct(productId); + return await adapter.getProduct({ id: productId }); } /** diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index c7faa02..8c8bc89 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -240,9 +240,11 @@ export class WPService implements IPlatformService { return allData; } - async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise { + async getProducts(site: any, params: Record = {}): Promise { const api = this.createApi(site, 'wc/v3'); - return await this.sdkGetPage(api, 'products', { page, per_page: pageSize }); + const page = params.page ?? 1; + const per_page = params.per_page ?? params.pageSize ?? 100; + return await this.sdkGetPage(api, 'products', { ...params, page, per_page }); } async getProduct(site: any, id: number): Promise { @@ -254,7 +256,7 @@ export class WPService implements IPlatformService { // 导出 WooCommerce 产品为特殊CSV(平台特性) async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise { - const list = await this.getProducts(site, page, pageSize); + const list = await this.getProducts(site, { page, per_page: pageSize }); const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity']; const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); From cd0bcedfadf9f37ac4f3664d9666658ef4852728 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 15:03:11 +0800 Subject: [PATCH 07/24] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96SKU=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加字典排序字段支持 优化产品同步流程,支持通过SKU同步 重构SKU模板生成逻辑,支持分类属性排序 完善产品导入导出功能,增加分类字段处理 统一产品操作方法,提升代码可维护性 --- package-lock.json | 17 -- src/adapter/shopyy.adapter.ts | 2 +- src/adapter/woocommerce.adapter.ts | 334 +++++++++++++++++++-------- src/controller/product.controller.ts | 43 ++-- src/db/seeds/template.seeder.ts | 39 +++- src/dto/site-api.dto.ts | 3 + src/dto/woocommerce.dto.ts | 4 +- src/entity/dict.entity.ts | 4 + src/service/product.service.ts | 118 +++++----- src/service/site-api.service.ts | 43 ++-- src/service/wp.service.ts | 2 +- 11 files changed, 377 insertions(+), 232 deletions(-) diff --git a/package-lock.json b/package-lock.json index e207cd1..405c79b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -523,23 +523,6 @@ "node": ">=18" } }, - "node_modules/@faker-js/faker": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz", - "integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", - "npm": ">=10" - } - }, "node_modules/@hapi/bourne": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz", diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index ffa66af..f5c990d 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -742,7 +742,7 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - mapCreateProductParams(data: Partial): any { + mapCreateProductParams(data: Partial): Partial { // 构建 ShopYY 产品创建参数 const params: any = { name: data.name || '', diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 0debb16..3627c22 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -133,9 +133,30 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户操作方法 - async getCustomer(where: {id: string | number}): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${where.id}`); + async getCustomer(where: Partial>): Promise { + const api = this.wpService.createApi(this.site, 'wc/v3'); + // 根据提供的条件构建查询参数 + let endpoint: string; + if (where.id) { + endpoint = `customers/${where.id}`; + } else if (where.email) { + // 使用邮箱查询客户 + const res = await api.get('customers', { params: { email: where.email } }); + if (!res.data || res.data.length === 0) { + throw new Error('Customer not found'); + } + return this.mapPlatformToUnifiedCustomer(res.data[0]); + } else if (where.phone) { + // 使用电话查询客户 + const res = await api.get('customers', { params: { search: where.phone } }); + if (!res.data || res.data.length === 0) { + throw new Error('Customer not found'); + } + return this.mapPlatformToUnifiedCustomer(res.data[0]); + } else { + throw new Error('Must provide at least one of id, email, or phone'); + } + const res = await api.get(endpoint); return this.mapPlatformToUnifiedCustomer(res.data); } @@ -158,7 +179,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有客户数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); // 处理orderBy参数,转换为WooCommerce API需要的格式 const requestParams = this.mapCustomerSearchParams(params || {}); @@ -168,20 +189,42 @@ export class WooCommerceAdapter implements ISiteAdapter { } async createCustomer(data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.post('customers', data); return this.mapPlatformToUnifiedCustomer(res.data); } - async updateCustomer(where: {id: string | number}, data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${where.id}`, data); + async updateCustomer(where: Partial>, data: Partial): Promise { + const api = this.wpService.createApi(this.site, 'wc/v3'); + let customerId: string | number; + + // 先根据条件获取客户ID + if (where.id) { + customerId = where.id; + } else { + // 如果没有提供ID,则先查询客户 + const customer = await this.getCustomer(where); + customerId = customer.id; + } + + const res = await api.put(`customers/${customerId}`, data); return this.mapPlatformToUnifiedCustomer(res.data); } - async deleteCustomer(where: {id: string | number}): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${where.id}`, { force: true }); + async deleteCustomer(where: Partial>): Promise { + const api = this.wpService.createApi(this.site, 'wc/v3'); + let customerId: string | number; + + // 先根据条件获取客户ID + if (where.id) { + customerId = where.id; + } else { + // 如果没有提供ID,则先查询客户 + const customer = await this.getCustomer(where); + customerId = customer.id; + } + + await api.delete(`customers/${customerId}`, { force: true }); return true; } @@ -225,7 +268,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有媒体数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const media = await this.wpService.sdkGetAll(api, 'media', params); return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); } @@ -403,7 +446,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 订单操作方法 async getOrder(where: {id: string | number}): Promise { // 获取单个订单详情 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.get(`orders/${where.id}`); return this.mapPlatformToUnifiedOrder(res.data); } @@ -445,7 +488,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有订单数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const orders = await this.wpService.sdkGetAll(api, 'orders', params); return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); } @@ -464,7 +507,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async createOrder(data: Partial): Promise { // 创建订单并返回统一订单DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); return this.mapPlatformToUnifiedOrder(res.data); } @@ -476,7 +519,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async deleteOrder(where: {id: string | number}): Promise { // 删除订单 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); await api.delete(`orders/${where.id}`, { force: true }); return true; } @@ -541,14 +584,14 @@ export class WooCommerceAdapter implements ISiteAdapter { async getOrderNotes(orderId: string | number): Promise { // 获取订单备注列表 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.get(`orders/${orderId}/notes`); return res.data; } async createOrderNote(orderId: string | number, data: any): Promise { // 创建订单备注 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.post(`orders/${orderId}/notes`, data); return res.data; } @@ -559,7 +602,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }): Promise { throw new Error('暂未实现'); // 取消订单履行 - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + // const api = this.wpService.createApi(this.site, 'wc/v3'); // try { // // 将订单状态改回处理中 @@ -582,15 +625,105 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapUnifiedToPlatformProduct(data: Partial) { - return data; + mapUnifiedToPlatformProduct(data: Partial): Partial { + // 将统一产品DTO映射为WooCommerce产品数据 + // 基本字段映射 + const mapped: Partial = { + id: data.id as number, + name: data.name, + type: data.type, + status: data.status, + sku: data.sku, + regular_price: data.regular_price, + sale_price: data.sale_price, + price: data.price, + stock_status: data.stock_status as 'instock' | 'outofstock' | 'onbackorder', + stock_quantity: data.stock_quantity, + // 映射更多WooCommerce产品特有的字段 + // featured: data.featured, + // catalog_visibility: data.catalog_visibility, + // date_on_sale_from: data.date_on_sale_from, + // date_on_sale_to: data.date_on_sale_to, + // virtual: data.virtual, + // downloadable: data.downloadable, + // description: data.description, + // short_description: data.short_description, + // slug: data.slug, + // manage_stock: data.manage_stock, + // backorders: data.backorders as 'no' | 'notify' | 'yes', + // sold_individually: data.sold_individually, + // weight: data.weight, + // dimensions: data.dimensions, + // shipping_class: data.shipping_class, + // tax_class: data.tax_class, + }; + + // 映射图片数据 + if (data.images && Array.isArray(data.images)) { + mapped.images = data.images.map(img => ({ + id: img.id as number, + src: img.src, + name: img.name, + alt: img.alt, + })); + } + + // 映射分类数据 + if (data.categories && Array.isArray(data.categories)) { + mapped.categories = data.categories.map(cat => ({ + // id: cat.id as number, //TODO + name: cat.name, + })); + } + + // 映射标签数据 + // TODO tags 应该可以设置 + // if (data.tags && Array.isArray(data.tags)) { + // mapped.tags = data.tags.map(tag => { + // return ({ + // // id: tag.id as number, + // name: tag.name, + // }); + // }); + // } + + // 映射属性数据 + if (data.attributes && Array.isArray(data.attributes)) { + mapped.attributes = data.attributes.map(attr => ({ + // id 由于我们这个主要用来存,所以不映射 id + name: attr.name, + visible: attr.visible, + variation: attr.variation, + options: attr.options + })); + } + + // 映射变体数据(注意:WooCommerce API 中变体通常通过单独的端点处理) + // 这里只映射变体的基本信息,具体创建/更新变体需要额外处理 + if (data.variations && Array.isArray(data.variations)) { + // 对于WooProduct类型,variations字段只存储变体ID + mapped.variations = data.variations.map(variation => variation.id as number); + } + + // 映射下载数据(如果产品是可下载的) + // if (data.downloads && Array.isArray(data.downloads)) { + // mapped.downloads = data.downloads.map(download => ({ + // id: download.id as number, + // name: download.name, + // file: download.file, + // })); + // } + + return mapped; } - mapCreateProductParams(data: Partial) { - return data; + mapCreateProductParams(data: Partial):Partial { + const {id,...mapped}= this.mapUnifiedToPlatformProduct(data); + // 创建不带 id + return mapped } - mapUpdateProductParams(data: Partial) { - return data; + mapUpdateProductParams(data: Partial): Partial { + return this.mapUnifiedToPlatformProduct(data); } mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { @@ -649,14 +782,14 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { + mapPlatformToUnifiedProduct(data: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties // 映射变体数据 - const mappedVariations = item.variations && Array.isArray(item.variations) - ? item.variations + const mappedVariations = data.variations && Array.isArray(data.variations) + ? data.variations .filter((variation: any) => typeof variation !== 'number') // 过滤掉数字类型的变体ID .map((variation: any) => { // 将变体属性转换为统一格式 @@ -683,7 +816,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return { id: variation.id, - name: variation.name || item.name, // 如果变体没有名称,使用父产品名称 + name: variation.name || data.name, // 如果变体没有名称,使用父产品名称 sku: variation.sku || '', regular_price: String(variation.regular_price || ''), sale_price: String(variation.sale_price || ''), @@ -697,34 +830,34 @@ export class WooCommerceAdapter implements ISiteAdapter { : []; return { - id: item.id, - date_created: item.date_created, - date_modified: item.date_modified, - type: item.type, // simple grouped external variable - status: item.status, // draft pending private publish - sku: item.sku, - name: item.name, + id: data.id, + date_created: data.date_created, + date_modified: data.date_modified, + type: data.type, // simple grouped external variable + status: data.status, // draft pending private publish + sku: data.sku, + name: data.name, //价格 - regular_price: item.regular_price, - sale_price: item.sale_price, - price: item.price, - stock_status: item.stock_status, - stock_quantity: item.stock_quantity, - images: (item.images || []).map((img: any) => ({ + regular_price: data.regular_price, + sale_price: data.sale_price, + price: data.price, + stock_status: data.stock_status, + stock_quantity: data.stock_quantity, + images: (data.images || []).map((img: any) => ({ id: img.id, src: img.src, name: img.name, alt: img.alt, })), - categories: (item.categories || []).map((c: any) => ({ + categories: (data.categories || []).map((c: any) => ({ id: c.id, name: c.name, })), - tags: (item.tags || []).map((t: any) => ({ + tags: (data.tags || []).map((t: any) => ({ id: t.id, name: t.name, })), - attributes: (item.attributes || []).map(attr => ({ + attributes: (data.attributes || []).map(attr => ({ id: attr.id, name: attr.name || '', position: attr.position, @@ -733,25 +866,39 @@ export class WooCommerceAdapter implements ISiteAdapter { options: attr.options || [] })), variations: mappedVariations, - permalink: item.permalink, - raw: item, + permalink: data.permalink, + raw: data, }; } - async getProduct({id, sku}: {id?: string, sku?:string}){ + // 判断是否是这个站点的sku + isSiteSkuThisSite(sku: string,){ + return sku.startsWith(this.site.skuPrefix+'-'); + } + async getProduct(where: Partial>): Promise{ + const { id, sku } = where; if(id) return this.getProductById(id); - if(sku) return this.getProductBySku(sku) - return this.getProductById(id || sku || ''); + if(sku) return this.getProductBySku(sku); + throw new Error('必须提供id或sku参数'); } async getProductBySku(sku: string){ - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products?sku=${sku}`); - const product = res.data[0]; + // const api = this.wpService.createApi(this.site, 'wc/v3'); + // const res = await api.get(`products`,{ + // sku + // }); + // const product = res.data[0]; + const res = await this.wpService.getProducts(this.site,{ + sku, + page:1, + per_page:1, + }); + const product = res?.items?.[0]; + if(!product) return null return this.mapPlatformToUnifiedProduct(product); } // 产品操作方法 async getProductById(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.get(`products/${id}`); const product = res.data; @@ -792,7 +939,7 @@ export class WooCommerceAdapter implements ISiteAdapter { try { // 批量获取该产品的所有变体数据 const variations = await this.wpService.sdkGetAll( - (this.wpService as any).createApi(this.site, 'wc/v3'), + this.wpService.createApi(this.site, 'wc/v3'), `products/${item.id}/variations` ); // 将完整的变体数据添加到产品对象中 @@ -818,7 +965,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有产品数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const products = await this.wpService.sdkGetAll(api, 'products', params); // 对于类型为 variable 的产品,需要加载完整的变体数据 @@ -848,41 +995,31 @@ export class WooCommerceAdapter implements ISiteAdapter { async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO - const res = await this.wpService.createProduct(this.site, data); + const createData = this.mapCreateProductParams(data); + const res = await this.wpService.createProduct(this.site, createData); return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { + async updateProduct(where: Partial>, data: Partial): Promise { // 更新产品并返回统一产品DTO - let productId: string; - if (where.id) { - productId = String(where.id); - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = String(product.id); - } else { - throw new Error('必须提供id或sku参数'); + const product = await this.getProduct(where); + if(!product){ + throw new Error('产品不存在'); } - const res = await this.wpService.updateProduct(this.site, productId, data as any); + const updateData = this.mapUpdateProductParams(data); + const res = await this.wpService.updateProduct(this.site, String(product.id), updateData as any); return res; } - async deleteProduct(where: {id?: string | number, sku?: string}): Promise { + async deleteProduct(where: Partial>): Promise { // 删除产品 - let productId: string; - if (where.id) { - productId = String(where.id); - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = String(product.id); - } else { - throw new Error('必须提供id或sku参数'); + const product = await this.getProduct(where); + if(!product){ + throw new Error('产品不存在'); } - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); try { - await api.delete(`products/${productId}`, { force: true }); + await api.delete(`products/${product.id}`, { force: true }); return true; } catch (e) { return false; @@ -902,7 +1039,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { + mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -913,7 +1050,7 @@ export class WooCommerceAdapter implements ISiteAdapter { rating: item.rating, status: item.status, date_created: item.date_created, - raw: item + date_modified: item.date_modified, }; } @@ -937,23 +1074,31 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllReviews(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有评论数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); } - async createReview(data: any): Promise { + async createReview(data: CreateReviewDTO): Promise { const res = await this.wpService.createReview(this.site, data); return this.mapPlatformToUnifiedReview(res); } - async updateReview(where: {id: number}, data: any): Promise { - const res = await this.wpService.updateReview(this.site, where.id, data); + async updateReview(where: Partial>, data: UpdateReviewDTO): Promise { + const { id } = where; + if (!id) { + throw new Error('必须提供评论ID'); + } + const res = await this.wpService.updateReview(this.site, Number(id), data); return this.mapPlatformToUnifiedReview(res); } - async deleteReview(where: {id: number}): Promise { - return await this.wpService.deleteReview(this.site, where.id); + async deleteReview(where: Partial>): Promise { + const { id } = where; + if (!id) { + throw new Error('必须提供评论ID'); + } + return await this.wpService.deleteReview(this.site, Number(id)); } // ========== 订阅映射方法 ========== @@ -999,14 +1144,15 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有订阅数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); } // ========== 变体映射方法 ========== mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { - return data; + // 使用mapVariation方法来实现统一的变体映射逻辑 + return this.mapVariation(data); } mapUnifiedToPlatformVariation(data: Partial) { return data; @@ -1089,7 +1235,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 获取所有产品变体 async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const variations = await this.wpService.sdkGetAll(api, `products/${productId}/variations`, params); // 获取产品名称用于变体显示 @@ -1287,7 +1433,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 获取所有webhooks async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); } catch (error) { @@ -1368,11 +1514,11 @@ export class WooCommerceAdapter implements ISiteAdapter { return links; } - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + batchProcessOrders?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); } - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + batchProcessCustomers?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); } } diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index aef81f8..c263d16 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -698,10 +698,10 @@ export class ProductController { // 从站点同步产品到本地 @ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes }) @Post('/sync-from-site') - async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number }) { + async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number ,sku: string}) { try { - const { siteId, siteProductId } = body; - const product = await this.productService.syncProductFromSite(siteId, siteProductId); + const { siteId, siteProductId, sku } = body; + const product = await this.productService.syncProductFromSite(siteId, siteProductId, sku); return successResponse(product); } catch (error) { return errorResponse(error?.message || error); @@ -713,25 +713,26 @@ export class ProductController { @Post('/batch-sync-from-site') async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) { try { - const { siteId, siteProductIds } = body; - const result = await this.productService.batchSyncFromSite(siteId, siteProductIds); - // 将服务层返回的结果转换为统一格式 - const errors = result.errors.map((error: string) => { - // 提取产品ID部分作为标识符 - const match = error.match(/站点产品ID (\d+) /); - const identifier = match ? match[1] : 'unknown'; - return { - identifier: identifier, - error: error - }; - }); + throw new Error('批量同步产品到本地暂未实现'); + // const { siteId, siteProductIds } = body; + // const result = await this.productService.batchSyncFromSite(siteId, siteProductIds.map((id) => ({ siteProductId: id, sku: '' }))); + // // 将服务层返回的结果转换为统一格式 + // const errors = result.errors.map((error: string) => { + // // 提取产品ID部分作为标识符 + // const match = error.match(/站点产品ID (\d+) /); + // const identifier = match ? match[1] : 'unknown'; + // return { + // identifier: identifier, + // error: error + // }; + // }); - return successResponse({ - total: siteProductIds.length, - processed: result.synced + errors.length, - synced: result.synced, - errors: errors - }); + // return successResponse({ + // total: siteProductIds.length, + // processed: result.synced + errors.length, + // synced: result.synced, + // errors: errors + // }); } catch (error) { return errorResponse(error?.message || error); } diff --git a/src/db/seeds/template.seeder.ts b/src/db/seeds/template.seeder.ts index f12a208..c531f73 100644 --- a/src/db/seeds/template.seeder.ts +++ b/src/db/seeds/template.seeder.ts @@ -23,19 +23,38 @@ export default class TemplateSeeder implements Seeder { const templates = [ { name: 'product.sku', - value: "<%= [it.category.shortName].concat(it.attributes.map(a => a.shortName)).join('-') %>", + value: `<% + // 按分类判断属性排序逻辑 + if (it.category.name === 'nicotine-pouches') { + // 1. 定义 nicotine-pouches 专属的属性固定顺序 + const fixedOrder = ['brand','category', 'flavor', 'strength', 'humidity']; + sortedAttrShortNames = fixedOrder.map(attrKey => { + if(attrKey === 'category') return it.category.shortName + // 排序 + const matchedAttr = it.attributes.find(a => a?.dict?.name === attrKey); + return matchedAttr ? matchedAttr.shortName : ''; + }).filter(Boolean); // 移除空值,避免多余的 "-" + } else { + // 非目标分类,保留 attributes 原有顺序 + sortedAttrShortNames = it.attributes.map(a => a.shortName); + } + + // 4. 拼接分类名 + 排序后的属性名 + %><%= sortedAttrShortNames.join('-') %><% +%>`, description: '产品SKU模板', testData: JSON.stringify({ - category: { - shortName: 'CAT', + "category": { + "name": "nicotine-pouches", + "shortName": "NP" }, - attributes: [ - { shortName: 'BR' }, - { shortName: 'FL' }, - { shortName: '10MG' }, - { shortName: 'DRY' }, - ], - }), + "attributes": [ + { "dict": {"name": "brand"},"shortName": "YOONE" }, + { "dict": {"name": "flavor"},"shortName": "FL" }, + { "dict": {"name": "strength"},"shortName": "10MG" }, + { "dict": {"name": "humidity"},"shortName": "DRY" } + ] +}), }, { name: 'product.title', diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index a93f125..247c5c9 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger'; import { UnifiedPaginationDTO, } from './api.dto'; +import { Dict } from '../entity/dict.entity'; // export class UnifiedOrderWhere{ // [] // } @@ -137,6 +138,8 @@ export class UnifiedProductAttributeDTO { @ApiProperty({ description: '变体属性值(单个值)', required: false }) option?: string; + // 这个是属性的父级字典项 + dict?: Dict; } export class UnifiedProductVariationDTO { diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index 3150a78..f562d64 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -117,9 +117,9 @@ export interface WooProduct { // 购买备注 purchase_note?: string; // 分类列表 - categories?: Array<{ id: number; name?: string; slug?: string }>; + categories?: Array<{ id?: number; name?: string; slug?: string }>; // 标签列表 - tags?: Array<{ id: number; name?: string; slug?: string }>; + tags?: Array<{ id?: number; name?: string; slug?: string }>; // 菜单排序 menu_order?: number; // 元数据 diff --git a/src/entity/dict.entity.ts b/src/entity/dict.entity.ts index dc0d9af..15776da 100644 --- a/src/entity/dict.entity.ts +++ b/src/entity/dict.entity.ts @@ -29,6 +29,10 @@ export class Dict { @OneToMany(() => DictItem, item => item.dict) items: DictItem[]; + // 排序 + @Column({ default: 0, comment: '排序' }) + sort: number; + // 是否可删除 @Column({ default: true, comment: '是否可删除' }) deletable: boolean; diff --git a/src/service/product.service.ts b/src/service/product.service.ts index bbde5d9..384eca8 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -28,7 +28,7 @@ import { StockPoint } from '../entity/stock_point.entity'; import { StockService } from './stock.service'; import { TemplateService } from './template.service'; -import { SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; +import { BatchErrorItem, BatchOperationResult, SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { UnifiedProductDTO } from '../dto/site-api.dto'; import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto'; import { Category } from '../entity/category.entity'; @@ -225,7 +225,7 @@ export class ProductService { where: { sku, }, - relations: ['category', 'attributes', 'attributes.dict', 'siteSkus'] + relations: ['category', 'attributes', 'attributes.dict'] }); } @@ -1440,7 +1440,7 @@ export class ProductService { // 解析属性字段(分号分隔多值) const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []); - + // 将属性解析为 DTO 输入 const attributes: any[] = []; @@ -1455,6 +1455,9 @@ export class ProductService { } } + // 处理分类字段 + const category = val(rec.category); + return { sku, name: val(rec.name), @@ -1464,6 +1467,7 @@ export class ProductService { promotionPrice: num(rec.promotionPrice), type: val(rec.type), siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined, + category, // 添加分类字段 attributes: attributes.length > 0 ? attributes : undefined, } as any; @@ -1483,10 +1487,15 @@ export class ProductService { if (data.price !== undefined) dto.price = Number(data.price); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); - if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); + // 处理分类字段 + if (data.categoryId !== undefined) { + dto.categoryId = Number(data.categoryId); + } else if (data.category) { + // 如果是字符串,需要后续在createProduct中处理 + dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }]; + } // 默认值和特殊处理 - dto.attributes = Array.isArray(data.attributes) ? data.attributes : []; // 如果有组件信息,透传 @@ -1508,7 +1517,13 @@ export class ProductService { if (data.price !== undefined) dto.price = Number(data.price); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); - if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); + // 处理分类字段 + if (data.categoryId !== undefined) { + dto.categoryId = Number(data.categoryId); + } else if (data.category) { + // 如果是字符串,需要后续在updateProduct中处理 + dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }]; + } if (data.type !== undefined) dto.type = data.type; if (data.attributes !== undefined) dto.attributes = data.attributes; @@ -1548,8 +1563,8 @@ export class ProductService { esc(p.price), esc(p.promotionPrice), esc(p.type), - esc(p.description), + esc(p.category ? p.category.name || p.category.title : ''), // 添加分类字段 ]; // 属性数据 @@ -1575,9 +1590,9 @@ export class ProductService { // 导出所有产品为 CSV 文本 async exportProductsCSV(): Promise { - // 查询所有产品及其属性(包含字典关系)和组成 + // 查询所有产品及其属性(包含字典关系)、组成和分类 const products = await this.productModel.find({ - relations: ['attributes', 'attributes.dict', 'components'], + relations: ['attributes', 'attributes.dict', 'components', 'category'], order: { id: 'ASC' }, }); @@ -1612,8 +1627,8 @@ export class ProductService { 'price', 'promotionPrice', 'type', - 'description', + 'category', ]; // 动态属性表头 @@ -1640,7 +1655,7 @@ export class ProductService { } // 从 CSV 导入产品;存在则更新,不存在则创建 - async importProductsCSV(file: any): Promise<{ created: number; updated: number; errors: string[] }> { + async importProductsCSV(file: any): Promise { let buffer: Buffer; if (Buffer.isBuffer(file)) { buffer = file; @@ -1676,19 +1691,19 @@ export class ProductService { console.log('First record keys:', Object.keys(records[0])); } } catch (e: any) { - return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] }; + throw new Error(`CSV 解析失败:${e?.message || e}`) } let created = 0; let updated = 0; - const errors: string[] = []; + const errors: BatchErrorItem[] = []; // 逐条处理记录 for (const rec of records) { try { const data = this.transformCsvRecordToData(rec); if (!data) { - errors.push('缺少 SKU 的记录已跳过'); + errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过'}); continue; } const { sku } = data; @@ -1708,11 +1723,11 @@ export class ProductService { updated += 1; } } catch (e: any) { - errors.push(`产品${rec?.sku}导入失败:${e?.message || String(e)}`); + errors.push({ identifier: '' + rec.sku, error: `产品${rec?.sku}导入失败:${e?.message || String(e)}`}); } } - return { created, updated, errors }; + return { total: records.length, processed: records.length - errors.length, created, updated, errors }; } // 将库存记录的 sku 添加到产品单品中 @@ -1831,9 +1846,7 @@ export class ProductService { } // 将本地产品转换为站点API所需格式 - const unifiedProduct = await this.convertLocalProductToUnifiedProduct(localProduct, params.siteSku); - - + const unifiedProduct = await this.mapLocalToUnifiedProduct(localProduct, params.siteSku); // 调用站点API的upsertProduct方法 try { @@ -1842,7 +1855,7 @@ export class ProductService { await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]); return result; } catch (error) { - throw new Error(`同步产品到站点失败: ${error.message}`); + throw new Error(`同步产品到站点失败: ${error?.response?.data?.message??error.message}`); } } @@ -1869,9 +1882,6 @@ export class ProductService { siteSku: item.siteSku }); - // 然后绑定站点SKU - await this.bindSiteSkus(item.productId, [item.siteSku]); - results.synced++; results.processed++; } catch (error) { @@ -1892,30 +1902,23 @@ export class ProductService { * @param siteProductId 站点产品ID * @returns 同步后的本地产品 */ - async syncProductFromSite(siteId: number, siteProductId: string | number): Promise { + async syncProductFromSite(siteId: number, siteProductId: string | number, sku: string): Promise { + const adapter = await this.siteApiService.getAdapter(siteId); + const siteProduct = await adapter.getProduct({ id: siteProductId }); // 从站点获取产品信息 - const siteProduct = await this.siteApiService.getProductFromSite(siteId, siteProductId); if (!siteProduct) { throw new Error(`站点产品 ID ${siteProductId} 不存在`); } - - // 检查是否已存在相同SKU的本地产品 - let localProduct = null; - if (siteProduct.sku) { - try { - localProduct = await this.findProductBySku(siteProduct.sku); - } catch (error) { - // 产品不存在,继续创建 - } - } - // 将站点产品转换为本地产品格式 - const productData = await this.convertSiteProductToLocalProduct(siteProduct); - - if (localProduct) { + const productData = await this.mapUnifiedToLocalProduct(siteProduct); + return await this.upsertProduct({sku}, productData); + } + async upsertProduct(where: Partial>, productData: any) { + const existingProduct = await this.productModel.findOne({ where: where}); + if (existingProduct) { // 更新现有产品 const updateData: UpdateProductDTO = productData; - return await this.updateProduct(localProduct.id, updateData); + return await this.updateProduct(existingProduct.id, updateData); } else { // 创建新产品 const createData: CreateProductDTO = productData; @@ -1929,18 +1932,18 @@ export class ProductService { * @param siteProductIds 站点产品ID数组 * @returns 批量同步结果 */ - async batchSyncFromSite(siteId: number, siteProductIds: (string | number)[]): Promise<{ synced: number, errors: string[] }> { + async batchSyncFromSite(siteId: number, data: Array<{siteProductId:string, sku: string}>): Promise<{ synced: number, errors: string[] }> { const results = { synced: 0, errors: [] }; - for (const siteProductId of siteProductIds) { + for (const item of data) { try { - await this.syncProductFromSite(siteId, siteProductId); + await this.syncProductFromSite(siteId, item.siteProductId, item.sku); results.synced++; } catch (error) { - results.errors.push(`站点产品ID ${siteProductId} 同步失败: ${error.message}`); + results.errors.push(`站点产品ID ${item.siteProductId} 同步失败: ${error.message}`); } } @@ -1952,7 +1955,7 @@ export class ProductService { * @param siteProduct 站点产品对象 * @returns 本地产品数据 */ - private async convertSiteProductToLocalProduct(siteProduct: any): Promise { + private async mapUnifiedToLocalProduct(siteProduct: any): Promise { const productData: any = { sku: siteProduct.sku, name: siteProduct.name, @@ -2015,18 +2018,20 @@ export class ProductService { * @param localProduct 本地产品对象 * @returns 统一产品对象 */ - private async convertLocalProductToUnifiedProduct(localProduct: Product,siteSku?: string): Promise> { + private async mapLocalToUnifiedProduct(localProduct: Product,siteSku?: string): Promise> { + const tags = localProduct.attributes?.map(a => ({name: a.name})) || []; // 将本地产品数据转换为UnifiedProductDTO格式 const unifiedProduct: any = { id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在,使用现有ID - name: localProduct.nameCn || localProduct.name || localProduct.sku, - type: 'simple', // 默认类型,可以根据实际需要调整 + name: localProduct.name, + type: localProduct.type === 'single'? 'simple' : 'bundle', // 默认类型,可以根据实际需要调整 status: 'publish', // 默认状态,可以根据实际需要调整 - sku: siteSku || await this.templateService.render('site.product.sku', { sku: localProduct.sku }), + sku: siteSku || await this.templateService.render('site.product.sku', { product: localProduct, sku: localProduct.sku }), regular_price: String(localProduct.price || 0), sale_price: String(localProduct.promotionPrice || localProduct.price || 0), price: String(localProduct.price || 0), - // stock_status: localProduct.stockQuantity && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock', + // TODO 库存暂时无法同步 + // stock_status: localProduct.components && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock', // stock_quantity: localProduct.stockQuantity || 0, // images: localProduct.images ? localProduct.images.map(img => ({ // id: img.id, @@ -2034,25 +2039,24 @@ export class ProductService { // name: img.name || '', // alt: img.alt || '' // })) : [], - tags: [], + tags, categories: localProduct.category ? [{ id: localProduct.category.id, name: localProduct.category.name }] : [], attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({ - id: attr.id, - name: attr.name, - position: 0, + id: attr.dict.id, + name: attr.dict.name, + position: attr.dict.sort || 0, visible: true, variation: false, - options: [attr.value] + options: [attr.name] })) : [], variations: [], date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(), date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(), raw: { - localProductId: localProduct.id, - localProductSku: localProduct.sku + ...localProduct } }; diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index 3aab4ac..002536d 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -39,7 +39,7 @@ export class SiteApiService { } return new ShopyyAdapter(site, this.shopyyService); } - + throw new Error(`Unsupported site type: ${site.type}`); } @@ -57,7 +57,7 @@ export class SiteApiService { try { // 使用站点SKU查询对应的ERP产品 const erpProduct = await this.productService.findProductBySiteSku(siteProduct.sku); - + // 将ERP产品信息合并到站点商品中 return { ...siteProduct, @@ -108,24 +108,20 @@ export class SiteApiService { */ async upsertProduct(siteId: number, product: Partial): Promise { const adapter = await this.getAdapter(siteId); - + // 首先尝试查找产品 - if (product.sku) { - // 如果没有提供ID但提供了SKU,尝试通过SKU查找产品 - try { - // 尝试搜索具有相同SKU的产品 - const existingProduct = await adapter.getProduct( { sku: product.sku }); - if (existingProduct) { - // 找到现有产品,更新它 - return await adapter.updateProduct({ id: existingProduct.id }, product); - } - // 产品不存在,执行创建 - return await adapter.createProduct(product); - } catch (error) { - // 搜索失败,继续执行创建逻辑 - console.log(`通过SKU搜索产品失败:`, error.message); - } + if (!product.sku) { + throw new Error('产品SKU不能为空'); } + // 尝试搜索具有相同SKU的产品 + const existingProduct = await adapter.getProduct({ sku: product.sku }); + if (existingProduct) { + // 找到现有产品,更新它 + return await adapter.updateProduct({ id: existingProduct.id }, product); + } + // 产品不存在,执行创建 + return await adapter.createProduct(product); + } /** @@ -175,17 +171,6 @@ export class SiteApiService { return await adapter.getProducts(params); } - /** - * 从站点获取单个产品 - * @param siteId 站点ID - * @param productId 产品ID - * @returns 站点产品 - */ - async getProductFromSite(siteId: number, productId: string | number): Promise { - const adapter = await this.getAdapter(siteId); - return await adapter.getProduct({ id: productId }); - } - /** * 从站点获取所有产品 * @param siteId 站点ID diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 8c8bc89..14cf044 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -44,7 +44,7 @@ export class WPService implements IPlatformService { * @param site 站点配置 * @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1 */ - private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { + public createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { return new WooCommerceRestApi({ url: site.apiUrl, consumerKey: site.consumerKey, From f797950b4cb91a1d7161920cae235ca45696d1c3 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 16:08:41 +0800 Subject: [PATCH 08/24] =?UTF-8?q?fix(sync=5Fshipment):=20=E6=8D=95?= =?UTF-8?q?=E8=8E=B7=E8=BF=90=E5=8D=95=E7=8A=B6=E6=80=81=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E5=BC=82=E5=B8=B8=E5=B9=B6=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加try-catch块来捕获updateShipmentState过程中可能出现的错误 使用logger记录错误信息以便后续排查 --- src/job/sync_shipment.job.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/job/sync_shipment.job.ts b/src/job/sync_shipment.job.ts index c532844..94707d9 100644 --- a/src/job/sync_shipment.job.ts +++ b/src/job/sync_shipment.job.ts @@ -75,10 +75,14 @@ export class SyncUniuniShipmentJob implements IJob{ '255': 'Gateway_To_Gateway_Transit' }; async onTick() { - const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); - shipments.forEach(shipment => { - this.logisticsService.updateShipmentState(shipment); - }); + try { + const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); + shipments.forEach(shipment => { + this.logisticsService.updateShipmentState(shipment); + }); + } catch (error) { + this.logger.error(`更新运单状态失败 ${error.message}`); + } } onComplete(result: any) { From 3664431931c81edd1c103bdcf390b33f86d4d2ef Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:17:03 +0800 Subject: [PATCH 09/24] =?UTF-8?q?feat(shopyy):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E5=95=86=E5=93=81=E6=9F=A5=E8=AF=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BA=A7=E5=93=81=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增ShopyyAllProductQuery类支持全量商品查询参数 - 实现getAllProducts方法支持带条件查询 - 优化getProductBySku方法使用新查询接口 - 公开request方法便于子类调用 - 增加错误日志记录产品查找失败情况 - 修复产品permalink生成逻辑 --- src/adapter/shopyy.adapter.ts | 46 +++++++++++++++++++++------ src/dto/shopyy.dto.ts | 56 +++++++++++++++++++++++++++++---- src/dto/site-api.dto.ts | 5 +++ src/service/shopyy.service.ts | 6 ++-- src/service/site-api.service.ts | 14 +++++++-- 5 files changed, 106 insertions(+), 21 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index f5c990d..cba2c5a 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -20,10 +20,11 @@ import { FulfillmentDTO, CreateReviewDTO, CreateVariationDTO, - UpdateReviewDTO + UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { + ShopyyAllProductQuery, ShopyyCustomer, ShopyyOrder, ShopyyOrderQuery, @@ -686,7 +687,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapPlatformToUnifiedProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + mapPlatformToUnifiedProduct(item: ShopyyProduct): UnifiedProductDTO { // 映射产品状态 function mapProductStatus(status: number) { return status === 1 ? 'publish' : 'draft'; @@ -725,7 +726,7 @@ export class ShopyyAdapter implements ISiteAdapter { name: c.title || '', })), variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], - permalink: item.permalink, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, date_created: typeof item.created_at === 'number' ? new Date(item.created_at * 1000).toISOString() @@ -782,7 +783,7 @@ export class ShopyyAdapter implements ISiteAdapter { // 添加分类信息 if (data.categories && data.categories.length > 0) { params.collections = data.categories.map((category: any) => ({ - id: category.id, + // id: category.id, title: category.name, })); } @@ -898,10 +899,30 @@ export class ShopyyAdapter implements ISiteAdapter { per_page, }; } + mapAllProductParams(params: UnifiedSearchParamsDTO): Partial{ + const mapped = { + ...params.where, + } as any + if(params.per_page){mapped.limit = params.per_page} + return mapped + } async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); + // 转换搜索参数 + const requestParams = this.mapAllProductParams(params); + const response = await this.shopyyService.request( + this.site, + 'products', + 'GET', + null, + requestParams + ); + if(response.code !==0){ + throw new Error(response.msg || '获取产品列表失败') + } + const { data = [] } = response; + const finalItems = data.map(this.mapPlatformToUnifiedProduct.bind(this)) + return finalItems } async createProduct(data: Partial): Promise { @@ -943,16 +964,17 @@ export class ShopyyAdapter implements ISiteAdapter { await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); return true; } - + // 通过sku获取产品详情的私有方法 private async getProductBySku(sku: string): Promise { // 使用Shopyy API的搜索功能通过sku查询产品 - const response = await this.shopyyService.getProducts(this.site, 1, 100); - const product = response.items.find((item: any) => item.sku === sku); + const response = await this.getAllProducts({ where: {sku} }); + console.log('getProductBySku', response) + const product = response?.[0] if (!product) { throw new Error(`未找到sku为${sku}的产品`); } - return this.mapPlatformToUnifiedProduct(product); + return product } async batchProcessProducts( @@ -965,6 +987,10 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapSearchParams(query) } + mapAllProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) + } + // ========== 评论映射方法 ========== mapUnifiedToPlatformReview(data: Partial) { diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index fc534d6..009d6ce 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -4,10 +4,53 @@ export interface ShopyyTag { id?: number; name?: string; } -export interface ShopyyProductQuery{ +export interface ShopyyProductQuery { page: string; limit: string; } +/** + * Shopyy 全量商品查询参数类 + * 用于封装获取 Shopyy 商品列表时的各种筛选和分页条件 + * 参考文档: https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse + */ +export class ShopyyAllProductQuery { + /** 分页大小,限制返回的商品数量 */ + limit?: string; + /** 起始ID,用于分页,返回ID大于该值的商品 */ + since_id?: string; + /** 商品ID,精确匹配单个商品 */ + id?: string; + /** 商品标题,支持模糊查询 */ + title?: string; + /** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */ + status?: string; + /** 商品SKU编码,库存保有单位,精确或模糊匹配 */ + sku?: string; + /** 商品SPU编码,标准化产品单元,用于归类同款商品 */ + spu?: string; + /** 商品分类ID,筛选指定分类下的商品 */ + collection_id?: string; + /** 变体价格最小值,筛选变体价格大于等于该值的商品 */ + variant_price_min?: string; + /** 变体价格最大值,筛选变体价格小于等于该值的商品 */ + variant_price_max?: string; + /** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */ + variant_compare_at_price_min?: string; + /** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */ + variant_compare_at_price_max?: string; + /** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */ + variant_weight_min?: string; + /** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */ + variant_weight_max?: string; + /** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + created_at_min?: string; + /** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + created_at_max?: string; + /** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + updated_at_min?: string; + /** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + updated_at_max?: string; +} // 产品类型 export interface ShopyyProduct { // 产品主键 @@ -249,17 +292,18 @@ export interface ShopyyOrder { // 物流回传时间 payment_tracking_at?: number; // 商品 - products?: Array<{ + products?: Array<{ // 订单商品表 id - order_product_id?: number; + order_product_id?: number; // 数量 - quantity?: number; + quantity?: number; // 更新时间 - updated_at?: number; + updated_at?: number; // 创建时间 created_at?: number; // 发货商品表 id - id?: number }>; + id?: number + }>; }>; shipping_zone_plans?: Array<{ shipping_price?: number | string; diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index 247c5c9..faee539 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -18,6 +18,11 @@ export enum OrderFulfillmentStatus { // 确认发货 CONFIRMED, } +// +export class UnifiedProductWhere { + sku?: string; + [prop:string]:any +} export class UnifiedTagDTO { // 标签DTO用于承载统一标签数据 @ApiProperty({ description: '标签ID' }) diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index c111a03..0081f55 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -158,7 +158,7 @@ export class ShopyyService { * @param params 请求参数 * @returns 响应数据 */ - private async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise { + async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise { const url = this.buildURL(site.apiUrl, endpoint); const headers = this.buildHeaders(site); @@ -206,13 +206,13 @@ export class ShopyyService { * @param pageSize 每页数量 * @returns 分页产品列表 */ - async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise { + async getProducts(site: any, page: number = 1, pageSize: number = 100, where: Record = {}): Promise { // ShopYY API: GET /products // 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含 const response = await this.request(site, 'products', 'GET', null, { page, page_size: pageSize, - fields: 'id,name,sku,handle,status,type,stock_status,stock_quantity,images,regular_price,sale_price,tags,variations' + ...where }); return { diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index 002536d..46d8ee4 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -1,4 +1,4 @@ -import { Inject, Provide } from '@midwayjs/core'; +import { ILogger, Inject, Provide } from '@midwayjs/core'; import { ShopyyAdapter } from '../adapter/shopyy.adapter'; import { WooCommerceAdapter } from '../adapter/woocommerce.adapter'; import { ISiteAdapter } from '../interface/site-adapter.interface'; @@ -22,6 +22,9 @@ export class SiteApiService { @Inject() productService: ProductService; + @Inject() + logger: ILogger; + async getAdapter(siteId: number): Promise { const site = await this.siteService.get(siteId, true); if (!site) { @@ -114,7 +117,14 @@ export class SiteApiService { throw new Error('产品SKU不能为空'); } // 尝试搜索具有相同SKU的产品 - const existingProduct = await adapter.getProduct({ sku: product.sku }); + let existingProduct + try { + + existingProduct = await adapter.getProduct({ sku: product.sku }); + } catch (error) { + this.logger.error(`[Site API] 查找产品失败, siteId: ${siteId}, sku: ${product.sku}, 错误信息: ${error.message}`); + existingProduct = null + } if (existingProduct) { // 找到现有产品,更新它 return await adapter.updateProduct({ id: existingProduct.id }, product); From 1814d9734b3144e80a33c298bc71228675b744d5 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 15:22:18 +0800 Subject: [PATCH 10/24] =?UTF-8?q?feat(=E8=AE=A2=E5=8D=95):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=8E=B7=E5=8F=96=E8=AE=A2=E5=8D=95=E6=80=BB=E6=95=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现订单总数统计接口,包括: 1. 在ISiteAdapter接口添加countOrders方法 2. 在WooCommerce和Shopyy适配器中实现该方法 3. 添加控制器端点暴露该功能 4. 优化订单查询参数映射逻辑 refactor(Shopyy): 重构搜索参数映射逻辑 将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性 --- src/adapter/shopyy.adapter.ts | 99 +++++++++++++++++++------ src/adapter/woocommerce.adapter.ts | 13 +++- src/controller/media.controller.ts | 79 -------------------- src/controller/site-api.controller.ts | 26 ++++++- src/dto/shopyy.dto.ts | 40 ++++++++++ src/entity/product.entity.ts | 3 - src/interface/site-adapter.interface.ts | 5 ++ src/service/shopyy.service.ts | 37 +++------ 8 files changed, 165 insertions(+), 137 deletions(-) delete mode 100644 src/controller/media.controller.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 10c51fb..c6aed2c 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -23,7 +23,9 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { ShopyyCustomer, ShopyyOrder, + ShopyyOrderQuery, ShopyyProduct, + ShopyyProductQuery, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; @@ -63,6 +65,31 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { + return this.mapSearchParams(params) + } + + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + private mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), + page, + limit, + } + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { const { search, page, per_page } = params; const shopyyParams: any = { @@ -393,14 +420,19 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) + } async getProducts( params: UnifiedSearchParamsDTO ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); const response = await this.shopyyService.fetchResourcePaged( this.site, 'products/list', - params + requestParams ); const { items = [], total, totalPages, page, per_page } = response; const finalItems = items.map((item) => ({ @@ -431,7 +463,6 @@ export class ShopyyAdapter implements ISiteAdapter { const res = await this.shopyyService.createProduct(this.site, data); return this.mapProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { // Shopyy update returns boolean? // shopyyService.updateProduct returns boolean. @@ -466,38 +497,49 @@ export class ShopyyAdapter implements ISiteAdapter { ): Promise { return await this.shopyyService.batchProcessProducts(this.site, data); } - mapUnifiedOrderQueryToShopyyQuery(params: UnifiedSearchParamsDTO) { - const { where = {} as any, ...restParams } = params || {} + /** + * 将统一的订单查询参数转换为 ShopYY 订单查询参数 + * 包含状态映射等特殊处理 + */ + private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 首先使用通用参数转换 + const baseParams = this.mapSearchParams(params); + + // 订单状态映射 const statusMap = { 'pending': '100', // 100 未完成 'processing': '110', // 110 待处理 'completed': "180", // 180 已完成(确认收货) 'cancelled': '190', // 190 取消 - } - const normalizedParams: any = { - ...restParams, - } - if (where) { - normalizedParams.where = { - ...where, - } - if (where.status) { - normalizedParams.where.status = statusMap[where.status]; + }; + + // 如果有状态参数,进行特殊映射 + if (baseParams.status) { + const unifiedStatus = baseParams.status + if (statusMap[unifiedStatus]) { + baseParams.status = statusMap[unifiedStatus]; } } - return normalizedParams + + // 处理ID参数 + if (baseParams.id) { + baseParams.ids = baseParams.id; + delete baseParams.id; + } + + return baseParams; } async getOrders( params: UnifiedSearchParamsDTO ): Promise> { - const normalizedParams = this.mapUnifiedOrderQueryToShopyyQuery(params); - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'orders', + normalizedParams + ); return { items: items.map(this.mapOrder.bind(this)), total, @@ -507,6 +549,17 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; + } + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { const data = await this.shopyyService.getAllOrders(this.site.id, params); return data.map(this.mapOrder.bind(this)); @@ -669,7 +722,7 @@ export class ShopyyAdapter implements ISiteAdapter { const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, 'media', // Shopyy的媒体API端点可能需要调整 - requestParams + requestParams ); return { items: items.map(this.mapMedia.bind(this)), diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index c407780..910572a 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -718,6 +718,18 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + async getOrder(id: string | number): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); @@ -1294,4 +1306,3 @@ export class WooCommerceAdapter implements ISiteAdapter { } } } - diff --git a/src/controller/media.controller.ts b/src/controller/media.controller.ts deleted file mode 100644 index cafd85f..0000000 --- a/src/controller/media.controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core'; -import { WPService } from '../service/wp.service'; -import { successResponse, errorResponse } from '../utils/response.util'; - -@Controller('/media') -export class MediaController { - @Inject() - wpService: WPService; - - @Get('/list') - async list( - @Query('siteId') siteId: number, - @Query('page') page: number = 1, - @Query('pageSize') pageSize: number = 20 - ) { - try { - if (!siteId) { - return errorResponse('siteId is required'); - } - const result = await this.wpService.getMedia(siteId, page, pageSize); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Post('/upload') - async upload(@Fields() fields, @Files() files) { - try { - const siteId = fields.siteId; - if (!siteId) { - return errorResponse('siteId is required'); - } - if (!files || files.length === 0) { - return errorResponse('file is required'); - } - const file = files[0]; - const result = await this.wpService.createMedia(siteId, file); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Post('/update/:id') - async update(@Param('id') id: number, @Body() body) { - try { - const siteId = body.siteId; - if (!siteId) { - return errorResponse('siteId is required'); - } - // 过滤出需要更新的字段 - const { title, caption, description, alt_text } = body; - const data: any = {}; - if (title !== undefined) data.title = title; - if (caption !== undefined) data.caption = caption; - if (description !== undefined) data.description = description; - if (alt_text !== undefined) data.alt_text = alt_text; - - const result = await this.wpService.updateMedia(siteId, id, data); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } - - @Del('/:id') - async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) { - try { - if (!siteId) { - return errorResponse('siteId is required'); - } - const result = await this.wpService.deleteMedia(siteId, id, force); - return successResponse(result); - } catch (error) { - return errorResponse(error.message); - } - } -} diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 9f69b87..5013390 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -672,6 +672,26 @@ export class SiteApiController { } } + @Get('/:siteId/orders/count') + @ApiOkResponse({ type: Object }) + async countOrders( + @Param('siteId') siteId: number, + @Query() query: any + ) { + this.logger.info(`[Site API] 获取订单总数开始, siteId: ${siteId}`); + try { + + + const adapter = await this.siteApiService.getAdapter(siteId); + const total = await adapter.countOrders(query); + this.logger.info(`[Site API] 获取订单总数成功, siteId: ${siteId}, total: ${total}`); + return successResponse({ total }); + } catch (error) { + this.logger.error(`[Site API] 获取订单总数失败, siteId: ${siteId}, 错误信息: ${error.message}`); + return errorResponse(error.message); + } + } + @Get('/:siteId/customers/:customerId/orders') @ApiOkResponse({ type: UnifiedOrderPaginationDTO }) async getCustomerOrders( @@ -1050,13 +1070,13 @@ export class SiteApiController { } } - @Get('/:siteId/orders/:orderId/trackings') + @Get('/:siteId/orders/:orderId/fulfillments') @ApiOkResponse({ type: Object }) - async getOrderTrackings( + async getOrderFulfillments( @Param('siteId') siteId: number, @Param('orderId') orderId: string ) { - this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`); + this.logger.info(`[Site API] 获取订单履约信息开始, siteId: ${siteId}, orderId: ${orderId}`); try { const adapter = await this.siteApiService.getAdapter(siteId); const data = await adapter.getOrderFulfillments(orderId); diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index 2bcbca7..fc534d6 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -4,6 +4,10 @@ export interface ShopyyTag { id?: number; name?: string; } +export interface ShopyyProductQuery{ + page: string; + limit: string; +} // 产品类型 export interface ShopyyProduct { // 产品主键 @@ -83,6 +87,42 @@ export interface ShopyyVariant { position?: number | string; sku_code?: string; } +// +// 订单查询参数类型 +export interface ShopyyOrderQuery { + // 订单ID集合 多个ID用','联接 例:1,2,3 + ids?: string; + // 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + status?: string; + // 物流状态 300 未发货;310 部分发货;320 已发货;330(确认收货) + fulfillment_status?: string; + // 支付状态 200 待支付;210 支付中;220 部分支付;230 已支付;240 支付失败;250 部分退款;260 已退款 ;290 已取消; + financial_status?: string; + // 支付时间 下限值 + pay_at_min?: string; + // 支付时间 上限值 + pay_at_max?: string; + // 创建开始时间 + created_at_min?: number; + // 创建结束时间 + created_at_max?: number; + // 更新时间开始 + updated_at_min?: string; + // 更新时间结束 + updated_at_max?: string; + // 起始ID + since_id?: string; + // 页码 + page?: string; + // 每页条数 + limit?: string; + // 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间 + order_field?: string; + // 排序方式(默认desc) desc=降序 asc=升序 + order_by?: string; + // 订单列表类型 + group?: string; +} // 订单类型 export interface ShopyyOrder { diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index ea763ec..1d7b296 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -65,9 +65,6 @@ export class Product { @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) promotionPrice: number; - - - // 分类关联 @ManyToOne(() => Category, category => category.products) @JoinColumn({ name: 'categoryId' }) diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 9baf0c7..84f6765 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -46,6 +46,11 @@ export interface ISiteAdapter { */ getOrders(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取订单总数 + */ + countOrders(params: Record): Promise; + /** * 获取所有订单 */ diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index d083e2f..c111a03 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -1,3 +1,6 @@ +/** + * https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse + */ import { ILogger, Inject, Provide } from '@midwayjs/core'; import axios, { AxiosRequestConfig } from 'axios'; import * as fs from 'fs'; @@ -180,41 +183,19 @@ export class ShopyyService { * 通用分页获取资源 */ public async fetchResourcePaged(site: any, endpoint: string, params: Record = {}) { - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - const where = params.where && typeof params.where === 'object' ? params.where : {}; - let orderby: string | undefined = params.orderby; - let order: 'asc' | 'desc' | undefined = params.orderDir as any; - if (!orderby && params.order && typeof params.order === 'object') { - const entries = Object.entries(params.order as Record); - if (entries.length > 0) { - const [field, dir] = entries[0]; - orderby = field; - order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; - } - } - // 映射统一入参到平台入参 - const requestParams = { - ...where, - ...(params.search ? { search: params.search } : {}), - ...(params.status ? { status: params.status } : {}), - ...(orderby ? { orderby } : {}), - ...(order ? { order } : {}), - page, - limit - }; - this.logger.debug('ShopYY API请求分页参数:'+ JSON.stringify(requestParams)); - const response = await this.request(site, endpoint, 'GET', null, requestParams); + const response = await this.request(site, endpoint, 'GET', null, params); + return this.mapPageResponse(response,params); + } + mapPageResponse(response:any,query: Record){ if (response?.code !== 0) { throw new Error(response?.msg) } - return { items: (response.data.list || []) as T[], total: response.data?.paginate?.total || 0, totalPages: response.data?.paginate?.pageTotal || 0, - page: response.data?.paginate?.current || requestParams.page, - per_page: response.data?.paginate?.pagesize || requestParams.limit, + page: response.data?.paginate?.current || query.page, + per_page: response.data?.paginate?.pagesize || query.limit, }; } From cb2a70d5af9174247706b81e09ab37bfe96597d5 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 18:10:00 +0800 Subject: [PATCH 11/24] =?UTF-8?q?refactor(interface):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E9=80=82=E9=85=8D=E5=99=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E6=8C=89=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组 移除废弃的 fulfillOrder 方法 新增多个数据映射方法以支持统一数据格式转换 --- src/adapter/shopyy.adapter.ts | 1335 ++++++++++++--------- src/adapter/woocommerce.adapter.ts | 1407 ++++++++++++----------- src/controller/site-api.controller.ts | 20 - src/interface/site-adapter.interface.ts | 469 ++++++-- src/transformer/database.transformer.ts | 1 + src/transformer/file.transformer.ts | 1 + src/transformer/shopyy.transformer.ts | 0 src/transformer/woocommerce.adpater.ts | 8 + 8 files changed, 1906 insertions(+), 1335 deletions(-) create mode 100644 src/transformer/database.transformer.ts create mode 100644 src/transformer/file.transformer.ts create mode 100644 src/transformer/shopyy.transformer.ts create mode 100644 src/transformer/woocommerce.adpater.ts diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index c6aed2c..12c85e6 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -17,7 +17,10 @@ import { UnifiedAddressDTO, UnifiedShippingLineDTO, OrderFulfillmentStatus, - FulfillmentDTO + FulfillmentDTO, + CreateReviewDTO, + CreateVariationDTO, + UpdateReviewDTO } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { @@ -32,6 +35,7 @@ import { import { OrderStatus, } from '../enums/base.enum'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class ShopyyAdapter implements ISiteAdapter { shopyyFinancialStatusMap= { '200': '待支付', @@ -44,142 +48,181 @@ export class ShopyyAdapter implements ISiteAdapter { '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); - this.mapOrder = this.mapOrder.bind(this); - this.mapMedia = this.mapMedia.bind(this); - // this.mapSubscription = this.mapSubscription.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedVariation = this.mapPlatformToUnifiedVariation.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); } - mapMedia(item: any): UnifiedMediaDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { + // 处理多地址结构 + const addresses = item.addresses || []; + const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); + + // 尝试从地址列表中获取billing和shipping + // 如果没有明确区分,默认使用默认地址或第一个地址 + const billingAddress = defaultAddress; + const shippingAddress = defaultAddress; + + const billing = { + first_name: billingAddress.first_name || item.first_name || '', + last_name: billingAddress.last_name || item.last_name || '', + fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), + company: billingAddress.company || '', + email: item.email || '', + phone: billingAddress.phone || item.contact || '', + address_1: billingAddress.address1 || '', + address_2: billingAddress.address2 || '', + city: billingAddress.city || '', + state: billingAddress.province || '', + postcode: billingAddress.zip || '', + country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' + }; + + const shipping = { + first_name: shippingAddress.first_name || item.first_name || '', + last_name: shippingAddress.last_name || item.last_name || '', + fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), + company: shippingAddress.company || '', + address_1: shippingAddress.address1 || '', + address_2: shippingAddress.address2 || '', + city: shippingAddress.city || '', + state: shippingAddress.province || '', + postcode: shippingAddress.zip || '', + country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' + }; + + return { + id: item.id || item.customer_id, + orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), + total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), + first_name: item.first_name || item.firstname || '', + last_name: item.last_name || item.lastname || '', + fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), + email: item.email || item.customer_email || '', + phone: item.contact || billing.phone || item.phone || '', + billing, + shipping, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), + raw: item, + }; + } + + mapUnifiedToPlatformCustomer(data: Partial) { + return data + } + + async getCustomer(id: string | number): Promise { + const customer = await this.shopyyService.getCustomer(this.site, id); + return this.mapPlatformToUnifiedCustomer(customer); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const { items, total, totalPages, page, per_page } = + await this.shopyyService.fetchCustomersPaged(this.site, params); + return { + items: items.map(this.mapPlatformToUnifiedCustomer.bind(this)), + total, + totalPages, + page, + per_page + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllCustomers 暂未实现 + throw new Error('Shopyy getAllCustomers 暂未实现'); + } + + async createCustomer(data: Partial): Promise { + const createdCustomer = await this.shopyyService.createCustomer(this.site, data); + return this.mapPlatformToUnifiedCustomer(createdCustomer); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + return this.mapPlatformToUnifiedCustomer(updatedCustomer); + } + + async deleteCustomer(id: string | number): Promise { + return await this.shopyyService.deleteCustomer(this.site, id); + } + + batchProcessCustomers?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { // 映射媒体项目 return { - id: item.id, - date_created: item.created_at, - date_modified: item.updated_at, - source_url: item.src, - title: item.alt || '', + id: data.id, + date_created: data.created_at, + date_modified: data.updated_at, + source_url: data.src, + title: data.alt || '', media_type: '', // Shopyy API未提供,暂时留空 mime_type: '', // Shopyy API未提供,暂时留空 }; } - private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { + mapUnifiedToPlatformMedia(data: Partial) { + return data + } + + async getMedia( + params: UnifiedSearchParamsDTO + ): Promise> { + const requestParams = this.mapMediaSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'media', // Shopyy的媒体API端点可能需要调整 + requestParams + ); + return { + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllMedia 暂未实现 + throw new Error('Shopyy getAllMedia 暂未实现'); + } + + async createMedia(file: any): Promise { + const createdMedia = await this.shopyyService.createMedia(this.site, file); + return this.mapPlatformToUnifiedMedia(createdMedia); + } + + async updateMedia(id: string | number, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + return this.mapPlatformToUnifiedMedia(updatedMedia); + } + + async deleteMedia(id: string | number): Promise { + return await this.shopyyService.deleteMedia(this.site, id); + } + + mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { return this.mapSearchParams(params) } - /** - * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 - * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 - */ - private mapSearchParams(params: UnifiedSearchParamsDTO): any { - // 处理分页参数 - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - - // 处理 where 条件 - const query: any = { - ...(params.where || {}), - page, - limit, - } - if(params.orderBy){ - const [field, dir] = Object.entries(params.orderBy)[0]; - query.order_by = dir === 'desc' ? 'desc' : 'asc'; - query.order_field = field - } - return query; - mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { - const { search, page, per_page } = params; - const shopyyParams: any = { - page: page || 1, - limit: per_page || 10, - }; - - if (search) { - shopyyParams.query = search; - } - - return shopyyParams; - } - - mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { - // 映射产品状态 - function mapProductStatus(status: number) { - return status === 1 ? 'publish' : 'draft'; - } - return { - id: item.id, - name: item.name || item.title, - type: String(item.product_type ?? ''), - status: mapProductStatus(item.status), - sku: item.variant?.sku || '', - regular_price: String(item.variant?.price ?? ''), - sale_price: String(item.special_price ?? ''), - price: String(item.price ?? ''), - stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: item.inventory_quantity, - images: (item.images || []).map((img: any) => ({ - id: img.id || 0, - src: img.src, - name: '', - alt: img.alt || '', - // 排序 - position: img.position || '', - })), - attributes: (item.options || []).map(option => ({ - id: option.id || 0, - name: option.option_name || '', - options: (option.values || []).map(value => value.option_value || ''), - })), - tags: (item.tags || []).map((t: any) => ({ - id: t.id || 0, - name: t.name || '', - })), - // shopyy叫做专辑 - categories: item.collections.map((c: any) => ({ - id: c.id || 0, - name: c.title || '', - })), - variations: item.variants?.map(this.mapVariation.bind(this)) || [], - permalink: item.permalink, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : String(item.created_at ?? ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : String(item.updated_at ?? ''), - raw: item, - }; - } - - mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { - // 映射变体 - return { - id: variant.id, - name: variant.sku || '', - sku: variant.sku || '', - regular_price: String(variant.price ?? ''), - sale_price: String(variant.special_price ?? ''), - price: String(variant.price ?? ''), - stock_status: - variant.inventory_tracking === 1 ? 'instock' : 'outofstock', - stock_quantity: variant.inventory_quantity, - }; - } - - 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 - } - mapOrder(item: ShopyyOrder): UnifiedOrderDTO { + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_address || {}; const shipping = (item as any).shipping_address || {}; @@ -348,99 +391,162 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } - shopyyFulfillmentStatusMap = { - // 未发货 - '300': OrderFulfillmentStatus.PENDING, - // 部分发货 - '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, - // 已发货 - '320': OrderFulfillmentStatus.FULFILLED, - // 已取消 - '330': OrderFulfillmentStatus.CANCELLED, - // 确认发货 + + mapUnifiedToPlatformOrder(data: Partial) { + return data } - mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { - // 处理多地址结构 - const addresses = item.addresses || []; - const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); - - // 尝试从地址列表中获取billing和shipping - // 如果没有明确区分,默认使用默认地址或第一个地址 - const billingAddress = defaultAddress; - const shippingAddress = defaultAddress; - - const billing = { - first_name: billingAddress.first_name || item.first_name || '', - last_name: billingAddress.last_name || item.last_name || '', - fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), - company: billingAddress.company || '', - email: item.email || '', - phone: billingAddress.phone || item.contact || '', - address_1: billingAddress.address1 || '', - address_2: billingAddress.address2 || '', - city: billingAddress.city || '', - state: billingAddress.province || '', - postcode: billingAddress.zip || '', - country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' - }; - - const shipping = { - first_name: shippingAddress.first_name || item.first_name || '', - last_name: shippingAddress.last_name || item.last_name || '', - fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), - company: shippingAddress.company || '', - address_1: shippingAddress.address1 || '', - address_2: shippingAddress.address2 || '', - city: shippingAddress.city || '', - state: shippingAddress.province || '', - postcode: shippingAddress.zip || '', - country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' - }; - - return { - id: item.id || item.customer_id, - orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), - total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), - first_name: item.first_name || item.firstname || '', - last_name: item.last_name || item.lastname || '', - fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), - email: item.email || item.customer_email || '', - phone: item.contact || billing.phone || item.phone || '', - billing, - shipping, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), - raw: item, - }; - } - mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { - return this.mapSearchParams(query) + mapCreateOrderParams(data: Partial): any { + return data } - async getProducts( + mapUpdateOrderParams(data: Partial): any { + // 构建 ShopYY 订单更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.status !== undefined) { + // 映射订单状态 + const statusMap = { + [OrderStatus.PENDING]: 100, // pending -> 100 未完成 + [OrderStatus.PROCESSING]: 110, // processing -> 110 待处理 + [OrderStatus.COMPLETED]: 180, // completed -> 180 已完成 + [OrderStatus.CANCEL]: 190 // cancel -> 190 取消 + }; + params.status = statusMap[data.status] || 100; + } + + if (data.payment_method !== undefined) { + params.payment_method = data.payment_method; + } + + if (data.billing) { + // 更新客户信息 + if (data.billing.first_name !== undefined) { + params.firstname = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.lastname = data.billing.last_name; + } + if (data.billing.email !== undefined) { + params.email = data.billing.email; + } + if (data.billing.phone !== undefined) { + params.phone = data.billing.phone; + } + + // 更新账单地址 + params.billing_address = params.billing_address || {}; + if (data.billing.first_name !== undefined) { + params.billing_address.first_name = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.billing_address.last_name = data.billing.last_name; + } + if (data.billing.company !== undefined) { + params.billing_address.company = data.billing.company; + } + if (data.billing.address_1 !== undefined) { + params.billing_address.address1 = data.billing.address_1; + } + if (data.billing.address_2 !== undefined) { + params.billing_address.address2 = data.billing.address_2; + } + if (data.billing.city !== undefined) { + params.billing_address.city = data.billing.city; + } + if (data.billing.state !== undefined) { + params.billing_address.province = data.billing.state; + } + if (data.billing.postcode !== undefined) { + params.billing_address.zip = data.billing.postcode; + } + if (data.billing.country !== undefined) { + params.billing_address.country_code = data.billing.country; + } + } + + if (data.shipping) { + // 更新送货地址 + params.shipping_address = params.shipping_address || {}; + if (data.shipping.first_name !== undefined) { + params.shipping_address.first_name = data.shipping.first_name; + } + if (data.shipping.last_name !== undefined) { + params.shipping_address.last_name = data.shipping.last_name; + } + if (data.shipping.company !== undefined) { + params.shipping_address.company = data.shipping.company; + } + if (data.shipping.address_1 !== undefined) { + params.shipping_address.address1 = data.shipping.address_1; + } + if (data.shipping.address_2 !== undefined) { + params.shipping_address.address2 = data.shipping.address_2; + } + if (data.shipping.city !== undefined) { + params.shipping_address.city = data.shipping.city; + } + if (data.shipping.state !== undefined) { + params.shipping_address.province = data.shipping.state; + } + if (data.shipping.postcode !== undefined) { + params.shipping_address.zip = data.shipping.postcode; + } + if (data.shipping.country !== undefined) { + params.shipping_address.country_code = data.shipping.country; + } + if (data.shipping.phone !== undefined) { + params.shipping_address.phone = data.shipping.phone; + } + } + + // 更新订单项 + if (data.line_items && data.line_items.length > 0) { + params.products = data.line_items.map((item: UnifiedOrderLineItemDTO) => ({ + product_id: item.product_id, + quantity: item.quantity, + // price: item.price || '0.00', + sku: item.sku || '', + })); + } + + // 更新物流信息 + if (data.shipping_lines && data.shipping_lines.length > 0) { + const shippingLine = data.shipping_lines[0]; + if (shippingLine.method_title !== undefined) { + params.shipping_method = shippingLine.method_title; + } + if (shippingLine.total !== undefined) { + params.shipping_price = shippingLine.total; + } + } + + // // 更新备注信息 + // if (data.note !== undefined) { + // params.note = data.note; + // } + + return params; + } + + async getOrder(id: string | number): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(id)); + return this.mapPlatformToUnifiedOrder(data); + } + + async getOrders( params: UnifiedSearchParamsDTO - ): Promise> { - // 转换搜索参数 - const requestParams = this.mapProductQuery(params); - const response = await this.shopyyService.fetchResourcePaged( + ): Promise> { + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, - 'products/list', - requestParams + 'orders', + normalizedParams ); - 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)) return { - items: finalItems as UnifiedProductDTO[], + items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), total, totalPages, page, @@ -448,34 +554,37 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + const data = await this.shopyyService.getAllOrders(this.site.id, params); + return data.map(this.mapPlatformToUnifiedOrder.bind(this)); } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapProduct(product); + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; } - async createProduct(data: Partial): Promise { - const res = await this.shopyyService.createProduct(this.site, data); - return this.mapProduct(res); - } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). - await this.shopyyService.updateProduct(this.site, String(id), data); - return true; + async createOrder(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateOrderParams(data); + const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); + return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateVariation(productId: string | number, variationId: string | number, data: any): Promise { - await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data); - return { ...data, id: variationId }; + async updateOrder(id: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateOrderParams(data); + return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + } + + async deleteOrder(id: string | number): Promise { + return await this.shopyyService.deleteOrder(this.site, id); } async getOrderNotes(orderId: string | number): Promise { @@ -486,22 +595,65 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.createOrderNote(this.site, orderId, data); } - async deleteProduct(id: string | number): Promise { - // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); - return true; + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + // 取消订单履行 + try { + // 调用 ShopyyService 的取消履行方法 + const cancelShipData = { + order_id: String(orderId), + fulfillment_id: data.shipment_id || '' + }; + const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); + + return { + success: result, + order_id: orderId, + shipment_id: data.shipment_id, + reason: data.reason, + cancelled_at: new Date().toISOString() + }; + } catch (error) { + throw new Error(`履行失败: ${error.message}`); + } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - return await this.shopyyService.batchProcessProducts(this.site, data); + async getOrderFulfillments(orderId: string | number): Promise { + return await this.shopyyService.getFulfillments(this.site, String(orderId)); } - /** - * 将统一的订单查询参数转换为 ShopYY 订单查询参数 - * 包含状态映射等特殊处理 - */ - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + + async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { + // 调用 Shopyy Service 的 createFulfillment 方法 + const fulfillmentData = { + tracking_number: data.tracking_number, + 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); + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: string; + tracking_provider?: string; + date_shipped?: string; + status_shipped?: string; + }): Promise { + return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + batchProcessOrders?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 首先使用通用参数转换 const baseParams = this.mapSearchParams(params); @@ -530,18 +682,205 @@ export class ShopyyAdapter implements ISiteAdapter { return baseParams; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - // 转换订单查询参数 - const normalizedParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + // 映射产品状态 + function mapProductStatus(status: number) { + return status === 1 ? 'publish' : 'draft'; + } return { - items: items.map(this.mapOrder.bind(this)), + id: item.id, + name: item.name || item.title, + type: String(item.product_type ?? ''), + status: mapProductStatus(item.status), + sku: item.variant?.sku || '', + regular_price: String(item.variant?.price ?? ''), + sale_price: String(item.special_price ?? ''), + price: String(item.price ?? ''), + stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: item.inventory_quantity, + images: (item.images || []).map((img: any) => ({ + id: img.id || 0, + src: img.src, + name: '', + alt: img.alt || '', + // 排序 + position: img.position || '', + })), + attributes: (item.options || []).map(option => ({ + id: option.id || 0, + name: option.option_name || '', + options: (option.values || []).map(value => value.option_value || ''), + })), + tags: (item.tags || []).map((t: any) => ({ + id: t.id || 0, + name: t.name || '', + })), + // shopyy叫做专辑 + categories: item.collections.map((c: any) => ({ + id: c.id || 0, + name: c.title || '', + })), + variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], + permalink: item.permalink, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : String(item.created_at ?? ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : String(item.updated_at ?? ''), + raw: item, + }; + } + + mapUnifiedToPlatformProduct(data: Partial) { + return data + } + + mapCreateProductParams(data: Partial): any { + // 构建 ShopYY 产品创建参数 + const params: any = { + name: data.name || '', + product_type: data.type || 1, // 默认简单产品 + status: this.mapStatus(data.status || 'publish'), + price: data.price || '0.00', + special_price: data.sale_price || '', + inventory_tracking: data.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: data.stock_quantity || 0, + }; + + // 添加变体信息 + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => ({ + sku: variation.sku || '', + price: variation.price || '0.00', + special_price: variation.sale_price || '', + inventory_tracking: variation.stock_quantity !== undefined ? 1 : 0, + inventory_quantity: variation.stock_quantity || 0, + })); + } + + // 添加图片信息 + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息 + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息 + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + mapUpdateProductParams(data: Partial): any { + // 映射产品状态: publish -> 1, draft -> 0 + const mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 构建 ShopYY 产品更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.name !== undefined) params.name = data.name; + if (data.type !== undefined) params.product_type = data.type; + if (data.status !== undefined) params.status = mapStatus(data.status); + if (data.price !== undefined) params.price = data.price; + if (data.sale_price !== undefined) params.special_price = data.sale_price; + if (data.sku !== undefined) params.sku = data.sku; + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + // 添加变体信息(如果存在) + if (data.variations && data.variations.length > 0) { + params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => { + const variationParams: any = {}; + if (variation.id !== undefined) variationParams.id = variation.id; + if (variation.sku !== undefined) variationParams.sku = variation.sku; + if (variation.price !== undefined) variationParams.price = variation.price; + if (variation.sale_price !== undefined) variationParams.special_price = variation.sale_price; + if (variation.stock_quantity !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_quantity; + } + if (variation.stock_status !== undefined) { + variationParams.inventory_tracking = 1; + variationParams.inventory_quantity = variation.stock_status === 'instock' ? (variation.stock_quantity || 1) : 0; + } + return variationParams; + }); + } + + // 添加图片信息(如果存在) + if (data.images && data.images.length > 0) { + params.images = data.images.map((image: any) => ({ + id: image.id, + src: image.src, + alt: image.alt || '', + position: image.position || 0, + })); + } + + // 添加标签信息(如果存在) + if (data.tags && data.tags.length > 0) { + params.tags = data.tags.map((tag: any) => tag.name || ''); + } + + // 添加分类信息(如果存在) + if (data.categories && data.categories.length > 0) { + params.collections = data.categories.map((category: any) => ({ + id: category.id, + title: category.name, + })); + } + + return params; + } + + async getProduct(id: string | number): Promise { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, id); + return this.mapPlatformToUnifiedProduct(product); + } + + async getProducts( + params: UnifiedSearchParamsDTO + ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); + const response = await this.shopyyService.fetchResourcePaged( + this.site, + 'products/list', + requestParams + ); + const { items = [], total, totalPages, page, per_page } = response; + const finalItems = items.map((item) => ({ + ...item, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, + })).map(this.mapPlatformToUnifiedProduct.bind(this)) + return { + items: finalItems as UnifiedProductDTO[], total, totalPages, page, @@ -549,207 +888,61 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams = { - where, - page: 1, - per_page: 1, - } - const data = await this.getOrders(searchParams); - return data.total || 0; + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllProducts 暂未实现 + throw new Error('Shopyy getAllProducts 暂未实现'); } - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - const data = await this.shopyyService.getAllOrders(this.site.id, params); - return data.map(this.mapOrder.bind(this)); + async createProduct(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateProductParams(data); + const res = await this.shopyyService.createProduct(this.site, requestParams); + return this.mapPlatformToUnifiedProduct(res); } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); - return this.mapOrder(data); + async updateProduct(id: string | number, data: Partial): Promise { + // Shopyy update returns boolean? + // shopyyService.updateProduct returns boolean. + // So I can't return the updated product. + // I have to fetch it again or return empty/input. + // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + // 使用映射方法转换参数 + const requestParams = this.mapUpdateProductParams(data); + await this.shopyyService.updateProduct(this.site, String(id), requestParams); + return true; } - async createOrder(data: Partial): Promise { - const createdOrder = await this.shopyyService.createOrder(this.site, data); - return this.mapOrder(createdOrder); + async deleteProduct(id: string | number): Promise { + // Use batch delete + await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + return true; } - async updateOrder(id: string | number, data: Partial): Promise { - return await this.shopyyService.updateOrder(this.site, String(id), data); + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + return await this.shopyyService.batchProcessProducts(this.site, data); } - async deleteOrder(id: string | number): Promise { - return await this.shopyyService.deleteOrder(this.site, id); + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) } - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - // 订单履行(发货) - try { - // 判断是否为部分发货(包含 items) - if (data.items && data.items.length > 0) { - // 部分发货 - const partShipData = { - order_number: String(orderId), - note: data.shipping_method || '', - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: '1', // 默认快递公司代码 - products: data.items.map(item => ({ - quantity: item.quantity, - order_product_id: String(item.order_item_id) - })) - }; - return await this.shopyyService.partFulfillOrder(this.site, partShipData); - } else { - // 批量发货(完整发货) - const batchShipData = { - order_number: String(orderId), - tracking_company: data.shipping_provider || '', - tracking_number: data.tracking_number || '', - courier_code: 1, // 默认快递公司代码 - note: data.shipping_method || '', - mode: null // 新增模式 - }; - return await this.shopyyService.batchFulfillOrders(this.site, batchShipData); - } - } catch (error) { - throw new Error(`履行失败: ${error.message}`); - } + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data } - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - // 取消订单履行 - try { - // 调用 ShopyyService 的取消履行方法 - const cancelShipData = { - order_id: String(orderId), - fulfillment_id: data.shipment_id || '' - }; - const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); - - return { - success: result, - order_id: orderId, - shipment_id: data.shipment_id, - reason: data.reason, - cancelled_at: new Date().toISOString() - }; - } catch (error) { - throw new Error(`取消履行失败: ${error.message}`); - } + mapUnifiedToPlatformReview(data: Partial) { + return data } - /** - * 获取订单履行信息 - * @param orderId 订单ID - * @returns 履行信息列表 - */ - async getOrderFulfillments(orderId: string | number): Promise { - return await this.shopyyService.getFulfillments(this.site, String(orderId)); + mapCreateReviewParams(data: CreateReviewDTO) { + return data } - /** - * 创建订单履行信息 - * @param orderId 订单ID - * @param data 履行数据 - * @returns 创建结果 - */ - async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise { - // 调用 Shopyy Service 的 createFulfillment 方法 - const fulfillmentData = { - tracking_number: data.tracking_number, - 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); - } - - /** - * 更新订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @param data 更新数据 - * @returns 更新结果 - */ - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: string; - tracking_provider?: string; - date_shipped?: string; - status_shipped?: string; - }): Promise { - return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); - } - - /** - * 删除订单履行信息 - * @param orderId 订单ID - * @param fulfillmentId 履行ID - * @returns 删除结果 - */ - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - throw new Error('Shopyy does not support subscriptions.'); - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllSubscriptions 暂未实现 - throw new Error('Shopyy getAllSubscriptions 暂未实现'); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapMediaSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'media', // Shopyy的媒体API端点可能需要调整 - requestParams - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllMedia 暂未实现 - throw new Error('Shopyy getAllMedia 暂未实现'); - } - - async createMedia(file: any): Promise { - const createdMedia = await this.shopyyService.createMedia(this.site, file); - return this.mapMedia(createdMedia); - } - - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); - return this.mapMedia(updatedMedia); - } - - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data } async getReviews( @@ -774,9 +967,18 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllReviews 暂未实现'); } - async getReview(id: string | number): Promise { - const review = await this.shopyyService.getReview(this.site, id); - return this.mapReview(review); + async createReview(data: any): Promise { + const createdReview = await this.shopyyService.createReview(this.site, data); + return this.mapReview(createdReview); + } + + async updateReview(id: string | number, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, id, data); + return this.mapReview(updatedReview); + } + + async deleteReview(id: string | number): Promise { + return await this.shopyyService.deleteReview(this.site, id); } mapReview(review: any): UnifiedReviewDTO { @@ -796,6 +998,7 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { const { search, page, per_page, where } = params; const shopyyParams: any = { @@ -811,38 +1014,133 @@ export class ShopyyAdapter implements ISiteAdapter { shopyyParams.status = where.status; } - // if (product_id) { - // shopyyParams.product_id = product_id; - // } - return shopyyParams; } - async createReview(data: any): Promise { - const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + mapUnifiedToPlatformSubscription(data: Partial) { + return data } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async getSubscriptions( + params: UnifiedSearchParamsDTO + ): Promise> { + throw new Error('Shopyy does not support subscriptions.'); } - // Webhook相关方法 - mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllSubscriptions 暂未实现 + throw new Error('Shopyy getAllSubscriptions 暂未实现'); + } + + // ========== 产品变体映射方法 ========== + mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { + // 映射变体 return { - id: item.id, - name: item.webhook_name || `Webhook-${item.id}`, - topic: item.event_code || '', - delivery_url: item.url || '', - status: 'active', + id: variant.id, + name: variant.sku || '', + sku: variant.sku || '', + regular_price: String(variant.price ?? ''), + sale_price: String(variant.special_price ?? ''), + price: String(variant.price ?? ''), + stock_status: + variant.inventory_tracking === 1 ? 'instock' : 'outofstock', + stock_quantity: variant.inventory_quantity, }; } + mapUnifiedToPlatformVariation(data: Partial) { + return data + } + + mapCreateVariationParams(data: CreateVariationDTO) { + return data + } + + mapUpdateVariationParams(data: Partial): any { + // 构建 ShopYY 变体更新参数(仅包含传入的字段) + const params: any = {}; + + // 仅当字段存在时才添加到更新参数中 + if (data.id !== undefined) { + params.id = data.id; + } + if (data.sku !== undefined) { + params.sku = data.sku; + } + if (data.price !== undefined) { + params.price = data.price; + } + if (data.sale_price !== undefined) { + params.special_price = data.sale_price; + } + + // 处理库存信息 + if (data.stock_quantity !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_quantity; + } + if (data.stock_status !== undefined) { + params.inventory_tracking = 1; + params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; + } + + return params; + } + + async getVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy getVariation 暂未实现'); + } + + async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getVariations 暂未实现'); + } + + async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getAllVariations 暂未实现'); + } + + async createVariation(productId: string | number, data: any): Promise { + throw new Error('Shopyy createVariation 暂未实现'); + } + + async updateVariation(productId: string | number, variationId: string | number, data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapUpdateVariationParams(data); + await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), requestParams); + return { ...data, id: variationId }; + } + + async deleteVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy deleteVariation 暂未实现'); + } + + // ========== Webhook映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data + } + + mapUnifiedToPlatformWebhook(data: Partial) { + return data + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data + } + + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data + } + + async getWebhook(id: string | number): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, id); + return this.mapWebhook(webhook); + } + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { @@ -859,11 +1157,6 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy getAllWebhooks 暂未实现'); } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); - } - async createWebhook(data: CreateWebhookDTO): Promise { const createdWebhook = await this.shopyyService.createWebhook(this.site, data); return this.mapWebhook(createdWebhook); @@ -878,6 +1171,17 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteWebhook(this.site, id); } + mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + return { + id: item.id, + name: item.webhook_name || `Webhook-${item.id}`, + topic: item.event_code || '', + delivery_url: item.url || '', + status: 'active', + }; + } + + // ========== 站点/其他方法 ========== async getLinks(): Promise> { // ShopYY站点的管理后台链接通常基于apiUrl构建 const url = this.site.websiteUrl @@ -897,59 +1201,58 @@ export class ShopyyAdapter implements ISiteAdapter { return links; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchCustomersPaged(this.site, params); - return { - items: items.map(this.mapCustomer.bind(this)), - total, - totalPages, + // ========== 辅助方法 ========== + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), page, - per_page - }; + limit, + } + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllCustomers 暂未实现 - throw new Error('Shopyy getAllCustomers 暂未实现'); + // 映射产品状态: publish -> 1, draft -> 0 + mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 映射库存状态: instock -> 1, outofstock -> 0 + mapStockStatus = (stockStatus: string) => { + return stockStatus === 'instock' ? 1 : 0; + }; + + 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 } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); - return this.mapCustomer(customer); + shopyyFulfillmentStatusMap = { + // 未发货 + '300': OrderFulfillmentStatus.PENDING, + // 部分发货 + '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, + // 已发货 + '320': OrderFulfillmentStatus.FULFILLED, + // 已取消 + '330': OrderFulfillmentStatus.CANCELLED, + // 确认发货 } - - async createCustomer(data: Partial): Promise { - const createdCustomer = await this.shopyyService.createCustomer(this.site, data); - return this.mapCustomer(createdCustomer); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); - return this.mapCustomer(updatedCustomer); - } - - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); - } - - async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getVariations 暂未实现'); - } - - async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { - throw new Error('Shopyy getAllVariations 暂未实现'); - } - - async getVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy getVariation 暂未实现'); - } - - async createVariation(productId: string | number, data: any): Promise { - throw new Error('Shopyy createVariation 暂未实现'); - } - - async deleteVariation(productId: string | number, variationId: string | number): Promise { - throw new Error('Shopyy deleteVariation 暂未实现'); - } -} +} \ No newline at end of file diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 910572a..1e1ade7 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -15,6 +15,8 @@ import { UpdateVariationDTO, UnifiedProductVariationDTO, UnifiedVariationPaginationDTO, + CreateReviewDTO, + UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { @@ -29,6 +31,7 @@ import { } from '../dto/woocommerce.dto'; import { Site } from '../entity/site.entity'; import { WPService } from '../service/wp.service'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 @@ -40,191 +43,236 @@ export class WooCommerceAdapter implements ISiteAdapter { this.mapOrder = this.mapOrder.bind(this); this.mapWebhook = this.mapWebhook.bind(this); } + batchProcessProducts?(data: BatchOperationDTO): Promise { + throw new Error('Method not implemented.'); + } + mapCreateVariationParams(data: CreateVariationDTO) { + throw new Error('Method not implemented.'); + } + mapUpdateVariationParams(data: UpdateVariationDTO) { + throw new Error('Method not implemented.'); + } - // 映射 WooCommerce webhook 到统一格式 - mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { + return data; + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data; + } + + mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + // 将 WooCommerce 客户数据映射为统一客户DTO + // 包含基础信息地址信息与时间信息 return { - id: webhook.id.toString(), - name: webhook.name, - status: webhook.status, - topic: webhook.topic, - delivery_url: webhook.delivery_url, - secret: webhook.secret, - api_version: webhook.api_version, - date_created: webhook.date_created, - date_modified: webhook.date_modified, - // metadata: webhook.meta_data || [], + id: item.id, + avatar: item.avatar_url, + email: item.email, + orders: Number(item.orders ?? 0), + total_spend: Number(item.total_spent ?? 0), + first_name: item.first_name, + last_name: item.last_name, + username: item.username, + phone: item.billing?.phone || item.shipping?.phone, + billing: item.billing, + shipping: item.shipping, + date_created: item.date_created, + date_modified: item.date_modified, + raw: item, }; } - // 获取站点的 webhooks 列表 - async getWebhooks(params: UnifiedSearchParamsDTO): Promise { - try { - const result = await this.wpService.getWebhooks(this.site, params); - - return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; - } catch (error) { - throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取所有webhooks - async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { - try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); - } catch (error) { - throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { - try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 创建新的 webhook - async createWebhook(data: CreateWebhookDTO): Promise { - try { - const params = { - name: data.name, - status: 'active', // 默认状态为活跃 - topic: data.topic, - delivery_url: data.delivery_url, - secret: data.secret, - api_version: data.api_version || 'wp/v2', - }; - const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { - try { - const params = { - ...(data.name ? { name: data.name } : {}), - ...(data.status ? { status: data.status } : {}), - ...(data.topic ? { topic: data.topic } : {}), - ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), - ...(data.secret ? { secret: data.secret } : {}), - ...(data.api_version ? { api_version: data.api_version } : {}), - }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { - try { - await this.wpService.deleteWebhook(this.site, id); - return true; - } catch (error) { - throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - async getLinks(): Promise> { - const baseUrl = this.site.apiUrl; - const links = [ - { title: '访问网站', url: baseUrl }, - { title: '管理后台', url: `${baseUrl}/wp-admin/` }, - { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, - { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, - { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, - { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, - { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, - { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, - { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, - ]; - return links; - } - - createMedia(file: any): Promise { - throw new Error('Method not implemented.'); - } - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - - - - mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { + mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; + const mapped: any = { ...(params.search ? { search: params.search } : {}), - ...(where.status ? { status: where.status } : {}), page, per_page, }; + // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 + if (params.orderBy) { + // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } + if (typeof params.orderBy === 'string') { + const [field, direction = 'desc'] = params.orderBy.split(':'); + mapped.orderby = field; + mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; + } else if (typeof params.orderBy === 'object') { + const entries = Object.entries(params.orderBy); + if (entries.length > 0) { + const [field, direction] = entries[0]; + mapped.orderby = field; + mapped.order = direction === 'asc' ? 'asc' : 'desc'; + } + } + } + const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); - if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); - if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); - if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); - if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); - if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); - if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); - if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); - if (where.offset !== undefined) mapped.offset = Number(where.offset); - if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); - if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); - if (where.slug) mapped.slug = String(where.slug); - if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { - if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); - if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); - if (where.status) mapped.status = String(where.status); - } - if (where.type) mapped.type = String(where.type); - if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); - if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); - if (where.sku) mapped.sku = String(where.sku); - if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); - if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); - if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); - if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); - if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); - if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); - if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); - if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); - if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); - if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); - if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); - if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); - if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); + const toNumber = (value: any): number | undefined => { + if (value === undefined || value === null || value === '') return undefined; + const n = Number(value); + return Number.isFinite(n) ? n : undefined; + }; + + if (where.exclude) mapped.exclude = toArray(where.exclude); + if (where.include) mapped.include = toArray(where.include); + if (where.ids) mapped.include = toArray(where.ids); + if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); + + if (where.email) mapped.email = String(where.email); + const roleSource = where.role; + if (roleSource !== undefined) mapped.role = String(roleSource); return mapped; } + // 客户操作方法 + async getCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`customers/${id}`); + return this.mapCustomer(res.data); + } + + async getCustomers(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapCustomerSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'customers', + requestParams + ); + return { + items: items.map((i: any) => this.mapCustomer(i)), + total, + totalPages, + page, + per_page, + + }; + } + + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有客户数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // 处理orderBy参数,转换为WooCommerce API需要的格式 + const requestParams = this.mapCustomerSearchParams(params || {}); + + const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); + return customers.map((customer: any) => this.mapCustomer(customer)); + } + + async createCustomer(data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('customers', data); + return this.mapCustomer(res.data); + } + + async updateCustomer(id: string | number, data: Partial): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.put(`customers/${id}`, data); + return this.mapCustomer(res.data); + } + + async deleteCustomer(id: string | number): Promise { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`customers/${id}`, { force: true }); + return true; + } + + // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { + return data; + } + mapUnifiedToPlatformMedia(data: Partial) { + return data; + } + + mapMedia(item: WpMedia): UnifiedMediaDTO { + // 将 WordPress 媒体数据映射为统一媒体DTO + // 兼容不同字段命名的时间信息 + return { + id: item.id, + title: + typeof item.title === 'string' + ? item.title + : item.title?.rendered || '', + media_type: item.media_type, + mime_type: item.mime_type, + source_url: item.source_url, + date_created: item.date_created ?? item.date, + date_modified: item.date_modified ?? item.modified, + }; + } + + // 媒体操作方法 + async getMedia(params: UnifiedSearchParamsDTO): Promise> { + // 获取媒体列表并映射为统一媒体DTO集合 + const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( + this.site, + params + ); + return { + items: items.map(this.mapMedia.bind(this)), + total, + totalPages, + page, + per_page, + }; + } + + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有媒体数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const media = await this.wpService.sdkGetAll(api, 'media', params); + return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + } + + createMedia(file: any): Promise { + throw new Error('Method not implemented.'); + } + + async updateMedia(id: string | number, data: any): Promise { + // 更新媒体信息 + return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + } + + async deleteMedia(id: string | number): Promise { + // 删除媒体资源 + await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + return true; + } + + async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { + // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 + const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); + return result as any; + } + + // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { + return data; + } + mapUnifiedToPlatformOrder(data: Partial) { + return data; + } + + mapCreateOrderParams(data: Partial) { + return data; + } + mapUpdateOrderParams(data: Partial) { + return data; + } + + mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 计算分页参数 const page = Number(params.page ?? 1); @@ -293,59 +341,328 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record { + private buildFullAddress(addr: any): string { + if (!addr) return ''; + const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); + return [ + name, + addr.company, + addr.address_1, + addr.address_2, + addr.city, + addr.state, + addr.postcode, + addr.country, + addr.phone + ].filter(Boolean).join(', '); + } + + mapOrder(item: WooOrder): UnifiedOrderDTO { + // 将 WooCommerce 订单数据映射为统一订单DTO + // 包含账单地址与收货地址以及创建与更新时间 + + // 映射物流追踪信息,将后端格式转换为前端期望的格式 + const fulfillments = (item.fulfillments || []).map((track: any) => ({ + tracking_number: track.tracking_number || '', + shipping_provider: track.shipping_provider || '', + shipping_method: track.shipping_method || '', + status: track.status || '', + date_created: track.date_created || '', + items: track.items || [], + })); + + return { + id: item.id, + number: item.number, + status: item.status, + currency: item.currency, + total: item.total, + customer_id: item.customer_id, + customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 + email: item.billing?.email || '', + customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), + refunds: item.refunds?.map?.(refund => ({ + id: refund.id, + reason: refund.reason, + total: refund.total, + })), + line_items: (item.line_items as any[]).map(li => ({ + ...li, + productId: li.product_id, + })), + customer_ip_address: item.customer_ip_address ?? '', + date_paid: item.date_paid ?? '', + utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', + device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', + source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', + billing: item.billing, + shipping: item.shipping, + billing_full_address: this.buildFullAddress(item.billing), + shipping_full_address: this.buildFullAddress(item.shipping), + payment_method: item.payment_method_title, + date_created: item.date_created, + date_modified: item.date_modified, + shipping_lines: item.shipping_lines, + fee_lines: item.fee_lines, + coupon_lines: item.coupon_lines, + fulfillments, + raw: item, + }; + } + + // 订单操作方法 + async getOrder(id: string | number): Promise { + // 获取单个订单详情 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${id}`); + return this.mapOrder(res.data); + } + + async getOrders(params: UnifiedSearchParamsDTO): Promise> { + const requestParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + + // 并行获取所有订单的履行信息 + const ordersWithFulfillments = await Promise.all( + items.map(async (order: any) => { + try { + // 获取订单的履行信息 + const fulfillments = await this.getOrderFulfillments(order.id); + // 将履行信息添加到订单对象中 + return { + ...order, + fulfillments: fulfillments || [] + }; + } catch (error) { + // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 + console.error(`获取订单 ${order.id} 的履行信息失败:`, error); + return { + ...order, + fulfillments: [] + }; + } + }) + ); + + return { + items: ordersWithFulfillments.map(this.mapOrder), + total, + totalPages, + page, + per_page, + }; + } + + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订单数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const orders = await this.wpService.sdkGetAll(api, 'orders', params); + return orders.map((order: any) => this.mapOrder(order)); + } + + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + + async createOrder(data: Partial): Promise { + // 创建订单并返回统一订单DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('orders', data); + return this.mapOrder(res.data); + } + + async updateOrder(id: string | number, data: Partial): Promise { + // 更新订单并返回布尔结果 + return await this.wpService.updateOrder(this.site, String(id), data as any); + } + + async deleteOrder(id: string | number): Promise { + // 删除订单 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + await api.delete(`orders/${id}`, { force: true }); + return true; + } + + async getOrderFulfillments(orderId: string | number): Promise { + return await this.wpService.getFulfillments(this.site, String(orderId)); + } + + async createOrderFulfillment(orderId: string | number, data: { + tracking_number: string; + shipping_provider: string; + shipping_method?: string; + status?: string; + date_created?: string; + items?: Array<{ + order_item_id: number; + quantity: number; + }>; + }): Promise { + const shipmentData: any = { + shipping_provider: data.shipping_provider, + tracking_number: data.tracking_number, + }; + + if (data.shipping_method) { + shipmentData.shipping_method = data.shipping_method; + } + + 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); + return response.data; + } + + async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: 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); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + + async getOrderNotes(orderId: string | number): Promise { + // 获取订单备注列表 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${orderId}/notes`); + return res.data; + } + + async createOrderNote(orderId: string | number, data: any): Promise { + // 创建订单备注 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post(`orders/${orderId}/notes`, data); + return res.data; + } + + async cancelFulfillment(orderId: string | number, data: { + reason?: string; + shipment_id?: string; + }): Promise { + throw new Error('暂未实现'); + // 取消订单履行 + // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + + // try { + // // 将订单状态改回处理中 + // await api.put(`orders/${orderId}`, { status: 'processing' }); + + // // 添加取消履行的备注 + // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; + // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); + + // return { + // success: true, + // order_id: orderId, + // shipment_id: data.shipment_id, + // reason: data.reason, + // cancelled_at: new Date().toISOString() + // }; + // } catch (error) { + // throw new Error(`取消履行失败: ${error.message}`); + // } + } + + // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { + return data; + } + mapUnifiedToPlatformProduct(data: Partial) { + return data; + } + + mapCreateProductParams(data: Partial) { + return data; + } + mapUpdateProductParams(data: Partial) { + return data; + } + + mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; - const mapped: any = { ...(params.search ? { search: params.search } : {}), + ...(where.status ? { status: where.status } : {}), page, per_page, }; - // 处理orderBy参数,转换为WooCommerce API的order和orderby格式 - if (params.orderBy) { - // 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" } - if (typeof params.orderBy === 'string') { - const [field, direction = 'desc'] = params.orderBy.split(':'); - mapped.orderby = field; - mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc'; - } else if (typeof params.orderBy === 'object') { - const entries = Object.entries(params.orderBy); - if (entries.length > 0) { - const [field, direction] = entries[0]; - mapped.orderby = field; - mapped.order = direction === 'asc' ? 'asc' : 'desc'; - } - } - } - const toArray = (value: any): any[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - const toNumber = (value: any): number | undefined => { - if (value === undefined || value === null || value === '') return undefined; - const n = Number(value); - return Number.isFinite(n) ? n : undefined; - }; - - if (where.exclude) mapped.exclude = toArray(where.exclude); - if (where.include) mapped.include = toArray(where.include); - if (where.ids) mapped.include = toArray(where.ids); - if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); - - if (where.email) mapped.email = String(where.email); - const roleSource = where.role; - if (roleSource !== undefined) mapped.role = String(roleSource); + if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); + if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); + if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); + if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); + if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); + if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); + if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); + if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); + if (where.offset !== undefined) mapped.offset = Number(where.offset); + if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); + if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); + if (where.slug) mapped.slug = String(where.slug); + if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { + if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); + if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); + if (where.status) mapped.status = String(where.status); + } + if (where.type) mapped.type = String(where.type); + if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); + if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); + if (where.sku) mapped.sku = String(where.sku); + if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); + if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); + if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); + if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); + if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); + if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); + if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); + if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); + if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); + if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); + if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); + if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); + if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); return mapped; } + mapProduct(item: WooProduct): UnifiedProductDTO { mapProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 @@ -434,120 +751,41 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - private buildFullAddress(addr: any): string { - if (!addr) return ''; - const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); - return [ - name, - addr.company, - addr.address_1, - addr.address_2, - addr.city, - addr.state, - addr.postcode, - addr.country, - addr.phone - ].filter(Boolean).join(', '); - } - mapOrder(item: WooOrder): UnifiedOrderDTO { - // 将 WooCommerce 订单数据映射为统一订单DTO - // 包含账单地址与收货地址以及创建与更新时间 - // 映射物流追踪信息,将后端格式转换为前端期望的格式 - const fulfillments = (item.fulfillments || []).map((track: any) => ({ - tracking_number: track.tracking_number || '', - shipping_provider: track.shipping_provider || '', - shipping_method: track.shipping_method || '', - status: track.status || '', - date_created: track.date_created || '', - items: track.items || [], - })); + // 产品操作方法 + async getProduct(id: string | number): Promise { + // 获取单个产品详情并映射为统一产品DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); + const product = res.data; - return { - id: item.id, - number: item.number, - status: item.status, - currency: item.currency, - total: item.total, - customer_id: item.customer_id, - customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 - email: item.billing?.email || '', - customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), - refunds: item.refunds?.map?.(refund => ({ - id: refund.id, - reason: refund.reason, - total: refund.total, - })), - line_items: (item.line_items as any[]).map(li => ({ - ...li, - productId: li.product_id, - })), - customer_ip_address: item.customer_ip_address ?? '', - date_paid: item.date_paid ?? '', - utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', - device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '', - source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '', - billing: item.billing, - shipping: item.shipping, - billing_full_address: this.buildFullAddress(item.billing), - shipping_full_address: this.buildFullAddress(item.shipping), - payment_method: item.payment_method_title, - date_created: item.date_created, - date_modified: item.date_modified, - shipping_lines: item.shipping_lines, - fee_lines: item.fee_lines, - coupon_lines: item.coupon_lines, - fulfillments, - raw: item, - }; + // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 + if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { + try { + // 批量获取该产品的所有变体数据 + const variations = await this.wpService.sdkGetAll( + api, + `products/${product.id}/variations` + ); + // 将完整的变体数据添加到产品对象中 + product.variations = variations; + } catch (error) { + // 如果获取变体失败,保持原有的 ID 数组 + console.error(`获取产品 ${product.id} 的变体数据失败:`, error); + } + } + + return this.mapProduct(product); } - mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { - // 将 WooCommerce 订阅数据映射为统一订阅DTO - // 若缺少创建时间则回退为开始时间 - return { - id: item.id, - status: item.status, - customer_id: item.customer_id, - billing_period: item.billing_period, - billing_interval: item.billing_interval, - date_created: item.date_created ?? item.start_date, - date_modified: item.date_modified, - start_date: item.start_date, - next_payment_date: item.next_payment_date, - line_items: item.line_items, - raw: item, - }; - } - - mapMedia(item: WpMedia): UnifiedMediaDTO { - // 将 WordPress 媒体数据映射为统一媒体DTO - // 兼容不同字段命名的时间信息 - return { - id: item.id, - title: - typeof item.title === 'string' - ? item.title - : item.title?.rendered || '', - media_type: item.media_type, - mime_type: item.mime_type, - source_url: item.source_url, - date_created: item.date_created ?? item.date, - date_modified: item.date_modified ?? item.modified, - }; - } - - async getProducts( - params: UnifiedSearchParamsDTO - ): Promise> { + async getProducts(params: UnifiedSearchParamsDTO): Promise> { // 获取产品列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products', + requestParams + ); // 对于类型为 variable 的产品,需要加载完整的变体数据 const productsWithVariations = await Promise.all( @@ -611,31 +849,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return productsWithVariations.map((product: any) => this.mapProduct(product)); } - async getProduct(id: string | number): Promise { - // 获取单个产品详情并映射为统一产品DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products/${id}`); - const product = res.data; - - // 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据 - if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) { - try { - // 批量获取该产品的所有变体数据 - const variations = await this.wpService.sdkGetAll( - api, - `products/${product.id}/variations` - ); - // 将完整的变体数据添加到产品对象中 - product.variations = variations; - } catch (error) { - // 如果获取变体失败,保持原有的 ID 数组 - console.error(`获取产品 ${product.id} 的变体数据失败:`, error); - } - } - - return this.mapProduct(product); - } - async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); @@ -645,21 +858,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async updateProduct(id: string | number, data: Partial): Promise { // 更新产品并返回统一产品DTO const res = await this.wpService.updateProduct(this.site, String(id), data as any); - return res - } - - async getOrderNotes(orderId: string | number): Promise { - // 获取订单备注列表 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${orderId}/notes`); - return res.data; - } - - async createOrderNote(orderId: string | number, data: any): Promise { - // 创建订单备注 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post(`orders/${orderId}/notes`, data); - return res.data; + return res; } async deleteProduct(id: string | number): Promise { @@ -673,211 +872,22 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - // 批量处理产品增删改 - return await this.wpService.batchProcessProducts(this.site, data); + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data; + } + mapUnifiedToPlatformReview(data: Partial) { + return data; } - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - - // 并行获取所有订单的履行信息 - const ordersWithFulfillments = await Promise.all( - items.map(async (order: any) => { - try { - // 获取订单的履行信息 - const fulfillments = await this.getOrderFulfillments(order.id); - // 将履行信息添加到订单对象中 - return { - ...order, - fulfillments: fulfillments || [] - }; - } catch (error) { - // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 - console.error(`获取订单 ${order.id} 的履行信息失败:`, error); - return { - ...order, - fulfillments: [] - }; - } - }) - ); - - return { - items: ordersWithFulfillments.map(this.mapOrder), - total, - totalPages, - page, - per_page, - }; - } - - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams: UnifiedSearchParamsDTO = { - where, - page: 1, - per_page: 1, - }; - const requestParams = this.mapOrderSearchParams(searchParams); - const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - return total || 0; - } - - async getOrder(id: string | number): Promise { - // 获取单个订单详情 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); - } - - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订单数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); - } - - async createOrder(data: Partial): Promise { - // 创建订单并返回统一订单DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('orders', data); - return this.mapOrder(res.data); - } - - async updateOrder(id: string | number, data: Partial): Promise { - // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); - } - - async deleteOrder(id: string | number): Promise { - // 删除订单 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); - return true; - } - - async fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - throw new Error('暂无实现') - // 订单履行(发货) - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 更新订单状态为已完成 - // await api.put(`orders/${orderId}`, { status: 'completed' }); - - // // 如果提供了物流信息,添加到订单备注 - // if (data.tracking_number || data.shipping_provider) { - // const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - // } - - // return { - // success: true, - // order_id: orderId, - // fulfillment_id: `fulfillment_${orderId}_${Date.now()}`, - // tracking_number: data.tracking_number, - // shipping_provider: data.shipping_provider, - // fulfilled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`履行失败: ${error.message}`); - // } - } - - async cancelFulfillment(orderId: string | number, data: { - reason?: string; - shipment_id?: string; - }): Promise { - throw new Error('暂未实现') - // 取消订单履行 - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // try { - // // 将订单状态改回处理中 - // await api.put(`orders/${orderId}`, { status: 'processing' }); - - // // 添加取消履行的备注 - // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; - // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); - - // return { - // success: true, - // order_id: orderId, - // shipment_id: data.shipment_id, - // reason: data.reason, - // cancelled_at: new Date().toISOString() - // }; - // } catch (error) { - // throw new Error(`取消履行失败: ${error.message}`); - // } - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取订阅列表并映射为统一订阅DTO集合 - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'subscriptions', - params - ); - return { - items: items.map(this.mapSubscription), - total, - totalPages, - page, - per_page, - - }; - } - - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订阅数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); - } - - async getMedia( - params: UnifiedSearchParamsDTO - ): Promise> { - // 获取媒体列表并映射为统一媒体DTO集合 - const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( - this.site, - params - ); - return { - items: items.map(this.mapMedia.bind(this)), - total, - totalPages, - page, - per_page, - }; - } - - async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有媒体数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + mapCreateReviewParams(data: CreateReviewDTO) { + return data; + } + mapUpdateReviewParams(data: UpdateReviewDTO) { + return data; } + mapReview(item: any): UnifiedReviewDTO & { raw: any } { mapReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { @@ -893,17 +903,15 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getReviews( - params: UnifiedSearchParamsDTO - ): Promise { + // 评论操作方法 + async getReviews(params: UnifiedSearchParamsDTO): Promise { // 获取评论列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged( - this.site, - 'products/reviews', - requestParams - ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products/reviews', + requestParams + ); return { items: items.map(this.mapReview.bind(this)), total, @@ -934,52 +942,42 @@ export class WooCommerceAdapter implements ISiteAdapter { return await this.wpService.deleteReview(this.site, id); } - async deleteMedia(id: string | number): Promise { - // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); - return true; + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data; + } + mapUnifiedToPlatformSubscription(data: Partial) { + return data; } - async updateMedia(id: string | number, data: any): Promise { - // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); - } - - async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> { - // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传 - const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids); - return result as any; - } - - mapCustomer(item: WooCustomer): UnifiedCustomerDTO { - // 将 WooCommerce 客户数据映射为统一客户DTO - // 包含基础信息地址信息与时间信息 + mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + // 将 WooCommerce 订阅数据映射为统一订阅DTO + // 若缺少创建时间则回退为开始时间 return { id: item.id, - avatar: item.avatar_url, - email: item.email, - orders: Number(item.orders ?? 0), - total_spend: Number(item.total_spent ?? 0), - first_name: item.first_name, - last_name: item.last_name, - username: item.username, - phone: item.billing?.phone || item.shipping?.phone, - billing: item.billing, - shipping: item.shipping, - date_created: item.date_created, + status: item.status, + customer_id: item.customer_id, + billing_period: item.billing_period, + billing_interval: item.billing_interval, + date_created: item.date_created ?? item.start_date, date_modified: item.date_modified, + start_date: item.start_date, + next_payment_date: item.next_payment_date, + line_items: item.line_items, raw: item, }; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { - const requestParams = this.mapCustomerSearchParams(params); + + // 订阅操作方法 + async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { + // 获取订阅列表并映射为统一订阅DTO集合 const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( this.site, - 'customers', - requestParams + 'subscriptions', + params ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map(this.mapSubscription), total, totalPages, page, @@ -988,100 +986,23 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有客户数据,不受分页限制 + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - - // 处理orderBy参数,转换为WooCommerce API需要的格式 - const requestParams = this.mapCustomerSearchParams(params || {}); - - const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); + return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); } - async getCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + // ========== 变体映射方法 ========== + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { + return data; } - - async createCustomer(data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('customers', data); - return this.mapCustomer(res.data); - } - - async updateCustomer(id: string | number, data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); - } - - async deleteCustomer(id: string | number): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); - return true; - } - - async getOrderFulfillments(orderId: string | number): Promise { - return await this.wpService.getFulfillments(this.site, String(orderId)); - } - - async createOrderFulfillment(orderId: string | number, data: { - tracking_number: string; - shipping_provider: string; - shipping_method?: string; - status?: string; - date_created?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise { - const shipmentData: any = { - shipping_provider: data.shipping_provider, - tracking_number: data.tracking_number, - }; - - if (data.shipping_method) { - shipmentData.shipping_method = data.shipping_method; - } - - 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); - return response.data; - } - - async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { - tracking_number?: 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); - } - - async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { - return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + mapUnifiedToPlatformVariation(data: Partial) { + return data; } // 映射 WooCommerce 变体到统一格式 + mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { // 将变体属性转换为统一格式 const mappedAttributes = variation.attributes && Array.isArray(variation.attributes) @@ -1131,6 +1052,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + // 变体操作方法 // 获取产品变体列表 async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { try { @@ -1305,4 +1227,145 @@ export class WooCommerceAdapter implements ISiteAdapter { throw new Error(`删除产品变体失败: ${error instanceof Error ? error.message : String(error)}`); } } + + // ========== 网络钩子映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data; + } + mapUnifiedToPlatformWebhook(data: Partial) { + return data; + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data; + } + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data; + } + + // 映射 WooCommerce webhook 到统一格式 + mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + return { + id: webhook.id.toString(), + name: webhook.name, + status: webhook.status, + topic: webhook.topic, + delivery_url: webhook.delivery_url, + secret: webhook.secret, + api_version: webhook.api_version, + date_created: webhook.date_created, + date_modified: webhook.date_modified, + // metadata: webhook.meta_data || [], + }; + } + + // 网络钩子操作方法 + // 获取站点的 webhooks 列表 + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { + try { + const result = await this.wpService.getWebhooks(this.site, params); + + return { + items: (result.items as WooWebhook[]).map(this.mapWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; + } catch (error) { + throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取所有webhooks + async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { + try { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); + return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + } catch (error) { + throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取单个 webhook 详情 + async getWebhook(id: string | number): Promise { + try { + const result = await this.wpService.getWebhook(this.site, id); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 创建新的 webhook + async createWebhook(data: CreateWebhookDTO): Promise { + try { + const params = { + name: data.name, + status: 'active', // 默认状态为活跃 + topic: data.topic, + delivery_url: data.delivery_url, + secret: data.secret, + api_version: data.api_version || 'wp/v2', + }; + const result = await this.wpService.createWebhook(this.site, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 更新现有的 webhook + async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + try { + const params = { + ...(data.name ? { name: data.name } : {}), + ...(data.status ? { status: data.status } : {}), + ...(data.topic ? { topic: data.topic } : {}), + ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), + ...(data.secret ? { secret: data.secret } : {}), + ...(data.api_version ? { api_version: data.api_version } : {}), + }; + const result = await this.wpService.updateWebhook(this.site, id, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 删除指定的 webhook + async deleteWebhook(id: string | number): Promise { + try { + await this.wpService.deleteWebhook(this.site, id); + return true; + } catch (error) { + throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // ========== 其他方法 ========== + async getLinks(): Promise> { + const baseUrl = this.site.apiUrl; + const links = [ + { title: '访问网站', url: baseUrl }, + { title: '管理后台', url: `${baseUrl}/wp-admin/` }, + { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, + { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, + { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, + { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, + { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, + { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, + { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, + ]; + return links; + } + + batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } + + batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 5013390..f4b1c83 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -7,7 +7,6 @@ import { CancelFulfillmentDTO, CreateReviewDTO, CreateWebhookDTO, - FulfillmentDTO, UnifiedCustomerDTO, UnifiedCustomerPaginationDTO, UnifiedMediaPaginationDTO, @@ -986,25 +985,6 @@ export class SiteApiController { } } - @Post('/:siteId/orders/:id/fulfill') - @ApiOkResponse({ type: Object }) - async fulfillOrder( - @Param('siteId') siteId: number, - @Param('id') id: string, - @Body() body: FulfillmentDTO - ) { - this.logger.info(`[Site API] 订单履约开始, siteId: ${siteId}, orderId: ${id}`); - try { - const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.fulfillOrder(id, body); - this.logger.info(`[Site API] 订单履约成功, siteId: ${siteId}, orderId: ${id}`); - return successResponse(data); - } catch (error) { - this.logger.error(`[Site API] 订单履约失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`); - return errorResponse(error.message); - } - } - @Post('/:siteId/orders/:id/cancel-fulfill') @ApiOkResponse({ type: Object }) async cancelFulfillment( diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 84f6765..630e5ed 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,57 +19,71 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { - mapOrder(order: any): UnifiedOrderDTO; - mapWebhook(webhook:any):UnifiedWebhookDTO; - mapProduct(product:any): UnifiedProductDTO; - mapReview(data: any): UnifiedReviewDTO; - mapCustomer(data: any): UnifiedCustomerDTO; - mapMedia(data: any): UnifiedMediaDTO; +export interface ISiteAdapter { + // ========== 客户映射方法 ========== /** - * 获取产品列表 + * 将平台客户数据转换为统一客户数据格式 + * @param data 平台特定客户数据 + * @returns 统一客户数据格式 */ - getProducts(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO; /** - * 获取所有产品 + * 将统一客户数据格式转换为平台客户数据 + * @param data 统一客户数据格式 + * @returns 平台特定客户数据 */ - getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformCustomer(data: Partial): any; /** - * 获取单个产品 + * 获取单个客户 */ - getProduct(id: string | number): Promise; + getCustomer(id: string | number): Promise; /** - * 获取订单列表 + * 获取客户列表 */ - getOrders(params: UnifiedSearchParamsDTO): Promise>; + getCustomers(params: UnifiedSearchParamsDTO): Promise>; /** - * 获取订单总数 + * 获取所有客户 */ - countOrders(params: Record): Promise; + getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; /** - * 获取所有订单 + * 创建客户 */ - getAllOrders(params?: UnifiedSearchParamsDTO): Promise; + createCustomer(data: Partial): Promise; /** - * 获取单个订单 + * 更新客户 */ - getOrder(id: string | number): Promise; + updateCustomer(id: string | number, data: Partial): Promise; /** - * 获取订阅列表 + * 删除客户 */ - getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + deleteCustomer(id: string | number): Promise; /** - * 获取所有订阅 + * 批量处理客户 */ - getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + batchProcessCustomers?(data: BatchOperationDTO): Promise; + + // ========== 媒体映射方法 ========== + /** + * 将平台媒体数据转换为统一媒体数据格式 + * @param data 平台特定媒体数据 + * @returns 统一媒体数据格式 + */ + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO; + + /** + * 将统一媒体数据格式转换为平台媒体数据 + * @param data 统一媒体数据格式 + * @returns 平台特定媒体数据 + */ + mapUnifiedToPlatformMedia(data: Partial): any; /** * 获取媒体列表 @@ -86,75 +100,69 @@ export interface ISiteAdapter { */ createMedia(file: any): Promise; + // ========== 订单映射方法 ========== /** - * 获取评论列表 + * 将平台订单数据转换为统一订单数据格式 + * @param data 平台特定订单数据 + * @returns 统一订单数据格式 */ - getReviews(params: UnifiedSearchParamsDTO): Promise>; + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO; /** - * 获取所有评论 + * 将统一订单数据格式转换为平台订单数据 + * @param data 统一订单数据格式 + * @returns 平台特定订单数据 */ - getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + mapUnifiedToPlatformOrder(data: Partial): any; /** - * 创建评论 + * 将统一订单创建参数转换为平台订单创建参数 + * @param data 统一订单创建参数 + * @returns 平台订单创建参数 */ - createReview(data: CreateReviewDTO): Promise; + mapCreateOrderParams(data: Partial): any; /** - * 更新评论 + * 将统一订单更新参数转换为平台订单更新参数 + * @param data 统一订单更新参数 + * @returns 平台订单更新参数 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + mapUpdateOrderParams(data: Partial): any; /** - * 删除评论 + * 获取单个订单 */ - deleteReview(id: number): Promise; + getOrder(id: string | number): Promise; /** - * 创建产品 + * 获取订单列表 */ - createProduct(data: Partial): Promise; + getOrders(params: UnifiedSearchParamsDTO): Promise>; /** - * 更新产品 + * 获取所有订单 */ - updateProduct(id: string | number, data: Partial): Promise; + getAllOrders(params?: UnifiedSearchParamsDTO): Promise; /** - * 删除产品 + * 获取订单总数 */ - deleteProduct(id: string | number): Promise; + countOrders(params: Record): Promise; /** - * 获取产品变体列表 + * 创建订单 */ - getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + createOrder(data: Partial): Promise; /** - * 获取所有产品变体 + * 更新订单 */ - getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + updateOrder(id: string | number, data: Partial): Promise; /** - * 获取单个产品变体 + * 删除订单 */ - getVariation(productId: string | number, variationId: string | number): Promise; - - /** - * 创建产品变体 - */ - createVariation(productId: string | number, data: CreateVariationDTO): Promise; - - /** - * 更新产品变体 - */ - updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; - - /** - * 删除产品变体 - */ - deleteVariation(productId: string | number, variationId: string | number): Promise; + deleteOrder(id: string | number): Promise; /** * 获取订单备注 @@ -166,71 +174,6 @@ export interface ISiteAdapter { */ createOrderNote(orderId: string | number, data: any): Promise; - batchProcessProducts?(data: BatchOperationDTO): Promise; - - createOrder(data: Partial): Promise; - updateOrder(id: string | number, data: Partial): Promise; - deleteOrder(id: string | number): Promise; - - batchProcessOrders?(data: BatchOperationDTO): Promise; - - getCustomers(params: UnifiedSearchParamsDTO): Promise>; - getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; - getCustomer(id: string | number): Promise; - createCustomer(data: Partial): Promise; - updateCustomer(id: string | number, data: Partial): Promise; - deleteCustomer(id: string | number): Promise; - - batchProcessCustomers?(data: BatchOperationDTO): Promise; - - /** - * 获取webhooks列表 - */ - getWebhooks(params: UnifiedSearchParamsDTO): Promise; - - /** - * 获取所有webhooks - */ - getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; - - /** - * 获取单个webhook - */ - getWebhook(id: string | number): Promise; - - /** - * 创建webhook - */ - createWebhook(data: CreateWebhookDTO): Promise; - - /** - * 更新webhook - */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; - - /** - * 删除webhook - */ - deleteWebhook(id: string | number): Promise; - - /** - * 获取站点链接列表 - */ - getLinks(): Promise>; - - /** - * 订单履行(发货) - */ - fulfillOrder(orderId: string | number, data: { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: Array<{ - order_item_id: number; - quantity: number; - }>; - }): Promise; - /** * 取消订单履行 */ @@ -278,4 +221,276 @@ export interface ISiteAdapter { * 删除订单履行信息 */ deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise; + + /** + * 批量处理订单 + */ + batchProcessOrders?(data: BatchOperationDTO): Promise; + + // ========== 产品映射方法 ========== + /** + * 将平台产品数据转换为统一产品数据格式 + * @param data 平台特定产品数据 + * @returns 统一产品数据格式 + */ + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO; + + /** + * 将统一产品数据格式转换为平台产品数据 + * @param data 统一产品数据格式 + * @returns 平台特定产品数据 + */ + mapUnifiedToPlatformProduct(data: Partial): any; + + /** + * 将统一产品创建参数转换为平台产品创建参数 + * @param data 统一产品创建参数 + * @returns 平台产品创建参数 + */ + mapCreateProductParams(data: Partial): any; + + /** + * 将统一产品更新参数转换为平台产品更新参数 + * @param data 统一产品更新参数 + * @returns 平台产品更新参数 + */ + mapUpdateProductParams(data: Partial): any; + + /** + * 获取单个产品 + */ + getProduct(id: string | number): Promise; + + /** + * 获取产品列表 + */ + getProducts(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有产品 + */ + getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品 + */ + createProduct(data: Partial): Promise; + + /** + * 更新产品 + */ + updateProduct(id: string | number, data: Partial): Promise; + + /** + * 删除产品 + */ + deleteProduct(id: string | number): Promise; + + /** + * 批量处理产品 + */ + batchProcessProducts?(data: BatchOperationDTO): Promise; + + // ========== 评论映射方法 ========== + /** + * 将平台评论数据转换为统一评论数据格式 + * @param data 平台特定评论数据 + * @returns 统一评论数据格式 + */ + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO; + + /** + * 将统一评论数据格式转换为平台评论数据 + * @param data 统一评论数据格式 + * @returns 平台特定评论数据 + */ + mapUnifiedToPlatformReview(data: Partial): any; + + /** + * 将统一评论创建参数转换为平台评论创建参数 + * @param data 统一评论创建参数 + * @returns 平台评论创建参数 + */ + mapCreateReviewParams(data: CreateReviewDTO): any; + + /** + * 将统一评论更新参数转换为平台评论更新参数 + * @param data 统一评论更新参数 + * @returns 平台评论更新参数 + */ + mapUpdateReviewParams(data: UpdateReviewDTO): any; + + /** + * 获取评论列表 + */ + getReviews(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有评论 + */ + getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建评论 + */ + createReview(data: CreateReviewDTO): Promise; + + /** + * 更新评论 + */ + updateReview(id: number, data: UpdateReviewDTO): Promise; + + /** + * 删除评论 + */ + deleteReview(id: number): Promise; + + // ========== 订阅映射方法 ========== + /** + * 将平台订阅数据转换为统一订阅数据格式 + * @param data 平台特定订阅数据 + * @returns 统一订阅数据格式 + */ + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO; + + /** + * 将统一订阅数据格式转换为平台订阅数据 + * @param data 统一订阅数据格式 + * @returns 平台特定订阅数据 + */ + mapUnifiedToPlatformSubscription(data: Partial): any; + + /** + * 获取订阅列表 + */ + getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取所有订阅 + */ + getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + + // ========== 产品变体映射方法 ========== + /** + * 将平台产品变体数据转换为统一产品变体数据格式 + * @param data 平台特定产品变体数据 + * @returns 统一产品变体数据格式 + */ + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO; + + /** + * 将统一产品变体数据格式转换为平台产品变体数据 + * @param data 统一产品变体数据格式 + * @returns 平台特定产品变体数据 + */ + mapUnifiedToPlatformVariation(data: Partial): any; + + /** + * 将统一产品变体创建参数转换为平台产品变体创建参数 + * @param data 统一产品变体创建参数 + * @returns 平台产品变体创建参数 + */ + mapCreateVariationParams(data: CreateVariationDTO): any; + + /** + * 将统一产品变体更新参数转换为平台产品变体更新参数 + * @param data 统一产品变体更新参数 + * @returns 平台产品变体更新参数 + */ + mapUpdateVariationParams(data: UpdateVariationDTO): any; + + /** + * 获取单个产品变体 + */ + getVariation(productId: string | number, variationId: string | number): Promise; + + /** + * 获取产品变体列表 + */ + getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有产品变体 + */ + getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建产品变体 + */ + createVariation(productId: string | number, data: CreateVariationDTO): Promise; + + /** + * 更新产品变体 + */ + updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; + + /** + * 删除产品变体 + */ + deleteVariation(productId: string | number, variationId: string | number): Promise; + + // ========== Webhook映射方法 ========== + /** + * 将平台Webhook数据转换为统一Webhook数据格式 + * @param data 平台特定Webhook数据 + * @returns 统一Webhook数据格式 + */ + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO; + + /** + * 将统一Webhook数据格式转换为平台Webhook数据 + * @param data 统一Webhook数据格式 + * @returns 平台特定Webhook数据 + */ + mapUnifiedToPlatformWebhook(data: Partial): any; + + /** + * 将统一Webhook创建参数转换为平台Webhook创建参数 + * @param data 统一Webhook创建参数 + * @returns 平台Webhook创建参数 + */ + mapCreateWebhookParams(data: CreateWebhookDTO): any; + + /** + * 将统一Webhook更新参数转换为平台Webhook更新参数 + * @param data 统一Webhook更新参数 + * @returns 平台Webhook更新参数 + */ + mapUpdateWebhookParams(data: UpdateWebhookDTO): any; + + /** + * 获取单个webhook + */ + getWebhook(id: string | number): Promise; + + /** + * 获取webhooks列表 + */ + getWebhooks(params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有webhooks + */ + getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; + + /** + * 创建webhook + */ + createWebhook(data: CreateWebhookDTO): Promise; + + /** + * 更新webhook + */ + updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + + /** + * 删除webhook + */ + deleteWebhook(id: string | number): Promise; + + // ========== 站点/其他方法 ========== + /** + * 获取站点链接列表 + */ + getLinks(): Promise>; } diff --git a/src/transformer/database.transformer.ts b/src/transformer/database.transformer.ts new file mode 100644 index 0000000..619ef04 --- /dev/null +++ b/src/transformer/database.transformer.ts @@ -0,0 +1 @@ +// 从 unified 到 数据库需要有个转换流程 diff --git a/src/transformer/file.transformer.ts b/src/transformer/file.transformer.ts new file mode 100644 index 0000000..b85ee23 --- /dev/null +++ b/src/transformer/file.transformer.ts @@ -0,0 +1 @@ +// 文件转换 diff --git a/src/transformer/shopyy.transformer.ts b/src/transformer/shopyy.transformer.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/transformer/woocommerce.adpater.ts b/src/transformer/woocommerce.adpater.ts new file mode 100644 index 0000000..e5b5c67 --- /dev/null +++ b/src/transformer/woocommerce.adpater.ts @@ -0,0 +1,8 @@ +import { UnifiedOrderDTO } from "../dto/site-api.dto"; + +export class ShipmentAdapter { + // 用于导出物流需要的数据 + mapFromOrder(order: UnifiedOrderDTO): any { + return order; + } +} \ No newline at end of file From 0f81499ccc0db09f754b76dc15519d99e09bb89c Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 20:33:23 +0800 Subject: [PATCH 12/24] =?UTF-8?q?refactor(api):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=8F=82=E6=95=B0=E4=B8=BA=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=E5=B9=B6=E6=94=AF=E6=8C=81=E5=A4=9A=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构所有接口方法,将直接传递id参数改为接受where条件对象 支持通过id、sku、email等多条件查询实体 优化产品服务逻辑,支持通过sku直接查询产品 统一各适配器实现,确保接口一致性 --- src/adapter/shopyy.adapter.ts | 140 ++++++++++------ src/adapter/woocommerce.adapter.ts | 213 ++++++++++++------------ src/controller/site-api.controller.ts | 42 ++--- src/interface/site-adapter.interface.ts | 34 ++-- src/service/order.service.ts | 2 +- src/service/site-api.service.ts | 28 +--- src/service/wp.service.ts | 8 +- 7 files changed, 248 insertions(+), 219 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 12c85e6..44766e3 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -122,8 +122,11 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); + async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise { + if(!where.id && !where.email && !where.phone){ + throw new Error('必须传入 id 或 email 或 phone') + } + const customer = await this.shopyyService.getCustomer(this.site, where.id); return this.mapPlatformToUnifiedCustomer(customer); } @@ -149,13 +152,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedCustomer(createdCustomer); } - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + async updateCustomer(where: {id: string | number}, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data); return this.mapPlatformToUnifiedCustomer(updatedCustomer); } - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); + async deleteCustomer(where: {id: string | number}): Promise { + return await this.shopyyService.deleteCustomer(this.site, where.id); } batchProcessCustomers?(data: BatchOperationDTO): Promise { @@ -208,13 +211,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedMedia(createdMedia); } - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + async updateMedia(where: {id: string | number}, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data); return this.mapPlatformToUnifiedMedia(updatedMedia); } - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + async deleteMedia(where: {id: string | number}): Promise { + return await this.shopyyService.deleteMedia(this.site, where.id); } mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { @@ -530,8 +533,8 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); + async getOrder(where: {id: string | number}): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(where.id)); return this.mapPlatformToUnifiedOrder(data); } @@ -577,14 +580,14 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 使用映射方法转换参数 const requestParams = this.mapUpdateOrderParams(data); - return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams); } - async deleteOrder(id: string | number): Promise { - return await this.shopyyService.deleteOrder(this.site, id); + async deleteOrder(where: {id: string | number}): Promise { + return await this.shopyyService.deleteOrder(this.site, where.id); } async getOrderNotes(orderId: string | number): Promise { @@ -858,10 +861,18 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapPlatformToUnifiedProduct(product); + async getProduct(where: {id?: string | number, sku?: string}): Promise { + if(!where.id && !where.sku){ + throw new Error('必须传入 id 或 sku') + } + if (where.id) { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, where.id); + return this.mapPlatformToUnifiedProduct(product); + } else if (where.sku) { + // 通过sku获取产品 + return this.getProductBySku(where.sku); + } } async getProducts( @@ -900,24 +911,50 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } // 使用映射方法转换参数 const requestParams = this.mapUpdateProductParams(data); - await this.shopyyService.updateProduct(this.site, String(id), requestParams); + await this.shopyyService.updateProduct(this.site, productId, requestParams); return true; } - async deleteProduct(id: string | number): Promise { + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { + let productId: string | number; + if (where.id) { + productId = where.id; + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = product.id; + } else { + throw new Error('必须提供id或sku参数'); + } // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); return true; } + // 通过sku获取产品详情的私有方法 + private async getProductBySku(sku: string): Promise { + // 使用Shopyy API的搜索功能通过sku查询产品 + const response = await this.shopyyService.getProducts(this.site, 1, 100); + const product = response.items.find((item: any) => item.sku === sku); + if (!product) { + throw new Error(`未找到sku为${sku}的产品`); + } + return this.mapPlatformToUnifiedProduct(product); + } + async batchProcessProducts( data: { create?: any[]; update?: any[]; delete?: Array } ): Promise { @@ -929,9 +966,6 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data - } mapUnifiedToPlatformReview(data: Partial) { return data @@ -954,7 +988,7 @@ export class ShopyyAdapter implements ISiteAdapter { requestParams ); return { - items: items.map(this.mapReview), + items: items.map(this.mapPlatformToUnifiedReview), total, totalPages, page, @@ -969,19 +1003,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createReview(data: any): Promise { const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + return this.mapPlatformToUnifiedReview(createdReview); } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + async updateReview(where: {id: string | number}, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(updatedReview); } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async deleteReview(where: {id: string | number}): Promise { + return await this.shopyyService.deleteReview(this.site, where.id); } - mapReview(review: any): UnifiedReviewDTO { + mapPlatformToUnifiedReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -1120,9 +1154,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== Webhook映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data - } + mapUnifiedToPlatformWebhook(data: Partial) { return data @@ -1136,15 +1168,15 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); + async getWebhook(where: {id: string | number}): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(webhook); } async getWebhooks(params: UnifiedSearchParamsDTO): Promise { const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params); return { - items: items.map(this.mapWebhook), + items: items.map(this.mapPlatformToUnifiedWebhook), total, totalPages, page, @@ -1159,19 +1191,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createWebhook(data: CreateWebhookDTO): Promise { const createdWebhook = await this.shopyyService.createWebhook(this.site, data); - return this.mapWebhook(createdWebhook); + return this.mapPlatformToUnifiedWebhook(createdWebhook); } - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { - const updatedWebhook = await this.shopyyService.updateWebhook(this.site, id, data); - return this.mapWebhook(updatedWebhook); + async updateWebhook(where: {id: string | number}, data: UpdateWebhookDTO): Promise { + const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data); + return this.mapPlatformToUnifiedWebhook(updatedWebhook); } - async deleteWebhook(id: string | number): Promise { - return await this.shopyyService.deleteWebhook(this.site, id); + async deleteWebhook(where: {id: string | number}): Promise { + return await this.shopyyService.deleteWebhook(this.site, where.id); } - mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { return { id: item.id, name: item.webhook_name || `Webhook-${item.id}`, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 1e1ade7..95e8567 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -36,12 +36,15 @@ import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 constructor(private site: Site, private wpService: WPService) { - this.mapProduct = this.mapProduct.bind(this); - this.mapReview = this.mapReview.bind(this); - this.mapCustomer = this.mapCustomer.bind(this); - this.mapMedia = this.mapMedia.bind(this); - this.mapOrder = this.mapOrder.bind(this); - this.mapWebhook = this.mapWebhook.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedReview = this.mapPlatformToUnifiedReview.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedWebhook = this.mapPlatformToUnifiedWebhook.bind(this); + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data } batchProcessProducts?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); @@ -54,14 +57,8 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 客户映射方法 ========== - mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { - return data; - } - mapUnifiedToPlatformCustomer(data: Partial) { - return data; - } - mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + mapPlatformToUnifiedCustomer(item: WooCustomer): UnifiedCustomerDTO { // 将 WooCommerce 客户数据映射为统一客户DTO // 包含基础信息地址信息与时间信息 return { @@ -136,10 +133,10 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户操作方法 - async getCustomer(id: string | number): Promise { + async getCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + const res = await api.get(`customers/${where.id}`); + return this.mapPlatformToUnifiedCustomer(res.data); } async getCustomers(params: UnifiedSearchParamsDTO): Promise> { @@ -150,7 +147,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map((i: any) => this.mapPlatformToUnifiedCustomer(i)), total, totalPages, page, @@ -167,36 +164,33 @@ export class WooCommerceAdapter implements ISiteAdapter { const requestParams = this.mapCustomerSearchParams(params || {}); const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + return customers.map((customer: any) => this.mapPlatformToUnifiedCustomer(customer)); } async createCustomer(data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('customers', data); - return this.mapCustomer(res.data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async updateCustomer(id: string | number, data: Partial): Promise { + async updateCustomer(where: {id: string | number}, data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); + const res = await api.put(`customers/${where.id}`, data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async deleteCustomer(id: string | number): Promise { + async deleteCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); + await api.delete(`customers/${where.id}`, { force: true }); return true; } // ========== 媒体映射方法 ========== - mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { - return data; - } mapUnifiedToPlatformMedia(data: Partial) { return data; } - mapMedia(item: WpMedia): UnifiedMediaDTO { + mapPlatformToUnifiedMedia(item: WpMedia): UnifiedMediaDTO { // 将 WordPress 媒体数据映射为统一媒体DTO // 兼容不同字段命名的时间信息 return { @@ -221,7 +215,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapMedia.bind(this)), + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), total, totalPages, page, @@ -233,21 +227,21 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有媒体数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); } createMedia(file: any): Promise { throw new Error('Method not implemented.'); } - async updateMedia(id: string | number, data: any): Promise { + async updateMedia(where: {id: string | number}, data: any): Promise { // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + return await this.wpService.updateMedia(Number(this.site.id), Number(where.id), data); } - async deleteMedia(id: string | number): Promise { + async deleteMedia(where: {id: string | number}): Promise { // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + await this.wpService.deleteMedia(Number(this.site.id), Number(where.id), true); return true; } @@ -258,9 +252,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 订单映射方法 ========== - mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { - return data; - } mapUnifiedToPlatformOrder(data: Partial) { return data; } @@ -357,7 +348,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ].filter(Boolean).join(', '); } - mapOrder(item: WooOrder): UnifiedOrderDTO { + mapPlatformToUnifiedOrder(item: WooOrder): UnifiedOrderDTO { // 将 WooCommerce 订单数据映射为统一订单DTO // 包含账单地址与收货地址以及创建与更新时间 @@ -411,11 +402,11 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 订单操作方法 - async getOrder(id: string | number): Promise { + async getOrder(where: {id: string | number}): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); + const res = await api.get(`orders/${where.id}`); + return this.mapPlatformToUnifiedOrder(res.data); } async getOrders(params: UnifiedSearchParamsDTO): Promise> { @@ -445,7 +436,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: ordersWithFulfillments.map(this.mapOrder), + items: ordersWithFulfillments.map(this.mapPlatformToUnifiedOrder), total, totalPages, page, @@ -457,7 +448,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订单数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); + return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); } async countOrders(where: Record): Promise { @@ -476,18 +467,18 @@ export class WooCommerceAdapter implements ISiteAdapter { // 创建订单并返回统一订单DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); - return this.mapOrder(res.data); + return this.mapPlatformToUnifiedOrder(res.data); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); + return await this.wpService.updateOrder(this.site, String(where.id), data as any); } - async deleteOrder(id: string | number): Promise { + async deleteOrder(where: {id: string | number}): Promise { // 删除订单 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); + await api.delete(`orders/${where.id}`, { force: true }); return true; } @@ -592,9 +583,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { - return data; - } mapUnifiedToPlatformProduct(data: Partial) { return data; } @@ -662,8 +650,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapProduct(item: WooProduct): UnifiedProductDTO { - mapProduct(item: WooProduct): UnifiedProductDTO { + mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -751,11 +738,22 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - + async getProduct({id, sku}: {id?: string, sku?:string}){ + if(id) return this.getProductById(id); + if(sku) return this.getProductBySku(sku) + return this.getProductById(id || sku || ''); + } + async getProductBySku(sku: string){ + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products?sku=${sku}`); + const product = res.data[0]; + return this.mapPlatformToUnifiedProduct(product); + } // 产品操作方法 - async getProduct(id: string | number): Promise { + async getProductById(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); const product = res.data; @@ -775,7 +773,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - return this.mapProduct(product); + return this.mapPlatformToUnifiedProduct(product); } async getProducts(params: UnifiedSearchParamsDTO): Promise> { @@ -810,7 +808,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: productsWithVariations.map(this.mapProduct), + items: productsWithVariations.map(this.mapPlatformToUnifiedProduct), total, totalPages, page, @@ -846,26 +844,46 @@ export class WooCommerceAdapter implements ISiteAdapter { }) ); - return productsWithVariations.map((product: any) => this.mapProduct(product)); + return productsWithVariations.map((product: any) => this.mapPlatformToUnifiedProduct(product)); } async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); - return this.mapProduct(res); + return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { // 更新产品并返回统一产品DTO - const res = await this.wpService.updateProduct(this.site, String(id), data as any); + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } + const res = await this.wpService.updateProduct(this.site, productId, data as any); return res; } - async deleteProduct(id: string | number): Promise { + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { // 删除产品 + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } const api = (this.wpService as any).createApi(this.site, 'wc/v3'); try { - await api.delete(`products/${id}`, { force: true }); + await api.delete(`products/${productId}`, { force: true }); return true; } catch (e) { return false; @@ -873,9 +891,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data; - } + mapUnifiedToPlatformReview(data: Partial) { return data; } @@ -887,8 +903,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapReview(item: any): UnifiedReviewDTO & { raw: any } { - mapReview(item: any): UnifiedReviewDTO & { raw: any } { + mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -913,7 +928,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map(this.mapReview.bind(this)), + items: items.map(this.mapPlatformToUnifiedReview.bind(this)), total, totalPages, page, @@ -925,32 +940,29 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有评论数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); - return reviews.map((review: any) => this.mapReview(review)); + return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); } async createReview(data: any): Promise { const res = await this.wpService.createReview(this.site, data); - return this.mapReview(res); + return this.mapPlatformToUnifiedReview(res); } - async updateReview(id: number, data: any): Promise { - const res = await this.wpService.updateReview(this.site, id, data); - return this.mapReview(res); + async updateReview(where: {id: number}, data: any): Promise { + const res = await this.wpService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(res); } - async deleteReview(id: number): Promise { - return await this.wpService.deleteReview(this.site, id); + async deleteReview(where: {id: number}): Promise { + return await this.wpService.deleteReview(this.site, where.id); } // ========== 订阅映射方法 ========== - mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { - return data; - } mapUnifiedToPlatformSubscription(data: Partial) { return data; } - mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + mapPlatformToUnifiedSubscription(item: WooSubscription): UnifiedSubscriptionDTO { // 将 WooCommerce 订阅数据映射为统一订阅DTO // 若缺少创建时间则回退为开始时间 return { @@ -977,7 +989,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapSubscription), + items: items.map(this.mapPlatformToUnifiedSubscription), total, totalPages, page, @@ -990,7 +1002,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); + return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); } // ========== 变体映射方法 ========== @@ -1229,9 +1241,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 网络钩子映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data; - } mapUnifiedToPlatformWebhook(data: Partial) { return data; } @@ -1244,7 +1253,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 映射 WooCommerce webhook 到统一格式 - mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(webhook: WooWebhook): UnifiedWebhookDTO { return { id: webhook.id.toString(), name: webhook.name, @@ -1266,12 +1275,12 @@ export class WooCommerceAdapter implements ISiteAdapter { const result = await this.wpService.getWebhooks(this.site, params); return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; + items: (result.items as WooWebhook[]).map(this.mapPlatformToUnifiedWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; } catch (error) { throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); } @@ -1282,17 +1291,17 @@ export class WooCommerceAdapter implements ISiteAdapter { try { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); } catch (error) { throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); } } // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { + async getWebhook(where: {id: string | number}): Promise { try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); } @@ -1310,14 +1319,14 @@ export class WooCommerceAdapter implements ISiteAdapter { api_version: data.api_version || 'wp/v2', }; const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + async updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise { try { const params = { ...(data.name ? { name: data.name } : {}), @@ -1327,17 +1336,17 @@ export class WooCommerceAdapter implements ISiteAdapter { ...(data.secret ? { secret: data.secret } : {}), ...(data.api_version ? { api_version: data.api_version } : {}), }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.updateWebhook(this.site, where.id, params); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { + async deleteWebhook(where: Partial>): Promise { try { - await this.wpService.deleteWebhook(this.site, id); + await this.wpService.deleteWebhook(this.site, where.id); return true; } catch (error) { throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index f4b1c83..00d8285 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -105,7 +105,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateReview(id, body); + const data = await adapter.updateReview({ id }, body); this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -123,7 +123,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.deleteReview(id); + const data = await adapter.deleteReview({ id }); this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -159,7 +159,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getWebhook(id); + const data = await adapter.getWebhook({ id }); this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -198,7 +198,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateWebhook(id, body); + const data = await adapter.updateWebhook({ id }, body); this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -216,7 +216,7 @@ export class SiteApiController { this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.deleteWebhook(id); + const data = await adapter.deleteWebhook({ id }); this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`); return successResponse(data); } catch (error) { @@ -326,7 +326,7 @@ export class SiteApiController { if (site.type === 'woocommerce') { const page = query.page || 1; const perPage = (query.per_page) || 100; - const res = await this.siteApiService.wpService.getProducts(site, page, perPage); + const res = await this.siteApiService.wpService.getProducts(site, { page, per_page: perPage }); const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity']; const rows = (res.items || []).map((p: any) => [p.id, p.name, p.type, p.status, p.sku, p.regular_price, p.sale_price, p.stock_status, p.stock_quantity]); const toCsvValue = (val: any) => { @@ -359,7 +359,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getProduct(id); + const data = await adapter.getProduct({ id }); // 如果获取到商品数据,则增强ERP产品信息 if (data) { @@ -484,7 +484,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateProduct(id, body); + const data = await adapter.updateProduct({ id }, body); this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`); return successResponse(data); } catch (error) { @@ -539,7 +539,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const success = await adapter.deleteProduct(id); + const success = await adapter.deleteProduct({ id }); this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`); return successResponse(success); } catch (error) { @@ -584,7 +584,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const data = await adapter.updateProduct(id, item); + const data = await adapter.updateProduct({ id }, item); updated.push(data); } catch (e) { errors.push({ @@ -597,7 +597,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteProduct(id); + const ok = await adapter.deleteProduct({ id }); if (ok) deleted.push(id); else errors.push({ identifier: String(id), @@ -771,7 +771,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getOrder(id); + const data = await adapter.getOrder({ id }); this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(data); } catch (error) { @@ -843,7 +843,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const ok = await adapter.updateOrder(id, body); + const ok = await adapter.updateOrder({ id }, body); this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(ok); } catch (error) { @@ -861,7 +861,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const ok = await adapter.deleteOrder(id); + const ok = await adapter.deleteOrder({ id }); this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(ok); } catch (error) { @@ -901,7 +901,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const ok = await adapter.updateOrder(id, item); + const ok = await adapter.updateOrder({ id }, item); if (ok) updated.push(item); else errors.push({ identifier: String(item.id || 'unknown'), @@ -918,7 +918,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteOrder(id); + const ok = await adapter.deleteOrder({ id }); if (ok) deleted.push(id); else errors.push({ identifier: String(id), @@ -1435,7 +1435,7 @@ export class SiteApiController { this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getCustomer(id); + const data = await adapter.getCustomer({ id }); this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(data); } catch (error) { @@ -1507,7 +1507,7 @@ export class SiteApiController { this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.updateCustomer(id, body); + const data = await adapter.updateCustomer({ id }, body); this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(data); } catch (error) { @@ -1525,7 +1525,7 @@ export class SiteApiController { this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const success = await adapter.deleteCustomer(id); + const success = await adapter.deleteCustomer({ id }); this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`); return successResponse(success); } catch (error) { @@ -1561,7 +1561,7 @@ export class SiteApiController { for (const item of body.update) { try { const id = item.id; - const data = await adapter.updateCustomer(id, item); + const data = await adapter.updateCustomer({ id }, item); updated.push(data); } catch (e) { failed.push({ action: 'update', item, error: (e as any).message }); @@ -1571,7 +1571,7 @@ export class SiteApiController { if (body.delete?.length) { for (const id of body.delete) { try { - const ok = await adapter.deleteCustomer(id); + const ok = await adapter.deleteCustomer({ id }); if (ok) deleted.push(id); else failed.push({ action: 'delete', id, error: 'delete failed' }); } catch (e) { diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 630e5ed..f290517 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,7 +19,7 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { // ========== 客户映射方法 ========== /** * 将平台客户数据转换为统一客户数据格式 @@ -38,7 +38,7 @@ export interface ISiteAdapter { /** * 获取单个客户 */ - getCustomer(id: string | number): Promise; + getCustomer(where: Partial>): Promise; /** * 获取客户列表 @@ -58,12 +58,12 @@ export interface ISiteAdapter { /** * 更新客户 */ - updateCustomer(id: string | number, data: Partial): Promise; + updateCustomer(where: Partial>, data: Partial): Promise; /** * 删除客户 */ - deleteCustomer(id: string | number): Promise; + deleteCustomer(where: Partial>): Promise; /** * 批量处理客户 @@ -132,7 +132,7 @@ export interface ISiteAdapter { /** * 获取单个订单 */ - getOrder(id: string | number): Promise; + getOrder(where: Partial>): Promise; /** * 获取订单列表 @@ -147,7 +147,7 @@ export interface ISiteAdapter { /** * 获取订单总数 */ - countOrders(params: Record): Promise; + countOrders(params: Record): Promise; /** * 创建订单 @@ -157,12 +157,12 @@ export interface ISiteAdapter { /** * 更新订单 */ - updateOrder(id: string | number, data: Partial): Promise; + updateOrder(where: Partial>, data: Partial): Promise; /** * 删除订单 */ - deleteOrder(id: string | number): Promise; + deleteOrder(where: Partial>): Promise; /** * 获取订单备注 @@ -259,7 +259,7 @@ export interface ISiteAdapter { /** * 获取单个产品 */ - getProduct(id: string | number): Promise; + getProduct(where: Partial>): Promise; /** * 获取产品列表 @@ -279,12 +279,12 @@ export interface ISiteAdapter { /** * 更新产品 */ - updateProduct(id: string | number, data: Partial): Promise; + updateProduct(where: Partial>, data: Partial): Promise; /** * 删除产品 */ - deleteProduct(id: string | number): Promise; + deleteProduct(where: Partial>): Promise; /** * 批量处理产品 @@ -338,12 +338,12 @@ export interface ISiteAdapter { /** * 更新评论 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + updateReview(where: Partial>, data: UpdateReviewDTO): Promise; /** * 删除评论 */ - deleteReview(id: number): Promise; + deleteReview(where: Partial>): Promise; // ========== 订阅映射方法 ========== /** @@ -461,7 +461,7 @@ export interface ISiteAdapter { /** * 获取单个webhook */ - getWebhook(id: string | number): Promise; + getWebhook(where: Partial>): Promise; /** * 获取webhooks列表 @@ -481,16 +481,16 @@ export interface ISiteAdapter { /** * 更新webhook */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise; /** * 删除webhook */ - deleteWebhook(id: string | number): Promise; + deleteWebhook(where: Partial>): Promise; // ========== 站点/其他方法 ========== /** * 获取站点链接列表 */ - getLinks(): Promise>; + getLinks(): Promise>; } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 5c5e47f..1aa19bd 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -202,7 +202,7 @@ export class OrderService { try { // 调用 WooCommerce API 获取订单 const adapter = await this.siteApiService.getAdapter(siteId); - const order = await adapter.getOrder(orderId); + const order = await adapter.getOrder({ id: orderId }); // 检查订单是否已存在,以区分创建和更新 const existingOrder = await this.orderModel.findOne({ diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index c57a899..3aab4ac 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -110,36 +110,22 @@ export class SiteApiService { const adapter = await this.getAdapter(siteId); // 首先尝试查找产品 - if (product.id) { - try { - // 尝试获取产品以确认它是否存在 - const existingProduct = await adapter.getProduct(product.id); - if (existingProduct) { - // 产品存在,执行更新 - return await adapter.updateProduct(product.id, product); - } - } catch (error) { - // 如果获取产品失败,可能是因为产品不存在,继续执行创建逻辑 - console.log(`产品 ${product.id} 不存在,将创建新产品:`, error.message); - } - } else if (product.sku) { + if (product.sku) { // 如果没有提供ID但提供了SKU,尝试通过SKU查找产品 try { // 尝试搜索具有相同SKU的产品 - const searchResult = await adapter.getProducts({ where: { sku: product.sku } }); - if (searchResult.items && searchResult.items.length > 0) { - const existingProduct = searchResult.items[0]; + const existingProduct = await adapter.getProduct( { sku: product.sku }); + if (existingProduct) { // 找到现有产品,更新它 - return await adapter.updateProduct(existingProduct.id, product); + return await adapter.updateProduct({ id: existingProduct.id }, product); } + // 产品不存在,执行创建 + return await adapter.createProduct(product); } catch (error) { // 搜索失败,继续执行创建逻辑 console.log(`通过SKU搜索产品失败:`, error.message); } } - - // 产品不存在,执行创建 - return await adapter.createProduct(product); } /** @@ -197,7 +183,7 @@ export class SiteApiService { */ async getProductFromSite(siteId: number, productId: string | number): Promise { const adapter = await this.getAdapter(siteId); - return await adapter.getProduct(productId); + return await adapter.getProduct({ id: productId }); } /** diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index c7faa02..8c8bc89 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -240,9 +240,11 @@ export class WPService implements IPlatformService { return allData; } - async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise { + async getProducts(site: any, params: Record = {}): Promise { const api = this.createApi(site, 'wc/v3'); - return await this.sdkGetPage(api, 'products', { page, per_page: pageSize }); + const page = params.page ?? 1; + const per_page = params.per_page ?? params.pageSize ?? 100; + return await this.sdkGetPage(api, 'products', { ...params, page, per_page }); } async getProduct(site: any, id: number): Promise { @@ -254,7 +256,7 @@ export class WPService implements IPlatformService { // 导出 WooCommerce 产品为特殊CSV(平台特性) async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise { - const list = await this.getProducts(site, page, pageSize); + const list = await this.getProducts(site, { page, per_page: pageSize }); const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity']; const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); From acfee35e73ccdb15439e622866fba7a634989521 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 15:22:18 +0800 Subject: [PATCH 13/24] =?UTF-8?q?feat(=E8=AE=A2=E5=8D=95):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=8E=B7=E5=8F=96=E8=AE=A2=E5=8D=95=E6=80=BB=E6=95=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现订单总数统计接口,包括: 1. 在ISiteAdapter接口添加countOrders方法 2. 在WooCommerce和Shopyy适配器中实现该方法 3. 添加控制器端点暴露该功能 4. 优化订单查询参数映射逻辑 refactor(Shopyy): 重构搜索参数映射逻辑 将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性 --- src/adapter/shopyy.adapter.ts | 412 ++++++++++++++---------- src/adapter/woocommerce.adapter.ts | 221 +++++-------- src/interface/site-adapter.interface.ts | 7 +- 3 files changed, 324 insertions(+), 316 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 44766e3..c8e5df5 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -27,8 +27,10 @@ import { ShopyyCustomer, ShopyyOrder, ShopyyOrderQuery, + ShopyyOrderQuery, ShopyyProduct, ShopyyProductQuery, + ShopyyProductQuery, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; @@ -56,45 +58,51 @@ export class ShopyyAdapter implements ISiteAdapter { // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); } - // ========== 客户映射方法 ========== - mapPlatformToUnifiedCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { - // 处理多地址结构 - const addresses = item.addresses || []; - const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); - - // 尝试从地址列表中获取billing和shipping - // 如果没有明确区分,默认使用默认地址或第一个地址 - const billingAddress = defaultAddress; - const shippingAddress = defaultAddress; - - const billing = { - first_name: billingAddress.first_name || item.first_name || '', - last_name: billingAddress.last_name || item.last_name || '', - fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), - company: billingAddress.company || '', - email: item.email || '', - phone: billingAddress.phone || item.contact || '', - address_1: billingAddress.address1 || '', - address_2: billingAddress.address2 || '', - city: billingAddress.city || '', - state: billingAddress.province || '', - postcode: billingAddress.zip || '', - country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' + private mapMedia(item: any): UnifiedMediaDTO { + // 映射媒体项目 + return { + id: item.id, + date_created: item.created_at, + date_modified: item.updated_at, + source_url: item.src, + title: item.alt || '', + media_type: '', // Shopyy API未提供,暂时留空 + mime_type: '', // Shopyy API未提供,暂时留空 }; + } - const shipping = { - first_name: shippingAddress.first_name || item.first_name || '', - last_name: shippingAddress.last_name || item.last_name || '', - fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), - company: shippingAddress.company || '', - address_1: shippingAddress.address1 || '', - address_2: shippingAddress.address2 || '', - city: shippingAddress.city || '', - state: shippingAddress.province || '', - postcode: shippingAddress.zip || '', - country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' - }; + private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { + return this.mapSearchParams(params) + } + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + private mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), + page, + limit, + } + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; + } + + private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + // 映射产品状态 + function mapProductStatus(status: number) { + return status === 1 ? 'publish' : 'draft'; + } return { id: item.id || item.customer_id, orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), @@ -394,153 +402,198 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } - - mapUnifiedToPlatformOrder(data: Partial) { - return data + shopyyFulfillmentStatusMap = { + // 未发货 + '300': OrderFulfillmentStatus.PENDING, + // 部分发货 + '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, + // 已发货 + '320': OrderFulfillmentStatus.FULFILLED, + // 已取消 + '330': OrderFulfillmentStatus.CANCELLED, + // 确认发货 } - mapCreateOrderParams(data: Partial): any { - return data + private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { + // 处理多地址结构 + const addresses = item.addresses || []; + const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); + + // 尝试从地址列表中获取billing和shipping + // 如果没有明确区分,默认使用默认地址或第一个地址 + const billingAddress = defaultAddress; + const shippingAddress = defaultAddress; + + const billing = { + first_name: billingAddress.first_name || item.first_name || '', + last_name: billingAddress.last_name || item.last_name || '', + fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), + company: billingAddress.company || '', + email: item.email || '', + phone: billingAddress.phone || item.contact || '', + address_1: billingAddress.address1 || '', + address_2: billingAddress.address2 || '', + city: billingAddress.city || '', + state: billingAddress.province || '', + postcode: billingAddress.zip || '', + country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' + }; + + const shipping = { + first_name: shippingAddress.first_name || item.first_name || '', + last_name: shippingAddress.last_name || item.last_name || '', + fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), + company: shippingAddress.company || '', + address_1: shippingAddress.address1 || '', + address_2: shippingAddress.address2 || '', + city: shippingAddress.city || '', + state: shippingAddress.province || '', + postcode: shippingAddress.zip || '', + country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' + }; + + return { + id: item.id || item.customer_id, + orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), + total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), + first_name: item.first_name || item.firstname || '', + last_name: item.last_name || item.lastname || '', + fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), + email: item.email || item.customer_email || '', + phone: item.contact || billing.phone || item.phone || '', + billing, + shipping, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), + raw: item, + }; + } + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) } - mapUpdateOrderParams(data: Partial): any { - // 构建 ShopYY 订单更新参数(仅包含传入的字段) - const params: any = {}; - - // 仅当字段存在时才添加到更新参数中 - if (data.status !== undefined) { - // 映射订单状态 - const statusMap = { - [OrderStatus.PENDING]: 100, // pending -> 100 未完成 - [OrderStatus.PROCESSING]: 110, // processing -> 110 待处理 - [OrderStatus.COMPLETED]: 180, // completed -> 180 已完成 - [OrderStatus.CANCEL]: 190 // cancel -> 190 取消 - }; - params.status = statusMap[data.status] || 100; - } - - if (data.payment_method !== undefined) { - params.payment_method = data.payment_method; - } - - if (data.billing) { - // 更新客户信息 - if (data.billing.first_name !== undefined) { - params.firstname = data.billing.first_name; - } - if (data.billing.last_name !== undefined) { - params.lastname = data.billing.last_name; - } - if (data.billing.email !== undefined) { - params.email = data.billing.email; - } - if (data.billing.phone !== undefined) { - params.phone = data.billing.phone; - } - - // 更新账单地址 - params.billing_address = params.billing_address || {}; - if (data.billing.first_name !== undefined) { - params.billing_address.first_name = data.billing.first_name; - } - if (data.billing.last_name !== undefined) { - params.billing_address.last_name = data.billing.last_name; - } - if (data.billing.company !== undefined) { - params.billing_address.company = data.billing.company; - } - if (data.billing.address_1 !== undefined) { - params.billing_address.address1 = data.billing.address_1; - } - if (data.billing.address_2 !== undefined) { - params.billing_address.address2 = data.billing.address_2; - } - if (data.billing.city !== undefined) { - params.billing_address.city = data.billing.city; - } - if (data.billing.state !== undefined) { - params.billing_address.province = data.billing.state; - } - if (data.billing.postcode !== undefined) { - params.billing_address.zip = data.billing.postcode; - } - if (data.billing.country !== undefined) { - params.billing_address.country_code = data.billing.country; - } - } - - if (data.shipping) { - // 更新送货地址 - params.shipping_address = params.shipping_address || {}; - if (data.shipping.first_name !== undefined) { - params.shipping_address.first_name = data.shipping.first_name; - } - if (data.shipping.last_name !== undefined) { - params.shipping_address.last_name = data.shipping.last_name; - } - if (data.shipping.company !== undefined) { - params.shipping_address.company = data.shipping.company; - } - if (data.shipping.address_1 !== undefined) { - params.shipping_address.address1 = data.shipping.address_1; - } - if (data.shipping.address_2 !== undefined) { - params.shipping_address.address2 = data.shipping.address_2; - } - if (data.shipping.city !== undefined) { - params.shipping_address.city = data.shipping.city; - } - if (data.shipping.state !== undefined) { - params.shipping_address.province = data.shipping.state; - } - if (data.shipping.postcode !== undefined) { - params.shipping_address.zip = data.shipping.postcode; - } - if (data.shipping.country !== undefined) { - params.shipping_address.country_code = data.shipping.country; - } - if (data.shipping.phone !== undefined) { - params.shipping_address.phone = data.shipping.phone; - } - } - - // 更新订单项 - if (data.line_items && data.line_items.length > 0) { - params.products = data.line_items.map((item: UnifiedOrderLineItemDTO) => ({ - product_id: item.product_id, - quantity: item.quantity, - // price: item.price || '0.00', - sku: item.sku || '', - })); - } - - // 更新物流信息 - if (data.shipping_lines && data.shipping_lines.length > 0) { - const shippingLine = data.shipping_lines[0]; - if (shippingLine.method_title !== undefined) { - params.shipping_method = shippingLine.method_title; - } - if (shippingLine.total !== undefined) { - params.shipping_price = shippingLine.total; - } - } - - // // 更新备注信息 - // if (data.note !== undefined) { - // params.note = data.note; - // } - - return params; + async getProducts( + params: UnifiedSearchParamsDTO + ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); + const response = await this.shopyyService.fetchResourcePaged( + this.site, + 'products/list', + requestParams + ); + 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)) + return { + items: finalItems as UnifiedProductDTO[], + total, + totalPages, + page, + per_page, + }; } - async getOrder(where: {id: string | number}): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(where.id)); - return this.mapPlatformToUnifiedOrder(data); + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllProducts 暂未实现 + throw new Error('Shopyy getAllProducts 暂未实现'); + } + + async getProduct(id: string | number): Promise { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, id); + return this.mapProduct(product); + } + + async createProduct(data: Partial): Promise { + const res = await this.shopyyService.createProduct(this.site, data); + return this.mapProduct(res); + } + async updateProduct(id: string | number, data: Partial): Promise { + // Shopyy update returns boolean? + // shopyyService.updateProduct returns boolean. + // So I can't return the updated product. + // I have to fetch it again or return empty/input. + // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + await this.shopyyService.updateProduct(this.site, String(id), data); + return true; + } + + async updateVariation(productId: string | number, variationId: string | number, data: any): Promise { + await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data); + return { ...data, id: variationId }; + } + + async getOrderNotes(orderId: string | number): Promise { + return await this.shopyyService.getOrderNotes(this.site, orderId); + } + + async createOrderNote(orderId: string | number, data: any): Promise { + return await this.shopyyService.createOrderNote(this.site, orderId, data); + } + + async deleteProduct(id: string | number): Promise { + // Use batch delete + await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + return true; + } + + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + return await this.shopyyService.batchProcessProducts(this.site, data); + } + /** + * 将统一的订单查询参数转换为 ShopYY 订单查询参数 + * 包含状态映射等特殊处理 + */ + private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 首先使用通用参数转换 + const baseParams = this.mapSearchParams(params); + + // 订单状态映射 + const statusMap = { + 'pending': '100', // 100 未完成 + 'processing': '110', // 110 待处理 + 'completed': "180", // 180 已完成(确认收货) + 'cancelled': '190', // 190 取消 + }; + + // 如果有状态参数,进行特殊映射 + if (baseParams.status) { + const unifiedStatus = baseParams.status + if (statusMap[unifiedStatus]) { + baseParams.status = statusMap[unifiedStatus]; + } + } + + // 处理ID参数 + if (baseParams.id) { + baseParams.ids = baseParams.id; + delete baseParams.id; + } + + return baseParams; } async getOrders( params: UnifiedSearchParamsDTO ): Promise> { + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'orders', + normalizedParams + ); // 转换订单查询参数 const normalizedParams = this.mapOrderSearchParams(params); const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( @@ -557,6 +610,17 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; + } + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { const data = await this.shopyyService.getAllOrders(this.site.id, params); return data.map(this.mapPlatformToUnifiedOrder.bind(this)); @@ -882,8 +946,8 @@ export class ShopyyAdapter implements ISiteAdapter { const requestParams = this.mapProductQuery(params); const response = await this.shopyyService.fetchResourcePaged( this.site, - 'products/list', - requestParams + 'media', // Shopyy的媒体API端点可能需要调整 + requestParams ); const { items = [], total, totalPages, page, per_page } = response; const finalItems = items.map((item) => ({ diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 95e8567..9986a38 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -890,10 +890,87 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - // ========== 评论映射方法 ========== + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + // 批量处理产品增删改 + return await this.wpService.batchProcessProducts(this.site, data); + } - mapUnifiedToPlatformReview(data: Partial) { - return data; + async getOrders( + params: UnifiedSearchParamsDTO + ): Promise> { + const requestParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = + await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + + // 并行获取所有订单的履行信息 + const ordersWithFulfillments = await Promise.all( + items.map(async (order: any) => { + try { + // 获取订单的履行信息 + const fulfillments = await this.getOrderFulfillments(order.id); + // 将履行信息添加到订单对象中 + return { + ...order, + fulfillments: fulfillments || [] + }; + } catch (error) { + // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 + console.error(`获取订单 ${order.id} 的履行信息失败:`, error); + return { + ...order, + fulfillments: [] + }; + } + }) + ); + + return { + items: ordersWithFulfillments.map(this.mapOrder), + total, + totalPages, + page, + per_page, + }; + } + + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + + async getOrder(id: string | number): Promise { + // 获取单个订单详情 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`orders/${id}`); + return this.mapOrder(res.data); + } + + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订单数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const orders = await this.wpService.sdkGetAll(api, 'orders', params); + return orders.map((order: any) => this.mapOrder(order)); + } + + async createOrder(data: Partial): Promise { + // 创建订单并返回统一订单DTO + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.post('orders', data); + return this.mapOrder(res.data); + } + + async updateOrder(id: string | number, data: Partial): Promise { + // 更新订单并返回布尔结果 + return await this.wpService.updateOrder(this.site, String(id), data as any); } mapCreateReviewParams(data: CreateReviewDTO) { @@ -1239,142 +1316,4 @@ export class WooCommerceAdapter implements ISiteAdapter { throw new Error(`删除产品变体失败: ${error instanceof Error ? error.message : String(error)}`); } } - - // ========== 网络钩子映射方法 ========== - mapUnifiedToPlatformWebhook(data: Partial) { - return data; - } - - mapCreateWebhookParams(data: CreateWebhookDTO) { - return data; - } - mapUpdateWebhookParams(data: UpdateWebhookDTO) { - return data; - } - - // 映射 WooCommerce webhook 到统一格式 - mapPlatformToUnifiedWebhook(webhook: WooWebhook): UnifiedWebhookDTO { - return { - id: webhook.id.toString(), - name: webhook.name, - status: webhook.status, - topic: webhook.topic, - delivery_url: webhook.delivery_url, - secret: webhook.secret, - api_version: webhook.api_version, - date_created: webhook.date_created, - date_modified: webhook.date_modified, - // metadata: webhook.meta_data || [], - }; - } - - // 网络钩子操作方法 - // 获取站点的 webhooks 列表 - async getWebhooks(params: UnifiedSearchParamsDTO): Promise { - try { - const result = await this.wpService.getWebhooks(this.site, params); - - return { - items: (result.items as WooWebhook[]).map(this.mapPlatformToUnifiedWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; - } catch (error) { - throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取所有webhooks - async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { - try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); - } catch (error) { - throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 获取单个 webhook 详情 - async getWebhook(where: {id: string | number}): Promise { - try { - const result = await this.wpService.getWebhook(this.site, where.id); - return this.mapPlatformToUnifiedWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 创建新的 webhook - async createWebhook(data: CreateWebhookDTO): Promise { - try { - const params = { - name: data.name, - status: 'active', // 默认状态为活跃 - topic: data.topic, - delivery_url: data.delivery_url, - secret: data.secret, - api_version: data.api_version || 'wp/v2', - }; - const result = await this.wpService.createWebhook(this.site, params); - return this.mapPlatformToUnifiedWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 更新现有的 webhook - async updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise { - try { - const params = { - ...(data.name ? { name: data.name } : {}), - ...(data.status ? { status: data.status } : {}), - ...(data.topic ? { topic: data.topic } : {}), - ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), - ...(data.secret ? { secret: data.secret } : {}), - ...(data.api_version ? { api_version: data.api_version } : {}), - }; - const result = await this.wpService.updateWebhook(this.site, where.id, params); - return this.mapPlatformToUnifiedWebhook(result as WooWebhook); - } catch (error) { - throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 删除指定的 webhook - async deleteWebhook(where: Partial>): Promise { - try { - await this.wpService.deleteWebhook(this.site, where.id); - return true; - } catch (error) { - throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // ========== 其他方法 ========== - async getLinks(): Promise> { - const baseUrl = this.site.apiUrl; - const links = [ - { title: '访问网站', url: baseUrl }, - { title: '管理后台', url: `${baseUrl}/wp-admin/` }, - { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, - { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, - { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, - { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, - { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, - { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, - { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, - ]; - return links; - } - - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } - - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { - throw new Error('Method not implemented.'); - } } diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index f290517..edba72b 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -43,7 +43,12 @@ export interface ISiteAdapter { /** * 获取客户列表 */ - getCustomers(params: UnifiedSearchParamsDTO): Promise>; + getOrders(params: UnifiedSearchParamsDTO): Promise>; + + /** + * 获取订单总数 + */ + countOrders(params: Record): Promise; /** * 获取所有客户 From d549227e030a6cb1d962a997beaee968408aec0c Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 18:10:00 +0800 Subject: [PATCH 14/24] =?UTF-8?q?refactor(interface):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E9=80=82=E9=85=8D=E5=99=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E6=8C=89=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组 移除废弃的 fulfillOrder 方法 新增多个数据映射方法以支持统一数据格式转换 --- src/adapter/shopyy.adapter.ts | 619 ++++++++++++------------ src/adapter/woocommerce.adapter.ts | 441 +++++++++++------ src/interface/site-adapter.interface.ts | 70 ++- 3 files changed, 660 insertions(+), 470 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index c8e5df5..e457284 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -21,6 +21,10 @@ import { CreateReviewDTO, CreateVariationDTO, UpdateReviewDTO + FulfillmentDTO, + CreateReviewDTO, + CreateVariationDTO, + UpdateReviewDTO } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { @@ -38,6 +42,7 @@ import { OrderStatus, } from '../enums/base.enum'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class ShopyyAdapter implements ISiteAdapter { shopyyFinancialStatusMap= { '200': '待支付', @@ -56,53 +61,53 @@ export class ShopyyAdapter implements ISiteAdapter { this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); + this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); + this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); + this.mapPlatformToUnifiedVariation = this.mapPlatformToUnifiedVariation.bind(this); + this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); + this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); + // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); } - private mapMedia(item: any): UnifiedMediaDTO { - // 映射媒体项目 - return { - id: item.id, - date_created: item.created_at, - date_modified: item.updated_at, - source_url: item.src, - title: item.alt || '', - media_type: '', // Shopyy API未提供,暂时留空 - mime_type: '', // Shopyy API未提供,暂时留空 + // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { + // 处理多地址结构 + const addresses = item.addresses || []; + const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); + + // 尝试从地址列表中获取billing和shipping + // 如果没有明确区分,默认使用默认地址或第一个地址 + const billingAddress = defaultAddress; + const shippingAddress = defaultAddress; + + const billing = { + first_name: billingAddress.first_name || item.first_name || '', + last_name: billingAddress.last_name || item.last_name || '', + fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), + company: billingAddress.company || '', + email: item.email || '', + phone: billingAddress.phone || item.contact || '', + address_1: billingAddress.address1 || '', + address_2: billingAddress.address2 || '', + city: billingAddress.city || '', + state: billingAddress.province || '', + postcode: billingAddress.zip || '', + country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' }; - } - private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { - return this.mapSearchParams(params) - } + const shipping = { + first_name: shippingAddress.first_name || item.first_name || '', + last_name: shippingAddress.last_name || item.last_name || '', + fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), + company: shippingAddress.company || '', + address_1: shippingAddress.address1 || '', + address_2: shippingAddress.address2 || '', + city: shippingAddress.city || '', + state: shippingAddress.province || '', + postcode: shippingAddress.zip || '', + country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' + }; - /** - * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 - * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 - */ - private mapSearchParams(params: UnifiedSearchParamsDTO): any { - // 处理分页参数 - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - - // 处理 where 条件 - const query: any = { - ...(params.where || {}), - page, - limit, - } - if(params.orderBy){ - const [field, dir] = Object.entries(params.orderBy)[0]; - query.order_by = dir === 'desc' ? 'desc' : 'asc'; - query.order_field = field - } - return query; - } - - private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { - // 映射产品状态 - function mapProductStatus(status: number) { - return status === 1 ? 'publish' : 'draft'; - } return { id: item.id || item.customer_id, orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), @@ -130,11 +135,8 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise { - if(!where.id && !where.email && !where.phone){ - throw new Error('必须传入 id 或 email 或 phone') - } - const customer = await this.shopyyService.getCustomer(this.site, where.id); + async getCustomer(id: string | number): Promise { + const customer = await this.shopyyService.getCustomer(this.site, id); return this.mapPlatformToUnifiedCustomer(customer); } @@ -160,13 +162,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedCustomer(createdCustomer); } - async updateCustomer(where: {id: string | number}, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data); + async updateCustomer(id: string | number, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); return this.mapPlatformToUnifiedCustomer(updatedCustomer); } - async deleteCustomer(where: {id: string | number}): Promise { - return await this.shopyyService.deleteCustomer(this.site, where.id); + async deleteCustomer(id: string | number): Promise { + return await this.shopyyService.deleteCustomer(this.site, id); } batchProcessCustomers?(data: BatchOperationDTO): Promise { @@ -219,13 +221,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedMedia(createdMedia); } - async updateMedia(where: {id: string | number}, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data); + async updateMedia(id: string | number, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); return this.mapPlatformToUnifiedMedia(updatedMedia); } - async deleteMedia(where: {id: string | number}): Promise { - return await this.shopyyService.deleteMedia(this.site, where.id); + async deleteMedia(id: string | number): Promise { + return await this.shopyyService.deleteMedia(this.site, id); } mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { @@ -402,186 +404,148 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } - shopyyFulfillmentStatusMap = { - // 未发货 - '300': OrderFulfillmentStatus.PENDING, - // 部分发货 - '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, - // 已发货 - '320': OrderFulfillmentStatus.FULFILLED, - // 已取消 - '330': OrderFulfillmentStatus.CANCELLED, - // 确认发货 + + mapUnifiedToPlatformOrder(data: Partial) { + return data } - private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { - // 处理多地址结构 - const addresses = item.addresses || []; - const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); - - // 尝试从地址列表中获取billing和shipping - // 如果没有明确区分,默认使用默认地址或第一个地址 - const billingAddress = defaultAddress; - const shippingAddress = defaultAddress; - - const billing = { - first_name: billingAddress.first_name || item.first_name || '', - last_name: billingAddress.last_name || item.last_name || '', - fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(), - company: billingAddress.company || '', - email: item.email || '', - phone: billingAddress.phone || item.contact || '', - address_1: billingAddress.address1 || '', - address_2: billingAddress.address2 || '', - city: billingAddress.city || '', - state: billingAddress.province || '', - postcode: billingAddress.zip || '', - country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || '' - }; - - const shipping = { - first_name: shippingAddress.first_name || item.first_name || '', - last_name: shippingAddress.last_name || item.last_name || '', - fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(), - company: shippingAddress.company || '', - address_1: shippingAddress.address1 || '', - address_2: shippingAddress.address2 || '', - city: shippingAddress.city || '', - state: shippingAddress.province || '', - postcode: shippingAddress.zip || '', - country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || '' - }; - - return { - id: item.id || item.customer_id, - orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), - total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), - first_name: item.first_name || item.firstname || '', - last_name: item.last_name || item.lastname || '', - fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), - email: item.email || item.customer_email || '', - phone: item.contact || billing.phone || item.phone || '', - billing, - shipping, - date_created: - typeof item.created_at === 'number' - ? new Date(item.created_at * 1000).toISOString() - : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), - date_modified: - typeof item.updated_at === 'number' - ? new Date(item.updated_at * 1000).toISOString() - : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), - raw: item, - }; - } - mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { - return this.mapSearchParams(query) + mapCreateOrderParams(data: Partial): any { + return data } - async getProducts( - params: UnifiedSearchParamsDTO - ): Promise> { - // 转换搜索参数 - const requestParams = this.mapProductQuery(params); - const response = await this.shopyyService.fetchResourcePaged( - this.site, - 'products/list', - requestParams - ); - 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)) - return { - items: finalItems as UnifiedProductDTO[], - total, - totalPages, - page, - per_page, - }; - } + mapUpdateOrderParams(data: Partial): any { + // 构建 ShopYY 订单更新参数(仅包含传入的字段) + const params: any = {}; - async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); - } + // 仅当字段存在时才添加到更新参数中 + if (data.status !== undefined) { + // 映射订单状态 + const statusMap = { + [OrderStatus.PENDING]: 100, // pending -> 100 未完成 + [OrderStatus.PROCESSING]: 110, // processing -> 110 待处理 + [OrderStatus.COMPLETED]: 180, // completed -> 180 已完成 + [OrderStatus.CANCEL]: 190 // cancel -> 190 取消 + }; + params.status = statusMap[data.status] || 100; + } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapProduct(product); - } + if (data.payment_method !== undefined) { + params.payment_method = data.payment_method; + } - async createProduct(data: Partial): Promise { - const res = await this.shopyyService.createProduct(this.site, data); - return this.mapProduct(res); - } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). - await this.shopyyService.updateProduct(this.site, String(id), data); - return true; - } + if (data.billing) { + // 更新客户信息 + if (data.billing.first_name !== undefined) { + params.firstname = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.lastname = data.billing.last_name; + } + if (data.billing.email !== undefined) { + params.email = data.billing.email; + } + if (data.billing.phone !== undefined) { + params.phone = data.billing.phone; + } - async updateVariation(productId: string | number, variationId: string | number, data: any): Promise { - await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data); - return { ...data, id: variationId }; - } - - async getOrderNotes(orderId: string | number): Promise { - return await this.shopyyService.getOrderNotes(this.site, orderId); - } - - async createOrderNote(orderId: string | number, data: any): Promise { - return await this.shopyyService.createOrderNote(this.site, orderId, data); - } - - async deleteProduct(id: string | number): Promise { - // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); - return true; - } - - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - return await this.shopyyService.batchProcessProducts(this.site, data); - } - /** - * 将统一的订单查询参数转换为 ShopYY 订单查询参数 - * 包含状态映射等特殊处理 - */ - private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { - // 首先使用通用参数转换 - const baseParams = this.mapSearchParams(params); - - // 订单状态映射 - const statusMap = { - 'pending': '100', // 100 未完成 - 'processing': '110', // 110 待处理 - 'completed': "180", // 180 已完成(确认收货) - 'cancelled': '190', // 190 取消 - }; - - // 如果有状态参数,进行特殊映射 - if (baseParams.status) { - const unifiedStatus = baseParams.status - if (statusMap[unifiedStatus]) { - baseParams.status = statusMap[unifiedStatus]; + // 更新账单地址 + params.billing_address = params.billing_address || {}; + if (data.billing.first_name !== undefined) { + params.billing_address.first_name = data.billing.first_name; + } + if (data.billing.last_name !== undefined) { + params.billing_address.last_name = data.billing.last_name; + } + if (data.billing.company !== undefined) { + params.billing_address.company = data.billing.company; + } + if (data.billing.address_1 !== undefined) { + params.billing_address.address1 = data.billing.address_1; + } + if (data.billing.address_2 !== undefined) { + params.billing_address.address2 = data.billing.address_2; + } + if (data.billing.city !== undefined) { + params.billing_address.city = data.billing.city; + } + if (data.billing.state !== undefined) { + params.billing_address.province = data.billing.state; + } + if (data.billing.postcode !== undefined) { + params.billing_address.zip = data.billing.postcode; + } + if (data.billing.country !== undefined) { + params.billing_address.country_code = data.billing.country; } } - - // 处理ID参数 - if (baseParams.id) { - baseParams.ids = baseParams.id; - delete baseParams.id; + + if (data.shipping) { + // 更新送货地址 + params.shipping_address = params.shipping_address || {}; + if (data.shipping.first_name !== undefined) { + params.shipping_address.first_name = data.shipping.first_name; + } + if (data.shipping.last_name !== undefined) { + params.shipping_address.last_name = data.shipping.last_name; + } + if (data.shipping.company !== undefined) { + params.shipping_address.company = data.shipping.company; + } + if (data.shipping.address_1 !== undefined) { + params.shipping_address.address1 = data.shipping.address_1; + } + if (data.shipping.address_2 !== undefined) { + params.shipping_address.address2 = data.shipping.address_2; + } + if (data.shipping.city !== undefined) { + params.shipping_address.city = data.shipping.city; + } + if (data.shipping.state !== undefined) { + params.shipping_address.province = data.shipping.state; + } + if (data.shipping.postcode !== undefined) { + params.shipping_address.zip = data.shipping.postcode; + } + if (data.shipping.country !== undefined) { + params.shipping_address.country_code = data.shipping.country; + } + if (data.shipping.phone !== undefined) { + params.shipping_address.phone = data.shipping.phone; + } } - - return baseParams; + + // 更新订单项 + if (data.line_items && data.line_items.length > 0) { + params.products = data.line_items.map((item: UnifiedOrderLineItemDTO) => ({ + product_id: item.product_id, + quantity: item.quantity, + // price: item.price || '0.00', + sku: item.sku || '', + })); + } + + // 更新物流信息 + if (data.shipping_lines && data.shipping_lines.length > 0) { + const shippingLine = data.shipping_lines[0]; + if (shippingLine.method_title !== undefined) { + params.shipping_method = shippingLine.method_title; + } + if (shippingLine.total !== undefined) { + params.shipping_price = shippingLine.total; + } + } + + // // 更新备注信息 + // if (data.note !== undefined) { + // params.note = data.note; + // } + + return params; + } + + async getOrder(id: string | number): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(id)); + return this.mapPlatformToUnifiedOrder(data); } async getOrders( @@ -602,6 +566,7 @@ export class ShopyyAdapter implements ISiteAdapter { normalizedParams ); return { + items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), total, totalPages, @@ -610,17 +575,6 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams = { - where, - page: 1, - per_page: 1, - } - const data = await this.getOrders(searchParams); - return data.total || 0; - } - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { const data = await this.shopyyService.getAllOrders(this.site.id, params); return data.map(this.mapPlatformToUnifiedOrder.bind(this)); @@ -642,12 +596,16 @@ export class ShopyyAdapter implements ISiteAdapter { const requestParams = this.mapCreateOrderParams(data); const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); return this.mapPlatformToUnifiedOrder(createdOrder); + // 使用映射方法转换参数 + const requestParams = this.mapCreateOrderParams(data); + const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); + return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateOrder(where: {id: string | number}, data: Partial): Promise { + async updateOrder(id: string | number, data: Partial): Promise { // 使用映射方法转换参数 const requestParams = this.mapUpdateOrderParams(data); - return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams); + return await this.shopyyService.updateOrder(this.site, String(id), requestParams); } async deleteOrder(where: {id: string | number}): Promise { @@ -658,6 +616,12 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.getOrderNotes(this.site, orderId); } + async createOrderNote(orderId: string | number, data: any): Promise { + return await this.shopyyService.createOrderNote(this.site, orderId, data); + async getOrderNotes(orderId: string | number): Promise { + return await this.shopyyService.getOrderNotes(this.site, orderId); + } + async createOrderNote(orderId: string | number, data: any): Promise { return await this.shopyyService.createOrderNote(this.site, orderId, data); } @@ -684,6 +648,7 @@ export class ShopyyAdapter implements ISiteAdapter { }; } catch (error) { throw new Error(`履行失败: ${error.message}`); + throw new Error(`履行失败: ${error.message}`); } } @@ -925,36 +890,39 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getProduct(where: {id?: string | number, sku?: string}): Promise { - if(!where.id && !where.sku){ - throw new Error('必须传入 id 或 sku') - } - if (where.id) { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, where.id); - return this.mapPlatformToUnifiedProduct(product); - } else if (where.sku) { - // 通过sku获取产品 - return this.getProductBySku(where.sku); - } + async getProduct(id: string | number): Promise { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, id); + return this.mapPlatformToUnifiedProduct(product); } + async getProducts( async getProducts( params: UnifiedSearchParamsDTO + ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); + const response = await this.shopyyService.fetchResourcePaged( ): Promise> { // 转换搜索参数 const requestParams = this.mapProductQuery(params); const response = await this.shopyyService.fetchResourcePaged( this.site, - 'media', // Shopyy的媒体API端点可能需要调整 - requestParams + 'products/list', + requestParams ); const { items = [], total, totalPages, page, per_page } = response; const finalItems = items.map((item) => ({ ...item, permalink: `${this.site.websiteUrl}/products/${item.handle}`, })).map(this.mapPlatformToUnifiedProduct.bind(this)) + const { items = [], total, totalPages, page, per_page } = response; + const finalItems = items.map((item) => ({ + ...item, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, + })).map(this.mapPlatformToUnifiedProduct.bind(this)) return { + items: finalItems as UnifiedProductDTO[], items: finalItems as UnifiedProductDTO[], total, totalPages, @@ -963,11 +931,19 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllProducts 暂未实现 + throw new Error('Shopyy getAllProducts 暂未实现'); async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { // Shopyy getAllProducts 暂未实现 throw new Error('Shopyy getAllProducts 暂未实现'); } + async createProduct(data: Partial): Promise { + // 使用映射方法转换参数 + const requestParams = this.mapCreateProductParams(data); + const res = await this.shopyyService.createProduct(this.site, requestParams); + return this.mapPlatformToUnifiedProduct(res); async createProduct(data: Partial): Promise { // 使用映射方法转换参数 const requestParams = this.mapCreateProductParams(data); @@ -975,50 +951,24 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { - let productId: string; - if (where.id) { - productId = String(where.id); - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = String(product.id); - } else { - throw new Error('必须提供id或sku参数'); - } + async updateProduct(id: string | number, data: Partial): Promise { + // Shopyy update returns boolean? + // shopyyService.updateProduct returns boolean. + // So I can't return the updated product. + // I have to fetch it again or return empty/input. + // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). // 使用映射方法转换参数 const requestParams = this.mapUpdateProductParams(data); - await this.shopyyService.updateProduct(this.site, productId, requestParams); + await this.shopyyService.updateProduct(this.site, String(id), requestParams); return true; } - async deleteProduct(where: {id?: string | number, sku?: string}): Promise { - let productId: string | number; - if (where.id) { - productId = where.id; - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = product.id; - } else { - throw new Error('必须提供id或sku参数'); - } + async deleteProduct(id: string | number): Promise { // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); + await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); return true; } - // 通过sku获取产品详情的私有方法 - private async getProductBySku(sku: string): Promise { - // 使用Shopyy API的搜索功能通过sku查询产品 - const response = await this.shopyyService.getProducts(this.site, 1, 100); - const product = response.items.find((item: any) => item.sku === sku); - if (!product) { - throw new Error(`未找到sku为${sku}的产品`); - } - return this.mapPlatformToUnifiedProduct(product); - } - async batchProcessProducts( data: { create?: any[]; update?: any[]; delete?: Array } ): Promise { @@ -1030,6 +980,9 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data + } mapUnifiedToPlatformReview(data: Partial) { return data @@ -1067,19 +1020,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createReview(data: any): Promise { const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapPlatformToUnifiedReview(createdReview); + return this.mapReview(createdReview); } - async updateReview(where: {id: string | number}, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data); - return this.mapPlatformToUnifiedReview(updatedReview); + async updateReview(id: string | number, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, id, data); + return this.mapReview(updatedReview); } - async deleteReview(where: {id: string | number}): Promise { - return await this.shopyyService.deleteReview(this.site, where.id); + async deleteReview(id: string | number): Promise { + return await this.shopyyService.deleteReview(this.site, id); } - mapPlatformToUnifiedReview(review: any): UnifiedReviewDTO { + mapReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -1096,7 +1049,6 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { const { search, page, per_page, where } = params; const shopyyParams: any = { @@ -1122,19 +1074,36 @@ export class ShopyyAdapter implements ISiteAdapter { mapUnifiedToPlatformSubscription(data: Partial) { return data + // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data } + mapUnifiedToPlatformSubscription(data: Partial) { + return data + } + + async getSubscriptions( + params: UnifiedSearchParamsDTO + ): Promise> { + throw new Error('Shopyy does not support subscriptions.'); async getSubscriptions( params: UnifiedSearchParamsDTO ): Promise> { throw new Error('Shopyy does not support subscriptions.'); } + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllSubscriptions 暂未实现 + throw new Error('Shopyy getAllSubscriptions 暂未实现'); async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { // Shopyy getAllSubscriptions 暂未实现 throw new Error('Shopyy getAllSubscriptions 暂未实现'); } + // ========== 产品变体映射方法 ========== + mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { + // 映射变体 // ========== 产品变体映射方法 ========== mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { // 映射变体 @@ -1218,7 +1187,9 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== Webhook映射方法 ========== - + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data + } mapUnifiedToPlatformWebhook(data: Partial) { return data @@ -1232,9 +1203,9 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getWebhook(where: {id: string | number}): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, where.id); - return this.mapPlatformToUnifiedWebhook(webhook); + async getWebhook(id: string | number): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, id); + return this.mapWebhook(webhook); } async getWebhooks(params: UnifiedSearchParamsDTO): Promise { @@ -1267,7 +1238,7 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteWebhook(this.site, where.id); } - mapPlatformToUnifiedWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { return { id: item.id, name: item.webhook_name || `Webhook-${item.id}`, @@ -1297,6 +1268,19 @@ export class ShopyyAdapter implements ISiteAdapter { return links; } + // ========== 辅助方法 ========== + /** + * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 + * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 + */ + mapSearchParams(params: UnifiedSearchParamsDTO): any { + // 处理分页参数 + const page = Number(params.page || 1); + const limit = Number(params.per_page ?? 20); + + // 处理 where 条件 + const query: any = { + ...(params.where || {}), // ========== 辅助方法 ========== /** * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 @@ -1318,6 +1302,14 @@ export class ShopyyAdapter implements ISiteAdapter { query.order_by = dir === 'desc' ? 'desc' : 'asc'; query.order_field = field } + return query; + limit, + } + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } return query; } @@ -1340,6 +1332,35 @@ export class ShopyyAdapter implements ISiteAdapter { [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled } + shopyyFulfillmentStatusMap = { + // 未发货 + '300': OrderFulfillmentStatus.PENDING, + // 部分发货 + '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, + // 已发货 + '320': OrderFulfillmentStatus.FULFILLED, + // 已取消 + '330': OrderFulfillmentStatus.CANCELLED, + // 确认发货 + // 映射产品状态: publish -> 1, draft -> 0 + mapStatus = (status: string) => { + return status === 'publish' ? 1 : 0; + }; + + // 映射库存状态: instock -> 1, outofstock -> 0 + mapStockStatus = (stockStatus: string) => { + return stockStatus === 'instock' ? 1 : 0; + }; + + 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 + } + shopyyFulfillmentStatusMap = { // 未发货 '300': OrderFulfillmentStatus.PENDING, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 9986a38..7950610 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -17,6 +17,8 @@ import { UnifiedVariationPaginationDTO, CreateReviewDTO, UpdateReviewDTO, + CreateReviewDTO, + UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { @@ -32,6 +34,7 @@ import { import { Site } from '../entity/site.entity'; import { WPService } from '../service/wp.service'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 @@ -43,9 +46,6 @@ export class WooCommerceAdapter implements ISiteAdapter { this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); this.mapPlatformToUnifiedWebhook = this.mapPlatformToUnifiedWebhook.bind(this); } - mapUnifiedToPlatformCustomer(data: Partial) { - return data - } batchProcessProducts?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); } @@ -57,8 +57,14 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 客户映射方法 ========== + mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { + return data; + } + mapUnifiedToPlatformCustomer(data: Partial) { + return data; + } - mapPlatformToUnifiedCustomer(item: WooCustomer): UnifiedCustomerDTO { + mapCustomer(item: WooCustomer): UnifiedCustomerDTO { // 将 WooCommerce 客户数据映射为统一客户DTO // 包含基础信息地址信息与时间信息 return { @@ -76,6 +82,20 @@ export class WooCommerceAdapter implements ISiteAdapter { date_created: item.date_created, date_modified: item.date_modified, raw: item, + id: item.id, + avatar: item.avatar_url, + email: item.email, + orders: Number(item.orders ?? 0), + total_spend: Number(item.total_spent ?? 0), + first_name: item.first_name, + last_name: item.last_name, + username: item.username, + phone: item.billing?.phone || item.shipping?.phone, + billing: item.billing, + shipping: item.shipping, + date_created: item.date_created, + date_modified: item.date_modified, + raw: item, }; } @@ -133,10 +153,10 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户操作方法 - async getCustomer(where: {id: string | number}): Promise { + async getCustomer(id: string | number): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${where.id}`); - return this.mapPlatformToUnifiedCustomer(res.data); + const res = await api.get(`customers/${id}`); + return this.mapCustomer(res.data); } async getCustomers(params: UnifiedSearchParamsDTO): Promise> { @@ -147,7 +167,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map((i: any) => this.mapPlatformToUnifiedCustomer(i)), + items: items.map((i: any) => this.mapCustomer(i)), total, totalPages, page, @@ -164,33 +184,36 @@ export class WooCommerceAdapter implements ISiteAdapter { const requestParams = this.mapCustomerSearchParams(params || {}); const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapPlatformToUnifiedCustomer(customer)); + return customers.map((customer: any) => this.mapCustomer(customer)); } async createCustomer(data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('customers', data); - return this.mapPlatformToUnifiedCustomer(res.data); + return this.mapCustomer(res.data); } - async updateCustomer(where: {id: string | number}, data: Partial): Promise { + async updateCustomer(id: string | number, data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${where.id}`, data); - return this.mapPlatformToUnifiedCustomer(res.data); + const res = await api.put(`customers/${id}`, data); + return this.mapCustomer(res.data); } - async deleteCustomer(where: {id: string | number}): Promise { + async deleteCustomer(id: string | number): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${where.id}`, { force: true }); + await api.delete(`customers/${id}`, { force: true }); return true; } // ========== 媒体映射方法 ========== + mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { + return data; + } mapUnifiedToPlatformMedia(data: Partial) { return data; } - mapPlatformToUnifiedMedia(item: WpMedia): UnifiedMediaDTO { + mapMedia(item: WpMedia): UnifiedMediaDTO { // 将 WordPress 媒体数据映射为统一媒体DTO // 兼容不同字段命名的时间信息 return { @@ -215,7 +238,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), + items: items.map(this.mapMedia.bind(this)), total, totalPages, page, @@ -227,21 +250,21 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有媒体数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); + return media.map((mediaItem: any) => this.mapMedia(mediaItem)); } createMedia(file: any): Promise { throw new Error('Method not implemented.'); } - async updateMedia(where: {id: string | number}, data: any): Promise { + async updateMedia(id: string | number, data: any): Promise { // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(where.id), data); + return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); } - async deleteMedia(where: {id: string | number}): Promise { + async deleteMedia(id: string | number): Promise { // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(where.id), true); + await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); return true; } @@ -252,6 +275,9 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 订单映射方法 ========== + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { + return data; + } mapUnifiedToPlatformOrder(data: Partial) { return data; } @@ -263,7 +289,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 计算分页参数 const page = Number(params.page ?? 1); @@ -348,7 +373,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ].filter(Boolean).join(', '); } - mapPlatformToUnifiedOrder(item: WooOrder): UnifiedOrderDTO { + mapOrder(item: WooOrder): UnifiedOrderDTO { // 将 WooCommerce 订单数据映射为统一订单DTO // 包含账单地址与收货地址以及创建与更新时间 @@ -402,11 +427,11 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 订单操作方法 - async getOrder(where: {id: string | number}): Promise { + async getOrder(id: string | number): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${where.id}`); - return this.mapPlatformToUnifiedOrder(res.data); + const res = await api.get(`orders/${id}`); + return this.mapOrder(res.data); } async getOrders(params: UnifiedSearchParamsDTO): Promise> { @@ -436,7 +461,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: ordersWithFulfillments.map(this.mapPlatformToUnifiedOrder), + items: ordersWithFulfillments.map(this.mapOrder), total, totalPages, page, @@ -448,7 +473,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订单数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); + return orders.map((order: any) => this.mapOrder(order)); } async countOrders(where: Record): Promise { @@ -467,18 +492,18 @@ export class WooCommerceAdapter implements ISiteAdapter { // 创建订单并返回统一订单DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); - return this.mapPlatformToUnifiedOrder(res.data); + return this.mapOrder(res.data); } - async updateOrder(where: {id: string | number}, data: Partial): Promise { + async updateOrder(id: string | number, data: Partial): Promise { // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(where.id), data as any); + return await this.wpService.updateOrder(this.site, String(id), data as any); } - async deleteOrder(where: {id: string | number}): Promise { + async deleteOrder(id: string | number): Promise { // 删除订单 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${where.id}`, { force: true }); + await api.delete(`orders/${id}`, { force: true }); return true; } @@ -583,6 +608,9 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== + mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { + return data; + } mapUnifiedToPlatformProduct(data: Partial) { return data; } @@ -612,6 +640,40 @@ export class WooCommerceAdapter implements ISiteAdapter { return String(value).split(',').map(v => v.trim()).filter(Boolean); }; + if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); + if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); + if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); + if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); + if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); + if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); + if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); + if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); + if (where.offset !== undefined) mapped.offset = Number(where.offset); + if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); + if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); + if (where.slug) mapped.slug = String(where.slug); + if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { + if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); + if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); + if (where.status) mapped.status = String(where.status); + } + if (where.type) mapped.type = String(where.type); + if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); + if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); + if (where.sku) mapped.sku = String(where.sku); + if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); + if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); + if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); + if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); + if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); + if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); + if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); + if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); + if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); + if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); + if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); + if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); + if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); @@ -650,7 +712,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { + mapProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -738,22 +800,11 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - async getProduct({id, sku}: {id?: string, sku?:string}){ - if(id) return this.getProductById(id); - if(sku) return this.getProductBySku(sku) - return this.getProductById(id || sku || ''); - } - async getProductBySku(sku: string){ - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products?sku=${sku}`); - const product = res.data[0]; - return this.mapPlatformToUnifiedProduct(product); - } + // 产品操作方法 - async getProductById(id: string | number): Promise { + async getProduct(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products/${id}`); const product = res.data; @@ -773,9 +824,10 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - return this.mapPlatformToUnifiedProduct(product); + return this.mapProduct(product); } + async getProducts(params: UnifiedSearchParamsDTO): Promise> { async getProducts(params: UnifiedSearchParamsDTO): Promise> { // 获取产品列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); @@ -784,6 +836,11 @@ export class WooCommerceAdapter implements ISiteAdapter { 'products', requestParams ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products', + requestParams + ); // 对于类型为 variable 的产品,需要加载完整的变体数据 const productsWithVariations = await Promise.all( @@ -844,7 +901,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }) ); - return productsWithVariations.map((product: any) => this.mapPlatformToUnifiedProduct(product)); + return productsWithVariations.map((product: any) => this.mapProduct(product)); } async createProduct(data: Partial): Promise { @@ -855,17 +912,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { // 更新产品并返回统一产品DTO - let productId: string; - if (where.id) { - productId = String(where.id); - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = String(product.id); - } else { - throw new Error('必须提供id或sku参数'); - } - const res = await this.wpService.updateProduct(this.site, productId, data as any); + const res = await this.wpService.updateProduct(this.site, String(id), data as any); return res; } @@ -890,87 +937,12 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - async batchProcessProducts( - data: { create?: any[]; update?: any[]; delete?: Array } - ): Promise { - // 批量处理产品增删改 - return await this.wpService.batchProcessProducts(this.site, data); + // ========== 评论映射方法 ========== + mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { + return data; } - - async getOrders( - params: UnifiedSearchParamsDTO - ): Promise> { - const requestParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = - await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - - // 并行获取所有订单的履行信息 - const ordersWithFulfillments = await Promise.all( - items.map(async (order: any) => { - try { - // 获取订单的履行信息 - const fulfillments = await this.getOrderFulfillments(order.id); - // 将履行信息添加到订单对象中 - return { - ...order, - fulfillments: fulfillments || [] - }; - } catch (error) { - // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 - console.error(`获取订单 ${order.id} 的履行信息失败:`, error); - return { - ...order, - fulfillments: [] - }; - } - }) - ); - - return { - items: ordersWithFulfillments.map(this.mapOrder), - total, - totalPages, - page, - per_page, - }; - } - - async countOrders(where: Record): Promise { - // 使用最小分页只获取总数 - const searchParams: UnifiedSearchParamsDTO = { - where, - page: 1, - per_page: 1, - }; - const requestParams = this.mapOrderSearchParams(searchParams); - const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); - return total || 0; - } - - async getOrder(id: string | number): Promise { - // 获取单个订单详情 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); - } - - async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订单数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); - } - - async createOrder(data: Partial): Promise { - // 创建订单并返回统一订单DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.post('orders', data); - return this.mapOrder(res.data); - } - - async updateOrder(id: string | number, data: Partial): Promise { - // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); + mapUnifiedToPlatformReview(data: Partial) { + return data; } mapCreateReviewParams(data: CreateReviewDTO) { @@ -980,7 +952,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { + mapReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -995,6 +967,8 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + // 评论操作方法 + async getReviews(params: UnifiedSearchParamsDTO): Promise { // 评论操作方法 async getReviews(params: UnifiedSearchParamsDTO): Promise { // 获取评论列表并使用统一分页结构返回 @@ -1004,6 +978,11 @@ export class WooCommerceAdapter implements ISiteAdapter { 'products/reviews', requestParams ); + const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( + this.site, + 'products/reviews', + requestParams + ); return { items: items.map(this.mapPlatformToUnifiedReview.bind(this)), total, @@ -1030,16 +1009,19 @@ export class WooCommerceAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedReview(res); } - async deleteReview(where: {id: number}): Promise { - return await this.wpService.deleteReview(this.site, where.id); + async deleteReview(id: number): Promise { + return await this.wpService.deleteReview(this.site, id); } // ========== 订阅映射方法 ========== + mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { + return data; + } mapUnifiedToPlatformSubscription(data: Partial) { return data; } - mapPlatformToUnifiedSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { // 将 WooCommerce 订阅数据映射为统一订阅DTO // 若缺少创建时间则回退为开始时间 return { @@ -1049,14 +1031,26 @@ export class WooCommerceAdapter implements ISiteAdapter { billing_period: item.billing_period, billing_interval: item.billing_interval, date_created: item.date_created ?? item.start_date, + status: item.status, + customer_id: item.customer_id, + billing_period: item.billing_period, + billing_interval: item.billing_interval, + date_created: item.date_created ?? item.start_date, date_modified: item.date_modified, start_date: item.start_date, next_payment_date: item.next_payment_date, line_items: item.line_items, + start_date: item.start_date, + next_payment_date: item.next_payment_date, + line_items: item.line_items, raw: item, }; } + // 订阅操作方法 + async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { + // 获取订阅列表并映射为统一订阅DTO集合 + // 订阅操作方法 async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { // 获取订阅列表并映射为统一订阅DTO集合 @@ -1064,9 +1058,11 @@ export class WooCommerceAdapter implements ISiteAdapter { this.site, 'subscriptions', params + 'subscriptions', + params ); return { - items: items.map(this.mapPlatformToUnifiedSubscription), + items: items.map(this.mapSubscription), total, totalPages, page, @@ -1075,13 +1071,21 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订阅数据,不受分页限制 async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); + return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); } + // ========== 变体映射方法 ========== + mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { + return data; + } + mapUnifiedToPlatformVariation(data: Partial) { + return data; // ========== 变体映射方法 ========== mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { return data; @@ -1091,7 +1095,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 映射 WooCommerce 变体到统一格式 - mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO { // 将变体属性转换为统一格式 const mappedAttributes = variation.attributes && Array.isArray(variation.attributes) @@ -1141,6 +1144,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + // 变体操作方法 // 变体操作方法 // 获取产品变体列表 async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { @@ -1316,4 +1320,145 @@ export class WooCommerceAdapter implements ISiteAdapter { throw new Error(`删除产品变体失败: ${error instanceof Error ? error.message : String(error)}`); } } + + // ========== 网络钩子映射方法 ========== + mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { + return data; + } + mapUnifiedToPlatformWebhook(data: Partial) { + return data; + } + + mapCreateWebhookParams(data: CreateWebhookDTO) { + return data; + } + mapUpdateWebhookParams(data: UpdateWebhookDTO) { + return data; + } + + // 映射 WooCommerce webhook 到统一格式 + mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + return { + id: webhook.id.toString(), + name: webhook.name, + status: webhook.status, + topic: webhook.topic, + delivery_url: webhook.delivery_url, + secret: webhook.secret, + api_version: webhook.api_version, + date_created: webhook.date_created, + date_modified: webhook.date_modified, + // metadata: webhook.meta_data || [], + }; + } + + // 网络钩子操作方法 + // 获取站点的 webhooks 列表 + async getWebhooks(params: UnifiedSearchParamsDTO): Promise { + try { + const result = await this.wpService.getWebhooks(this.site, params); + + return { + items: (result.items as WooWebhook[]).map(this.mapWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; + } catch (error) { + throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取所有webhooks + async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { + try { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); + return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + } catch (error) { + throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 获取单个 webhook 详情 + async getWebhook(id: string | number): Promise { + try { + const result = await this.wpService.getWebhook(this.site, id); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 创建新的 webhook + async createWebhook(data: CreateWebhookDTO): Promise { + try { + const params = { + name: data.name, + status: 'active', // 默认状态为活跃 + topic: data.topic, + delivery_url: data.delivery_url, + secret: data.secret, + api_version: data.api_version || 'wp/v2', + }; + const result = await this.wpService.createWebhook(this.site, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 更新现有的 webhook + async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + try { + const params = { + ...(data.name ? { name: data.name } : {}), + ...(data.status ? { status: data.status } : {}), + ...(data.topic ? { topic: data.topic } : {}), + ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}), + ...(data.secret ? { secret: data.secret } : {}), + ...(data.api_version ? { api_version: data.api_version } : {}), + }; + const result = await this.wpService.updateWebhook(this.site, id, params); + return this.mapWebhook(result as WooWebhook); + } catch (error) { + throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 删除指定的 webhook + async deleteWebhook(id: string | number): Promise { + try { + await this.wpService.deleteWebhook(this.site, id); + return true; + } catch (error) { + throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // ========== 其他方法 ========== + async getLinks(): Promise> { + const baseUrl = this.site.apiUrl; + const links = [ + { title: '访问网站', url: baseUrl }, + { title: '管理后台', url: `${baseUrl}/wp-admin/` }, + { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` }, + { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` }, + { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` }, + { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` }, + { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` }, + { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` }, + { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` }, + ]; + return links; + } + + batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } + + batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index edba72b..83c21ae 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,7 +19,7 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { // ========== 客户映射方法 ========== /** * 将平台客户数据转换为统一客户数据格式 @@ -36,19 +36,16 @@ export interface ISiteAdapter { mapUnifiedToPlatformCustomer(data: Partial): any; /** + * 获取单个客户 * 获取单个客户 */ - getCustomer(where: Partial>): Promise; + getCustomer(id: string | number): Promise; /** + * 获取客户列表 * 获取客户列表 */ - getOrders(params: UnifiedSearchParamsDTO): Promise>; - - /** - * 获取订单总数 - */ - countOrders(params: Record): Promise; + getCustomers(params: UnifiedSearchParamsDTO): Promise>; /** * 获取所有客户 @@ -63,12 +60,12 @@ export interface ISiteAdapter { /** * 更新客户 */ - updateCustomer(where: Partial>, data: Partial): Promise; + updateCustomer(id: string | number, data: Partial): Promise; /** * 删除客户 */ - deleteCustomer(where: Partial>): Promise; + deleteCustomer(id: string | number): Promise; /** * 批量处理客户 @@ -105,69 +102,96 @@ export interface ISiteAdapter { */ createMedia(file: any): Promise; + // ========== 订单映射方法 ========== // ========== 订单映射方法 ========== /** + * 将平台订单数据转换为统一订单数据格式 + * @param data 平台特定订单数据 + * @returns 统一订单数据格式 * 将平台订单数据转换为统一订单数据格式 * @param data 平台特定订单数据 * @returns 统一订单数据格式 */ mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO; + mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO; /** + * 将统一订单数据格式转换为平台订单数据 + * @param data 统一订单数据格式 + * @returns 平台特定订单数据 * 将统一订单数据格式转换为平台订单数据 * @param data 统一订单数据格式 * @returns 平台特定订单数据 */ mapUnifiedToPlatformOrder(data: Partial): any; + mapUnifiedToPlatformOrder(data: Partial): any; /** + * 将统一订单创建参数转换为平台订单创建参数 + * @param data 统一订单创建参数 + * @returns 平台订单创建参数 * 将统一订单创建参数转换为平台订单创建参数 * @param data 统一订单创建参数 * @returns 平台订单创建参数 */ mapCreateOrderParams(data: Partial): any; + mapCreateOrderParams(data: Partial): any; /** + * 将统一订单更新参数转换为平台订单更新参数 + * @param data 统一订单更新参数 + * @returns 平台订单更新参数 * 将统一订单更新参数转换为平台订单更新参数 * @param data 统一订单更新参数 * @returns 平台订单更新参数 */ mapUpdateOrderParams(data: Partial): any; + mapUpdateOrderParams(data: Partial): any; /** + * 获取单个订单 * 获取单个订单 */ - getOrder(where: Partial>): Promise; + getOrder(id: string | number): Promise; /** + * 获取订单列表 * 获取订单列表 */ getOrders(params: UnifiedSearchParamsDTO): Promise>; + getOrders(params: UnifiedSearchParamsDTO): Promise>; /** + * 获取所有订单 * 获取所有订单 */ getAllOrders(params?: UnifiedSearchParamsDTO): Promise; + getAllOrders(params?: UnifiedSearchParamsDTO): Promise; /** + * 获取订单总数 * 获取订单总数 */ - countOrders(params: Record): Promise; + countOrders(params: Record): Promise; /** + * 创建订单 * 创建订单 */ createOrder(data: Partial): Promise; + createOrder(data: Partial): Promise; /** + * 更新订单 * 更新订单 */ - updateOrder(where: Partial>, data: Partial): Promise; + updateOrder(id: string | number, data: Partial): Promise; /** + * 删除订单 * 删除订单 */ - deleteOrder(where: Partial>): Promise; + deleteOrder(id: string | number): Promise; /** * 获取订单备注 @@ -264,7 +288,7 @@ export interface ISiteAdapter { /** * 获取单个产品 */ - getProduct(where: Partial>): Promise; + getProduct(id: string | number): Promise; /** * 获取产品列表 @@ -284,12 +308,12 @@ export interface ISiteAdapter { /** * 更新产品 */ - updateProduct(where: Partial>, data: Partial): Promise; + updateProduct(id: string | number, data: Partial): Promise; /** * 删除产品 */ - deleteProduct(where: Partial>): Promise; + deleteProduct(id: string | number): Promise; /** * 批量处理产品 @@ -343,12 +367,12 @@ export interface ISiteAdapter { /** * 更新评论 */ - updateReview(where: Partial>, data: UpdateReviewDTO): Promise; + updateReview(id: number, data: UpdateReviewDTO): Promise; /** * 删除评论 */ - deleteReview(where: Partial>): Promise; + deleteReview(id: number): Promise; // ========== 订阅映射方法 ========== /** @@ -466,7 +490,7 @@ export interface ISiteAdapter { /** * 获取单个webhook */ - getWebhook(where: Partial>): Promise; + getWebhook(id: string | number): Promise; /** * 获取webhooks列表 @@ -486,16 +510,16 @@ export interface ISiteAdapter { /** * 更新webhook */ - updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise; + updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; /** * 删除webhook */ - deleteWebhook(where: Partial>): Promise; + deleteWebhook(id: string | number): Promise; // ========== 站点/其他方法 ========== /** * 获取站点链接列表 */ - getLinks(): Promise>; + getLinks(): Promise>; } From f867f256aed3c018e0ef9ce0575fbace71484f10 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 7 Jan 2026 20:33:23 +0800 Subject: [PATCH 15/24] =?UTF-8?q?refactor(api):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=8F=82=E6=95=B0=E4=B8=BA=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=E5=B9=B6=E6=94=AF=E6=8C=81=E5=A4=9A=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构所有接口方法,将直接传递id参数改为接受where条件对象 支持通过id、sku、email等多条件查询实体 优化产品服务逻辑,支持通过sku直接查询产品 统一各适配器实现,确保接口一致性 --- src/adapter/shopyy.adapter.ts | 120 +++++++++------ src/adapter/woocommerce.adapter.ts | 188 +++++++++++++----------- src/interface/site-adapter.interface.ts | 34 ++--- 3 files changed, 198 insertions(+), 144 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index e457284..6528355 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -135,8 +135,11 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getCustomer(id: string | number): Promise { - const customer = await this.shopyyService.getCustomer(this.site, id); + async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise { + if(!where.id && !where.email && !where.phone){ + throw new Error('必须传入 id 或 email 或 phone') + } + const customer = await this.shopyyService.getCustomer(this.site, where.id); return this.mapPlatformToUnifiedCustomer(customer); } @@ -162,13 +165,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedCustomer(createdCustomer); } - async updateCustomer(id: string | number, data: Partial): Promise { - const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data); + async updateCustomer(where: {id: string | number}, data: Partial): Promise { + const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data); return this.mapPlatformToUnifiedCustomer(updatedCustomer); } - async deleteCustomer(id: string | number): Promise { - return await this.shopyyService.deleteCustomer(this.site, id); + async deleteCustomer(where: {id: string | number}): Promise { + return await this.shopyyService.deleteCustomer(this.site, where.id); } batchProcessCustomers?(data: BatchOperationDTO): Promise { @@ -221,13 +224,13 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedMedia(createdMedia); } - async updateMedia(id: string | number, data: any): Promise { - const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data); + async updateMedia(where: {id: string | number}, data: any): Promise { + const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data); return this.mapPlatformToUnifiedMedia(updatedMedia); } - async deleteMedia(id: string | number): Promise { - return await this.shopyyService.deleteMedia(this.site, id); + async deleteMedia(where: {id: string | number}): Promise { + return await this.shopyyService.deleteMedia(this.site, where.id); } mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { @@ -543,8 +546,8 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getOrder(id: string | number): Promise { - const data = await this.shopyyService.getOrder(this.site.id, String(id)); + async getOrder(where: {id: string | number}): Promise { + const data = await this.shopyyService.getOrder(this.site.id, String(where.id)); return this.mapPlatformToUnifiedOrder(data); } @@ -602,10 +605,10 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedOrder(createdOrder); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 使用映射方法转换参数 const requestParams = this.mapUpdateOrderParams(data); - return await this.shopyyService.updateOrder(this.site, String(id), requestParams); + return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams); } async deleteOrder(where: {id: string | number}): Promise { @@ -890,10 +893,18 @@ export class ShopyyAdapter implements ISiteAdapter { return params; } - async getProduct(id: string | number): Promise { - // 使用ShopyyService获取单个产品 - const product = await this.shopyyService.getProduct(this.site, id); - return this.mapPlatformToUnifiedProduct(product); + async getProduct(where: {id?: string | number, sku?: string}): Promise { + if(!where.id && !where.sku){ + throw new Error('必须传入 id 或 sku') + } + if (where.id) { + // 使用ShopyyService获取单个产品 + const product = await this.shopyyService.getProduct(this.site, where.id); + return this.mapPlatformToUnifiedProduct(product); + } else if (where.sku) { + // 通过sku获取产品 + return this.getProductBySku(where.sku); + } } async getProducts( @@ -951,24 +962,50 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(id: string | number, data: Partial): Promise { - // Shopyy update returns boolean? - // shopyyService.updateProduct returns boolean. - // So I can't return the updated product. - // I have to fetch it again or return empty/input. - // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock). + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } // 使用映射方法转换参数 const requestParams = this.mapUpdateProductParams(data); - await this.shopyyService.updateProduct(this.site, String(id), requestParams); + await this.shopyyService.updateProduct(this.site, productId, requestParams); return true; } - async deleteProduct(id: string | number): Promise { + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { + let productId: string | number; + if (where.id) { + productId = where.id; + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = product.id; + } else { + throw new Error('必须提供id或sku参数'); + } // Use batch delete - await this.shopyyService.batchProcessProducts(this.site, { delete: [id] }); + await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); return true; } + // 通过sku获取产品详情的私有方法 + private async getProductBySku(sku: string): Promise { + // 使用Shopyy API的搜索功能通过sku查询产品 + const response = await this.shopyyService.getProducts(this.site, 1, 100); + const product = response.items.find((item: any) => item.sku === sku); + if (!product) { + throw new Error(`未找到sku为${sku}的产品`); + } + return this.mapPlatformToUnifiedProduct(product); + } + async batchProcessProducts( data: { create?: any[]; update?: any[]; delete?: Array } ): Promise { @@ -980,9 +1017,6 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data - } mapUnifiedToPlatformReview(data: Partial) { return data @@ -1020,19 +1054,19 @@ export class ShopyyAdapter implements ISiteAdapter { async createReview(data: any): Promise { const createdReview = await this.shopyyService.createReview(this.site, data); - return this.mapReview(createdReview); + return this.mapPlatformToUnifiedReview(createdReview); } - async updateReview(id: string | number, data: any): Promise { - const updatedReview = await this.shopyyService.updateReview(this.site, id, data); - return this.mapReview(updatedReview); + async updateReview(where: {id: string | number}, data: any): Promise { + const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(updatedReview); } - async deleteReview(id: string | number): Promise { - return await this.shopyyService.deleteReview(this.site, id); + async deleteReview(where: {id: string | number}): Promise { + return await this.shopyyService.deleteReview(this.site, where.id); } - mapReview(review: any): UnifiedReviewDTO { + mapPlatformToUnifiedReview(review: any): UnifiedReviewDTO { // 将ShopYY评论数据映射到统一评论DTO格式 return { id: review.id || review.review_id, @@ -1187,9 +1221,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== Webhook映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data - } + mapUnifiedToPlatformWebhook(data: Partial) { return data @@ -1203,9 +1235,9 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - async getWebhook(id: string | number): Promise { - const webhook = await this.shopyyService.getWebhook(this.site, id); - return this.mapWebhook(webhook); + async getWebhook(where: {id: string | number}): Promise { + const webhook = await this.shopyyService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(webhook); } async getWebhooks(params: UnifiedSearchParamsDTO): Promise { @@ -1238,7 +1270,7 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteWebhook(this.site, where.id); } - mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(item: ShopyyWebhook): UnifiedWebhookDTO { return { id: item.id, name: item.webhook_name || `Webhook-${item.id}`, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 7950610..3f72e75 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -46,6 +46,9 @@ export class WooCommerceAdapter implements ISiteAdapter { this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); this.mapPlatformToUnifiedWebhook = this.mapPlatformToUnifiedWebhook.bind(this); } + mapUnifiedToPlatformCustomer(data: Partial) { + return data + } batchProcessProducts?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); } @@ -57,14 +60,8 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 客户映射方法 ========== - mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO { - return data; - } - mapUnifiedToPlatformCustomer(data: Partial) { - return data; - } - mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + mapPlatformToUnifiedCustomer(item: WooCustomer): UnifiedCustomerDTO { // 将 WooCommerce 客户数据映射为统一客户DTO // 包含基础信息地址信息与时间信息 return { @@ -153,10 +150,10 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户操作方法 - async getCustomer(id: string | number): Promise { + async getCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${id}`); - return this.mapCustomer(res.data); + const res = await api.get(`customers/${where.id}`); + return this.mapPlatformToUnifiedCustomer(res.data); } async getCustomers(params: UnifiedSearchParamsDTO): Promise> { @@ -167,7 +164,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { - items: items.map((i: any) => this.mapCustomer(i)), + items: items.map((i: any) => this.mapPlatformToUnifiedCustomer(i)), total, totalPages, page, @@ -184,36 +181,33 @@ export class WooCommerceAdapter implements ISiteAdapter { const requestParams = this.mapCustomerSearchParams(params || {}); const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams); - return customers.map((customer: any) => this.mapCustomer(customer)); + return customers.map((customer: any) => this.mapPlatformToUnifiedCustomer(customer)); } async createCustomer(data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('customers', data); - return this.mapCustomer(res.data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async updateCustomer(id: string | number, data: Partial): Promise { + async updateCustomer(where: {id: string | number}, data: Partial): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${id}`, data); - return this.mapCustomer(res.data); + const res = await api.put(`customers/${where.id}`, data); + return this.mapPlatformToUnifiedCustomer(res.data); } - async deleteCustomer(id: string | number): Promise { + async deleteCustomer(where: {id: string | number}): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${id}`, { force: true }); + await api.delete(`customers/${where.id}`, { force: true }); return true; } // ========== 媒体映射方法 ========== - mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO { - return data; - } mapUnifiedToPlatformMedia(data: Partial) { return data; } - mapMedia(item: WpMedia): UnifiedMediaDTO { + mapPlatformToUnifiedMedia(item: WpMedia): UnifiedMediaDTO { // 将 WordPress 媒体数据映射为统一媒体DTO // 兼容不同字段命名的时间信息 return { @@ -238,7 +232,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapMedia.bind(this)), + items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), total, totalPages, page, @@ -250,21 +244,21 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有媒体数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const media = await this.wpService.sdkGetAll(api, 'media', params); - return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); } createMedia(file: any): Promise { throw new Error('Method not implemented.'); } - async updateMedia(id: string | number, data: any): Promise { + async updateMedia(where: {id: string | number}, data: any): Promise { // 更新媒体信息 - return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); + return await this.wpService.updateMedia(Number(this.site.id), Number(where.id), data); } - async deleteMedia(id: string | number): Promise { + async deleteMedia(where: {id: string | number}): Promise { // 删除媒体资源 - await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); + await this.wpService.deleteMedia(Number(this.site.id), Number(where.id), true); return true; } @@ -275,9 +269,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 订单映射方法 ========== - mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO { - return data; - } mapUnifiedToPlatformOrder(data: Partial) { return data; } @@ -373,7 +364,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ].filter(Boolean).join(', '); } - mapOrder(item: WooOrder): UnifiedOrderDTO { + mapPlatformToUnifiedOrder(item: WooOrder): UnifiedOrderDTO { // 将 WooCommerce 订单数据映射为统一订单DTO // 包含账单地址与收货地址以及创建与更新时间 @@ -427,11 +418,11 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 订单操作方法 - async getOrder(id: string | number): Promise { + async getOrder(where: {id: string | number}): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`orders/${id}`); - return this.mapOrder(res.data); + const res = await api.get(`orders/${where.id}`); + return this.mapPlatformToUnifiedOrder(res.data); } async getOrders(params: UnifiedSearchParamsDTO): Promise> { @@ -461,7 +452,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: ordersWithFulfillments.map(this.mapOrder), + items: ordersWithFulfillments.map(this.mapPlatformToUnifiedOrder), total, totalPages, page, @@ -473,7 +464,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订单数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const orders = await this.wpService.sdkGetAll(api, 'orders', params); - return orders.map((order: any) => this.mapOrder(order)); + return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); } async countOrders(where: Record): Promise { @@ -492,18 +483,18 @@ export class WooCommerceAdapter implements ISiteAdapter { // 创建订单并返回统一订单DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); - return this.mapOrder(res.data); + return this.mapPlatformToUnifiedOrder(res.data); } - async updateOrder(id: string | number, data: Partial): Promise { + async updateOrder(where: {id: string | number}, data: Partial): Promise { // 更新订单并返回布尔结果 - return await this.wpService.updateOrder(this.site, String(id), data as any); + return await this.wpService.updateOrder(this.site, String(where.id), data as any); } - async deleteOrder(id: string | number): Promise { + async deleteOrder(where: {id: string | number}): Promise { // 删除订单 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`orders/${id}`, { force: true }); + await api.delete(`orders/${where.id}`, { force: true }); return true; } @@ -608,9 +599,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO { - return data; - } mapUnifiedToPlatformProduct(data: Partial) { return data; } @@ -712,7 +700,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapProduct(item: WooProduct): UnifiedProductDTO { + mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties @@ -800,11 +788,22 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - + async getProduct({id, sku}: {id?: string, sku?:string}){ + if(id) return this.getProductById(id); + if(sku) return this.getProductBySku(sku) + return this.getProductById(id || sku || ''); + } + async getProductBySku(sku: string){ + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products?sku=${sku}`); + const product = res.data[0]; + return this.mapPlatformToUnifiedProduct(product); + } // 产品操作方法 - async getProduct(id: string | number): Promise { + async getProductById(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const res = await api.get(`products/${id}`); const product = res.data; @@ -824,7 +823,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - return this.mapProduct(product); + return this.mapPlatformToUnifiedProduct(product); } async getProducts(params: UnifiedSearchParamsDTO): Promise> { @@ -865,6 +864,7 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { + items: productsWithVariations.map(this.mapPlatformToUnifiedProduct), items: productsWithVariations.map(this.mapPlatformToUnifiedProduct), total, totalPages, @@ -901,21 +901,34 @@ export class WooCommerceAdapter implements ISiteAdapter { }) ); - return productsWithVariations.map((product: any) => this.mapProduct(product)); + return productsWithVariations.map((product: any) => this.mapPlatformToUnifiedProduct(product)); } async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); return this.mapPlatformToUnifiedProduct(res); + return this.mapPlatformToUnifiedProduct(res); } + async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { // 更新产品并返回统一产品DTO - const res = await this.wpService.updateProduct(this.site, String(id), data as any); + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } + const res = await this.wpService.updateProduct(this.site, productId, data as any); return res; } + async deleteProduct(where: {id?: string | number, sku?: string}): Promise { async deleteProduct(where: {id?: string | number, sku?: string}): Promise { // 删除产品 let productId: string; @@ -928,8 +941,19 @@ export class WooCommerceAdapter implements ISiteAdapter { } else { throw new Error('必须提供id或sku参数'); } + let productId: string; + if (where.id) { + productId = String(where.id); + } else if (where.sku) { + // 通过sku获取产品ID + const product = await this.getProductBySku(where.sku); + productId = String(product.id); + } else { + throw new Error('必须提供id或sku参数'); + } const api = (this.wpService as any).createApi(this.site, 'wc/v3'); try { + await api.delete(`products/${productId}`, { force: true }); await api.delete(`products/${productId}`, { force: true }); return true; } catch (e) { @@ -938,9 +962,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 评论映射方法 ========== - mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO { - return data; - } + mapUnifiedToPlatformReview(data: Partial) { return data; } @@ -952,7 +974,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapReview(item: any): UnifiedReviewDTO & { raw: any } { + mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -984,6 +1006,7 @@ export class WooCommerceAdapter implements ISiteAdapter { requestParams ); return { + items: items.map(this.mapPlatformToUnifiedReview.bind(this)), items: items.map(this.mapPlatformToUnifiedReview.bind(this)), total, totalPages, @@ -997,31 +1020,33 @@ export class WooCommerceAdapter implements ISiteAdapter { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); + return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); } async createReview(data: any): Promise { const res = await this.wpService.createReview(this.site, data); return this.mapPlatformToUnifiedReview(res); + return this.mapPlatformToUnifiedReview(res); } + async updateReview(where: {id: number}, data: any): Promise { + const res = await this.wpService.updateReview(this.site, where.id, data); + return this.mapPlatformToUnifiedReview(res); async updateReview(where: {id: number}, data: any): Promise { const res = await this.wpService.updateReview(this.site, where.id, data); return this.mapPlatformToUnifiedReview(res); } - async deleteReview(id: number): Promise { - return await this.wpService.deleteReview(this.site, id); + async deleteReview(where: {id: number}): Promise { + return await this.wpService.deleteReview(this.site, where.id); } // ========== 订阅映射方法 ========== - mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { - return data; - } mapUnifiedToPlatformSubscription(data: Partial) { return data; } - mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + mapPlatformToUnifiedSubscription(item: WooSubscription): UnifiedSubscriptionDTO { // 将 WooCommerce 订阅数据映射为统一订阅DTO // 若缺少创建时间则回退为开始时间 return { @@ -1062,7 +1087,7 @@ export class WooCommerceAdapter implements ISiteAdapter { params ); return { - items: items.map(this.mapSubscription), + items: items.map(this.mapPlatformToUnifiedSubscription), total, totalPages, page, @@ -1077,7 +1102,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有订阅数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); - return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); + return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); } // ========== 变体映射方法 ========== @@ -1322,9 +1347,6 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 网络钩子映射方法 ========== - mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO { - return data; - } mapUnifiedToPlatformWebhook(data: Partial) { return data; } @@ -1337,7 +1359,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 映射 WooCommerce webhook 到统一格式 - mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO { + mapPlatformToUnifiedWebhook(webhook: WooWebhook): UnifiedWebhookDTO { return { id: webhook.id.toString(), name: webhook.name, @@ -1359,12 +1381,12 @@ export class WooCommerceAdapter implements ISiteAdapter { const result = await this.wpService.getWebhooks(this.site, params); return { - items: (result.items as WooWebhook[]).map(this.mapWebhook), - total: result.total, - page: Number(params.page || 1), - per_page: Number(params.per_page || 20), - totalPages: result.totalPages, - }; + items: (result.items as WooWebhook[]).map(this.mapPlatformToUnifiedWebhook), + total: result.total, + page: Number(params.page || 1), + per_page: Number(params.per_page || 20), + totalPages: result.totalPages, + }; } catch (error) { throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`); } @@ -1375,17 +1397,17 @@ export class WooCommerceAdapter implements ISiteAdapter { try { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); - return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); } catch (error) { throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); } } // 获取单个 webhook 详情 - async getWebhook(id: string | number): Promise { + async getWebhook(where: {id: string | number}): Promise { try { - const result = await this.wpService.getWebhook(this.site, id); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.getWebhook(this.site, where.id); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`); } @@ -1403,14 +1425,14 @@ export class WooCommerceAdapter implements ISiteAdapter { api_version: data.api_version || 'wp/v2', }; const result = await this.wpService.createWebhook(this.site, params); - return this.mapWebhook(result as WooWebhook); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 更新现有的 webhook - async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise { + async updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise { try { const params = { ...(data.name ? { name: data.name } : {}), @@ -1420,17 +1442,17 @@ export class WooCommerceAdapter implements ISiteAdapter { ...(data.secret ? { secret: data.secret } : {}), ...(data.api_version ? { api_version: data.api_version } : {}), }; - const result = await this.wpService.updateWebhook(this.site, id, params); - return this.mapWebhook(result as WooWebhook); + const result = await this.wpService.updateWebhook(this.site, where.id, params); + return this.mapPlatformToUnifiedWebhook(result as WooWebhook); } catch (error) { throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`); } } // 删除指定的 webhook - async deleteWebhook(id: string | number): Promise { + async deleteWebhook(where: Partial>): Promise { try { - await this.wpService.deleteWebhook(this.site, id); + await this.wpService.deleteWebhook(this.site, where.id); return true; } catch (error) { throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`); diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 83c21ae..32a0730 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -19,7 +19,7 @@ import { import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -export interface ISiteAdapter { +export interface ISiteAdapter { // ========== 客户映射方法 ========== /** * 将平台客户数据转换为统一客户数据格式 @@ -39,7 +39,7 @@ export interface ISiteAdapter { * 获取单个客户 * 获取单个客户 */ - getCustomer(id: string | number): Promise; + getCustomer(where: Partial>): Promise; /** * 获取客户列表 @@ -60,12 +60,12 @@ export interface ISiteAdapter { /** * 更新客户 */ - updateCustomer(id: string | number, data: Partial): Promise; + updateCustomer(where: Partial>, data: Partial): Promise; /** * 删除客户 */ - deleteCustomer(id: string | number): Promise; + deleteCustomer(where: Partial>): Promise; /** * 批量处理客户 @@ -152,7 +152,7 @@ export interface ISiteAdapter { * 获取单个订单 * 获取单个订单 */ - getOrder(id: string | number): Promise; + getOrder(where: Partial>): Promise; /** * 获取订单列表 @@ -172,7 +172,7 @@ export interface ISiteAdapter { * 获取订单总数 * 获取订单总数 */ - countOrders(params: Record): Promise; + countOrders(params: Record): Promise; /** * 创建订单 @@ -185,13 +185,13 @@ export interface ISiteAdapter { * 更新订单 * 更新订单 */ - updateOrder(id: string | number, data: Partial): Promise; + updateOrder(where: Partial>, data: Partial): Promise; /** * 删除订单 * 删除订单 */ - deleteOrder(id: string | number): Promise; + deleteOrder(where: Partial>): Promise; /** * 获取订单备注 @@ -288,7 +288,7 @@ export interface ISiteAdapter { /** * 获取单个产品 */ - getProduct(id: string | number): Promise; + getProduct(where: Partial>): Promise; /** * 获取产品列表 @@ -308,12 +308,12 @@ export interface ISiteAdapter { /** * 更新产品 */ - updateProduct(id: string | number, data: Partial): Promise; + updateProduct(where: Partial>, data: Partial): Promise; /** * 删除产品 */ - deleteProduct(id: string | number): Promise; + deleteProduct(where: Partial>): Promise; /** * 批量处理产品 @@ -367,12 +367,12 @@ export interface ISiteAdapter { /** * 更新评论 */ - updateReview(id: number, data: UpdateReviewDTO): Promise; + updateReview(where: Partial>, data: UpdateReviewDTO): Promise; /** * 删除评论 */ - deleteReview(id: number): Promise; + deleteReview(where: Partial>): Promise; // ========== 订阅映射方法 ========== /** @@ -490,7 +490,7 @@ export interface ISiteAdapter { /** * 获取单个webhook */ - getWebhook(id: string | number): Promise; + getWebhook(where: Partial>): Promise; /** * 获取webhooks列表 @@ -510,16 +510,16 @@ export interface ISiteAdapter { /** * 更新webhook */ - updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise; + updateWebhook(where: Partial>, data: UpdateWebhookDTO): Promise; /** * 删除webhook */ - deleteWebhook(id: string | number): Promise; + deleteWebhook(where: Partial>): Promise; // ========== 站点/其他方法 ========== /** * 获取站点链接列表 */ - getLinks(): Promise>; + getLinks(): Promise>; } From 2e62a0cdb2e801e81afedd406e91dbba75917f26 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 15:03:11 +0800 Subject: [PATCH 16/24] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96SKU=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加字典排序字段支持 优化产品同步流程,支持通过SKU同步 重构SKU模板生成逻辑,支持分类属性排序 完善产品导入导出功能,增加分类字段处理 统一产品操作方法,提升代码可维护性 --- package-lock.json | 17 -- src/adapter/shopyy.adapter.ts | 2 +- src/adapter/woocommerce.adapter.ts | 356 ++++++++++++++++++--------- src/controller/product.controller.ts | 43 ++-- src/db/seeds/template.seeder.ts | 39 ++- src/dto/site-api.dto.ts | 3 + src/dto/woocommerce.dto.ts | 4 +- src/entity/dict.entity.ts | 4 + src/service/product.service.ts | 118 ++++----- src/service/site-api.service.ts | 43 ++-- src/service/wp.service.ts | 2 +- 11 files changed, 379 insertions(+), 252 deletions(-) diff --git a/package-lock.json b/package-lock.json index e207cd1..405c79b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -523,23 +523,6 @@ "node": ">=18" } }, - "node_modules/@faker-js/faker": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz", - "integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", - "npm": ">=10" - } - }, "node_modules/@hapi/bourne": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz", diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 6528355..f0abdcb 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -774,7 +774,7 @@ export class ShopyyAdapter implements ISiteAdapter { return data } - mapCreateProductParams(data: Partial): any { + mapCreateProductParams(data: Partial): Partial { // 构建 ShopYY 产品创建参数 const params: any = { name: data.name || '', diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 3f72e75..8a61b80 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -150,9 +150,30 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户操作方法 - async getCustomer(where: {id: string | number}): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`customers/${where.id}`); + async getCustomer(where: Partial>): Promise { + const api = this.wpService.createApi(this.site, 'wc/v3'); + // 根据提供的条件构建查询参数 + let endpoint: string; + if (where.id) { + endpoint = `customers/${where.id}`; + } else if (where.email) { + // 使用邮箱查询客户 + const res = await api.get('customers', { params: { email: where.email } }); + if (!res.data || res.data.length === 0) { + throw new Error('Customer not found'); + } + return this.mapPlatformToUnifiedCustomer(res.data[0]); + } else if (where.phone) { + // 使用电话查询客户 + const res = await api.get('customers', { params: { search: where.phone } }); + if (!res.data || res.data.length === 0) { + throw new Error('Customer not found'); + } + return this.mapPlatformToUnifiedCustomer(res.data[0]); + } else { + throw new Error('Must provide at least one of id, email, or phone'); + } + const res = await api.get(endpoint); return this.mapPlatformToUnifiedCustomer(res.data); } @@ -175,7 +196,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有客户数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); // 处理orderBy参数,转换为WooCommerce API需要的格式 const requestParams = this.mapCustomerSearchParams(params || {}); @@ -185,20 +206,42 @@ export class WooCommerceAdapter implements ISiteAdapter { } async createCustomer(data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.post('customers', data); return this.mapPlatformToUnifiedCustomer(res.data); } - async updateCustomer(where: {id: string | number}, data: Partial): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.put(`customers/${where.id}`, data); + async updateCustomer(where: Partial>, data: Partial): Promise { + const api = this.wpService.createApi(this.site, 'wc/v3'); + let customerId: string | number; + + // 先根据条件获取客户ID + if (where.id) { + customerId = where.id; + } else { + // 如果没有提供ID,则先查询客户 + const customer = await this.getCustomer(where); + customerId = customer.id; + } + + const res = await api.put(`customers/${customerId}`, data); return this.mapPlatformToUnifiedCustomer(res.data); } - async deleteCustomer(where: {id: string | number}): Promise { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - await api.delete(`customers/${where.id}`, { force: true }); + async deleteCustomer(where: Partial>): Promise { + const api = this.wpService.createApi(this.site, 'wc/v3'); + let customerId: string | number; + + // 先根据条件获取客户ID + if (where.id) { + customerId = where.id; + } else { + // 如果没有提供ID,则先查询客户 + const customer = await this.getCustomer(where); + customerId = customer.id; + } + + await api.delete(`customers/${customerId}`, { force: true }); return true; } @@ -242,7 +285,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有媒体数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const media = await this.wpService.sdkGetAll(api, 'media', params); return media.map((mediaItem: any) => this.mapPlatformToUnifiedMedia(mediaItem)); } @@ -420,7 +463,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 订单操作方法 async getOrder(where: {id: string | number}): Promise { // 获取单个订单详情 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.get(`orders/${where.id}`); return this.mapPlatformToUnifiedOrder(res.data); } @@ -462,7 +505,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有订单数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const orders = await this.wpService.sdkGetAll(api, 'orders', params); return orders.map((order: any) => this.mapPlatformToUnifiedOrder(order)); } @@ -481,7 +524,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async createOrder(data: Partial): Promise { // 创建订单并返回统一订单DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); return this.mapPlatformToUnifiedOrder(res.data); } @@ -493,7 +536,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async deleteOrder(where: {id: string | number}): Promise { // 删除订单 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); await api.delete(`orders/${where.id}`, { force: true }); return true; } @@ -558,14 +601,14 @@ export class WooCommerceAdapter implements ISiteAdapter { async getOrderNotes(orderId: string | number): Promise { // 获取订单备注列表 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.get(`orders/${orderId}/notes`); return res.data; } async createOrderNote(orderId: string | number, data: any): Promise { // 创建订单备注 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.post(`orders/${orderId}/notes`, data); return res.data; } @@ -576,7 +619,7 @@ export class WooCommerceAdapter implements ISiteAdapter { }): Promise { throw new Error('暂未实现'); // 取消订单履行 - // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + // const api = this.wpService.createApi(this.site, 'wc/v3'); // try { // // 将订单状态改回处理中 @@ -599,15 +642,105 @@ export class WooCommerceAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapUnifiedToPlatformProduct(data: Partial) { - return data; + mapUnifiedToPlatformProduct(data: Partial): Partial { + // 将统一产品DTO映射为WooCommerce产品数据 + // 基本字段映射 + const mapped: Partial = { + id: data.id as number, + name: data.name, + type: data.type, + status: data.status, + sku: data.sku, + regular_price: data.regular_price, + sale_price: data.sale_price, + price: data.price, + stock_status: data.stock_status as 'instock' | 'outofstock' | 'onbackorder', + stock_quantity: data.stock_quantity, + // 映射更多WooCommerce产品特有的字段 + // featured: data.featured, + // catalog_visibility: data.catalog_visibility, + // date_on_sale_from: data.date_on_sale_from, + // date_on_sale_to: data.date_on_sale_to, + // virtual: data.virtual, + // downloadable: data.downloadable, + // description: data.description, + // short_description: data.short_description, + // slug: data.slug, + // manage_stock: data.manage_stock, + // backorders: data.backorders as 'no' | 'notify' | 'yes', + // sold_individually: data.sold_individually, + // weight: data.weight, + // dimensions: data.dimensions, + // shipping_class: data.shipping_class, + // tax_class: data.tax_class, + }; + + // 映射图片数据 + if (data.images && Array.isArray(data.images)) { + mapped.images = data.images.map(img => ({ + id: img.id as number, + src: img.src, + name: img.name, + alt: img.alt, + })); + } + + // 映射分类数据 + if (data.categories && Array.isArray(data.categories)) { + mapped.categories = data.categories.map(cat => ({ + // id: cat.id as number, //TODO + name: cat.name, + })); + } + + // 映射标签数据 + // TODO tags 应该可以设置 + // if (data.tags && Array.isArray(data.tags)) { + // mapped.tags = data.tags.map(tag => { + // return ({ + // // id: tag.id as number, + // name: tag.name, + // }); + // }); + // } + + // 映射属性数据 + if (data.attributes && Array.isArray(data.attributes)) { + mapped.attributes = data.attributes.map(attr => ({ + // id 由于我们这个主要用来存,所以不映射 id + name: attr.name, + visible: attr.visible, + variation: attr.variation, + options: attr.options + })); + } + + // 映射变体数据(注意:WooCommerce API 中变体通常通过单独的端点处理) + // 这里只映射变体的基本信息,具体创建/更新变体需要额外处理 + if (data.variations && Array.isArray(data.variations)) { + // 对于WooProduct类型,variations字段只存储变体ID + mapped.variations = data.variations.map(variation => variation.id as number); + } + + // 映射下载数据(如果产品是可下载的) + // if (data.downloads && Array.isArray(data.downloads)) { + // mapped.downloads = data.downloads.map(download => ({ + // id: download.id as number, + // name: download.name, + // file: download.file, + // })); + // } + + return mapped; } - mapCreateProductParams(data: Partial) { - return data; + mapCreateProductParams(data: Partial):Partial { + const {id,...mapped}= this.mapUnifiedToPlatformProduct(data); + // 创建不带 id + return mapped } - mapUpdateProductParams(data: Partial) { - return data; + mapUpdateProductParams(data: Partial): Partial { + return this.mapUnifiedToPlatformProduct(data); } mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { @@ -700,14 +833,14 @@ export class WooCommerceAdapter implements ISiteAdapter { return mapped; } - mapPlatformToUnifiedProduct(item: WooProduct): UnifiedProductDTO { + mapPlatformToUnifiedProduct(data: WooProduct): UnifiedProductDTO { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties // 映射变体数据 - const mappedVariations = item.variations && Array.isArray(item.variations) - ? item.variations + const mappedVariations = data.variations && Array.isArray(data.variations) + ? data.variations .filter((variation: any) => typeof variation !== 'number') // 过滤掉数字类型的变体ID .map((variation: any) => { // 将变体属性转换为统一格式 @@ -734,7 +867,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return { id: variation.id, - name: variation.name || item.name, // 如果变体没有名称,使用父产品名称 + name: variation.name || data.name, // 如果变体没有名称,使用父产品名称 sku: variation.sku || '', regular_price: String(variation.regular_price || ''), sale_price: String(variation.sale_price || ''), @@ -748,34 +881,34 @@ export class WooCommerceAdapter implements ISiteAdapter { : []; return { - id: item.id, - date_created: item.date_created, - date_modified: item.date_modified, - type: item.type, // simple grouped external variable - status: item.status, // draft pending private publish - sku: item.sku, - name: item.name, + id: data.id, + date_created: data.date_created, + date_modified: data.date_modified, + type: data.type, // simple grouped external variable + status: data.status, // draft pending private publish + sku: data.sku, + name: data.name, //价格 - regular_price: item.regular_price, - sale_price: item.sale_price, - price: item.price, - stock_status: item.stock_status, - stock_quantity: item.stock_quantity, - images: (item.images || []).map((img: any) => ({ + regular_price: data.regular_price, + sale_price: data.sale_price, + price: data.price, + stock_status: data.stock_status, + stock_quantity: data.stock_quantity, + images: (data.images || []).map((img: any) => ({ id: img.id, src: img.src, name: img.name, alt: img.alt, })), - categories: (item.categories || []).map((c: any) => ({ + categories: (data.categories || []).map((c: any) => ({ id: c.id, name: c.name, })), - tags: (item.tags || []).map((t: any) => ({ + tags: (data.tags || []).map((t: any) => ({ id: t.id, name: t.name, })), - attributes: (item.attributes || []).map(attr => ({ + attributes: (data.attributes || []).map(attr => ({ id: attr.id, name: attr.name || '', position: attr.position, @@ -784,25 +917,39 @@ export class WooCommerceAdapter implements ISiteAdapter { options: attr.options || [] })), variations: mappedVariations, - permalink: item.permalink, - raw: item, + permalink: data.permalink, + raw: data, }; } - async getProduct({id, sku}: {id?: string, sku?:string}){ + // 判断是否是这个站点的sku + isSiteSkuThisSite(sku: string,){ + return sku.startsWith(this.site.skuPrefix+'-'); + } + async getProduct(where: Partial>): Promise{ + const { id, sku } = where; if(id) return this.getProductById(id); - if(sku) return this.getProductBySku(sku) - return this.getProductById(id || sku || ''); + if(sku) return this.getProductBySku(sku); + throw new Error('必须提供id或sku参数'); } async getProductBySku(sku: string){ - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - const res = await api.get(`products?sku=${sku}`); - const product = res.data[0]; + // const api = this.wpService.createApi(this.site, 'wc/v3'); + // const res = await api.get(`products`,{ + // sku + // }); + // const product = res.data[0]; + const res = await this.wpService.getProducts(this.site,{ + sku, + page:1, + per_page:1, + }); + const product = res?.items?.[0]; + if(!product) return null return this.mapPlatformToUnifiedProduct(product); } // 产品操作方法 async getProductById(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const res = await api.get(`products/${id}`); const product = res.data; @@ -849,7 +996,7 @@ export class WooCommerceAdapter implements ISiteAdapter { try { // 批量获取该产品的所有变体数据 const variations = await this.wpService.sdkGetAll( - (this.wpService as any).createApi(this.site, 'wc/v3'), + this.wpService.createApi(this.site, 'wc/v3'), `products/${item.id}/variations` ); // 将完整的变体数据添加到产品对象中 @@ -876,7 +1023,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有产品数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const products = await this.wpService.sdkGetAll(api, 'products', params); // 对于类型为 variable 的产品,需要加载完整的变体数据 @@ -906,55 +1053,31 @@ export class WooCommerceAdapter implements ISiteAdapter { async createProduct(data: Partial): Promise { // 创建产品并返回统一产品DTO - const res = await this.wpService.createProduct(this.site, data); - return this.mapPlatformToUnifiedProduct(res); + const createData = this.mapCreateProductParams(data); + const res = await this.wpService.createProduct(this.site, createData); return this.mapPlatformToUnifiedProduct(res); } - async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { - async updateProduct(where: {id?: string | number, sku?: string}, data: Partial): Promise { + async updateProduct(where: Partial>, data: Partial): Promise { // 更新产品并返回统一产品DTO - let productId: string; - if (where.id) { - productId = String(where.id); - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = String(product.id); - } else { - throw new Error('必须提供id或sku参数'); + const product = await this.getProduct(where); + if(!product){ + throw new Error('产品不存在'); } - const res = await this.wpService.updateProduct(this.site, productId, data as any); + const updateData = this.mapUpdateProductParams(data); + const res = await this.wpService.updateProduct(this.site, String(product.id), updateData as any); return res; } - async deleteProduct(where: {id?: string | number, sku?: string}): Promise { - async deleteProduct(where: {id?: string | number, sku?: string}): Promise { + async deleteProduct(where: Partial>): Promise { // 删除产品 - let productId: string; - if (where.id) { - productId = String(where.id); - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = String(product.id); - } else { - throw new Error('必须提供id或sku参数'); + const product = await this.getProduct(where); + if(!product){ + throw new Error('产品不存在'); } - let productId: string; - if (where.id) { - productId = String(where.id); - } else if (where.sku) { - // 通过sku获取产品ID - const product = await this.getProductBySku(where.sku); - productId = String(product.id); - } else { - throw new Error('必须提供id或sku参数'); - } - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); try { - await api.delete(`products/${productId}`, { force: true }); - await api.delete(`products/${productId}`, { force: true }); + await api.delete(`products/${product.id}`, { force: true }); return true; } catch (e) { return false; @@ -974,7 +1097,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return data; } - mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO & { raw: any } { + mapPlatformToUnifiedReview(item: any): UnifiedReviewDTO { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -985,7 +1108,7 @@ export class WooCommerceAdapter implements ISiteAdapter { rating: item.rating, status: item.status, date_created: item.date_created, - raw: item + date_modified: item.date_modified, }; } @@ -1017,28 +1140,33 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllReviews(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有评论数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); } - async createReview(data: any): Promise { + async createReview(data: CreateReviewDTO): Promise { const res = await this.wpService.createReview(this.site, data); return this.mapPlatformToUnifiedReview(res); return this.mapPlatformToUnifiedReview(res); } - async updateReview(where: {id: number}, data: any): Promise { - const res = await this.wpService.updateReview(this.site, where.id, data); - return this.mapPlatformToUnifiedReview(res); - async updateReview(where: {id: number}, data: any): Promise { - const res = await this.wpService.updateReview(this.site, where.id, data); + async updateReview(where: Partial>, data: UpdateReviewDTO): Promise { + const { id } = where; + if (!id) { + throw new Error('必须提供评论ID'); + } + const res = await this.wpService.updateReview(this.site, Number(id), data); return this.mapPlatformToUnifiedReview(res); } - async deleteReview(where: {id: number}): Promise { - return await this.wpService.deleteReview(this.site, where.id); + async deleteReview(where: Partial>): Promise { + const { id } = where; + if (!id) { + throw new Error('必须提供评论ID'); + } + return await this.wpService.deleteReview(this.site, Number(id)); } // ========== 订阅映射方法 ========== @@ -1098,22 +1226,22 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有订阅数据,不受分页限制 - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // 使用sdkGetAll获取所有订阅数据,不受分页限制 - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); } // ========== 变体映射方法 ========== mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { - return data; + // 使用mapVariation方法来实现统一的变体映射逻辑 + return this.mapVariation(data); } mapUnifiedToPlatformVariation(data: Partial) { return data; // ========== 变体映射方法 ========== mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { - return data; + // 使用mapVariation方法来实现统一的变体映射逻辑 + return this.mapVariation(data); } mapUnifiedToPlatformVariation(data: Partial) { return data; @@ -1197,7 +1325,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 获取所有产品变体 async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const variations = await this.wpService.sdkGetAll(api, `products/${productId}/variations`, params); // 获取产品名称用于变体显示 @@ -1395,7 +1523,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // 获取所有webhooks async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { try { - const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const api = this.wpService.createApi(this.site, 'wc/v3'); const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); return webhooks.map((webhook: any) => this.mapPlatformToUnifiedWebhook(webhook)); } catch (error) { @@ -1476,11 +1604,11 @@ export class WooCommerceAdapter implements ISiteAdapter { return links; } - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + batchProcessOrders?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); } - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise { + batchProcessCustomers?(data: BatchOperationDTO): Promise { throw new Error('Method not implemented.'); } } diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index aef81f8..c263d16 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -698,10 +698,10 @@ export class ProductController { // 从站点同步产品到本地 @ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes }) @Post('/sync-from-site') - async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number }) { + async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number ,sku: string}) { try { - const { siteId, siteProductId } = body; - const product = await this.productService.syncProductFromSite(siteId, siteProductId); + const { siteId, siteProductId, sku } = body; + const product = await this.productService.syncProductFromSite(siteId, siteProductId, sku); return successResponse(product); } catch (error) { return errorResponse(error?.message || error); @@ -713,25 +713,26 @@ export class ProductController { @Post('/batch-sync-from-site') async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) { try { - const { siteId, siteProductIds } = body; - const result = await this.productService.batchSyncFromSite(siteId, siteProductIds); - // 将服务层返回的结果转换为统一格式 - const errors = result.errors.map((error: string) => { - // 提取产品ID部分作为标识符 - const match = error.match(/站点产品ID (\d+) /); - const identifier = match ? match[1] : 'unknown'; - return { - identifier: identifier, - error: error - }; - }); + throw new Error('批量同步产品到本地暂未实现'); + // const { siteId, siteProductIds } = body; + // const result = await this.productService.batchSyncFromSite(siteId, siteProductIds.map((id) => ({ siteProductId: id, sku: '' }))); + // // 将服务层返回的结果转换为统一格式 + // const errors = result.errors.map((error: string) => { + // // 提取产品ID部分作为标识符 + // const match = error.match(/站点产品ID (\d+) /); + // const identifier = match ? match[1] : 'unknown'; + // return { + // identifier: identifier, + // error: error + // }; + // }); - return successResponse({ - total: siteProductIds.length, - processed: result.synced + errors.length, - synced: result.synced, - errors: errors - }); + // return successResponse({ + // total: siteProductIds.length, + // processed: result.synced + errors.length, + // synced: result.synced, + // errors: errors + // }); } catch (error) { return errorResponse(error?.message || error); } diff --git a/src/db/seeds/template.seeder.ts b/src/db/seeds/template.seeder.ts index f12a208..c531f73 100644 --- a/src/db/seeds/template.seeder.ts +++ b/src/db/seeds/template.seeder.ts @@ -23,19 +23,38 @@ export default class TemplateSeeder implements Seeder { const templates = [ { name: 'product.sku', - value: "<%= [it.category.shortName].concat(it.attributes.map(a => a.shortName)).join('-') %>", + value: `<% + // 按分类判断属性排序逻辑 + if (it.category.name === 'nicotine-pouches') { + // 1. 定义 nicotine-pouches 专属的属性固定顺序 + const fixedOrder = ['brand','category', 'flavor', 'strength', 'humidity']; + sortedAttrShortNames = fixedOrder.map(attrKey => { + if(attrKey === 'category') return it.category.shortName + // 排序 + const matchedAttr = it.attributes.find(a => a?.dict?.name === attrKey); + return matchedAttr ? matchedAttr.shortName : ''; + }).filter(Boolean); // 移除空值,避免多余的 "-" + } else { + // 非目标分类,保留 attributes 原有顺序 + sortedAttrShortNames = it.attributes.map(a => a.shortName); + } + + // 4. 拼接分类名 + 排序后的属性名 + %><%= sortedAttrShortNames.join('-') %><% +%>`, description: '产品SKU模板', testData: JSON.stringify({ - category: { - shortName: 'CAT', + "category": { + "name": "nicotine-pouches", + "shortName": "NP" }, - attributes: [ - { shortName: 'BR' }, - { shortName: 'FL' }, - { shortName: '10MG' }, - { shortName: 'DRY' }, - ], - }), + "attributes": [ + { "dict": {"name": "brand"},"shortName": "YOONE" }, + { "dict": {"name": "flavor"},"shortName": "FL" }, + { "dict": {"name": "strength"},"shortName": "10MG" }, + { "dict": {"name": "humidity"},"shortName": "DRY" } + ] +}), }, { name: 'product.title', diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index a93f125..247c5c9 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger'; import { UnifiedPaginationDTO, } from './api.dto'; +import { Dict } from '../entity/dict.entity'; // export class UnifiedOrderWhere{ // [] // } @@ -137,6 +138,8 @@ export class UnifiedProductAttributeDTO { @ApiProperty({ description: '变体属性值(单个值)', required: false }) option?: string; + // 这个是属性的父级字典项 + dict?: Dict; } export class UnifiedProductVariationDTO { diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index 3150a78..f562d64 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -117,9 +117,9 @@ export interface WooProduct { // 购买备注 purchase_note?: string; // 分类列表 - categories?: Array<{ id: number; name?: string; slug?: string }>; + categories?: Array<{ id?: number; name?: string; slug?: string }>; // 标签列表 - tags?: Array<{ id: number; name?: string; slug?: string }>; + tags?: Array<{ id?: number; name?: string; slug?: string }>; // 菜单排序 menu_order?: number; // 元数据 diff --git a/src/entity/dict.entity.ts b/src/entity/dict.entity.ts index dc0d9af..15776da 100644 --- a/src/entity/dict.entity.ts +++ b/src/entity/dict.entity.ts @@ -29,6 +29,10 @@ export class Dict { @OneToMany(() => DictItem, item => item.dict) items: DictItem[]; + // 排序 + @Column({ default: 0, comment: '排序' }) + sort: number; + // 是否可删除 @Column({ default: true, comment: '是否可删除' }) deletable: boolean; diff --git a/src/service/product.service.ts b/src/service/product.service.ts index bbde5d9..384eca8 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -28,7 +28,7 @@ import { StockPoint } from '../entity/stock_point.entity'; import { StockService } from './stock.service'; import { TemplateService } from './template.service'; -import { SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; +import { BatchErrorItem, BatchOperationResult, SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { UnifiedProductDTO } from '../dto/site-api.dto'; import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto'; import { Category } from '../entity/category.entity'; @@ -225,7 +225,7 @@ export class ProductService { where: { sku, }, - relations: ['category', 'attributes', 'attributes.dict', 'siteSkus'] + relations: ['category', 'attributes', 'attributes.dict'] }); } @@ -1440,7 +1440,7 @@ export class ProductService { // 解析属性字段(分号分隔多值) const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []); - + // 将属性解析为 DTO 输入 const attributes: any[] = []; @@ -1455,6 +1455,9 @@ export class ProductService { } } + // 处理分类字段 + const category = val(rec.category); + return { sku, name: val(rec.name), @@ -1464,6 +1467,7 @@ export class ProductService { promotionPrice: num(rec.promotionPrice), type: val(rec.type), siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined, + category, // 添加分类字段 attributes: attributes.length > 0 ? attributes : undefined, } as any; @@ -1483,10 +1487,15 @@ export class ProductService { if (data.price !== undefined) dto.price = Number(data.price); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); - if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); + // 处理分类字段 + if (data.categoryId !== undefined) { + dto.categoryId = Number(data.categoryId); + } else if (data.category) { + // 如果是字符串,需要后续在createProduct中处理 + dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }]; + } // 默认值和特殊处理 - dto.attributes = Array.isArray(data.attributes) ? data.attributes : []; // 如果有组件信息,透传 @@ -1508,7 +1517,13 @@ export class ProductService { if (data.price !== undefined) dto.price = Number(data.price); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); - if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); + // 处理分类字段 + if (data.categoryId !== undefined) { + dto.categoryId = Number(data.categoryId); + } else if (data.category) { + // 如果是字符串,需要后续在updateProduct中处理 + dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }]; + } if (data.type !== undefined) dto.type = data.type; if (data.attributes !== undefined) dto.attributes = data.attributes; @@ -1548,8 +1563,8 @@ export class ProductService { esc(p.price), esc(p.promotionPrice), esc(p.type), - esc(p.description), + esc(p.category ? p.category.name || p.category.title : ''), // 添加分类字段 ]; // 属性数据 @@ -1575,9 +1590,9 @@ export class ProductService { // 导出所有产品为 CSV 文本 async exportProductsCSV(): Promise { - // 查询所有产品及其属性(包含字典关系)和组成 + // 查询所有产品及其属性(包含字典关系)、组成和分类 const products = await this.productModel.find({ - relations: ['attributes', 'attributes.dict', 'components'], + relations: ['attributes', 'attributes.dict', 'components', 'category'], order: { id: 'ASC' }, }); @@ -1612,8 +1627,8 @@ export class ProductService { 'price', 'promotionPrice', 'type', - 'description', + 'category', ]; // 动态属性表头 @@ -1640,7 +1655,7 @@ export class ProductService { } // 从 CSV 导入产品;存在则更新,不存在则创建 - async importProductsCSV(file: any): Promise<{ created: number; updated: number; errors: string[] }> { + async importProductsCSV(file: any): Promise { let buffer: Buffer; if (Buffer.isBuffer(file)) { buffer = file; @@ -1676,19 +1691,19 @@ export class ProductService { console.log('First record keys:', Object.keys(records[0])); } } catch (e: any) { - return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] }; + throw new Error(`CSV 解析失败:${e?.message || e}`) } let created = 0; let updated = 0; - const errors: string[] = []; + const errors: BatchErrorItem[] = []; // 逐条处理记录 for (const rec of records) { try { const data = this.transformCsvRecordToData(rec); if (!data) { - errors.push('缺少 SKU 的记录已跳过'); + errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过'}); continue; } const { sku } = data; @@ -1708,11 +1723,11 @@ export class ProductService { updated += 1; } } catch (e: any) { - errors.push(`产品${rec?.sku}导入失败:${e?.message || String(e)}`); + errors.push({ identifier: '' + rec.sku, error: `产品${rec?.sku}导入失败:${e?.message || String(e)}`}); } } - return { created, updated, errors }; + return { total: records.length, processed: records.length - errors.length, created, updated, errors }; } // 将库存记录的 sku 添加到产品单品中 @@ -1831,9 +1846,7 @@ export class ProductService { } // 将本地产品转换为站点API所需格式 - const unifiedProduct = await this.convertLocalProductToUnifiedProduct(localProduct, params.siteSku); - - + const unifiedProduct = await this.mapLocalToUnifiedProduct(localProduct, params.siteSku); // 调用站点API的upsertProduct方法 try { @@ -1842,7 +1855,7 @@ export class ProductService { await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]); return result; } catch (error) { - throw new Error(`同步产品到站点失败: ${error.message}`); + throw new Error(`同步产品到站点失败: ${error?.response?.data?.message??error.message}`); } } @@ -1869,9 +1882,6 @@ export class ProductService { siteSku: item.siteSku }); - // 然后绑定站点SKU - await this.bindSiteSkus(item.productId, [item.siteSku]); - results.synced++; results.processed++; } catch (error) { @@ -1892,30 +1902,23 @@ export class ProductService { * @param siteProductId 站点产品ID * @returns 同步后的本地产品 */ - async syncProductFromSite(siteId: number, siteProductId: string | number): Promise { + async syncProductFromSite(siteId: number, siteProductId: string | number, sku: string): Promise { + const adapter = await this.siteApiService.getAdapter(siteId); + const siteProduct = await adapter.getProduct({ id: siteProductId }); // 从站点获取产品信息 - const siteProduct = await this.siteApiService.getProductFromSite(siteId, siteProductId); if (!siteProduct) { throw new Error(`站点产品 ID ${siteProductId} 不存在`); } - - // 检查是否已存在相同SKU的本地产品 - let localProduct = null; - if (siteProduct.sku) { - try { - localProduct = await this.findProductBySku(siteProduct.sku); - } catch (error) { - // 产品不存在,继续创建 - } - } - // 将站点产品转换为本地产品格式 - const productData = await this.convertSiteProductToLocalProduct(siteProduct); - - if (localProduct) { + const productData = await this.mapUnifiedToLocalProduct(siteProduct); + return await this.upsertProduct({sku}, productData); + } + async upsertProduct(where: Partial>, productData: any) { + const existingProduct = await this.productModel.findOne({ where: where}); + if (existingProduct) { // 更新现有产品 const updateData: UpdateProductDTO = productData; - return await this.updateProduct(localProduct.id, updateData); + return await this.updateProduct(existingProduct.id, updateData); } else { // 创建新产品 const createData: CreateProductDTO = productData; @@ -1929,18 +1932,18 @@ export class ProductService { * @param siteProductIds 站点产品ID数组 * @returns 批量同步结果 */ - async batchSyncFromSite(siteId: number, siteProductIds: (string | number)[]): Promise<{ synced: number, errors: string[] }> { + async batchSyncFromSite(siteId: number, data: Array<{siteProductId:string, sku: string}>): Promise<{ synced: number, errors: string[] }> { const results = { synced: 0, errors: [] }; - for (const siteProductId of siteProductIds) { + for (const item of data) { try { - await this.syncProductFromSite(siteId, siteProductId); + await this.syncProductFromSite(siteId, item.siteProductId, item.sku); results.synced++; } catch (error) { - results.errors.push(`站点产品ID ${siteProductId} 同步失败: ${error.message}`); + results.errors.push(`站点产品ID ${item.siteProductId} 同步失败: ${error.message}`); } } @@ -1952,7 +1955,7 @@ export class ProductService { * @param siteProduct 站点产品对象 * @returns 本地产品数据 */ - private async convertSiteProductToLocalProduct(siteProduct: any): Promise { + private async mapUnifiedToLocalProduct(siteProduct: any): Promise { const productData: any = { sku: siteProduct.sku, name: siteProduct.name, @@ -2015,18 +2018,20 @@ export class ProductService { * @param localProduct 本地产品对象 * @returns 统一产品对象 */ - private async convertLocalProductToUnifiedProduct(localProduct: Product,siteSku?: string): Promise> { + private async mapLocalToUnifiedProduct(localProduct: Product,siteSku?: string): Promise> { + const tags = localProduct.attributes?.map(a => ({name: a.name})) || []; // 将本地产品数据转换为UnifiedProductDTO格式 const unifiedProduct: any = { id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在,使用现有ID - name: localProduct.nameCn || localProduct.name || localProduct.sku, - type: 'simple', // 默认类型,可以根据实际需要调整 + name: localProduct.name, + type: localProduct.type === 'single'? 'simple' : 'bundle', // 默认类型,可以根据实际需要调整 status: 'publish', // 默认状态,可以根据实际需要调整 - sku: siteSku || await this.templateService.render('site.product.sku', { sku: localProduct.sku }), + sku: siteSku || await this.templateService.render('site.product.sku', { product: localProduct, sku: localProduct.sku }), regular_price: String(localProduct.price || 0), sale_price: String(localProduct.promotionPrice || localProduct.price || 0), price: String(localProduct.price || 0), - // stock_status: localProduct.stockQuantity && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock', + // TODO 库存暂时无法同步 + // stock_status: localProduct.components && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock', // stock_quantity: localProduct.stockQuantity || 0, // images: localProduct.images ? localProduct.images.map(img => ({ // id: img.id, @@ -2034,25 +2039,24 @@ export class ProductService { // name: img.name || '', // alt: img.alt || '' // })) : [], - tags: [], + tags, categories: localProduct.category ? [{ id: localProduct.category.id, name: localProduct.category.name }] : [], attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({ - id: attr.id, - name: attr.name, - position: 0, + id: attr.dict.id, + name: attr.dict.name, + position: attr.dict.sort || 0, visible: true, variation: false, - options: [attr.value] + options: [attr.name] })) : [], variations: [], date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(), date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(), raw: { - localProductId: localProduct.id, - localProductSku: localProduct.sku + ...localProduct } }; diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index 3aab4ac..002536d 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -39,7 +39,7 @@ export class SiteApiService { } return new ShopyyAdapter(site, this.shopyyService); } - + throw new Error(`Unsupported site type: ${site.type}`); } @@ -57,7 +57,7 @@ export class SiteApiService { try { // 使用站点SKU查询对应的ERP产品 const erpProduct = await this.productService.findProductBySiteSku(siteProduct.sku); - + // 将ERP产品信息合并到站点商品中 return { ...siteProduct, @@ -108,24 +108,20 @@ export class SiteApiService { */ async upsertProduct(siteId: number, product: Partial): Promise { const adapter = await this.getAdapter(siteId); - + // 首先尝试查找产品 - if (product.sku) { - // 如果没有提供ID但提供了SKU,尝试通过SKU查找产品 - try { - // 尝试搜索具有相同SKU的产品 - const existingProduct = await adapter.getProduct( { sku: product.sku }); - if (existingProduct) { - // 找到现有产品,更新它 - return await adapter.updateProduct({ id: existingProduct.id }, product); - } - // 产品不存在,执行创建 - return await adapter.createProduct(product); - } catch (error) { - // 搜索失败,继续执行创建逻辑 - console.log(`通过SKU搜索产品失败:`, error.message); - } + if (!product.sku) { + throw new Error('产品SKU不能为空'); } + // 尝试搜索具有相同SKU的产品 + const existingProduct = await adapter.getProduct({ sku: product.sku }); + if (existingProduct) { + // 找到现有产品,更新它 + return await adapter.updateProduct({ id: existingProduct.id }, product); + } + // 产品不存在,执行创建 + return await adapter.createProduct(product); + } /** @@ -175,17 +171,6 @@ export class SiteApiService { return await adapter.getProducts(params); } - /** - * 从站点获取单个产品 - * @param siteId 站点ID - * @param productId 产品ID - * @returns 站点产品 - */ - async getProductFromSite(siteId: number, productId: string | number): Promise { - const adapter = await this.getAdapter(siteId); - return await adapter.getProduct({ id: productId }); - } - /** * 从站点获取所有产品 * @param siteId 站点ID diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 8c8bc89..14cf044 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -44,7 +44,7 @@ export class WPService implements IPlatformService { * @param site 站点配置 * @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1 */ - private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { + public createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { return new WooCommerceRestApi({ url: site.apiUrl, consumerKey: site.consumerKey, From bdc2af3514bdeeef559ff69e585b75933d3c7726 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 16:08:41 +0800 Subject: [PATCH 17/24] =?UTF-8?q?fix(sync=5Fshipment):=20=E6=8D=95?= =?UTF-8?q?=E8=8E=B7=E8=BF=90=E5=8D=95=E7=8A=B6=E6=80=81=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E5=BC=82=E5=B8=B8=E5=B9=B6=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加try-catch块来捕获updateShipmentState过程中可能出现的错误 使用logger记录错误信息以便后续排查 --- src/job/sync_shipment.job.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/job/sync_shipment.job.ts b/src/job/sync_shipment.job.ts index c532844..94707d9 100644 --- a/src/job/sync_shipment.job.ts +++ b/src/job/sync_shipment.job.ts @@ -75,10 +75,14 @@ export class SyncUniuniShipmentJob implements IJob{ '255': 'Gateway_To_Gateway_Transit' }; async onTick() { - const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); - shipments.forEach(shipment => { - this.logisticsService.updateShipmentState(shipment); - }); + try { + const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); + shipments.forEach(shipment => { + this.logisticsService.updateShipmentState(shipment); + }); + } catch (error) { + this.logger.error(`更新运单状态失败 ${error.message}`); + } } onComplete(result: any) { From 8bdc438a489ec0326d6095ff7580cb2fced1b591 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:17:03 +0800 Subject: [PATCH 18/24] =?UTF-8?q?feat(shopyy):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E5=95=86=E5=93=81=E6=9F=A5=E8=AF=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BA=A7=E5=93=81=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增ShopyyAllProductQuery类支持全量商品查询参数 - 实现getAllProducts方法支持带条件查询 - 优化getProductBySku方法使用新查询接口 - 公开request方法便于子类调用 - 增加错误日志记录产品查找失败情况 - 修复产品permalink生成逻辑 --- src/adapter/shopyy.adapter.ts | 62 ++++++++++++++++++++------------- src/dto/shopyy.dto.ts | 56 +++++++++++++++++++++++++---- src/dto/site-api.dto.ts | 5 +++ src/service/shopyy.service.ts | 6 ++-- src/service/site-api.service.ts | 14 ++++++-- 5 files changed, 108 insertions(+), 35 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index f0abdcb..3c1ca40 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -20,14 +20,11 @@ import { FulfillmentDTO, CreateReviewDTO, CreateVariationDTO, - UpdateReviewDTO - FulfillmentDTO, - CreateReviewDTO, - CreateVariationDTO, - UpdateReviewDTO + UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { + ShopyyAllProductQuery, ShopyyCustomer, ShopyyOrder, ShopyyOrderQuery, @@ -718,7 +715,7 @@ export class ShopyyAdapter implements ISiteAdapter { } // ========== 产品映射方法 ========== - mapPlatformToUnifiedProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { + mapPlatformToUnifiedProduct(item: ShopyyProduct): UnifiedProductDTO { // 映射产品状态 function mapProductStatus(status: number) { return status === 1 ? 'publish' : 'draft'; @@ -757,7 +754,7 @@ export class ShopyyAdapter implements ISiteAdapter { name: c.title || '', })), variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], - permalink: item.permalink, + permalink: `${this.site.websiteUrl}/products/${item.handle}`, date_created: typeof item.created_at === 'number' ? new Date(item.created_at * 1000).toISOString() @@ -814,7 +811,7 @@ export class ShopyyAdapter implements ISiteAdapter { // 添加分类信息 if (data.categories && data.categories.length > 0) { params.collections = data.categories.map((category: any) => ({ - id: category.id, + // id: category.id, title: category.name, })); } @@ -941,20 +938,32 @@ export class ShopyyAdapter implements ISiteAdapter { per_page, }; } - - async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); - async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllProducts 暂未实现 - throw new Error('Shopyy getAllProducts 暂未实现'); + mapAllProductParams(params: UnifiedSearchParamsDTO): Partial{ + const mapped = { + ...params.where, + } as any + if(params.per_page){mapped.limit = params.per_page} + return mapped + } + + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // 转换搜索参数 + const requestParams = this.mapAllProductParams(params); + const response = await this.shopyyService.request( + this.site, + 'products', + 'GET', + null, + requestParams + ); + if(response.code !==0){ + throw new Error(response.msg || '获取产品列表失败') + } + const { data = [] } = response; + const finalItems = data.map(this.mapPlatformToUnifiedProduct.bind(this)) + return finalItems } - async createProduct(data: Partial): Promise { - // 使用映射方法转换参数 - const requestParams = this.mapCreateProductParams(data); - const res = await this.shopyyService.createProduct(this.site, requestParams); - return this.mapPlatformToUnifiedProduct(res); async createProduct(data: Partial): Promise { // 使用映射方法转换参数 const requestParams = this.mapCreateProductParams(data); @@ -994,16 +1003,17 @@ export class ShopyyAdapter implements ISiteAdapter { await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); return true; } - + // 通过sku获取产品详情的私有方法 private async getProductBySku(sku: string): Promise { // 使用Shopyy API的搜索功能通过sku查询产品 - const response = await this.shopyyService.getProducts(this.site, 1, 100); - const product = response.items.find((item: any) => item.sku === sku); + const response = await this.getAllProducts({ where: {sku} }); + console.log('getProductBySku', response) + const product = response?.[0] if (!product) { throw new Error(`未找到sku为${sku}的产品`); } - return this.mapPlatformToUnifiedProduct(product); + return product } async batchProcessProducts( @@ -1016,6 +1026,10 @@ export class ShopyyAdapter implements ISiteAdapter { return this.mapSearchParams(query) } + mapAllProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) + } + // ========== 评论映射方法 ========== mapUnifiedToPlatformReview(data: Partial) { diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index fc534d6..009d6ce 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -4,10 +4,53 @@ export interface ShopyyTag { id?: number; name?: string; } -export interface ShopyyProductQuery{ +export interface ShopyyProductQuery { page: string; limit: string; } +/** + * Shopyy 全量商品查询参数类 + * 用于封装获取 Shopyy 商品列表时的各种筛选和分页条件 + * 参考文档: https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse + */ +export class ShopyyAllProductQuery { + /** 分页大小,限制返回的商品数量 */ + limit?: string; + /** 起始ID,用于分页,返回ID大于该值的商品 */ + since_id?: string; + /** 商品ID,精确匹配单个商品 */ + id?: string; + /** 商品标题,支持模糊查询 */ + title?: string; + /** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */ + status?: string; + /** 商品SKU编码,库存保有单位,精确或模糊匹配 */ + sku?: string; + /** 商品SPU编码,标准化产品单元,用于归类同款商品 */ + spu?: string; + /** 商品分类ID,筛选指定分类下的商品 */ + collection_id?: string; + /** 变体价格最小值,筛选变体价格大于等于该值的商品 */ + variant_price_min?: string; + /** 变体价格最大值,筛选变体价格小于等于该值的商品 */ + variant_price_max?: string; + /** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */ + variant_compare_at_price_min?: string; + /** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */ + variant_compare_at_price_max?: string; + /** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */ + variant_weight_min?: string; + /** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */ + variant_weight_max?: string; + /** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + created_at_min?: string; + /** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + created_at_max?: string; + /** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + updated_at_min?: string; + /** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + updated_at_max?: string; +} // 产品类型 export interface ShopyyProduct { // 产品主键 @@ -249,17 +292,18 @@ export interface ShopyyOrder { // 物流回传时间 payment_tracking_at?: number; // 商品 - products?: Array<{ + products?: Array<{ // 订单商品表 id - order_product_id?: number; + order_product_id?: number; // 数量 - quantity?: number; + quantity?: number; // 更新时间 - updated_at?: number; + updated_at?: number; // 创建时间 created_at?: number; // 发货商品表 id - id?: number }>; + id?: number + }>; }>; shipping_zone_plans?: Array<{ shipping_price?: number | string; diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index 247c5c9..faee539 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -18,6 +18,11 @@ export enum OrderFulfillmentStatus { // 确认发货 CONFIRMED, } +// +export class UnifiedProductWhere { + sku?: string; + [prop:string]:any +} export class UnifiedTagDTO { // 标签DTO用于承载统一标签数据 @ApiProperty({ description: '标签ID' }) diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index c111a03..0081f55 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -158,7 +158,7 @@ export class ShopyyService { * @param params 请求参数 * @returns 响应数据 */ - private async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise { + async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise { const url = this.buildURL(site.apiUrl, endpoint); const headers = this.buildHeaders(site); @@ -206,13 +206,13 @@ export class ShopyyService { * @param pageSize 每页数量 * @returns 分页产品列表 */ - async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise { + async getProducts(site: any, page: number = 1, pageSize: number = 100, where: Record = {}): Promise { // ShopYY API: GET /products // 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含 const response = await this.request(site, 'products', 'GET', null, { page, page_size: pageSize, - fields: 'id,name,sku,handle,status,type,stock_status,stock_quantity,images,regular_price,sale_price,tags,variations' + ...where }); return { diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index 002536d..46d8ee4 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -1,4 +1,4 @@ -import { Inject, Provide } from '@midwayjs/core'; +import { ILogger, Inject, Provide } from '@midwayjs/core'; import { ShopyyAdapter } from '../adapter/shopyy.adapter'; import { WooCommerceAdapter } from '../adapter/woocommerce.adapter'; import { ISiteAdapter } from '../interface/site-adapter.interface'; @@ -22,6 +22,9 @@ export class SiteApiService { @Inject() productService: ProductService; + @Inject() + logger: ILogger; + async getAdapter(siteId: number): Promise { const site = await this.siteService.get(siteId, true); if (!site) { @@ -114,7 +117,14 @@ export class SiteApiService { throw new Error('产品SKU不能为空'); } // 尝试搜索具有相同SKU的产品 - const existingProduct = await adapter.getProduct({ sku: product.sku }); + let existingProduct + try { + + existingProduct = await adapter.getProduct({ sku: product.sku }); + } catch (error) { + this.logger.error(`[Site API] 查找产品失败, siteId: ${siteId}, sku: ${product.sku}, 错误信息: ${error.message}`); + existingProduct = null + } if (existingProduct) { // 找到现有产品,更新它 return await adapter.updateProduct({ id: existingProduct.id }, product); From 56268c9caedddfe31609f05143ad5b28ee2f4ae5 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:35:25 +0800 Subject: [PATCH 19/24] =?UTF-8?q?refactor(adapter):=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E4=BB=A3=E7=A0=81=E5=B9=B6=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E6=98=A0=E5=B0=84=E6=96=B9=E6=B3=95=E5=91=BD?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除shopyy和woocommerce适配器中的重复代码 将mapOrder重命名为mapPlatformToUnifiedOrder以保持命名一致性 --- src/adapter/shopyy.adapter.ts | 106 --------------------------- src/adapter/woocommerce.adapter.ts | 90 ----------------------- src/controller/webhook.controller.ts | 4 +- 3 files changed, 2 insertions(+), 198 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 3c1ca40..cba2c5a 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -28,10 +28,8 @@ import { ShopyyCustomer, ShopyyOrder, ShopyyOrderQuery, - ShopyyOrderQuery, ShopyyProduct, ShopyyProductQuery, - ShopyyProductQuery, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; @@ -39,7 +37,6 @@ import { OrderStatus, } from '../enums/base.enum'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class ShopyyAdapter implements ISiteAdapter { shopyyFinancialStatusMap= { '200': '待支付', @@ -58,12 +55,6 @@ export class ShopyyAdapter implements ISiteAdapter { this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); - this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); - this.mapPlatformToUnifiedProduct = this.mapPlatformToUnifiedProduct.bind(this); - this.mapPlatformToUnifiedVariation = this.mapPlatformToUnifiedVariation.bind(this); - this.mapPlatformToUnifiedOrder = this.mapPlatformToUnifiedOrder.bind(this); - this.mapPlatformToUnifiedMedia = this.mapPlatformToUnifiedMedia.bind(this); - // this.mapPlatformToUnifiedSubscription = this.mapPlatformToUnifiedSubscription.bind(this); } // ========== 客户映射方法 ========== @@ -551,13 +542,6 @@ export class ShopyyAdapter implements ISiteAdapter { async getOrders( params: UnifiedSearchParamsDTO ): Promise> { - // 转换订单查询参数 - const normalizedParams = this.mapOrderSearchParams(params); - const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); // 转换订单查询参数 const normalizedParams = this.mapOrderSearchParams(params); const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( @@ -566,7 +550,6 @@ export class ShopyyAdapter implements ISiteAdapter { normalizedParams ); return { - items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), items: items.map(this.mapPlatformToUnifiedOrder.bind(this)), total, totalPages, @@ -596,10 +579,6 @@ export class ShopyyAdapter implements ISiteAdapter { const requestParams = this.mapCreateOrderParams(data); const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); return this.mapPlatformToUnifiedOrder(createdOrder); - // 使用映射方法转换参数 - const requestParams = this.mapCreateOrderParams(data); - const createdOrder = await this.shopyyService.createOrder(this.site, requestParams); - return this.mapPlatformToUnifiedOrder(createdOrder); } async updateOrder(where: {id: string | number}, data: Partial): Promise { @@ -616,12 +595,6 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.getOrderNotes(this.site, orderId); } - async createOrderNote(orderId: string | number, data: any): Promise { - return await this.shopyyService.createOrderNote(this.site, orderId, data); - async getOrderNotes(orderId: string | number): Promise { - return await this.shopyyService.getOrderNotes(this.site, orderId); - } - async createOrderNote(orderId: string | number, data: any): Promise { return await this.shopyyService.createOrderNote(this.site, orderId, data); } @@ -648,7 +621,6 @@ export class ShopyyAdapter implements ISiteAdapter { }; } catch (error) { throw new Error(`履行失败: ${error.message}`); - throw new Error(`履行失败: ${error.message}`); } } @@ -904,13 +876,8 @@ export class ShopyyAdapter implements ISiteAdapter { } } - async getProducts( async getProducts( params: UnifiedSearchParamsDTO - ): Promise> { - // 转换搜索参数 - const requestParams = this.mapProductQuery(params); - const response = await this.shopyyService.fetchResourcePaged( ): Promise> { // 转换搜索参数 const requestParams = this.mapProductQuery(params); @@ -924,13 +891,7 @@ export class ShopyyAdapter implements ISiteAdapter { ...item, permalink: `${this.site.websiteUrl}/products/${item.handle}`, })).map(this.mapPlatformToUnifiedProduct.bind(this)) - const { items = [], total, totalPages, page, per_page } = response; - const finalItems = items.map((item) => ({ - ...item, - permalink: `${this.site.websiteUrl}/products/${item.handle}`, - })).map(this.mapPlatformToUnifiedProduct.bind(this)) return { - items: finalItems as UnifiedProductDTO[], items: finalItems as UnifiedProductDTO[], total, totalPages, @@ -1122,36 +1083,19 @@ export class ShopyyAdapter implements ISiteAdapter { mapUnifiedToPlatformSubscription(data: Partial) { return data - // ========== 订阅映射方法 ========== - mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO { - return data } - mapUnifiedToPlatformSubscription(data: Partial) { - return data - } - - async getSubscriptions( - params: UnifiedSearchParamsDTO - ): Promise> { - throw new Error('Shopyy does not support subscriptions.'); async getSubscriptions( params: UnifiedSearchParamsDTO ): Promise> { throw new Error('Shopyy does not support subscriptions.'); } - async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { - // Shopyy getAllSubscriptions 暂未实现 - throw new Error('Shopyy getAllSubscriptions 暂未实现'); async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { // Shopyy getAllSubscriptions 暂未实现 throw new Error('Shopyy getAllSubscriptions 暂未实现'); } - // ========== 产品变体映射方法 ========== - mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { - // 映射变体 // ========== 产品变体映射方法 ========== mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { // 映射变体 @@ -1314,19 +1258,6 @@ export class ShopyyAdapter implements ISiteAdapter { return links; } - // ========== 辅助方法 ========== - /** - * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 - * 将统一的搜索参数转换为 ShopYY API 所需的参数格式 - */ - mapSearchParams(params: UnifiedSearchParamsDTO): any { - // 处理分页参数 - const page = Number(params.page || 1); - const limit = Number(params.per_page ?? 20); - - // 处理 where 条件 - const query: any = { - ...(params.where || {}), // ========== 辅助方法 ========== /** * 通用搜索参数转换方法,处理 where 和 orderBy 的转换 @@ -1348,14 +1279,6 @@ export class ShopyyAdapter implements ISiteAdapter { query.order_by = dir === 'desc' ? 'desc' : 'asc'; query.order_field = field } - return query; - limit, - } - if(params.orderBy){ - const [field, dir] = Object.entries(params.orderBy)[0]; - query.order_by = dir === 'desc' ? 'desc' : 'asc'; - query.order_field = field - } return query; } @@ -1378,35 +1301,6 @@ export class ShopyyAdapter implements ISiteAdapter { [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled } - shopyyFulfillmentStatusMap = { - // 未发货 - '300': OrderFulfillmentStatus.PENDING, - // 部分发货 - '310': OrderFulfillmentStatus.PARTIALLY_FULFILLED, - // 已发货 - '320': OrderFulfillmentStatus.FULFILLED, - // 已取消 - '330': OrderFulfillmentStatus.CANCELLED, - // 确认发货 - // 映射产品状态: publish -> 1, draft -> 0 - mapStatus = (status: string) => { - return status === 'publish' ? 1 : 0; - }; - - // 映射库存状态: instock -> 1, outofstock -> 0 - mapStockStatus = (stockStatus: string) => { - return stockStatus === 'instock' ? 1 : 0; - }; - - 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 - } - shopyyFulfillmentStatusMap = { // 未发货 '300': OrderFulfillmentStatus.PENDING, diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 8a61b80..3627c22 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -17,8 +17,6 @@ import { UnifiedVariationPaginationDTO, CreateReviewDTO, UpdateReviewDTO, - CreateReviewDTO, - UpdateReviewDTO, } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { @@ -34,7 +32,6 @@ import { import { Site } from '../entity/site.entity'; import { WPService } from '../service/wp.service'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 @@ -79,20 +76,6 @@ export class WooCommerceAdapter implements ISiteAdapter { date_created: item.date_created, date_modified: item.date_modified, raw: item, - id: item.id, - avatar: item.avatar_url, - email: item.email, - orders: Number(item.orders ?? 0), - total_spend: Number(item.total_spent ?? 0), - first_name: item.first_name, - last_name: item.last_name, - username: item.username, - phone: item.billing?.phone || item.shipping?.phone, - billing: item.billing, - shipping: item.shipping, - date_created: item.date_created, - date_modified: item.date_modified, - raw: item, }; } @@ -761,40 +744,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return String(value).split(',').map(v => v.trim()).filter(Boolean); }; - if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); - if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); - if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); - if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after); - if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before); - if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt); - if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds); - if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids); - if (where.offset !== undefined) mapped.offset = Number(where.offset); - if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); - if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); - if (where.slug) mapped.slug = String(where.slug); - if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) { - if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus); - if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus); - if (where.status) mapped.status = String(where.status); - } - if (where.type) mapped.type = String(where.type); - if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes); - if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes); - if (where.sku) mapped.sku = String(where.sku); - if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured); - if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId); - if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId); - if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass); - if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName); - if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm); - if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass); - if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale); - if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice); - if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice); - if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus); - if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); - if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields); if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before); @@ -973,7 +922,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return this.mapPlatformToUnifiedProduct(product); } - async getProducts(params: UnifiedSearchParamsDTO): Promise> { async getProducts(params: UnifiedSearchParamsDTO): Promise> { // 获取产品列表并使用统一分页结构返回 const requestParams = this.mapProductSearchParams(params); @@ -982,11 +930,6 @@ export class WooCommerceAdapter implements ISiteAdapter { 'products', requestParams ); - const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( - this.site, - 'products', - requestParams - ); // 对于类型为 variable 的产品,需要加载完整的变体数据 const productsWithVariations = await Promise.all( @@ -1011,7 +954,6 @@ export class WooCommerceAdapter implements ISiteAdapter { ); return { - items: productsWithVariations.map(this.mapPlatformToUnifiedProduct), items: productsWithVariations.map(this.mapPlatformToUnifiedProduct), total, totalPages, @@ -1112,8 +1054,6 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - // 评论操作方法 - async getReviews(params: UnifiedSearchParamsDTO): Promise { // 评论操作方法 async getReviews(params: UnifiedSearchParamsDTO): Promise { // 获取评论列表并使用统一分页结构返回 @@ -1123,13 +1063,7 @@ export class WooCommerceAdapter implements ISiteAdapter { 'products/reviews', requestParams ); - const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( - this.site, - 'products/reviews', - requestParams - ); return { - items: items.map(this.mapPlatformToUnifiedReview.bind(this)), items: items.map(this.mapPlatformToUnifiedReview.bind(this)), total, totalPages, @@ -1143,13 +1077,11 @@ export class WooCommerceAdapter implements ISiteAdapter { const api = this.wpService.createApi(this.site, 'wc/v3'); const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); - return reviews.map((review: any) => this.mapPlatformToUnifiedReview(review)); } async createReview(data: CreateReviewDTO): Promise { const res = await this.wpService.createReview(this.site, data); return this.mapPlatformToUnifiedReview(res); - return this.mapPlatformToUnifiedReview(res); } async updateReview(where: Partial>, data: UpdateReviewDTO): Promise { @@ -1184,26 +1116,14 @@ export class WooCommerceAdapter implements ISiteAdapter { billing_period: item.billing_period, billing_interval: item.billing_interval, date_created: item.date_created ?? item.start_date, - status: item.status, - customer_id: item.customer_id, - billing_period: item.billing_period, - billing_interval: item.billing_interval, - date_created: item.date_created ?? item.start_date, date_modified: item.date_modified, start_date: item.start_date, next_payment_date: item.next_payment_date, line_items: item.line_items, - start_date: item.start_date, - next_payment_date: item.next_payment_date, - line_items: item.line_items, raw: item, }; } - // 订阅操作方法 - async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { - // 获取订阅列表并映射为统一订阅DTO集合 - // 订阅操作方法 async getSubscriptions(params: UnifiedSearchParamsDTO): Promise> { // 获取订阅列表并映射为统一订阅DTO集合 @@ -1211,8 +1131,6 @@ export class WooCommerceAdapter implements ISiteAdapter { this.site, 'subscriptions', params - 'subscriptions', - params ); return { items: items.map(this.mapPlatformToUnifiedSubscription), @@ -1231,13 +1149,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return subscriptions.map((subscription: any) => this.mapPlatformToUnifiedSubscription(subscription)); } - // ========== 变体映射方法 ========== - mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { - // 使用mapVariation方法来实现统一的变体映射逻辑 - return this.mapVariation(data); - } - mapUnifiedToPlatformVariation(data: Partial) { - return data; // ========== 变体映射方法 ========== mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO { // 使用mapVariation方法来实现统一的变体映射逻辑 @@ -1297,7 +1208,6 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - // 变体操作方法 // 变体操作方法 // 获取产品变体列表 async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index 37fcc5a..6132182 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -96,7 +96,7 @@ export class WebhookController { break; case 'order.created': case 'order.updated': - const order = adapter.mapOrder(body) + const order = adapter.mapPlatformToUnifiedOrder(body) await this.orderService.syncSingleOrder(siteId, order); break; case 'order.deleted': @@ -162,7 +162,7 @@ export class WebhookController { break; case 'orders/create': case 'orders/update': - const order = adapter.mapOrder(body) + const order = adapter.mapPlatformToUnifiedOrder(body) await this.orderService.syncSingleOrder(siteId, order); break; case 'orders/delete': From ee2018362e5c7febebb81ca86ca11e26f414b121 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:46:13 +0800 Subject: [PATCH 20/24] =?UTF-8?q?docs:=20=E7=BB=9F=E4=B8=80=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=8B=AC=E5=8F=B7=E6=A0=BC=E5=BC=8F=E4=B8=BA=E5=85=A8?= =?UTF-8?q?=E8=A7=92=E6=8B=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将代码中的中文括号格式从半角"()"统一修改为全角"()",并删除测试文档文件test-site-sku-methods.md --- src/adapter/shopyy.adapter.ts | 20 ++--- src/adapter/woocommerce.adapter.ts | 6 +- src/config/config.local.ts | 8 +- src/configuration.ts | 2 +- src/controller/site-api.controller.ts | 6 +- src/db/seeds/dict.seeder.ts | 44 +++++------ src/dto/api.dto.ts | 12 +-- src/dto/batch.dto.ts | 14 ++-- src/dto/customer.dto.ts | 16 ++-- src/dto/shopyy.dto.ts | 26 +++---- src/dto/site-api.dto.ts | 2 +- src/dto/site.dto.ts | 2 +- src/dto/woocommerce.dto.ts | 12 +-- src/enums/base.enum.ts | 2 +- src/interface/platform.interface.ts | 2 +- src/service/customer.service.ts | 8 +- src/service/dict.service.ts | 4 +- src/service/order.service.ts | 6 +- src/service/product.service.ts | 10 +-- src/service/site-api.service.ts | 2 +- src/service/wp.service.ts | 2 +- test-site-sku-methods.md | 105 -------------------------- 22 files changed, 105 insertions(+), 206 deletions(-) delete mode 100644 test-site-sku-methods.md diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index cba2c5a..d9215bb 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -405,7 +405,7 @@ export class ShopyyAdapter implements ISiteAdapter { } mapUpdateOrderParams(data: Partial): any { - // 构建 ShopYY 订单更新参数(仅包含传入的字段) + // 构建 ShopYY 订单更新参数(仅包含传入的字段) const params: any = {}; // 仅当字段存在时才添加到更新参数中 @@ -665,7 +665,7 @@ export class ShopyyAdapter implements ISiteAdapter { const statusMap = { 'pending': '100', // 100 未完成 'processing': '110', // 110 待处理 - 'completed': "180", // 180 已完成(确认收货) + 'completed': "180", // 180 已完成(确认收货) 'cancelled': '190', // 190 取消 }; @@ -797,7 +797,7 @@ export class ShopyyAdapter implements ISiteAdapter { return status === 'publish' ? 1 : 0; }; - // 构建 ShopYY 产品更新参数(仅包含传入的字段) + // 构建 ShopYY 产品更新参数(仅包含传入的字段) const params: any = {}; // 仅当字段存在时才添加到更新参数中 @@ -816,7 +816,7 @@ export class ShopyyAdapter implements ISiteAdapter { params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; } - // 添加变体信息(如果存在) + // 添加变体信息(如果存在) if (data.variations && data.variations.length > 0) { params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => { const variationParams: any = {}; @@ -836,7 +836,7 @@ export class ShopyyAdapter implements ISiteAdapter { }); } - // 添加图片信息(如果存在) + // 添加图片信息(如果存在) if (data.images && data.images.length > 0) { params.images = data.images.map((image: any) => ({ id: image.id, @@ -846,12 +846,12 @@ export class ShopyyAdapter implements ISiteAdapter { })); } - // 添加标签信息(如果存在) + // 添加标签信息(如果存在) if (data.tags && data.tags.length > 0) { params.tags = data.tags.map((tag: any) => tag.name || ''); } - // 添加分类信息(如果存在) + // 添加分类信息(如果存在) if (data.categories && data.categories.length > 0) { params.collections = data.categories.map((category: any) => ({ id: category.id, @@ -1121,7 +1121,7 @@ export class ShopyyAdapter implements ISiteAdapter { } mapUpdateVariationParams(data: Partial): any { - // 构建 ShopYY 变体更新参数(仅包含传入的字段) + // 构建 ShopYY 变体更新参数(仅包含传入的字段) const params: any = {}; // 仅当字段存在时才添加到更新参数中 @@ -1292,12 +1292,12 @@ export class ShopyyAdapter implements ISiteAdapter { return stockStatus === 'instock' ? 1 : 0; }; - shopyyOrderStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + shopyyOrderStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; [100]: OrderStatus.PENDING, // 100 未完成 转为 pending [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing // 已发货 - [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed + [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled } diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 3627c22..0849358 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -609,7 +609,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // await api.put(`orders/${orderId}`, { status: 'processing' }); // // 添加取消履行的备注 - // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; + // const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; // await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); // return { @@ -698,14 +698,14 @@ export class WooCommerceAdapter implements ISiteAdapter { })); } - // 映射变体数据(注意:WooCommerce API 中变体通常通过单独的端点处理) + // 映射变体数据(注意:WooCommerce API 中变体通常通过单独的端点处理) // 这里只映射变体的基本信息,具体创建/更新变体需要额外处理 if (data.variations && Array.isArray(data.variations)) { // 对于WooProduct类型,variations字段只存储变体ID mapped.variations = data.variations.map(variation => variation.id as number); } - // 映射下载数据(如果产品是可下载的) + // 映射下载数据(如果产品是可下载的) // if (data.downloads && Array.isArray(data.downloads)) { // mapped.downloads = data.downloads.map(download => ({ // id: download.id as number, diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 6d381aa..b82c927 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -7,8 +7,11 @@ export default { // dataSource: { // default: { // host: '13.212.62.127', + // port: "3306", // username: 'root', // password: 'Yoone!@.2025', + // database: 'inventory_v2', + // synchronize: true, // }, // }, // }, @@ -16,10 +19,11 @@ export default { dataSource: { default: { host: 'localhost', - port: "3306", + port: "23306", username: 'root', - password: 'root', + password: '12345678', database: 'inventory', + synchronize: true, }, }, }, diff --git a/src/configuration.ts b/src/configuration.ts index 16a5716..a27dbb0 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -99,7 +99,7 @@ export class MainConfiguration { } /** - * 初始化数据库(如果不存在则创建) + * 初始化数据库(如果不存在则创建) */ private async initializeDatabase(): Promise { // 使用注入的数据库配置 diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 00d8285..8eb45e1 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -299,7 +299,7 @@ export class SiteApiController { } } - // 平台特性:产品导出(特殊CSV,走平台服务) + // 平台特性:产品导出(特殊CSV,走平台服务) @Get('/:siteId/links') async getLinks( @Param('siteId') siteId: number @@ -429,7 +429,7 @@ export class SiteApiController { } } - // 平台特性:产品导入(特殊CSV,走平台服务) + // 平台特性:产品导入(特殊CSV,走平台服务) @Post('/:siteId/products/import-special') @ApiOkResponse({ type: Object }) async importProductsSpecial( @@ -443,7 +443,7 @@ export class SiteApiController { const created: any[] = []; const failed: any[] = []; if (site.type === 'woocommerce') { - // 解析 CSV 为对象数组(若传入 items 则优先 items) + // 解析 CSV 为对象数组(若传入 items 则优先 items) let payloads = items; if (!payloads.length && csvText) { const lines = csvText.split(/\r?\n/).filter(Boolean); diff --git a/src/db/seeds/dict.seeder.ts b/src/db/seeds/dict.seeder.ts index d015a05..37ef843 100644 --- a/src/db/seeds/dict.seeder.ts +++ b/src/db/seeds/dict.seeder.ts @@ -126,7 +126,7 @@ const flavorsData = [ { name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' }, { name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' }, { name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' }, - { name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' }, + { name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' }, { name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' }, { name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' }, { name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' }, @@ -137,7 +137,7 @@ const flavorsData = [ { name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' }, { name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' }, { name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' }, - { name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' }, + { name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' }, { name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' }, { name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' }, { name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' }, @@ -145,7 +145,7 @@ const flavorsData = [ { name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' }, { name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' }, { name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' }, - { name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' }, + { name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' }, { name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' }, { name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' }, { name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' }, @@ -168,7 +168,7 @@ const flavorsData = [ { name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' }, { name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' }, { name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' }, - { name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' }, + { name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' }, { name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' }, { name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' }, { name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' }, @@ -196,7 +196,7 @@ const flavorsData = [ { name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' }, { name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' }, { name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' }, - { name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' }, + { name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' }, { name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' }, { name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' }, { name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' }, @@ -230,7 +230,7 @@ const flavorsData = [ { name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' }, { name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' }, { name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' }, - { name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' }, + { name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' }, { name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' }, { name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' }, { name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' }, @@ -310,7 +310,7 @@ const flavorsData = [ { name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' }, { name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' }, { name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' }, - { name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' }, + { name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' }, { name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' }, { name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' }, { name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' }, @@ -340,14 +340,14 @@ const flavorsData = [ { name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' }, { name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' }, { name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' }, - { name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' }, + { name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' }, { name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' }, { name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' }, { name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' }, { name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' }, { name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' }, { name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' }, - { name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' }, + { name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' }, { name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' }, { name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' }, { name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' }, @@ -492,13 +492,13 @@ const flavorsData = [ { name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' }, { name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' }, { name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' }, - { name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' }, + { name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' }, { name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' }, { name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' }, { name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' }, { name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' }, { name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' }, - { name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' }, + { name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' }, { name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' }, { name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' }, { name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' }, @@ -508,12 +508,12 @@ const flavorsData = [ { name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' }, { name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' }, { name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' }, - { name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' }, + { name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' }, { name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' }, { name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' }, { name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' }, { name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' }, - { name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' }, + { name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' }, { name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' }, { name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' }, { name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' }, @@ -603,7 +603,7 @@ const flavorsData = [ { name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' }, { name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' }, { name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' }, - { name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' }, + { name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' }, { name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' }, { name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' }, { name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' }, @@ -625,8 +625,8 @@ const flavorsData = [ { name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' }, { name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' }, { name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' }, - { name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' }, - { name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' }, + { name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' }, + { name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' }, { name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' }, { name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' }, { name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' }, @@ -664,7 +664,7 @@ const flavorsData = [ { name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' }, { name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' }, { name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' }, - { name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' }, + { name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' }, { name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' }, { name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' }, { name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' }, @@ -680,10 +680,10 @@ const flavorsData = [ { name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' }, { name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' }, { name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' }, - { name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' }, + { name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' }, { name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' }, { name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' }, - { name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' }, + { name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' }, { name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' }, { name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' }, { name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' }, @@ -704,7 +704,7 @@ const flavorsData = [ { name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' }, { name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' }, { name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' }, - { name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' }, + { name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' }, { name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' }, { name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' }, { name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' }, @@ -728,7 +728,7 @@ const flavorsData = [ { name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' }, { name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' }, { name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' }, - { name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' }, + { name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' }, { name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' }, { name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' }, { name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' }, @@ -750,7 +750,7 @@ const flavorsData = [ { name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' }, { name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' }, { name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' }, - { name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' }, + { name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' }, { name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' }, { name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' }, { name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' }, diff --git a/src/dto/api.dto.ts b/src/dto/api.dto.ts index 1388a4e..9eb5102 100644 --- a/src/dto/api.dto.ts +++ b/src/dto/api.dto.ts @@ -56,7 +56,7 @@ export class UnifiedSearchParamsDTO> { * 批量操作错误项 */ export interface BatchErrorItem { - // 错误项标识(可以是ID、邮箱等) + // 错误项标识(可以是ID、邮箱等) identifier: string; // 错误信息 error: string; @@ -76,7 +76,7 @@ export interface BatchOperationResult { updated?: number; // 删除数量 deleted?: number; - // 跳过的数量(如数据已存在或无需处理) + // 跳过的数量(如数据已存在或无需处理) skipped?: number; // 错误列表 errors: BatchErrorItem[]; @@ -101,7 +101,7 @@ export class SyncOperationResult implements BatchOperationResult { * 批量操作错误项DTO */ export class BatchErrorItemDTO { - @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) + @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) @Rule(RuleType.string().required()) identifier: string; @@ -164,7 +164,7 @@ export class SyncParamsDTO { @Rule(RuleType.string().optional()) endDate?: string; - @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) + @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) @Rule(RuleType.boolean().optional()) force?: boolean = false; } @@ -183,7 +183,7 @@ export class BatchQueryDTO { } /** - * 批量操作结果类(泛型支持) + * 批量操作结果类(泛型支持) */ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { @ApiProperty({ description: '操作成功的数据列表', type: Array }) @@ -191,7 +191,7 @@ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { } /** - * 同步操作结果类(泛型支持) + * 同步操作结果类(泛型支持) */ export class SyncOperationResultDTOGeneric extends SyncOperationResultDTO { @ApiProperty({ description: '同步成功的数据列表', type: Array }) diff --git a/src/dto/batch.dto.ts b/src/dto/batch.dto.ts index 67b11cd..b55d5f0 100644 --- a/src/dto/batch.dto.ts +++ b/src/dto/batch.dto.ts @@ -5,7 +5,7 @@ import { Rule, RuleType } from '@midwayjs/validate'; * 批量操作错误项 */ export interface BatchErrorItem { - // 错误项标识(可以是ID、邮箱等) + // 错误项标识(可以是ID、邮箱等) identifier: string; // 错误信息 error: string; @@ -25,7 +25,7 @@ export interface BatchOperationResult { updated?: number; // 删除数量 deleted?: number; - // 跳过的数量(如数据已存在或无需处理) + // 跳过的数量(如数据已存在或无需处理) skipped?: number; // 错误列表 errors: BatchErrorItem[]; @@ -43,7 +43,7 @@ export interface SyncOperationResult extends BatchOperationResult { * 批量操作错误项DTO */ export class BatchErrorItemDTO { - @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) + @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) @Rule(RuleType.string().required()) identifier: string; @@ -114,7 +114,7 @@ export class BatchDeleteDTO { } /** - * 批量操作请求DTO(包含增删改) + * 批量操作请求DTO(包含增删改) */ export class BatchOperationDTO { @ApiProperty({ description: '要创建的数据列表', type: Array, required: false }) @@ -175,7 +175,7 @@ export class SyncParamsDTO { @Rule(RuleType.string().optional()) endDate?: string; - @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) + @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) @Rule(RuleType.boolean().optional()) force?: boolean = false; } @@ -194,7 +194,7 @@ export class BatchQueryDTO { } /** - * 批量操作结果类(泛型支持) + * 批量操作结果类(泛型支持) */ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { @ApiProperty({ description: '操作成功的数据列表', type: Array }) @@ -202,7 +202,7 @@ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { } /** - * 同步操作结果类(泛型支持) + * 同步操作结果类(泛型支持) */ export class SyncOperationResultDTOGeneric extends SyncOperationResultDTO { @ApiProperty({ description: '同步成功的数据列表', type: Array }) diff --git a/src/dto/customer.dto.ts b/src/dto/customer.dto.ts index 99a343d..b93b2b3 100644 --- a/src/dto/customer.dto.ts +++ b/src/dto/customer.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger'; import { UnifiedSearchParamsDTO } from './api.dto'; import { Customer } from '../entity/customer.entity'; -// 客户基本信息DTO(用于响应) +// 客户基本信息DTO(用于响应) export class CustomerDTO extends Customer{ @ApiProperty({ description: '客户ID' }) id: number; @@ -163,11 +163,11 @@ export class UpdateCustomerDTO { tags?: string[]; } -// 查询单个客户响应DTO(继承基本信息) +// 查询单个客户响应DTO(继承基本信息) export class GetCustomerDTO extends CustomerDTO { // 可以添加额外的详细信息字段 } -// 客户统计信息DTO(包含订单统计) +// 客户统计信息DTO(包含订单统计) export class CustomerStatisticDTO extends CustomerDTO { @ApiProperty({ description: '创建日期' }) date_created: Date; @@ -209,7 +209,7 @@ export class CustomerStatisticWhereDTO { customerId?: number; } -// 客户统计查询参数DTO(继承通用查询参数) +// 客户统计查询参数DTO(继承通用查询参数) export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO; // 客户统计列表响应DTO @@ -259,7 +259,7 @@ export class BatchDeleteCustomerDTO { // ====================== 查询操作 ====================== -// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数) +// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数) export class CustomerWhereDTO { @ApiProperty({ description: '邮箱筛选', required: false }) email?: string; @@ -284,10 +284,10 @@ export class CustomerWhereDTO { role?: string; } -// 客户查询参数DTO(继承通用查询参数) +// 客户查询参数DTO(继承通用查询参数) export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO; -// 客户列表响应DTO(参考site-api.dto.ts中的分页格式) +// 客户列表响应DTO(参考site-api.dto.ts中的分页格式) export class CustomerListResponseDTO { @ApiProperty({ description: '客户列表', type: [CustomerDTO] }) items: CustomerDTO[]; @@ -359,6 +359,6 @@ export class SyncCustomersDTO { @ApiProperty({ description: '站点ID' }) siteId: number; - @ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false }) + @ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false }) params?: UnifiedSearchParamsDTO; } \ No newline at end of file diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index 009d6ce..5aed39f 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -22,7 +22,7 @@ export class ShopyyAllProductQuery { id?: string; /** 商品标题,支持模糊查询 */ title?: string; - /** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */ + /** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */ status?: string; /** 商品SKU编码,库存保有单位,精确或模糊匹配 */ sku?: string; @@ -34,21 +34,21 @@ export class ShopyyAllProductQuery { variant_price_min?: string; /** 变体价格最大值,筛选变体价格小于等于该值的商品 */ variant_price_max?: string; - /** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */ + /** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */ variant_compare_at_price_min?: string; - /** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */ + /** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */ variant_compare_at_price_max?: string; - /** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */ + /** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */ variant_weight_min?: string; - /** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */ + /** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */ variant_weight_max?: string; - /** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ created_at_min?: string; - /** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ created_at_max?: string; - /** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ updated_at_min?: string; - /** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ updated_at_max?: string; } // 产品类型 @@ -135,7 +135,7 @@ export interface ShopyyVariant { export interface ShopyyOrderQuery { // 订单ID集合 多个ID用','联接 例:1,2,3 ids?: string; - // 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + // 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; status?: string; // 物流状态 300 未发货;310 部分发货;320 已发货;330(确认收货) fulfillment_status?: string; @@ -159,9 +159,9 @@ export interface ShopyyOrderQuery { page?: string; // 每页条数 limit?: string; - // 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间 + // 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间 order_field?: string; - // 排序方式(默认desc) desc=降序 asc=升序 + // 排序方式(默认desc) desc=降序 asc=升序 order_by?: string; // 订单列表类型 group?: string; @@ -513,7 +513,7 @@ export class ShopyyFulfillmentDTO { "tracking_number": string; "courier_code": number; "note": string; - "mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增) + "mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增) } // https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse export class ShopyPartFulfillmentDTO { diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index faee539..ccf0a87 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -141,7 +141,7 @@ export class UnifiedProductAttributeDTO { @ApiProperty({ description: '属性选项', type: [String] }) options: string[]; - @ApiProperty({ description: '变体属性值(单个值)', required: false }) + @ApiProperty({ description: '变体属性值(单个值)', required: false }) option?: string; // 这个是属性的父级字典项 dict?: Dict; diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index 27d8bde..4ad43da 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -152,7 +152,7 @@ export class QuerySiteDTO { @Rule(RuleType.boolean().optional()) isDisabled?: boolean; - @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) + @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) @Rule(RuleType.string().optional()) ids?: string; } diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index f562d64..cb09484 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -8,11 +8,11 @@ export interface WooProduct { id: number; // 创建时间 date_created: string; - // 创建时间(GMT) + // 创建时间(GMT) date_created_gmt: string; // 更新时间 date_modified: string; - // 更新时间(GMT) + // 更新时间(GMT) date_modified_gmt: string; // 产品类型 simple grouped external variable type: string; @@ -130,11 +130,11 @@ export interface WooVariation { id: number; // 创建时间 date_created: string; - // 创建时间(GMT) + // 创建时间(GMT) date_created_gmt: string; // 更新时间 date_modified: string; - // 更新时间(GMT) + // 更新时间(GMT) date_modified_gmt: string; // 变体描述 description: string; @@ -150,11 +150,11 @@ export interface WooVariation { price_html?: string; // 促销开始日期 date_on_sale_from?: string; - // 促销开始日期(GMT) + // 促销开始日期(GMT) date_on_sale_from_gmt?: string; // 促销结束日期 date_on_sale_to?: string; - // 促销结束日期(GMT) + // 促销结束日期(GMT) date_on_sale_to_gmt?: string; // 是否在促销中 on_sale: boolean; diff --git a/src/enums/base.enum.ts b/src/enums/base.enum.ts index 3b87088..68be364 100644 --- a/src/enums/base.enum.ts +++ b/src/enums/base.enum.ts @@ -42,7 +42,7 @@ export enum OrderStatus { REFUNDED = 'refunded', // 已退款 FAILED = 'failed', // 失败订单 DRAFT = 'draft', // 草稿 - AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现) + AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现) // TRASH = 'trash', // refund 也就是退款相关的状态 diff --git a/src/interface/platform.interface.ts b/src/interface/platform.interface.ts index f1681ed..231cd8d 100644 --- a/src/interface/platform.interface.ts +++ b/src/interface/platform.interface.ts @@ -53,7 +53,7 @@ export interface IPlatformService { getOrder(siteId: number, orderId: string): Promise; /** - * 获取订阅列表(如果平台支持) + * 获取订阅列表(如果平台支持) * @param siteId 站点ID * @returns 订阅列表数据 */ diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index 02ecb44..28e61b4 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -66,7 +66,7 @@ export class CustomerService { } if (typeof dateValue === 'number') { - // 处理Unix时间戳(秒或毫秒) + // 处理Unix时间戳(秒或毫秒) return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000); } @@ -95,7 +95,7 @@ export class CustomerService { } /** - * 创建或更新客户(upsert) + * 创建或更新客户(upsert) * 如果客户存在则更新,不存在则创建 */ async upsertCustomer( @@ -192,7 +192,7 @@ export class CustomerService { } /** - * 获取客户统计列表(包含订单统计信息) + * 获取客户统计列表(包含订单统计信息) * 支持分页、搜索和排序功能 * 使用原生SQL查询实现复杂的统计逻辑 */ @@ -363,7 +363,7 @@ export class CustomerService { } /** - * 获取纯粹的客户列表(不包含订单统计信息) + * 获取纯粹的客户列表(不包含订单统计信息) * 支持基本的分页、搜索和排序功能 * 使用TypeORM查询构建器实现 */ diff --git a/src/service/dict.service.ts b/src/service/dict.service.ts index 2f34a7c..14a5a0b 100644 --- a/src/service/dict.service.ts +++ b/src/service/dict.service.ts @@ -239,7 +239,7 @@ export class DictService { } // 更新或创建字典项 (Upsert) - // 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的 + // 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的 async upsertDictItem(dictId: number, itemData: { name: string; title: string; @@ -252,7 +252,7 @@ export class DictService { // 格式化 name const formattedName = this.formatName(itemData.name); - // 查找是否已存在该字典项(根据 name 和 dictId) + // 查找是否已存在该字典项(根据 name 和 dictId) const existingItem = await this.dictItemModel.findOne({ where: { name: formattedName, diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 1aa19bd..355790c 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -479,7 +479,7 @@ export class OrderService { // 如果不能更新 ERP 状态,则保留原有的 orderStatus entity.orderStatus = existingOrder.orderStatus; } - // 更新订单数据(包括 shipping、billing 等字段) + // 更新订单数据(包括 shipping、billing 等字段) await this.orderModel.update(existingOrder.id, entity); entity.id = existingOrder.id; return entity; @@ -2568,7 +2568,7 @@ export class OrderService { * @param {any[]} data 数据数组 * @param {Object} options 配置选项 * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' - * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) + * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) * @param {boolean} [options.writeFile=false] 是否写入文件 * @returns {string|Buffer} 根据type返回字符串或Buffer */ @@ -2617,7 +2617,7 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: // 获取当前用户目录 const userHomeDir = os.homedir(); - // 构建目标路径(下载目录) + // 构建目标路径(下载目录) const downloadsDir = path.join(userHomeDir, 'Downloads'); // 确保下载目录存在 diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 384eca8..d518fca 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -235,7 +235,7 @@ export class ProductService { .leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('attribute.dict', 'dict') .leftJoinAndSelect('product.category', 'category'); - // 处理分页参数(支持新旧两种格式) + // 处理分页参数(支持新旧两种格式) const page = query.page || 1; const pageSize = query.per_page || 10; @@ -393,7 +393,7 @@ export class ProductService { qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) }); } - // 品牌过滤(向后兼容) + // 品牌过滤(向后兼容) if (brandId) { qb.andWhere(qb => { const subQuery = qb @@ -423,7 +423,7 @@ export class ProductService { }); } - // 分类过滤(向后兼容) + // 分类过滤(向后兼容) if (categoryId) { qb.andWhere('product.categoryId = :categoryId', { categoryId }); } @@ -443,7 +443,7 @@ export class ProductService { qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds }); } - // 处理排序(支持新旧两种格式) + // 处理排序(支持新旧两种格式) if (orderBy) { if (typeof orderBy === 'string') { // 如果orderBy是字符串,尝试解析JSON @@ -1765,7 +1765,7 @@ export class ProductService { } - // 根据ID获取产品详情(包含站点SKU) + // 根据ID获取产品详情(包含站点SKU) async getProductById(id: number): Promise { const product = await this.productModel.findOne({ where: { id }, diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index 46d8ee4..22b17e4 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -152,7 +152,7 @@ export class SiteApiService { const result = await this.upsertProduct(siteId, product); // 判断是创建还是更新 if (result && result.id) { - // 简单判断:如果产品原本没有ID而现在有了,说明是创建的 + // 简单判断:如果产品原本没有ID而现在有了,说明是创建的 if (!product.id || !product.id.toString().trim()) { results.created.push(result); } else { diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 14cf044..5dd355e 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -254,7 +254,7 @@ export class WPService implements IPlatformService { } - // 导出 WooCommerce 产品为特殊CSV(平台特性) + // 导出 WooCommerce 产品为特殊CSV(平台特性) async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise { const list = await this.getProducts(site, { page, per_page: pageSize }); const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity']; diff --git a/test-site-sku-methods.md b/test-site-sku-methods.md deleted file mode 100644 index 18081a8..0000000 --- a/test-site-sku-methods.md +++ /dev/null @@ -1,105 +0,0 @@ -# 产品站点SKU查询方法测试文档 - -## 新增和更新的API接口 - -### 1. 根据产品ID获取站点SKU列表 -**接口**: `GET /product/:id/site-skus` -**功能**: 获取指定产品的所有站点SKU列表 -**返回**: 站点SKU对象数组,按创建时间升序排列 - -### 2. 根据站点SKU查询产品 -**接口**: `GET /product/site-sku/:siteSku` -**功能**: 根据站点SKU代码查询对应的产品信息 -**返回**: 完整的产品对象(包含站点SKU、分类、属性等关联数据) - -### 3. 根据产品ID获取产品详情 -**接口**: `GET /product/:id` -**功能**: 获取产品的完整详情信息 -**返回**: 完整的产品对象(包含站点SKU、分类、属性、组成等关联数据) - -### 4. 现有接口的增强 - -#### 4.1 根据SKU查询产品 -**接口**: `GET /product/sku/:sku` -**增强**: 现在返回的产品信息包含关联的站点SKU数据 - -#### 4.2 搜索产品 -**接口**: `GET /product/search?name=:name` -**增强**: 搜索结果现在包含每个产品的站点SKU数据 - -#### 4.3 获取产品列表 -**接口**: `GET /product/list` -**增强**: 产品列表中的每个产品现在都包含站点SKU数据 - -## 服务层新增方法 - -### ProductService新增方法 - -1. **getProductSiteSkus(productId: number)**: Promise - - 获取指定产品的所有站点SKU - - 包含产品关联信息 - - 按创建时间排序 - -2. **getProductById(id: number)**: Promise - - 根据产品ID获取完整产品信息 - - 包含站点SKU、分类、属性、组成等所有关联数据 - - 自动处理单品和混装商品的组成信息 - -3. **findProductBySiteSku(siteSku: string)**: Promise - - 根据站点SKU查询对应的产品 - - 返回完整的产品信息 - - 如果站点SKU不存在则抛出错误 - -### 现有方法增强 - -1. **findProductsByName(name: string)**: 现在包含站点SKU数据 -2. **findProductBySku(sku: string)**: 现在包含站点SKU数据 -3. **getProductList**: 已经包含站点SKU数据(无需更改) - -## 使用示例 - -### 获取产品的站点SKU列表 -```javascript -// GET /product/123/site-skus -// 返回: -[ - { - "id": 1, - "siteSku": "SITE-SKU-001", - "productId": 123, - "createdAt": "2024-01-01T00:00:00Z", - "updatedAt": "2024-01-01T00:00:00Z" - } -] -``` - -### 根据站点SKU查询产品 -```javascript -// GET /product/site-sku/SITE-SKU-001 -// 返回完整的产品对象,包含: -// - 基本信息(SKU、名称、价格等) -// - 分类信息 -// - 属性信息 -// - 站点SKU列表 -// - 组成信息 -``` - -### 获取产品详情 -```javascript -// GET /product/123 -// 返回完整的产品对象,与站点SKU查询类似 -``` - -## 数据库查询优化 - -所有新增和更新的方法都使用了TypeORM的关联查询,确保: -- 一次查询获取所有需要的数据 -- 避免N+1查询问题 -- 包含必要的关联关系(分类、属性、站点SKU、组成) - -## 错误处理 - -所有方法都包含适当的错误处理: -- 产品不存在时抛出明确的错误信息 -- 站点SKU不存在时抛出明确的错误信息 -- 控制器层统一处理错误并返回适当的HTTP响应 \ No newline at end of file From fd7b3dae588b04ffe1a1b4f58101cec38d06d762 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:54:44 +0800 Subject: [PATCH 21/24] =?UTF-8?q?chore:=20config.local=20=E8=BF=98?= =?UTF-8?q?=E5=8E=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.local.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/config/config.local.ts b/src/config/config.local.ts index b82c927..a8326f6 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -19,11 +19,10 @@ export default { dataSource: { default: { host: 'localhost', - port: "23306", + port: "3306", username: 'root', - password: '12345678', - database: 'inventory', - synchronize: true, + password: 'root', + database: 'inventory' }, }, }, From a5ab693852b4b4f14eacfb808e3b5769d56fe6ac Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:56:15 +0800 Subject: [PATCH 22/24] =?UTF-8?q?docs(dto):=20=E4=BF=AE=E6=AD=A3=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E4=B8=AD=E7=9A=84=E4=B8=AD=E6=96=87=E6=8B=AC=E5=8F=B7?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dto/api.dto.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dto/api.dto.ts b/src/dto/api.dto.ts index 9eb5102..6cd4c41 100644 --- a/src/dto/api.dto.ts +++ b/src/dto/api.dto.ts @@ -56,7 +56,7 @@ export class UnifiedSearchParamsDTO> { * 批量操作错误项 */ export interface BatchErrorItem { - // 错误项标识(可以是ID、邮箱等) + // 错误项标识(可以是ID、邮箱等) identifier: string; // 错误信息 error: string; @@ -76,7 +76,7 @@ export interface BatchOperationResult { updated?: number; // 删除数量 deleted?: number; - // 跳过的数量(如数据已存在或无需处理) + // 跳过的数量(如数据已存在或无需处理) skipped?: number; // 错误列表 errors: BatchErrorItem[]; @@ -101,7 +101,7 @@ export class SyncOperationResult implements BatchOperationResult { * 批量操作错误项DTO */ export class BatchErrorItemDTO { - @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) + @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) @Rule(RuleType.string().required()) identifier: string; @@ -164,7 +164,7 @@ export class SyncParamsDTO { @Rule(RuleType.string().optional()) endDate?: string; - @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) + @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) @Rule(RuleType.boolean().optional()) force?: boolean = false; } @@ -183,7 +183,7 @@ export class BatchQueryDTO { } /** - * 批量操作结果类(泛型支持) + * 批量操作结果类(泛型支持) */ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { @ApiProperty({ description: '操作成功的数据列表', type: Array }) @@ -191,7 +191,7 @@ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { } /** - * 同步操作结果类(泛型支持) + * 同步操作结果类(泛型支持) */ export class SyncOperationResultDTOGeneric extends SyncOperationResultDTO { @ApiProperty({ description: '同步成功的数据列表', type: Array }) From 44a7578c505217618eb86bb0a3ccdc84a4f2f407 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:57:51 +0800 Subject: [PATCH 23/24] =?UTF-8?q?docs(dto):=20=E4=BF=AE=E6=AD=A3=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E4=B8=AD=E7=9A=84=E6=8B=AC=E5=8F=B7=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dto/batch.dto.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dto/batch.dto.ts b/src/dto/batch.dto.ts index b55d5f0..4551a3e 100644 --- a/src/dto/batch.dto.ts +++ b/src/dto/batch.dto.ts @@ -5,7 +5,7 @@ import { Rule, RuleType } from '@midwayjs/validate'; * 批量操作错误项 */ export interface BatchErrorItem { - // 错误项标识(可以是ID、邮箱等) + // 错误项标识(可以是ID、邮箱等) identifier: string; // 错误信息 error: string; @@ -25,7 +25,7 @@ export interface BatchOperationResult { updated?: number; // 删除数量 deleted?: number; - // 跳过的数量(如数据已存在或无需处理) + // 跳过的数量(如数据已存在或无需处理) skipped?: number; // 错误列表 errors: BatchErrorItem[]; @@ -43,7 +43,7 @@ export interface SyncOperationResult extends BatchOperationResult { * 批量操作错误项DTO */ export class BatchErrorItemDTO { - @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) + @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) @Rule(RuleType.string().required()) identifier: string; @@ -114,7 +114,7 @@ export class BatchDeleteDTO { } /** - * 批量操作请求DTO(包含增删改) + * 批量操作请求DTO(包含增删改) */ export class BatchOperationDTO { @ApiProperty({ description: '要创建的数据列表', type: Array, required: false }) @@ -175,7 +175,7 @@ export class SyncParamsDTO { @Rule(RuleType.string().optional()) endDate?: string; - @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) + @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) @Rule(RuleType.boolean().optional()) force?: boolean = false; } @@ -194,7 +194,7 @@ export class BatchQueryDTO { } /** - * 批量操作结果类(泛型支持) + * 批量操作结果类(泛型支持) */ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { @ApiProperty({ description: '操作成功的数据列表', type: Array }) @@ -202,7 +202,7 @@ export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { } /** - * 同步操作结果类(泛型支持) + * 同步操作结果类(泛型支持) */ export class SyncOperationResultDTOGeneric extends SyncOperationResultDTO { @ApiProperty({ description: '同步成功的数据列表', type: Array }) From 541276ec202a9a27142adb599d7a249ec88aae36 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 19:04:46 +0800 Subject: [PATCH 24/24] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=A0=87=E7=82=B9=E7=AC=A6=E5=8F=B7=E5=92=8C=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一将中文注释和文档中的全角括号和冒号改为半角格式 修正部分TODO注释的标点符号 统一接口文档中的描述符号格式 --- permutation_fix.md | 184 ---------------------------- src/configuration.ts | 2 +- src/dto/customer.dto.ts | 16 +-- src/dto/shopyy.dto.ts | 28 ++--- src/dto/site-api.dto.ts | 2 +- src/dto/site.dto.ts | 2 +- src/dto/woocommerce.dto.ts | 14 +-- src/enums/base.enum.ts | 2 +- src/interface/platform.interface.ts | 2 +- src/service/customer.service.ts | 18 +-- src/service/dict.service.ts | 4 +- src/service/order.service.ts | 8 +- src/service/shopyy.service.ts | 2 +- src/service/wp.service.ts | 2 +- 14 files changed, 51 insertions(+), 235 deletions(-) delete mode 100644 permutation_fix.md diff --git a/permutation_fix.md b/permutation_fix.md deleted file mode 100644 index de227d0..0000000 --- a/permutation_fix.md +++ /dev/null @@ -1,184 +0,0 @@ -# Permutation页面列表显示问题分析和修复方案 - -## 问题分析 - -经过代码分析,发现了以下几个可能导致列表不显示的问题: - -### 1. API路径不匹配 -前端代码中引用的API函数名与后端控制器中的路径不一致: -- 前端:`productcontrollerGetcategoriesall`、`productcontrollerGetcategoryattributes`、`productcontrollerGetproductlist` -- 后端实际的API路径:`/product/categories/all`、`/product/category/:id/attributes`、`/product/list` - -### 2. 数据格式问题 -- `getCategoryAttributes`返回的数据结构与前端期望的不匹配 -- 属性值获取逻辑可能存在问题 - -### 3. 组合生成逻辑问题 -- 在生成排列组合时,数据结构和键值对应可能不正确 - -## 修复方案 - -### 后端修复 - -1. **修改getCategoryAttributes方法** - 在`/Users/zksu/Developer/work/workcode/API/src/service/product.service.ts`中: - -```typescript -// 获取分类下的属性配置 -async getCategoryAttributes(categoryId: number): Promise { - const category = await this.categoryModel.findOne({ - where: { id: categoryId }, - relations: ['attributes', 'attributes.attributeDict', 'attributes.attributeDict.items'], - }); - - if (!category) { - return []; - } - - // 格式化返回,匹配前端期望的数据结构 - return category.attributes.map(attr => ({ - id: attr.id, - dictId: attr.attributeDict.id, - name: attr.attributeDict.name, // 用于generateKeyFromPermutation - title: attr.attributeDict.title, // 用于列标题 - dict: { - id: attr.attributeDict.id, - name: attr.attributeDict.name, - title: attr.attributeDict.title, - items: attr.attributeDict.items || [] - } - })); -} -``` - -2. **确保dict/items接口可用** - 检查字典项获取接口: - -在`/Users/zksu/Developer/work/workcode/API/src/controller/dict.controller.ts`中添加或确认: - -```typescript -@Get('/items') -async getDictItems(@Query('dictId') dictId: number) { - try { - const dict = await this.dictModel.findOne({ - where: { id: dictId }, - relations: ['items'] - }); - - if (!dict) { - return []; - } - - return dict.items || []; - } catch (error) { - return errorResponse(error?.message || error); - } -} -``` - -### 前端修复建议 - -1. **添加错误处理和调试信息**: - -```typescript -// 在获取属性值的地方添加错误处理 -const fetchData = async () => { - setLoading(true); - try { - // 1. Fetch Attributes - const attrRes = await productcontrollerGetcategoryattributes({ - id: categoryId, - }); - console.log('Attributes response:', attrRes); // 调试用 - const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || []; - setAttributes(attrs); - - // 2. Fetch Attribute Values (Dict Items) - const valuesMap: Record = {}; - for (const attr of attrs) { - const dictId = attr.dict?.id || attr.dictId; - if (dictId) { - try { - const itemsRes = await request('/dict/items', { - params: { dictId }, - }); - console.log(`Dict items for ${attr.name}:`, itemsRes); // 调试用 - valuesMap[attr.name] = itemsRes || []; - } catch (error) { - console.error(`Failed to fetch items for dict ${dictId}:`, error); - valuesMap[attr.name] = []; - } - } - } - setAttributeValues(valuesMap); - - // 3. Fetch Existing Products - await fetchProducts(categoryId); - } catch (error) { - console.error('Error in fetchData:', error); - message.error('获取数据失败'); - } finally { - setLoading(false); - } -}; -``` - -2. **修复组合生成逻辑**: - -```typescript -// 修改generateKeyFromPermutation函数 -const generateKeyFromPermutation = (perm: any) => { - const parts = Object.keys(perm).map((attrName) => { - const valItem = perm[attrName]; - const val = valItem.name || valItem.value; // 兼容不同的数据格式 - return `${attrName}:${val}`; - }); - return parts.sort().join('|'); -}; - -// 修改generateAttributeKey函数 -const generateAttributeKey = (attrs: any[]) => { - const parts = attrs.map((a) => { - const key = a.dict?.name || a.dictName || a.name; - const val = a.name || a.value; - return `${key}:${val}`; - }); - return parts.sort().join('|'); -}; -``` - -3. **添加空状态处理**: - -```typescript -// 在ProTable中添加空状态提示 - -``` - -## 调试步骤 - -1. **检查网络请求**: - - 打开浏览器开发者工具 - - 检查 `/product/categories/all` 请求是否成功 - - 检查 `/product/category/:id/attributes` 请求返回的数据格式 - - 检查 `/dict/items?dictId=:id` 请求是否成功 - - 检查 `/product/list` 请求是否成功 - -2. **检查控制台日志**: - - 查看属性数据是否正确加载 - - 查看属性值是否正确获取 - - 查看排列组合是否正确生成 - -3. **检查数据结构**: - - 确认 `attributes` 数组是否正确 - - 确认 `attributeValues` 对象是否正确填充 - - 确认 `permutations` 数组是否正确生成 - -## 测试验证 - -1. 选择一个有属性配置的分类 -2. 确认属性有对应的字典项 -3. 检查排列组合是否正确显示 -4. 验证现有产品匹配是否正确 \ No newline at end of file diff --git a/src/configuration.ts b/src/configuration.ts index a27dbb0..9711264 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -99,7 +99,7 @@ export class MainConfiguration { } /** - * 初始化数据库(如果不存在则创建) + * 初始化数据库(如果不存在则创建) */ private async initializeDatabase(): Promise { // 使用注入的数据库配置 diff --git a/src/dto/customer.dto.ts b/src/dto/customer.dto.ts index b93b2b3..74e78b8 100644 --- a/src/dto/customer.dto.ts +++ b/src/dto/customer.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger'; import { UnifiedSearchParamsDTO } from './api.dto'; import { Customer } from '../entity/customer.entity'; -// 客户基本信息DTO(用于响应) +// 客户基本信息DTO(用于响应) export class CustomerDTO extends Customer{ @ApiProperty({ description: '客户ID' }) id: number; @@ -163,11 +163,11 @@ export class UpdateCustomerDTO { tags?: string[]; } -// 查询单个客户响应DTO(继承基本信息) +// 查询单个客户响应DTO(继承基本信息) export class GetCustomerDTO extends CustomerDTO { // 可以添加额外的详细信息字段 } -// 客户统计信息DTO(包含订单统计) +// 客户统计信息DTO(包含订单统计) export class CustomerStatisticDTO extends CustomerDTO { @ApiProperty({ description: '创建日期' }) date_created: Date; @@ -209,7 +209,7 @@ export class CustomerStatisticWhereDTO { customerId?: number; } -// 客户统计查询参数DTO(继承通用查询参数) +// 客户统计查询参数DTO(继承通用查询参数) export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO; // 客户统计列表响应DTO @@ -259,7 +259,7 @@ export class BatchDeleteCustomerDTO { // ====================== 查询操作 ====================== -// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数) +// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数) export class CustomerWhereDTO { @ApiProperty({ description: '邮箱筛选', required: false }) email?: string; @@ -284,10 +284,10 @@ export class CustomerWhereDTO { role?: string; } -// 客户查询参数DTO(继承通用查询参数) +// 客户查询参数DTO(继承通用查询参数) export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO; -// 客户列表响应DTO(参考site-api.dto.ts中的分页格式) +// 客户列表响应DTO(参考site-api.dto.ts中的分页格式) export class CustomerListResponseDTO { @ApiProperty({ description: '客户列表', type: [CustomerDTO] }) items: CustomerDTO[]; @@ -359,6 +359,6 @@ export class SyncCustomersDTO { @ApiProperty({ description: '站点ID' }) siteId: number; - @ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false }) + @ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false }) params?: UnifiedSearchParamsDTO; } \ No newline at end of file diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index 5aed39f..e775bdd 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -22,7 +22,7 @@ export class ShopyyAllProductQuery { id?: string; /** 商品标题,支持模糊查询 */ title?: string; - /** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */ + /** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */ status?: string; /** 商品SKU编码,库存保有单位,精确或模糊匹配 */ sku?: string; @@ -34,21 +34,21 @@ export class ShopyyAllProductQuery { variant_price_min?: string; /** 变体价格最大值,筛选变体价格小于等于该值的商品 */ variant_price_max?: string; - /** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */ + /** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */ variant_compare_at_price_min?: string; - /** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */ + /** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */ variant_compare_at_price_max?: string; - /** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */ + /** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */ variant_weight_min?: string; - /** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */ + /** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */ variant_weight_max?: string; - /** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ created_at_min?: string; - /** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ created_at_max?: string; - /** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ updated_at_min?: string; - /** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ + /** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */ updated_at_max?: string; } // 产品类型 @@ -133,9 +133,9 @@ export interface ShopyyVariant { // // 订单查询参数类型 export interface ShopyyOrderQuery { - // 订单ID集合 多个ID用','联接 例:1,2,3 + // 订单ID集合 多个ID用','联接 例:1,2,3 ids?: string; - // 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + // 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; status?: string; // 物流状态 300 未发货;310 部分发货;320 已发货;330(确认收货) fulfillment_status?: string; @@ -159,9 +159,9 @@ export interface ShopyyOrderQuery { page?: string; // 每页条数 limit?: string; - // 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间 + // 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间 order_field?: string; - // 排序方式(默认desc) desc=降序 asc=升序 + // 排序方式(默认desc) desc=降序 asc=升序 order_by?: string; // 订单列表类型 group?: string; @@ -513,7 +513,7 @@ export class ShopyyFulfillmentDTO { "tracking_number": string; "courier_code": number; "note": string; - "mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增) + "mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增) } // https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse export class ShopyPartFulfillmentDTO { diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index ccf0a87..5c8e1d2 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -141,7 +141,7 @@ export class UnifiedProductAttributeDTO { @ApiProperty({ description: '属性选项', type: [String] }) options: string[]; - @ApiProperty({ description: '变体属性值(单个值)', required: false }) + @ApiProperty({ description: '变体属性值(单个值)', required: false }) option?: string; // 这个是属性的父级字典项 dict?: Dict; diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index 4ad43da..7c0267d 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -152,7 +152,7 @@ export class QuerySiteDTO { @Rule(RuleType.boolean().optional()) isDisabled?: boolean; - @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) + @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) @Rule(RuleType.string().optional()) ids?: string; } diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index cb09484..5077450 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -8,11 +8,11 @@ export interface WooProduct { id: number; // 创建时间 date_created: string; - // 创建时间(GMT) + // 创建时间(GMT) date_created_gmt: string; // 更新时间 date_modified: string; - // 更新时间(GMT) + // 更新时间(GMT) date_modified_gmt: string; // 产品类型 simple grouped external variable type: string; @@ -20,7 +20,7 @@ export interface WooProduct { status: string; // 是否为特色产品 featured: boolean; - // 目录可见性选项:visible, catalog, search and hidden. Default is visible. + // 目录可见性选项:visible, catalog, search and hidden. Default is visible. catalog_visibility: string; // 常规价格 @@ -130,11 +130,11 @@ export interface WooVariation { id: number; // 创建时间 date_created: string; - // 创建时间(GMT) + // 创建时间(GMT) date_created_gmt: string; // 更新时间 date_modified: string; - // 更新时间(GMT) + // 更新时间(GMT) date_modified_gmt: string; // 变体描述 description: string; @@ -150,11 +150,11 @@ export interface WooVariation { price_html?: string; // 促销开始日期 date_on_sale_from?: string; - // 促销开始日期(GMT) + // 促销开始日期(GMT) date_on_sale_from_gmt?: string; // 促销结束日期 date_on_sale_to?: string; - // 促销结束日期(GMT) + // 促销结束日期(GMT) date_on_sale_to_gmt?: string; // 是否在促销中 on_sale: boolean; diff --git a/src/enums/base.enum.ts b/src/enums/base.enum.ts index 68be364..c79c4c9 100644 --- a/src/enums/base.enum.ts +++ b/src/enums/base.enum.ts @@ -42,7 +42,7 @@ export enum OrderStatus { REFUNDED = 'refunded', // 已退款 FAILED = 'failed', // 失败订单 DRAFT = 'draft', // 草稿 - AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现) + AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现) // TRASH = 'trash', // refund 也就是退款相关的状态 diff --git a/src/interface/platform.interface.ts b/src/interface/platform.interface.ts index 231cd8d..1fd066e 100644 --- a/src/interface/platform.interface.ts +++ b/src/interface/platform.interface.ts @@ -53,7 +53,7 @@ export interface IPlatformService { getOrder(siteId: number, orderId: string): Promise; /** - * 获取订阅列表(如果平台支持) + * 获取订阅列表(如果平台支持) * @param siteId 站点ID * @returns 订阅列表数据 */ diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index 28e61b4..47f430f 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -66,7 +66,7 @@ export class CustomerService { } if (typeof dateValue === 'number') { - // 处理Unix时间戳(秒或毫秒) + // 处理Unix时间戳(秒或毫秒) return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000); } @@ -95,7 +95,7 @@ export class CustomerService { } /** - * 创建或更新客户(upsert) + * 创建或更新客户(upsert) * 如果客户存在则更新,不存在则创建 */ async upsertCustomer( @@ -157,24 +157,24 @@ export class CustomerService { /** * 从站点同步客户数据 - * 第一步:调用adapter获取站点客户数据 - * 第二步:通过upsertManyCustomers保存这些客户 + * 第一步:调用adapter获取站点客户数据 + * 第二步:通过upsertManyCustomers保存这些客户 */ async syncCustomersFromSite( siteId: number, params?: UnifiedSearchParamsDTO ): Promise { try { - // 第一步:获取适配器并从站点获取客户数据 + // 第一步:获取适配器并从站点获取客户数据 const adapter = await this.siteApiService.getAdapter(siteId); const siteCustomers = await adapter.getAllCustomers(params || {}); - // 第二步:将站点客户数据转换为客户实体数据 + // 第二步:将站点客户数据转换为客户实体数据 const customersData = siteCustomers.map(siteCustomer => { return this.mapSiteCustomerToCustomer(siteCustomer, siteId); }) - // 第三步:批量upsert客户数据 + // 第三步:批量upsert客户数据 const upsertResult = await this.upsertManyCustomers(customersData); return { total: siteCustomers.length, @@ -192,7 +192,7 @@ export class CustomerService { } /** - * 获取客户统计列表(包含订单统计信息) + * 获取客户统计列表(包含订单统计信息) * 支持分页、搜索和排序功能 * 使用原生SQL查询实现复杂的统计逻辑 */ @@ -363,7 +363,7 @@ export class CustomerService { } /** - * 获取纯粹的客户列表(不包含订单统计信息) + * 获取纯粹的客户列表(不包含订单统计信息) * 支持基本的分页、搜索和排序功能 * 使用TypeORM查询构建器实现 */ diff --git a/src/service/dict.service.ts b/src/service/dict.service.ts index 14a5a0b..6d683a6 100644 --- a/src/service/dict.service.ts +++ b/src/service/dict.service.ts @@ -239,7 +239,7 @@ export class DictService { } // 更新或创建字典项 (Upsert) - // 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的 + // 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的 async upsertDictItem(dictId: number, itemData: { name: string; title: string; @@ -252,7 +252,7 @@ export class DictService { // 格式化 name const formattedName = this.formatName(itemData.name); - // 查找是否已存在该字典项(根据 name 和 dictId) + // 查找是否已存在该字典项(根据 name 和 dictId) const existingItem = await this.dictItemModel.findOne({ where: { name: formattedName, diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 355790c..3dfd68b 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -479,7 +479,7 @@ export class OrderService { // 如果不能更新 ERP 状态,则保留原有的 orderStatus entity.orderStatus = existingOrder.orderStatus; } - // 更新订单数据(包括 shipping、billing 等字段) + // 更新订单数据(包括 shipping、billing 等字段) await this.orderModel.update(existingOrder.id, entity); entity.id = existingOrder.id; return entity; @@ -2567,8 +2567,8 @@ export class OrderService { * 导出数据为CSV格式 * @param {any[]} data 数据数组 * @param {Object} options 配置选项 - * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' - * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) + * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' + * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) * @param {boolean} [options.writeFile=false] 是否写入文件 * @returns {string|Buffer} 根据type返回字符串或Buffer */ @@ -2617,7 +2617,7 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: // 获取当前用户目录 const userHomeDir = os.homedir(); - // 构建目标路径(下载目录) + // 构建目标路径(下载目录) const downloadsDir = path.join(userHomeDir, 'Downloads'); // 确保下载目录存在 diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index 0081f55..4a663cb 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -128,7 +128,7 @@ export class ShopyyService { * @returns 完整URL */ private buildURL(baseUrl: string, endpoint: string): string { - // ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint} + // ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint} const base = baseUrl.replace(/\/$/, ''); const end = endpoint.replace(/^\//, ''); return `${base}/${end}`; diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 5dd355e..a4d66e5 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -254,7 +254,7 @@ export class WPService implements IPlatformService { } - // 导出 WooCommerce 产品为特殊CSV(平台特性) + // 导出 WooCommerce 产品为特殊CSV(平台特性) async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise { const list = await this.getProducts(site, { page, per_page: pageSize }); const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];