精简api
This commit is contained in:
parent
41f3eb5adf
commit
b9cb60e3d0
|
|
@ -35,6 +35,7 @@ import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228
|
||||||
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
||||||
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
||||||
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
||||||
|
import { RestoreRecommendationsSeedButton as RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da } from '../../../components/seed/RestoreRecommendationsSeedButton'
|
||||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||||
|
|
||||||
|
|
@ -76,6 +77,7 @@ export const importMap = {
|
||||||
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
||||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||||
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||||
|
"/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton": RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da,
|
||||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin Management API
|
||||||
|
* Combined endpoint for data clearing, stats, and diagnostics
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/admin?action=stats
|
||||||
|
* Get Payload collection statistics
|
||||||
|
*/
|
||||||
|
async function getStats() {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
const [products, preorderProducts] = await Promise.all([
|
||||||
|
payload.find({ collection: 'products', limit: 0 }),
|
||||||
|
payload.find({ collection: 'preorder-products', limit: 0 }),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
products: products.totalDocs,
|
||||||
|
preorderProducts: preorderProducts.totalDocs,
|
||||||
|
total: products.totalDocs + preorderProducts.totalDocs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/admin
|
||||||
|
* Clear Payload data (preserves Users and Media)
|
||||||
|
* Query params: ?collections=products,preorderProducts,announcements,articles
|
||||||
|
*/
|
||||||
|
async function clearData(searchParams: URLSearchParams) {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
const collectionsParam = searchParams.get('collections')
|
||||||
|
const collections = collectionsParam
|
||||||
|
? collectionsParam.split(',')
|
||||||
|
: ['products', 'preorder-products', 'announcements', 'articles']
|
||||||
|
|
||||||
|
const results: Record<string, number> = {}
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
for (const collection of collections) {
|
||||||
|
// Protect critical collections
|
||||||
|
if (['users', 'media'].includes(collection)) {
|
||||||
|
errors.push(`Skipped protected collection: ${collection}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleted = await payload.delete({
|
||||||
|
collection: collection as any,
|
||||||
|
where: {},
|
||||||
|
})
|
||||||
|
results[collection] = deleted.docs?.length || 0
|
||||||
|
console.log(`✅ Cleared ${results[collection]} documents from ${collection}`)
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `Failed to clear ${collection}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
console.error('❌', errorMsg)
|
||||||
|
errors.push(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Data cleared successfully',
|
||||||
|
results,
|
||||||
|
errors: errors.length > 0 ? errors : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const action = searchParams.get('action')
|
||||||
|
|
||||||
|
if (action === 'stats') {
|
||||||
|
const stats = await getStats()
|
||||||
|
return NextResponse.json({ success: true, ...stats })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid action. Valid actions: stats',
|
||||||
|
}, { status: 400 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[admin] GET error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const result = await clearData(searchParams)
|
||||||
|
return NextResponse.json(result)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[admin] DELETE error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
import config from '@payload-config'
|
import config from '@payload-config'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
|
import { getAllMedusaProducts } from '@/lib/medusa'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量同步选中的产品到 Medusa
|
* Batch Sync Selected Products
|
||||||
* POST /api/batch-sync-medusa
|
* POST /api/batch-sync-medusa
|
||||||
* Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate: boolean }
|
* Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate?: boolean }
|
||||||
*/
|
*/
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -14,83 +15,87 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ success: false, error: 'No product IDs provided' },
|
||||||
success: false,
|
|
||||||
error: 'No product IDs provided',
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!collection || !['products', 'preorder-products'].includes(collection)) {
|
if (!collection || !['products', 'preorder-products'].includes(collection)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ success: false, error: 'Invalid collection' },
|
||||||
success: false,
|
|
||||||
error: 'Invalid collection',
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
// Get all Medusa products once
|
||||||
|
const medusaProducts = await getAllMedusaProducts()
|
||||||
|
const medusaProductMap = new Map(medusaProducts.map(p => [p.id, p]))
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
total: ids.length,
|
total: ids.length,
|
||||||
success: 0,
|
success: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
|
skipped: 0,
|
||||||
details: [] as any[],
|
details: [] as any[],
|
||||||
}
|
}
|
||||||
|
|
||||||
// 逐个同步选中的产品
|
// Sync each selected product
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
try {
|
try {
|
||||||
// 获取产品信息
|
|
||||||
const product = await payload.findByID({
|
const product = await payload.findByID({
|
||||||
collection: collection as 'products' | 'preorder-products',
|
collection: collection as 'products' | 'preorder-products',
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!product || !product.medusaId) {
|
if (!product || !product.medusaId) {
|
||||||
results.failed++
|
results.skipped++
|
||||||
results.details.push({
|
results.details.push({
|
||||||
id,
|
id,
|
||||||
title: product?.title || 'Unknown',
|
title: product?.title || 'Unknown',
|
||||||
status: 'failed',
|
status: 'skipped',
|
||||||
error: 'No Medusa ID',
|
reason: 'No Medusa ID',
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用单个产品同步 API
|
const medusaProduct = medusaProductMap.get(product.medusaId)
|
||||||
const syncResponse = await fetch(
|
|
||||||
`${request.url.replace('/api/batch-sync-medusa', '/api/sync-medusa')}?medusaId=${product.medusaId}&forceUpdate=${forceUpdate}`,
|
if (!medusaProduct) {
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: request.headers,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const syncResult = await syncResponse.json()
|
|
||||||
|
|
||||||
if (syncResult.success) {
|
|
||||||
results.success++
|
|
||||||
results.details.push({
|
|
||||||
id,
|
|
||||||
medusaId: product.medusaId,
|
|
||||||
title: product.title,
|
|
||||||
status: 'success',
|
|
||||||
action: syncResult.action,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
results.failed++
|
results.failed++
|
||||||
results.details.push({
|
results.details.push({
|
||||||
id,
|
id,
|
||||||
medusaId: product.medusaId,
|
medusaId: product.medusaId,
|
||||||
title: product.title,
|
title: product.title,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
error: syncResult.error || syncResult.message,
|
error: 'Product not found in Medusa',
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update basic fields from Medusa
|
||||||
|
const updateData: any = {
|
||||||
|
lastSyncedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceUpdate || !product.title) updateData.title = medusaProduct.title
|
||||||
|
if (forceUpdate || !product.handle) updateData.handle = medusaProduct.handle
|
||||||
|
if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail?.url
|
||||||
|
|
||||||
|
await payload.update({
|
||||||
|
collection: collection as 'products' | 'preorder-products',
|
||||||
|
id,
|
||||||
|
data: updateData,
|
||||||
|
})
|
||||||
|
|
||||||
|
results.success++
|
||||||
|
results.details.push({
|
||||||
|
id,
|
||||||
|
medusaId: product.medusaId,
|
||||||
|
title: product.title,
|
||||||
|
status: 'success',
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.failed++
|
results.failed++
|
||||||
results.details.push({
|
results.details.push({
|
||||||
|
|
@ -103,11 +108,11 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `批量同步完成: ${results.success} 成功, ${results.failed} 失败`,
|
message: `Batch sync completed: ${results.success} success, ${results.failed} failed, ${results.skipped} skipped`,
|
||||||
results,
|
results,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Batch sync error:', error)
|
console.error('[batch-sync-medusa] Error:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
import { getPayload } from 'payload'
|
|
||||||
import config from '@payload-config'
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理数据库数据(保留 Users 和 Media)
|
|
||||||
* GET /api/clear-data?confirm=true
|
|
||||||
*/
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const confirm = searchParams.get('confirm')
|
|
||||||
|
|
||||||
// 安全检查:必须明确确认
|
|
||||||
if (confirm !== 'true') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: '需要确认参数:?confirm=true',
|
|
||||||
message:
|
|
||||||
'此操作将删除 Products, Announcements, Articles 的所有数据(保留 Users 和 Media)',
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
products: 0,
|
|
||||||
preorderProducts: 0,
|
|
||||||
announcements: 0,
|
|
||||||
articles: 0,
|
|
||||||
errors: [] as string[],
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理 Products
|
|
||||||
try {
|
|
||||||
const deletedProducts = await payload.delete({
|
|
||||||
collection: 'products',
|
|
||||||
where: {},
|
|
||||||
})
|
|
||||||
results.products = deletedProducts.docs?.length || 0
|
|
||||||
console.log(`✅ 已清理 ${results.products} 个商品`)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = `清理 Products 失败: ${error instanceof Error ? error.message : '未知错误'}`
|
|
||||||
console.error('❌', errorMsg)
|
|
||||||
results.errors.push(errorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理 Preorder Products
|
|
||||||
try {
|
|
||||||
const deletedPreorderProducts = await payload.delete({
|
|
||||||
collection: 'preorder-products',
|
|
||||||
where: {},
|
|
||||||
})
|
|
||||||
results.preorderProducts = deletedPreorderProducts.docs?.length || 0
|
|
||||||
console.log(`✅ 已清理 ${results.preorderProducts} 个预购商品`)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = `清理 Preorder Products 失败: ${error instanceof Error ? error.message : '未知错误'}`
|
|
||||||
console.error('❌', errorMsg)
|
|
||||||
results.errors.push(errorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理 Announcements
|
|
||||||
try {
|
|
||||||
const deletedAnnouncements = await payload.delete({
|
|
||||||
collection: 'announcements',
|
|
||||||
where: {},
|
|
||||||
})
|
|
||||||
results.announcements = deletedAnnouncements.docs?.length || 0
|
|
||||||
console.log(`✅ 已清理 ${results.announcements} 个公告`)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = `清理 Announcements 失败: ${error instanceof Error ? error.message : '未知错误'}`
|
|
||||||
console.error('❌', errorMsg)
|
|
||||||
results.errors.push(errorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理 Articles
|
|
||||||
try {
|
|
||||||
const deletedArticles = await payload.delete({
|
|
||||||
collection: 'articles',
|
|
||||||
where: {},
|
|
||||||
})
|
|
||||||
results.articles = deletedArticles.docs?.length || 0
|
|
||||||
console.log(`✅ 已清理 ${results.articles} 个文章`)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = `清理 Articles 失败: ${error instanceof Error ? error.message : '未知错误'}`
|
|
||||||
console.error('❌', errorMsg)
|
|
||||||
results.errors.push(errorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `数据清理完成!已删除 ${results.products} 个商品、${results.preorderProducts} 个预购商品、${results.announcements} 个公告、${results.articles} 个文章`,
|
|
||||||
results,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Clear data error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/clear-data
|
|
||||||
* 带认证的清理接口
|
|
||||||
*/
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
|
|
||||||
// 可选:添加认证检查
|
|
||||||
// const { user } = await payload.auth({ headers: request.headers })
|
|
||||||
// if (!user?.roles?.includes('admin')) {
|
|
||||||
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
// }
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { collections = ['products', 'announcements', 'articles'] } = body
|
|
||||||
|
|
||||||
const results: Record<string, number> = {}
|
|
||||||
const errors: string[] = []
|
|
||||||
|
|
||||||
for (const collection of collections) {
|
|
||||||
if (['users', 'media'].includes(collection)) {
|
|
||||||
errors.push(`跳过受保护的集合: ${collection}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const deleted = await payload.delete({
|
|
||||||
collection,
|
|
||||||
where: {},
|
|
||||||
})
|
|
||||||
results[collection] = deleted.docs?.length || 0
|
|
||||||
console.log(`✅ 已清理 ${results[collection]} 个 ${collection}`)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = `清理 ${collection} 失败: ${error instanceof Error ? error.message : '未知错误'}`
|
|
||||||
console.error('❌', errorMsg)
|
|
||||||
errors.push(errorMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: '数据清理完成',
|
|
||||||
results,
|
|
||||||
errors: errors.length > 0 ? errors : undefined,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Clear data error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import { getPayload } from 'payload'
|
|
||||||
import config from '@payload-config'
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
export async function POST() {
|
|
||||||
try {
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
|
|
||||||
// Delete all products
|
|
||||||
const products = await payload.find({
|
|
||||||
collection: 'products',
|
|
||||||
limit: 1000,
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const product of products.docs) {
|
|
||||||
await payload.delete({
|
|
||||||
collection: 'products',
|
|
||||||
id: product.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all preorder products
|
|
||||||
const preorderProducts = await payload.find({
|
|
||||||
collection: 'preorder-products',
|
|
||||||
limit: 1000,
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const product of preorderProducts.docs) {
|
|
||||||
await payload.delete({
|
|
||||||
collection: 'preorder-products',
|
|
||||||
id: product.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `Cleared ${products.docs.length} products and ${preorderProducts.docs.length} preorder-products`,
|
|
||||||
deleted: {
|
|
||||||
products: products.docs.length,
|
|
||||||
preorderProducts: preorderProducts.docs.length,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[clear-payload-collections] Error:', error)
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
}, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import { getAllMedusaProducts } from '@/lib/medusa'
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 诊断 API - 检查 Medusa 产品的 metadata
|
|
||||||
* GET /api/diagnose-products
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const products = await getAllMedusaProducts()
|
|
||||||
|
|
||||||
const diagnosis = products.map((product) => ({
|
|
||||||
id: product.id,
|
|
||||||
title: product.title,
|
|
||||||
handle: product.handle,
|
|
||||||
metadata: product.metadata,
|
|
||||||
is_preorder_value: product.metadata?.is_preorder,
|
|
||||||
is_preorder_type: typeof product.metadata?.is_preorder,
|
|
||||||
should_be_in_collection:
|
|
||||||
product.metadata?.is_preorder === true || product.metadata?.is_preorder === 'true'
|
|
||||||
? 'preorder-products'
|
|
||||||
: 'products',
|
|
||||||
}))
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
total: products.length,
|
|
||||||
preorder_count: diagnosis.filter((d) => d.should_be_in_collection === 'preorder-products')
|
|
||||||
.length,
|
|
||||||
regular_count: diagnosis.filter((d) => d.should_be_in_collection === 'products').length,
|
|
||||||
products: diagnosis,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { getPayload } from 'payload'
|
|
||||||
import config from '@payload-config'
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
|
|
||||||
const products = await payload.find({
|
|
||||||
collection: 'products',
|
|
||||||
limit: 0, // Just get count
|
|
||||||
})
|
|
||||||
|
|
||||||
const preorderProducts = await payload.find({
|
|
||||||
collection: 'preorder-products',
|
|
||||||
limit: 0, // Just get count
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
products: products.totalDocs,
|
|
||||||
preorderProducts: preorderProducts.totalDocs,
|
|
||||||
total: products.totalDocs + preorderProducts.totalDocs,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[payload-stats] Error:', error)
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
}, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Route: Restore Product Recommendations from Seed
|
||||||
|
* POST /api/restore-recommendations-seed
|
||||||
|
*
|
||||||
|
* This server-side route uses Payload's local API to update the global config
|
||||||
|
* which requires proper authentication context that client-side fetch doesn't have.
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { enabled, lists } = body
|
||||||
|
|
||||||
|
if (!lists || !Array.isArray(lists)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Invalid lists data' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Payload instance with proper context
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
// Update the global using Payload's local API
|
||||||
|
const result = await payload.updateGlobal({
|
||||||
|
slug: 'product-recommendations',
|
||||||
|
data: {
|
||||||
|
enabled: enabled ?? true,
|
||||||
|
lists: lists,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Successfully restored ${lists.length} recommendation list(s)`,
|
||||||
|
data: result,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restoring recommendations seed:', error)
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button } from '@payloadcms/ui'
|
||||||
|
import { AVAILABLE_SEEDS, type SeedKey } from './data/20260221-product-recommendations-seeds'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore Recommendations Seed Button
|
||||||
|
* Quick restore predefined product recommendation configurations
|
||||||
|
*/
|
||||||
|
export function RestoreRecommendationsSeedButton({ className }: Props) {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [showSeedSelector, setShowSeedSelector] = useState(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find products by seed IDs
|
||||||
|
* Returns polymorphic relationship format: { relationTo, value }
|
||||||
|
*/
|
||||||
|
const findProductsBySeedIds = async (
|
||||||
|
seedIds: string[],
|
||||||
|
): Promise<Array<{ relationTo: string; value: string }>> => {
|
||||||
|
const products: Array<{ relationTo: string; value: string }> = []
|
||||||
|
|
||||||
|
for (const seedId of seedIds) {
|
||||||
|
try {
|
||||||
|
// Try products collection first
|
||||||
|
const productsResponse = await fetch(
|
||||||
|
`/api/products?where[seedId][equals]=${seedId}&limit=1`,
|
||||||
|
)
|
||||||
|
const productsData = await productsResponse.json()
|
||||||
|
|
||||||
|
if (productsData.docs && productsData.docs.length > 0) {
|
||||||
|
products.push({
|
||||||
|
relationTo: 'products',
|
||||||
|
value: productsData.docs[0].id,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try preorder-products if not found
|
||||||
|
const preorderResponse = await fetch(
|
||||||
|
`/api/preorder-products?where[seedId][equals]=${seedId}&limit=1`,
|
||||||
|
)
|
||||||
|
const preorderData = await preorderResponse.json()
|
||||||
|
|
||||||
|
if (preorderData.docs && preorderData.docs.length > 0) {
|
||||||
|
products.push({
|
||||||
|
relationTo: 'preorder-products',
|
||||||
|
value: preorderData.docs[0].id,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.warn(`Product not found for seedId: ${seedId}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error finding product ${seedId}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return products
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore recommendation list from seed
|
||||||
|
*/
|
||||||
|
const handleRestoreSeed = async (seedKey: SeedKey) => {
|
||||||
|
const seed = AVAILABLE_SEEDS[seedKey]
|
||||||
|
|
||||||
|
if (!confirm(
|
||||||
|
`Restore recommendation list configuration?\n\n` +
|
||||||
|
`Will create:\n${seed.lists.map(list => `• ${list.title} (${list.productSeedIds.length} products)`).join('\n')}\n\n` +
|
||||||
|
`Current configuration will be overwritten!`
|
||||||
|
)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('🔄 Finding products...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find all product IDs in polymorphic relationship format
|
||||||
|
const listsWithProductIds = await Promise.all(
|
||||||
|
seed.lists.map(async (list) => {
|
||||||
|
const products = await findProductsBySeedIds(list.productSeedIds)
|
||||||
|
return {
|
||||||
|
title: list.title,
|
||||||
|
subtitle: list.subtitle || '',
|
||||||
|
products: products,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter out lists with no products found
|
||||||
|
const validLists = listsWithProductIds.filter((list) => list.products.length > 0)
|
||||||
|
|
||||||
|
if (validLists.length === 0) {
|
||||||
|
setMessage('❌ No matching products found. Please run seed script first.')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage('💾 Updating configuration...')
|
||||||
|
|
||||||
|
// Update product-recommendations global via server-side API
|
||||||
|
const updateResponse = await fetch('/api/restore-recommendations-seed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled: seed.enabled,
|
||||||
|
lists: validLists,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await updateResponse.json()
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Update failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage(`✅ Successfully restored ${validLists.length} recommendation list(s)!`)
|
||||||
|
setShowSeedSelector(false)
|
||||||
|
|
||||||
|
// Refresh page after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 2000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore seed configuration:', error)
|
||||||
|
setMessage('❌ Restore failed: ' + (error instanceof Error ? error.message : 'Unknown error'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowSeedSelector(!showSeedSelector)}
|
||||||
|
buttonStyle="pill"
|
||||||
|
size="small"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
🌱 {showSeedSelector ? 'Hide' : 'Restore from Seed'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSeedSelector && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '1.5rem',
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-150)',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4 style={{ marginTop: 0, marginBottom: '1rem', fontSize: '0.9rem', fontWeight: 600 }}>
|
||||||
|
📦 Available Seed Configurations
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
|
{Object.entries(AVAILABLE_SEEDS).map(([key, seed]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-100)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>
|
||||||
|
{key === 'batch02' ? 'Batch 02 Recommendations' : key}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: 'var(--theme-elevation-500)' }}>
|
||||||
|
{seed.lists.length} list(s), {' '}
|
||||||
|
{seed.lists.reduce((sum, list) => sum + list.productSeedIds.length, 0)} product(s) total
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleRestoreSeed(key as SeedKey)}
|
||||||
|
buttonStyle="primary"
|
||||||
|
size="small"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid var(--theme-elevation-100)' }}>
|
||||||
|
{seed.lists.map((list, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
paddingLeft: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: 500, color: 'var(--theme-elevation-700)' }}>
|
||||||
|
{list.title}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: 'var(--theme-elevation-500)', fontSize: '0.8rem' }}>
|
||||||
|
{list.subtitle || `${list.productSeedIds.length} product(s)`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: message.includes('❌')
|
||||||
|
? 'var(--theme-error-100)'
|
||||||
|
: message.includes('✅')
|
||||||
|
? 'var(--theme-success-100)'
|
||||||
|
: 'var(--theme-info-100)',
|
||||||
|
color: message.includes('❌')
|
||||||
|
? 'var(--theme-error-900)'
|
||||||
|
: message.includes('✅')
|
||||||
|
? 'var(--theme-success-900)'
|
||||||
|
: 'var(--theme-info-900)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Product Recommendations Seed Data
|
||||||
|
* Predefined recommendation lists for quick restoration
|
||||||
|
* Date: 2026-02-21
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface RecommendationListSeed {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
productSeedIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecommendationsSeed {
|
||||||
|
enabled: boolean
|
||||||
|
lists: RecommendationListSeed[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch 02 Recommendations
|
||||||
|
* - PreGame: Preorder games collection (4 games)
|
||||||
|
* - PreMod: Preorder modification collection (2 metal shells + 1 custom console)
|
||||||
|
*/
|
||||||
|
export const BATCH_02_RECOMMENDATIONS: RecommendationsSeed = {
|
||||||
|
enabled: true,
|
||||||
|
lists: [
|
||||||
|
{
|
||||||
|
title: 'PreGame - Preorder Games',
|
||||||
|
subtitle: 'Selected indie games, now available for preorder! Support indie developers and get early access to new releases.',
|
||||||
|
productSeedIds: [
|
||||||
|
'game-urcicus', // Urcicus - GBA Game
|
||||||
|
'game-mikoto-nikki', // Mikoto Nikki - GBA Game
|
||||||
|
'game-passaway', // Passaway - GB Game
|
||||||
|
'game-judys-adventure-dx', // Judys Adventure DX - GBA Game
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PreMod - Preorder Modifications',
|
||||||
|
subtitle: 'Premium custom shells and consoles, full metal construction, artisan craftsmanship. Limited preorder available!',
|
||||||
|
productSeedIds: [
|
||||||
|
'shell-gba-sp-metal-unhinged', // Metal Shell - GBA SP (Unhinged Mod)
|
||||||
|
'shell-gba-metal', // Metal Shell - GBA
|
||||||
|
'console-retro-tetra', // Retro Tetra Console
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All available seed configurations
|
||||||
|
*/
|
||||||
|
export const AVAILABLE_SEEDS = {
|
||||||
|
batch02: BATCH_02_RECOMMENDATIONS,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SeedKey = keyof typeof AVAILABLE_SEEDS
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { RestoreRecommendationsSeedButton } from './RestoreRecommendationsSeedButton'
|
||||||
|
export { AVAILABLE_SEEDS, BATCH_02_RECOMMENDATIONS } from './data/20260221-product-recommendations-seeds'
|
||||||
|
export type { RecommendationListSeed, RecommendationsSeed, SeedKey } from './data/20260221-product-recommendations-seeds'
|
||||||
|
|
@ -26,6 +26,20 @@ export const ProductRecommendations: GlobalConfig = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'seedActions',
|
||||||
|
type: 'ui',
|
||||||
|
label: {
|
||||||
|
en: 'Quick Actions',
|
||||||
|
zh: '快捷操作',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
components: {
|
||||||
|
Field: '/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'enabled',
|
name: 'enabled',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue