205 lines
5.7 KiB
TypeScript
205 lines
5.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { getServerSession } from 'next-auth'
|
|
import { authOptions } from '@/lib/auth'
|
|
import { prisma } from '@/lib/prisma'
|
|
import {
|
|
buildMaterialWhereFromSearchParams,
|
|
createMaterialQueryContext,
|
|
MaterialFilterDimension,
|
|
MaterialQueryError,
|
|
normalizeMaterialTypeForUi,
|
|
normalizeSelectableMaterialTypes,
|
|
} from '@/lib/material-query'
|
|
|
|
type FacetField = 'categoryName' | 'sceneName' | 'platformName' | 'productCategoryName'
|
|
|
|
const FIELD_TO_EXCLUDE: Record<FacetField, MaterialFilterDimension> = {
|
|
categoryName: 'category',
|
|
sceneName: 'scene',
|
|
platformName: 'platform',
|
|
productCategoryName: 'productCategory',
|
|
}
|
|
|
|
function toFacetItems(rows: any[], field: FacetField) {
|
|
return rows
|
|
.map((row) => {
|
|
const name = String(row[field] || '').trim()
|
|
return {
|
|
id: name,
|
|
name,
|
|
count: row._count?._all || 0,
|
|
}
|
|
})
|
|
.filter((item) => item.name && item.count > 0)
|
|
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
|
|
}
|
|
|
|
function toMonthItems(rows: Array<{ createdAt: Date }>) {
|
|
const monthMap = new Map<string, number>()
|
|
|
|
for (const row of rows) {
|
|
const date = new Date(row.createdAt)
|
|
const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
|
|
monthMap.set(value, (monthMap.get(value) || 0) + 1)
|
|
}
|
|
|
|
return Array.from(monthMap.entries())
|
|
.map(([value, count]) => {
|
|
const [, month] = value.split('-')
|
|
return {
|
|
value,
|
|
label: `${value.slice(0, 4)}年${Number.parseInt(month, 10)}月`,
|
|
count,
|
|
}
|
|
})
|
|
.sort((a, b) => b.value.localeCompare(a.value))
|
|
}
|
|
|
|
function toMaterialTypeItems(rows: any[]) {
|
|
const countMap = new Map<string, number>()
|
|
|
|
for (const row of rows) {
|
|
const normalizedType = normalizeMaterialTypeForUi(row.materialType)
|
|
if (!normalizedType) {
|
|
continue
|
|
}
|
|
countMap.set(normalizedType, (countMap.get(normalizedType) || 0) + (row._count?._all || 0))
|
|
}
|
|
|
|
const orderedTypes = normalizeSelectableMaterialTypes(Array.from(countMap.keys()))
|
|
return orderedTypes.map((type) => ({
|
|
id: type,
|
|
name: type,
|
|
count: countMap.get(type) || 0,
|
|
}))
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const session = await getServerSession(authOptions)
|
|
if (!session?.user || !(session.user as any).id) {
|
|
return NextResponse.json(
|
|
{ error: '未登录', code: 'UNAUTHORIZED' },
|
|
{ status: 401 },
|
|
)
|
|
}
|
|
|
|
const searchParams = request.nextUrl.searchParams
|
|
const user = session.user as any
|
|
const context = await createMaterialQueryContext(searchParams, user)
|
|
|
|
const buildWhere = async (exclude?: MaterialFilterDimension) =>
|
|
buildMaterialWhereFromSearchParams(searchParams, user, {
|
|
context,
|
|
...(exclude ? { exclude } : {}),
|
|
})
|
|
|
|
const countWithFilters = async (exclude?: MaterialFilterDimension) => {
|
|
const result = await buildWhere(exclude)
|
|
if (result.empty) {
|
|
return 0
|
|
}
|
|
return prisma.material.count({ where: result.where })
|
|
}
|
|
|
|
const groupByField = async (field: FacetField) => {
|
|
const result = await buildWhere(FIELD_TO_EXCLUDE[field])
|
|
if (result.empty) {
|
|
return []
|
|
}
|
|
|
|
const rows = await (prisma.material.groupBy as any)({
|
|
by: [field],
|
|
where: result.where,
|
|
_count: { _all: true },
|
|
})
|
|
|
|
return toFacetItems(rows, field)
|
|
}
|
|
|
|
const loadMaterialTypes = async () => {
|
|
const result = await buildWhere('materialType')
|
|
if (result.empty) {
|
|
return []
|
|
}
|
|
|
|
const rows = await (prisma.material.groupBy as any)({
|
|
by: ['materialType'],
|
|
where: result.where,
|
|
_count: { _all: true },
|
|
})
|
|
|
|
return toMaterialTypeItems(rows)
|
|
}
|
|
|
|
const loadMonths = async () => {
|
|
const result = await buildWhere('month')
|
|
if (result.empty) {
|
|
return []
|
|
}
|
|
|
|
const rows = await prisma.material.findMany({
|
|
where: result.where,
|
|
select: { createdAt: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
})
|
|
|
|
return toMonthItems(rows)
|
|
}
|
|
|
|
// 每批最多并发 3 条统计查询,避免多个首页请求同时耗尽 Prisma 连接池。
|
|
const [total, categories, scenes] = await Promise.all([
|
|
countWithFilters(),
|
|
groupByField('categoryName'),
|
|
groupByField('sceneName'),
|
|
])
|
|
const [platforms, productCategories, months] = await Promise.all([
|
|
groupByField('platformName'),
|
|
groupByField('productCategoryName'),
|
|
loadMonths(),
|
|
])
|
|
const [materialTypes, materialTypeTotal, categoryTotal] = await Promise.all([
|
|
loadMaterialTypes(),
|
|
countWithFilters('materialType'),
|
|
countWithFilters('category'),
|
|
])
|
|
const [sceneTotal, platformTotal, productCategoryTotal] = await Promise.all([
|
|
countWithFilters('scene'),
|
|
countWithFilters('platform'),
|
|
countWithFilters('productCategory'),
|
|
])
|
|
const monthTotal = await countWithFilters('month')
|
|
|
|
return NextResponse.json({
|
|
total,
|
|
categories,
|
|
scenes,
|
|
platforms,
|
|
productCategories,
|
|
months,
|
|
materialTypes,
|
|
totals: {
|
|
materialType: materialTypeTotal,
|
|
category: categoryTotal,
|
|
scene: sceneTotal,
|
|
platform: platformTotal,
|
|
productCategory: productCategoryTotal,
|
|
month: monthTotal,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
if (error instanceof MaterialQueryError) {
|
|
return NextResponse.json(
|
|
{ error: error.message, code: error.code },
|
|
{ status: error.status },
|
|
)
|
|
}
|
|
|
|
console.error('[Materials Facets] Error:', error)
|
|
return NextResponse.json(
|
|
{ error: '获取首页筛选统计失败', code: 'SERVER_ERROR' },
|
|
{ status: 500 },
|
|
)
|
|
}
|
|
}
|