209 lines
6.0 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.

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 countGroupedRows(rows: any[]) {
return rows.reduce((total, row) => total + Number(row._count?._all || 0), 0)
}
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 { items: [], total: 0 }
}
const rows = await (prisma.material.groupBy as any)({
by: [field],
where: result.where,
_count: { _all: true },
})
return {
items: toFacetItems(rows, field),
total: countGroupedRows(rows),
}
}
const loadMaterialTypes = async () => {
const result = await buildWhere('materialType')
if (result.empty) {
return { items: [], total: 0 }
}
const rows = await (prisma.material.groupBy as any)({
by: ['materialType'],
where: result.where,
_count: { _all: true },
})
return {
items: toMaterialTypeItems(rows),
total: countGroupedRows(rows),
}
}
const loadMonths = async () => {
const result = await buildWhere('month')
if (result.empty) {
return { items: [], total: 0 }
}
const rows = await prisma.material.findMany({
where: result.where,
select: { createdAt: true },
orderBy: { createdAt: 'desc' },
})
return {
items: toMonthItems(rows),
total: rows.length,
}
}
// 复用 groupBy 的计数汇总,避免为每个筛选维度再执行一次 count:
// 整个 facets 请求由 13 条 SQL 降为 7 条,同时每批最多并发 3 条。
const [total, categoryResult, sceneResult] = await Promise.all([
countWithFilters(),
groupByField('categoryName'),
groupByField('sceneName'),
])
const [platformResult, productCategoryResult, monthResult] = await Promise.all([
groupByField('platformName'),
groupByField('productCategoryName'),
loadMonths(),
])
const materialTypeResult = await loadMaterialTypes()
return NextResponse.json({
total,
categories: categoryResult.items,
scenes: sceneResult.items,
platforms: platformResult.items,
productCategories: productCategoryResult.items,
months: monthResult.items,
materialTypes: materialTypeResult.items,
totals: {
materialType: materialTypeResult.total,
category: categoryResult.total,
scene: sceneResult.total,
platform: platformResult.total,
productCategory: productCategoryResult.total,
month: monthResult.total,
},
})
} 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 },
)
}
}