/** * Medusa API 客户端 */ const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000' const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' // Payload API key - 用于同步产品数据(包括 metadata) const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || '' interface MedusaProduct { id: string title: string handle: string description: string thumbnail: string status: string created_at: string updated_at: string metadata?: Record & { is_preorder?: boolean | string seed_id?: string } images?: Array<{ id: string url: string }> variants?: Array<{ id: string title: string prices: Array<{ amount: number currency_code: string }> }> tags?: Array<{ id: string value: string }> // 完整的关联对象 collection?: { id: string title: string handle: string } type?: { id: string value: string } categories?: Array<{ id: string name: string handle: string }> // 物理属性 height?: number width?: number length?: number weight?: number // 海关与物流 mid_code?: string hs_code?: string origin_country?: string // 兼容旧字段 collection_id?: string type_id?: string } interface MedusaResponse { products?: T[] product?: T count?: number offset?: number limit?: number } /** * 获取所有 Medusa 商品(使用 admin API 以获取 metadata) */ export async function getAllMedusaProducts(): Promise { try { const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products-sync`, { headers: { 'Content-Type': 'application/json', 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY, 'x-payload-api-key': PAYLOAD_API_KEY, }, }) if (!response.ok) { const errorText = await response.text() console.error(`[getAllMedusaProducts] Error response:`, errorText) throw new Error(`Failed to fetch products: ${response.status} ${response.statusText} - ${errorText}`) } const data: MedusaResponse = await response.json() return data.products || [] } catch (error) { console.error('[getAllMedusaProducts] Error fetching Medusa products:', error) throw error } } /** * 获取单个 Medusa 商品 */ export async function getMedusaProduct(productId: string): Promise { try { const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products/${productId}`, { headers: { 'Content-Type': 'application/json', 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY, }, }) if (!response.ok) { if (response.status === 404) { return null } throw new Error(`Failed to fetch product: ${response.statusText}`) } const data: MedusaResponse = await response.json() return data.product || null } catch (error) { console.error(`Error fetching Medusa product ${productId}:`, error) throw error } } /** * 分页获取 Medusa 商品 * 注意:由于使用自定义同步端点,实际会获取所有产品然后在内存中分页 */ export async function getMedusaProductsPaginated( offset: number = 0, limit: number = 100, ): Promise<{ products: MedusaProduct[]; count: number }> { try { // 获取所有产品 const allProducts = await getAllMedusaProducts() // 在内存中分页 const products = allProducts.slice(offset, offset + limit) return { products, count: allProducts.length, } } catch (error) { console.error('Error fetching Medusa products (paginated):', error) throw error } } /** * 转换 Medusa 商品为 Payload 格式 */ /** * 从 URL 下载并上传图片到 Payload Media 集合 */ export async function uploadImageFromUrl(imageUrl: string, payload: any): Promise { if (!imageUrl) return null try { // 下载图片 const response = await fetch(imageUrl) if (!response.ok) { console.error(`Failed to download image: ${imageUrl}`) return null } const blob = await response.blob() const arrayBuffer = await blob.arrayBuffer() const buffer = Buffer.from(arrayBuffer) // 从 URL 提取文件名 const urlParts = imageUrl.split('/') const filename = urlParts[urlParts.length - 1] || 'product-image.jpg' // 上传到 Media 集合 const media = await payload.create({ collection: 'media', data: { alt: filename, }, file: { data: buffer, mimetype: blob.type || 'image/jpeg', name: filename, size: buffer.length, }, }) return media.id } catch (error) { console.error(`Error uploading image from URL ${imageUrl}:`, error) return null } } /** * 判断产品是否为预售商品 * 检查 metadata.is_preorder 和 metadata.preorder (支持布尔值和字符串) */ export function isPreorderProduct(product: MedusaProduct): boolean { const isPreorderValue = product.metadata?.is_preorder const preorderValue = product.metadata?.preorder // 尝试两个字段,支持多种类型 const value = isPreorderValue !== undefined ? isPreorderValue : preorderValue // 支持布尔值 true、字符串 "true"、数字 1、字符串 "1" if (value === true || value === 'true' || value === '1' || value === 1) { return true } return false } /** * 获取产品应该同步到的 collection */ export function getProductCollection(product: MedusaProduct): 'preorder-products' | 'products' { return isPreorderProduct(product) ? 'preorder-products' : 'products' } /** * 转换 Medusa 商品数据到 Payload 格式 * 直接保存 Medusa 的图片 URL,不上传到 Media 集合 */ export function transformMedusaProductToPayload(product: MedusaProduct) { // 优先使用 thumbnail,如果没有则使用第一张图片的 URL let thumbnailUrl = product.thumbnail if (!thumbnailUrl && product.images && product.images.length > 0) { thumbnailUrl = product.images[0].url } // 提取 tags(逗号分隔) const tags = product.tags?.map(tag => tag.value).join(', ') || null // 提取 type(优先使用 type.value,否则使用 type_id 或 metadata) const type = product.type?.value || product.type_id || product.metadata?.type || null // 提取 collection(优先使用 collection.title,否则使用 collection_id 或 metadata) const collection = product.collection?.title || product.collection_id || product.metadata?.collection || null // 提取 category(从 categories 数组或 metadata) const category = product.categories?.[0]?.name || product.metadata?.category || null return { medusaId: product.id, seedId: product.metadata?.seed_id || null, title: product.title, handle: product.handle, thumbnail: thumbnailUrl || null, status: 'draft', lastSyncedAt: new Date().toISOString(), // Medusa 默认属性 tags: tags, type: type, collection: collection, category: category, // 物理属性(优先使用直接字段,否则从 metadata 中提取) height: product.height || (product.metadata?.height ? Number(product.metadata.height) : null), width: product.width || (product.metadata?.width ? Number(product.metadata.width) : null), length: product.length || (product.metadata?.length ? Number(product.metadata.length) : null), weight: product.weight || (product.metadata?.weight ? Number(product.metadata.weight) : null), // 海关与物流(优先使用直接字段,否则从 metadata 中提取) midCode: product.mid_code || product.metadata?.mid_code || product.metadata?.midCode || null, hsCode: product.hs_code || product.metadata?.hs_code || product.metadata?.hsCode || null, countryOfOrigin: product.origin_country || product.metadata?.country_of_origin || product.metadata?.countryOfOrigin || null, } } /** * 转换 Medusa 商品数据到 Payload 格式(异步版本) * 将图片上传到 Media 集合并返回 Media ID */ export async function transformMedusaProductToPayloadAsync( product: MedusaProduct, payload: any, ): Promise<{ medusaId: string title: string handle: string thumbnail: string | null status: string lastSyncedAt: string }> { // 优先使用 thumbnail,如果没有则使用第一张图片的 URL let thumbnailUrl = product.thumbnail if (!thumbnailUrl && product.images && product.images.length > 0) { thumbnailUrl = product.images[0].url } // 上传图片到 Media 集合 let thumbnailId: string | null = null if (thumbnailUrl) { thumbnailId = await uploadImageFromUrl(thumbnailUrl, payload) } return { medusaId: product.id, title: product.title, handle: product.handle, thumbnail: thumbnailId, status: 'draft', lastSyncedAt: new Date().toISOString(), } }