gbmake-payload/src/lib/medusa.ts

316 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, any> & {
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<T> {
products?: T[]
product?: T
count?: number
offset?: number
limit?: number
}
/**
* 获取所有 Medusa 商品(使用 admin API 以获取 metadata
*/
export async function getAllMedusaProducts(): Promise<MedusaProduct[]> {
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<MedusaProduct> = 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<MedusaProduct | null> {
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<MedusaProduct> = 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<string | null> {
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(),
}
}