api 结构大精简
This commit is contained in:
parent
b9cb60e3d0
commit
1860affd69
|
|
@ -4,7 +4,7 @@ import { NextRequest } from 'next/server'
|
|||
|
||||
/**
|
||||
* 批量删除日志 API
|
||||
* DELETE /api/delete-logs?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD
|
||||
* DELETE /api/admin/log?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD
|
||||
*/
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
|
|
@ -4,7 +4,7 @@ import config from '@payload-config'
|
|||
|
||||
/**
|
||||
* API Route: Restore Product Recommendations from Seed
|
||||
* POST /api/restore-recommendations-seed
|
||||
* POST /api/admin/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.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
/**
|
||||
* GET /api/home
|
||||
* 获取首页所有数据:公告 + Hero Slider + 产品推荐
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// 获取首页公告(已发布且在首页显示)
|
||||
const announcements = await payload.find({
|
||||
collection: 'announcements',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
showOnHomepage: {
|
||||
equals: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
sort: '-priority',
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
// 获取 Hero Slider
|
||||
const heroSlider = await payload.findGlobal({
|
||||
slug: 'hero-slider',
|
||||
})
|
||||
|
||||
// 获取产品推荐
|
||||
const productRecommendations = await payload.findGlobal({
|
||||
slug: 'product-recommendations',
|
||||
})
|
||||
|
||||
// 构建响应数据
|
||||
const response = {
|
||||
announcements: announcements.docs.map((announcement) => ({
|
||||
id: announcement.id,
|
||||
title: announcement.title,
|
||||
type: announcement.type,
|
||||
summary: announcement.summary,
|
||||
priority: announcement.priority,
|
||||
publishedAt: announcement.publishedAt,
|
||||
})),
|
||||
heroSlider: {
|
||||
slides: heroSlider.slides || [],
|
||||
},
|
||||
productRecommendations: {
|
||||
enabled: productRecommendations.enabled || false,
|
||||
lists: productRecommendations.lists || [],
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(response, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching homepage data:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch homepage data',
|
||||
message: error.message,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import payload from 'payload'
|
||||
|
||||
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||
const MEDUSA_ADMIN_API_KEY = process.env.MEDUSA_ADMIN_API_KEY || ''
|
||||
|
||||
/**
|
||||
* 刷新预购商品的订单计数
|
||||
* POST /api/preorders/refresh-order-counts
|
||||
*
|
||||
* Body:
|
||||
* - productIds?: string[] - 要刷新的商品 ID 列表(Payload ID)
|
||||
* - refreshAll?: boolean - 是否刷新所有预购商品
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { productIds, refreshAll } = body
|
||||
|
||||
if (!productIds && !refreshAll) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请提供 productIds 或设置 refreshAll' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let products: any[] = []
|
||||
|
||||
if (refreshAll) {
|
||||
// 获取所有预购商品
|
||||
const result = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
limit: 1000,
|
||||
where: {
|
||||
medusaId: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
products = result.docs
|
||||
} else {
|
||||
// 获取指定的商品
|
||||
const result = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
limit: productIds.length,
|
||||
where: {
|
||||
id: {
|
||||
in: productIds,
|
||||
},
|
||||
medusaId: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
products = result.docs
|
||||
}
|
||||
|
||||
if (products.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '没有找到要刷新的商品' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// 统计更新结果
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
const errors: string[] = []
|
||||
|
||||
// 为每个商品刷新订单计数
|
||||
for (const product of products) {
|
||||
try {
|
||||
const medusaId = product.medusaId
|
||||
|
||||
// 从 Medusa 获取订单数据
|
||||
const response = await fetch(
|
||||
`${MEDUSA_BACKEND_URL}/admin/orders?product_id=${medusaId}&limit=1000`,
|
||||
{
|
||||
headers: {
|
||||
'x-medusa-access-token': MEDUSA_ADMIN_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Medusa API 返回错误: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const orders = data.orders || []
|
||||
|
||||
// 计算真实订单数
|
||||
let realOrderCount = 0
|
||||
|
||||
// 筛选有效订单(排除取消的)
|
||||
const validOrders = orders.filter((order: any) =>
|
||||
order.status !== 'canceled' && order.payment_status !== 'not_paid'
|
||||
)
|
||||
|
||||
// 遍历有效订单,统计该商品的数量
|
||||
for (const order of validOrders) {
|
||||
const items = order.items || []
|
||||
for (const item of items) {
|
||||
if (item.product_id === medusaId) {
|
||||
realOrderCount += item.quantity || 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Payload 中的订单计数
|
||||
await payload.update({
|
||||
collection: 'preorder-products',
|
||||
id: product.id,
|
||||
data: {
|
||||
orderCount: realOrderCount,
|
||||
},
|
||||
})
|
||||
|
||||
successCount++
|
||||
} catch (error) {
|
||||
console.error(`刷新商品 ${product.id} (${product.title}) 失败:`, error)
|
||||
failCount++
|
||||
errors.push(`${product.title}: ${error instanceof Error ? error.message : '未知错误'}`)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `刷新完成: ${successCount} 个成功, ${failCount} 个失败`,
|
||||
successCount,
|
||||
failCount,
|
||||
errors,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('刷新订单计数失败:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个商品的订单计数(不更新数据库)
|
||||
* GET /api/preorders/refresh-order-counts?productId=xxx
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const productId = searchParams.get('productId')
|
||||
|
||||
if (!productId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请提供 productId' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const product = await payload.findByID({
|
||||
collection: 'preorder-products',
|
||||
id: productId,
|
||||
})
|
||||
|
||||
if (!product || !product.medusaId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '商品不存在或没有 Medusa ID' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
currentCount: product.orderCount || 0,
|
||||
medusaId: product.medusaId,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取订单计数失败:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { getCache, setCache } from '@/lib/redis'
|
||||
|
||||
/**
|
||||
* GET /api/public/hero-slider
|
||||
* 获取首页幻灯片数据(带缓存)
|
||||
* 需要 x-store-api-key 验证
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
// 验证 API Key
|
||||
const apiKey = req.headers.get('x-store-api-key')
|
||||
const validApiKey = process.env.PAYLOAD_API_KEY
|
||||
|
||||
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 生成缓存 key
|
||||
const cacheKey = 'hero-slider:data'
|
||||
|
||||
// 尝试从缓存获取
|
||||
const cached = await getCache(cacheKey)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: cached,
|
||||
cached: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
const payload = await getPayload({ config })
|
||||
const result = await payload.findGlobal({
|
||||
slug: 'hero-slider' as any,
|
||||
depth: 2, // 填充图片关联数据
|
||||
})
|
||||
|
||||
// 获取所有幻灯片
|
||||
const slides = (result as any).slides || []
|
||||
|
||||
const responseData = {
|
||||
slides,
|
||||
totalSlides: slides.length,
|
||||
}
|
||||
|
||||
// 缓存结果(1 小时)
|
||||
await setCache(cacheKey, responseData, 3600)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: responseData,
|
||||
cached: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Hero slider API error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch hero slider',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { getCache, setCache } from '@/lib/redis'
|
||||
|
||||
/**
|
||||
* GET /api/public/homepage
|
||||
* 获取首页所有数据:幻灯片 + 商品推荐列表(带缓存)
|
||||
*
|
||||
* 这个 API 组合了多个数据源,提供完整的首页内容
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
// 生成缓存 key
|
||||
const cacheKey = 'homepage:data'
|
||||
|
||||
// 尝试从缓存获取
|
||||
const cached = await getCache(cacheKey)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: cached,
|
||||
cached: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 从数据库获取数据
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// 并行获取所有数据
|
||||
const [heroSliderResult, productRecommendationsResult] = await Promise.all([
|
||||
// 获取幻灯片数据
|
||||
payload
|
||||
.findGlobal({
|
||||
slug: 'hero-slider' as any,
|
||||
depth: 2,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch hero slider:', error)
|
||||
return null
|
||||
}),
|
||||
|
||||
// 获取商品推荐数据
|
||||
payload
|
||||
.findGlobal({
|
||||
slug: 'product-recommendations' as any,
|
||||
depth: 3,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch product recommendations:', error)
|
||||
return null
|
||||
}),
|
||||
])
|
||||
|
||||
// 处理幻灯片数据
|
||||
let heroSlider = {
|
||||
slides: [],
|
||||
totalSlides: 0,
|
||||
}
|
||||
|
||||
if (heroSliderResult) {
|
||||
const slides = (heroSliderResult as any).slides || []
|
||||
|
||||
heroSlider = {
|
||||
slides,
|
||||
totalSlides: slides.length,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理商品推荐数据
|
||||
let productRecommendations = {
|
||||
enabled: false,
|
||||
lists: [],
|
||||
totalLists: 0,
|
||||
}
|
||||
|
||||
if (productRecommendationsResult && (productRecommendationsResult as any).enabled) {
|
||||
// 获取所有列表
|
||||
const lists = (productRecommendationsResult as any).lists || []
|
||||
|
||||
// 处理每个列表
|
||||
const processedLists = lists.map((list: any) => {
|
||||
const products = Array.isArray(list.products)
|
||||
? list.products.filter((product: any) => product && product.status === 'published')
|
||||
: []
|
||||
|
||||
return {
|
||||
id: list.id,
|
||||
title: list.title,
|
||||
subtitle: list.subtitle,
|
||||
products,
|
||||
totalProducts: products.length,
|
||||
}
|
||||
})
|
||||
|
||||
productRecommendations = {
|
||||
enabled: (productRecommendationsResult as any).enabled,
|
||||
lists: processedLists,
|
||||
totalLists: processedLists.length,
|
||||
}
|
||||
}
|
||||
|
||||
// 组合响应数据
|
||||
const responseData = {
|
||||
heroSlider,
|
||||
productRecommendations,
|
||||
meta: {
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
},
|
||||
}
|
||||
|
||||
// 缓存结果(1 小时)
|
||||
await setCache(cacheKey, responseData, 3600)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: responseData,
|
||||
cached: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Homepage API error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch homepage data',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { getCache, setCache } from '@/lib/redis'
|
||||
|
||||
/**
|
||||
* GET /api/public/product-recommendations
|
||||
* 获取商品推荐列表数据(带缓存)
|
||||
* 需要 x-store-api-key 验证
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
// 验证 API Key
|
||||
const apiKey = req.headers.get('x-store-api-key')
|
||||
const validApiKey = process.env.PAYLOAD_API_KEY
|
||||
|
||||
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 生成缓存 key
|
||||
const cacheKey = 'product-recommendations:data'
|
||||
|
||||
// 尝试从缓存获取
|
||||
const cached = await getCache(cacheKey)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: cached,
|
||||
cached: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
const payload = await getPayload({ config })
|
||||
const result = await payload.findGlobal({
|
||||
slug: 'product-recommendations' as any,
|
||||
depth: 3, // 填充商品和图片关联数据
|
||||
})
|
||||
|
||||
// 如果功能未启用,返回空数据
|
||||
if (!(result as any).enabled) {
|
||||
const emptyData = {
|
||||
enabled: false,
|
||||
lists: [],
|
||||
}
|
||||
|
||||
// 缓存空数据(较短时间)
|
||||
await setCache(cacheKey, emptyData, 600) // 10 分钟
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: emptyData,
|
||||
cached: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取所有列表
|
||||
const lists = (result as any).lists || []
|
||||
|
||||
// 处理每个列表
|
||||
const processedLists = lists.map((list: any) => {
|
||||
const products = Array.isArray(list.products)
|
||||
? list.products.filter((product: any) => product && product.status === 'published')
|
||||
: []
|
||||
|
||||
return {
|
||||
id: list.id,
|
||||
title: list.title,
|
||||
subtitle: list.subtitle,
|
||||
products,
|
||||
totalProducts: products.length,
|
||||
}
|
||||
})
|
||||
|
||||
const responseData = {
|
||||
enabled: (result as any).enabled,
|
||||
lists: processedLists,
|
||||
totalLists: processedLists.length,
|
||||
}
|
||||
|
||||
// 缓存结果(1 小时)
|
||||
await setCache(cacheKey, responseData, 3600)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: responseData,
|
||||
cached: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Product recommendations API error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch product recommendations',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { getCache, setCache } from '@/lib/redis'
|
||||
|
||||
/**
|
||||
* GET /api/public/products/[id]
|
||||
* 获取单个产品详情(带缓存)
|
||||
* 支持参数:
|
||||
* - collection: 'preorder-products' | 'products' (可选,如不指定则自动搜索)
|
||||
*/
|
||||
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = params
|
||||
const searchParams = req.nextUrl.searchParams
|
||||
const collection = searchParams.get('collection')
|
||||
|
||||
// 生成缓存 key
|
||||
const cacheKey = `products:detail:${id}:collection=${collection || 'auto'}`
|
||||
|
||||
// 尝试从缓存获取
|
||||
const cached = await getCache(cacheKey)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: cached,
|
||||
cached: true,
|
||||
})
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
let result: any = null
|
||||
let foundCollection: string = ''
|
||||
|
||||
if (collection) {
|
||||
// 如果指定了 collection,直接查询
|
||||
try {
|
||||
result = await payload.findByID({
|
||||
collection: collection as any,
|
||||
id,
|
||||
depth: 2,
|
||||
})
|
||||
foundCollection = collection
|
||||
} catch {
|
||||
// 找不到
|
||||
}
|
||||
} else {
|
||||
// 自动搜索各个 collection
|
||||
const collections = ['preorder-products', 'products']
|
||||
|
||||
for (const col of collections) {
|
||||
try {
|
||||
result = await payload.findByID({
|
||||
collection: col as any,
|
||||
id,
|
||||
depth: 2,
|
||||
})
|
||||
foundCollection = col
|
||||
break
|
||||
} catch {
|
||||
// 继续尝试下一个
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
},
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
// 只有已发布的产品才返回
|
||||
if (result.status !== 'published') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Product not found or not published',
|
||||
},
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
// 添加 collection 信息
|
||||
const resultWithMeta = {
|
||||
...result,
|
||||
_collection: foundCollection,
|
||||
}
|
||||
|
||||
// 缓存结果(1 小时)
|
||||
await setCache(cacheKey, resultWithMeta, 3600)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: resultWithMeta,
|
||||
cached: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Product detail API error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch product',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { getCache, setCache } from '@/lib/redis'
|
||||
|
||||
/**
|
||||
* GET /api/public/products
|
||||
* 获取产品列表(带缓存)
|
||||
* 支持参数:
|
||||
* - type: 'preorder' | 'order' | 'all' (默认 'all')
|
||||
* - page: 页码
|
||||
* - limit: 每页数量
|
||||
* - status: 'draft' | 'published' (默认 'published')
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const searchParams = req.nextUrl.searchParams
|
||||
const page = parseInt(searchParams.get('page') || '1', 10)
|
||||
const limit = parseInt(searchParams.get('limit') || '10', 10)
|
||||
const status = searchParams.get('status') || 'published'
|
||||
const type = searchParams.get('type') || 'all'
|
||||
|
||||
// 生成缓存 key
|
||||
const cacheKey = `products:list:type=${type}:page=${page}:limit=${limit}:status=${status}`
|
||||
|
||||
// 尝试从缓存获取
|
||||
const cached = await getCache(cacheKey)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: cached,
|
||||
cached: true,
|
||||
})
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
const where = { status: { equals: status } }
|
||||
|
||||
let result
|
||||
|
||||
if (type === 'all') {
|
||||
// 查询所有类型
|
||||
const [preorders, products] = await Promise.all([
|
||||
payload.find({
|
||||
collection: 'preorder-products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
}),
|
||||
payload.find({
|
||||
collection: 'products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
}),
|
||||
])
|
||||
|
||||
// 合并结果
|
||||
result = {
|
||||
docs: [
|
||||
...preorders.docs.map((doc) => ({ ...doc, _type: 'preorder-products' })),
|
||||
...products.docs.map((doc) => ({ ...doc, _type: 'products' })),
|
||||
],
|
||||
totalDocs: preorders.totalDocs + products.totalDocs,
|
||||
limit,
|
||||
page,
|
||||
totalPages: Math.ceil((preorders.totalDocs + products.totalDocs) / limit),
|
||||
hasNextPage: page < Math.ceil((preorders.totalDocs + products.totalDocs) / limit),
|
||||
hasPrevPage: page > 1,
|
||||
}
|
||||
} else if (type === 'preorder') {
|
||||
// 只查询预售商品
|
||||
result = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
})
|
||||
} else if (type === 'order') {
|
||||
// 只查询现货商品
|
||||
result = await payload.find({
|
||||
collection: 'products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
})
|
||||
} else {
|
||||
// 旧的 products collection
|
||||
result = await payload.find({
|
||||
collection: 'products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// 缓存结果(1 小时)
|
||||
await setCache(cacheKey, result, 3600)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result,
|
||||
cached: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Products API error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch products',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import payload from 'payload'
|
||||
|
||||
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||
const MEDUSA_ADMIN_API_KEY = process.env.MEDUSA_ADMIN_API_KEY || ''
|
||||
|
||||
/**
|
||||
* 刷新预购商品的订单计数
|
||||
* POST /api/refresh-order-counts
|
||||
*
|
||||
* Body:
|
||||
* - productIds?: string[] - 要刷新的商品 ID 列表(Payload ID)
|
||||
* - refreshAll?: boolean - 是否刷新所有预购商品
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { productIds, refreshAll } = body
|
||||
|
||||
if (!productIds && !refreshAll) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请提供 productIds 或设置 refreshAll' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let products: any[] = []
|
||||
|
||||
if (refreshAll) {
|
||||
// 获取所有预购商品
|
||||
const result = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
limit: 1000,
|
||||
where: {
|
||||
medusaId: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
products = result.docs
|
||||
} else {
|
||||
// 获取指定的商品
|
||||
const result = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
limit: productIds.length,
|
||||
where: {
|
||||
id: {
|
||||
in: productIds,
|
||||
},
|
||||
medusaId: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
products = result.docs
|
||||
}
|
||||
|
||||
if (products.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '没有找到要刷新的商品' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// 统计更新结果
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
const errors: string[] = []
|
||||
|
||||
// 为每个商品刷新订单计数
|
||||
for (const product of products) {
|
||||
try {
|
||||
const orderCount = await fetchProductOrderCount(product.medusaId)
|
||||
|
||||
await payload.update({
|
||||
collection: 'preorder-products',
|
||||
id: product.id,
|
||||
data: {
|
||||
orderCount,
|
||||
},
|
||||
})
|
||||
|
||||
successCount++
|
||||
} catch (error) {
|
||||
failCount++
|
||||
const errorMsg = `商品 ${product.title} (${product.medusaId}): ${
|
||||
error instanceof Error ? error.message : '未知错误'
|
||||
}`
|
||||
errors.push(errorMsg)
|
||||
console.error('刷新订单计数失败:', errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `刷新完成!成功: ${successCount},失败: ${failCount}`,
|
||||
details: {
|
||||
total: products.length,
|
||||
success: successCount,
|
||||
failed: failCount,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('刷新订单计数 API 错误:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Medusa 获取指定商品的订单数量
|
||||
*/
|
||||
async function fetchProductOrderCount(productId: string): Promise<number> {
|
||||
try {
|
||||
// 使用 Medusa Admin API 查询订单
|
||||
// 注意:这里需要使用 Query API 或者 Admin Orders API
|
||||
// 查询所有包含该商品的已完成订单
|
||||
|
||||
const response = await fetch(
|
||||
`${MEDUSA_BACKEND_URL}/admin/orders?fields=id,items&items[product_id]=${productId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '',
|
||||
...(MEDUSA_ADMIN_API_KEY && { 'Authorization': `Bearer ${MEDUSA_ADMIN_API_KEY}` }),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Medusa API 返回错误: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// 计算订单中该商品的总数量
|
||||
let totalQuantity = 0
|
||||
|
||||
if (data.orders && Array.isArray(data.orders)) {
|
||||
for (const order of data.orders) {
|
||||
if (order.items && Array.isArray(order.items)) {
|
||||
for (const item of order.items) {
|
||||
if (item.product_id === productId) {
|
||||
totalQuantity += item.quantity || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalQuantity
|
||||
} catch (error) {
|
||||
console.error(`获取商品 ${productId} 订单数量失败:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { getAllMedusaProducts } from '@/lib/medusa'
|
|||
|
||||
/**
|
||||
* Batch Sync Selected Products
|
||||
* POST /api/batch-sync-medusa
|
||||
* POST /api/sync/batch-medusa
|
||||
* Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate?: boolean }
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
|
|
@ -80,8 +80,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
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
|
||||
if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail
|
||||
|
||||
await payload.update({
|
||||
collection: collection as 'products' | 'preorder-products',
|
||||
|
|
@ -125,10 +125,10 @@ function mergeProductData(existingProduct: any, newData: any, forceUpdate: boole
|
|||
|
||||
/**
|
||||
* 同步 Medusa 商品到 Payload CMS
|
||||
* GET /api/sync-medusa - 同步所有商品
|
||||
* GET /api/sync-medusa?medusaId=prod_xxx - 同步单个商品
|
||||
* GET /api/sync-medusa?medusaId=prod_xxx&collection=preorder-products - 指定目标 collection
|
||||
* GET /api/sync-medusa?forceUpdate=true - 强制更新所有字段
|
||||
* GET /api/sync/medusa - 同步所有商品
|
||||
* GET /api/sync/medusa?medusaId=prod_xxx - 同步单个商品
|
||||
* GET /api/sync/medusa?medusaId=prod_xxx&collection=preorder-products - 指定目标 collection
|
||||
* GET /api/sync/medusa?forceUpdate=true - 强制更新所有字段
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const origin = request.headers.get('origin')
|
||||
|
|
@ -481,7 +481,7 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
|||
}
|
||||
|
||||
/**
|
||||
* POST /api/sync-medusa
|
||||
* POST /api/sync/medusa
|
||||
* 手动触发同步(与 GET 参数相同)
|
||||
* Body: { medusaId?, collection?, forceUpdate? }
|
||||
*/
|
||||
|
|
@ -18,7 +18,7 @@ export async function OPTIONS(request: Request) {
|
|||
|
||||
/**
|
||||
* 同步单个产品并返回完整的产品数据
|
||||
* POST /api/sync-product
|
||||
* POST /api/sync/product
|
||||
* Body: { medusaId: string, collection?: 'products' | 'preorder-products', forceUpdate?: boolean }
|
||||
*
|
||||
* 返回: { success: true, product: {...完整的产品数据}, action: 'created' | 'updated' | 'skipped' }
|
||||
|
|
@ -23,7 +23,7 @@ export function RefreshOrderCountField() {
|
|||
setMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/refresh-order-counts', {
|
||||
const response = await fetch('/api/preorders/refresh-order-counts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export function RestoreRecommendationsSeedButton({ className }: Props) {
|
|||
setMessage('💾 Updating configuration...')
|
||||
|
||||
// Update product-recommendations global via server-side API
|
||||
const updateResponse = await fetch('/api/restore-recommendations-seed', {
|
||||
const updateResponse = await fetch('/api/admin/restore-recommendations-seed', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function RefreshOrderCountButton() {
|
|||
setMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/refresh-order-counts', {
|
||||
const response = await fetch('/api/preorders/refresh-order-counts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -71,7 +71,7 @@ export function RefreshOrderCountButton() {
|
|||
setLoading(true)
|
||||
setMessage('')
|
||||
|
||||
const response = await fetch('/api/refresh-order-counts', {
|
||||
const response = await fetch('/api/preorders/refresh-order-counts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export function UnifiedSyncButton() {
|
|||
setMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sync-medusa?forceUpdate=false', {
|
||||
const response = await fetch('/api/sync/medusa?forceUpdate=false', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ export function UnifiedSyncButton() {
|
|||
setLoading(true)
|
||||
setMessage('')
|
||||
|
||||
const response = await fetch('/api/batch-sync-medusa', {
|
||||
const response = await fetch('/api/sync/batch-medusa', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -124,7 +124,7 @@ export function UnifiedSyncButton() {
|
|||
setShowForceAllConfirm(false)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sync-medusa?forceUpdate=true', {
|
||||
const response = await fetch('/api/sync/medusa?forceUpdate=true', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export default function LogsManagerView() {
|
|||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/delete-logs?startDate=${deleteStartDate}&endDate=${deleteEndDate}`,
|
||||
`/api/admin/log?startDate=${deleteStartDate}&endDate=${deleteEndDate}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
import type { Endpoint } from 'payload'
|
||||
|
||||
export const homepageDataEndpoint: Endpoint = {
|
||||
path: '/homepage-data',
|
||||
method: 'get',
|
||||
handler: async (req) => {
|
||||
try {
|
||||
const payload = req.payload
|
||||
|
||||
// 获取首页公告(已发布且在首页显示)
|
||||
const announcements = await payload.find({
|
||||
collection: 'announcements',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
showOnHomepage: {
|
||||
equals: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
sort: '-priority',
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
// 获取 Hero Slider
|
||||
const heroSlider = await payload.findGlobal({
|
||||
slug: 'hero-slider',
|
||||
})
|
||||
|
||||
// 获取产品推荐
|
||||
const productRecommendations = await payload.findGlobal({
|
||||
slug: 'product-recommendations',
|
||||
})
|
||||
|
||||
// 构建响应数据
|
||||
const response = {
|
||||
announcements: announcements.docs.map((announcement) => ({
|
||||
id: announcement.id,
|
||||
title: announcement.title,
|
||||
type: announcement.type,
|
||||
summary: announcement.summary,
|
||||
priority: announcement.priority,
|
||||
publishedAt: announcement.publishedAt,
|
||||
})),
|
||||
heroSlider: {
|
||||
slides: heroSlider.slides || [],
|
||||
},
|
||||
productRecommendations: {
|
||||
enabled: productRecommendations.enabled || false,
|
||||
lists: productRecommendations.lists || [],
|
||||
},
|
||||
}
|
||||
|
||||
return Response.json(response, { status: 200 })
|
||||
} catch (error: any) {
|
||||
req.payload.logger.error('Error fetching homepage data:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to fetch homepage data',
|
||||
message: error.message,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import { SiteAccess } from './globals/SiteAccess'
|
|||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import { zh } from '@payloadcms/translations/languages/zh'
|
||||
import { homepageDataEndpoint } from './endpoints/homepage'
|
||||
|
||||
// 导入自定义翻译
|
||||
import enProducts from './translations/en/products.json'
|
||||
|
|
@ -51,7 +50,6 @@ export default buildConfig({
|
|||
},
|
||||
collections: [Users, Media, Products, PreorderProducts, Announcements, Articles, Logs],
|
||||
globals: [AdminSettings, LogsManager, HeroSlider, ProductRecommendations, SiteAccess],
|
||||
endpoints: [homepageDataEndpoint],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
typescript: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue