From 8bdc438a489ec0326d6095ff7580cb2fced1b591 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 8 Jan 2026 18:17:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(shopyy):=20=E5=AE=9E=E7=8E=B0=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E5=95=86=E5=93=81=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BA=A7=E5=93=81=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=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);