feat(shopyy): 实现全量商品查询功能并优化产品相关逻辑

- 新增ShopyyAllProductQuery类支持全量商品查询参数
- 实现getAllProducts方法支持带条件查询
- 优化getProductBySku方法使用新查询接口
- 公开request方法便于子类调用
- 增加错误日志记录产品查找失败情况
- 修复产品permalink生成逻辑
This commit is contained in:
tikkhun 2026-01-08 18:17:03 +08:00
parent bdc2af3514
commit 8bdc438a48
5 changed files with 108 additions and 35 deletions

View File

@ -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<UnifiedProductDTO[]> {
// Shopyy getAllProducts 暂未实现
throw new Error('Shopyy getAllProducts 暂未实现');
async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> {
// Shopyy getAllProducts 暂未实现
throw new Error('Shopyy getAllProducts 暂未实现');
mapAllProductParams(params: UnifiedSearchParamsDTO): Partial<ShopyyAllProductQuery>{
const mapped = {
...params.where,
} as any
if(params.per_page){mapped.limit = params.per_page}
return mapped
}
async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> {
// 转换搜索参数
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<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
// 使用映射方法转换参数
const requestParams = this.mapCreateProductParams(data);
const res = await this.shopyyService.createProduct(this.site, requestParams);
return this.mapPlatformToUnifiedProduct(res);
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
// 使用映射方法转换参数
const requestParams = this.mapCreateProductParams(data);
@ -998,12 +1007,13 @@ export class ShopyyAdapter implements ISiteAdapter {
// 通过sku获取产品详情的私有方法
private async getProductBySku(sku: string): Promise<UnifiedProductDTO> {
// 使用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<UnifiedReviewDTO>) {

View File

@ -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 {
// 产品主键
@ -259,7 +302,8 @@ export interface ShopyyOrder {
// 创建时间
created_at?: number;
// 发货商品表 id
id?: number }>;
id?: number
}>;
}>;
shipping_zone_plans?: Array<{
shipping_price?: number | string;

View File

@ -18,6 +18,11 @@ export enum OrderFulfillmentStatus {
// 确认发货
CONFIRMED,
}
//
export class UnifiedProductWhere {
sku?: string;
[prop:string]:any
}
export class UnifiedTagDTO {
// 标签DTO用于承载统一标签数据
@ApiProperty({ description: '标签ID' })

View File

@ -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<any> {
async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> {
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<any> {
async getProducts(site: any, page: number = 1, pageSize: number = 100, where: Record<string, any> = {}): Promise<any> {
// 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 {

View File

@ -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<ISiteAdapter> {
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);