2026-09-07 12:20:03 +08:00

205 lines
7.5 KiB
TypeScript

import { getServerSession } from 'next-auth'
import { NextRequest, NextResponse } from 'next/server'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import {
buildMaterialWhereFromSearchParams,
createMaterialQueryContext,
normalizeMaterialTypeForUi,
} from '@/lib/material-query'
import { MATERIAL_TYPE_OPTIONS } from '@/lib/material-type-options'
import { calculateReuseRate, type ValueMetricsMaterial } from '@/lib/value-metrics'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export const revalidate = 0
const PRIVATE_HEADERS = {
'Cache-Control': 'private, no-store, max-age=0',
}
const MAX_DETAIL_LIMIT = 100
type MaterialTypeCountRow = {
materialType: string
_count: { _all: number }
}
function typeOrder(type: string) {
const index = MATERIAL_TYPE_OPTIONS.indexOf(type as typeof MATERIAL_TYPE_OPTIONS[number])
return index === -1 ? MATERIAL_TYPE_OPTIONS.length : index
}
function buildTypeSummaries(
totalRows: MaterialTypeCountRow[],
reusedRows: MaterialTypeCountRow[],
) {
const grouped = new Map<string, { rawTypes: Set<string>; totalCount: number; reusedCount: number }>()
for (const row of totalRows) {
const label = normalizeMaterialTypeForUi(row.materialType)
if (!label) continue
const current = grouped.get(label) || { rawTypes: new Set<string>(), totalCount: 0, reusedCount: 0 }
current.rawTypes.add(row.materialType)
current.totalCount += Number(row._count?._all || 0)
grouped.set(label, current)
}
for (const row of reusedRows) {
const label = normalizeMaterialTypeForUi(row.materialType)
if (!label) continue
const current = grouped.get(label) || { rawTypes: new Set<string>(), totalCount: 0, reusedCount: 0 }
current.rawTypes.add(row.materialType)
current.reusedCount += Number(row._count?._all || 0)
grouped.set(label, current)
}
return Array.from(grouped.entries())
.map(([label, value]) => ({
key: label,
label,
totalCount: value.totalCount,
reusedCount: value.reusedCount,
reuseRate: calculateReuseRate(value.reusedCount, value.totalCount),
rawTypes: Array.from(value.rawTypes),
}))
.sort((a, b) => typeOrder(a.label) - typeOrder(b.label) || a.label.localeCompare(b.label, 'zh-CN'))
}
function toMaterialDetail(material: any): ValueMetricsMaterial {
const authorName = String(material.user?.realName || material.user?.username || material.authorName || '').trim() || '未标注'
const title = String(material.reportTitle || material.productModel || material.taskNo || '').trim() || `素材 ${String(material.id).slice(0, 8)}`
return {
id: String(material.id),
title,
materialType: normalizeMaterialTypeForUi(material.materialType) || String(material.materialType || '未分类'),
authorName,
createdAt: new Date(material.createdAt).toISOString(),
downloadCount: Number(material.downloadCount || 0),
categoryName: String(material.categoryName || '').trim(),
sceneName: String(material.sceneName || '').trim(),
platformName: String(material.platformName || '').trim(),
}
}
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user || !(session.user as any).id) {
return NextResponse.json({ success: false, error: '未登录', code: 'UNAUTHORIZED' }, { status: 401, headers: PRIVATE_HEADERS })
}
// 复用素材资产库的筛选、删除状态、素材类型权限和私密任务详情准入规则,
// 保证价值指标不会比用户在资产库里能看到的范围更宽。
const user = session.user as any
const baseParams = new URLSearchParams()
const context = await createMaterialQueryContext(baseParams, user)
const { where, empty } = await buildMaterialWhereFromSearchParams(baseParams, user, { context })
if (empty) {
return NextResponse.json({
success: true,
data: {
types: [],
selectedType: null,
selectedTypeSummary: null,
materials: [],
materialsTotal: 0,
hasMore: false,
summary: { totalAssets: 0, reusedAssets: 0, reuseRate: null },
},
}, { headers: PRIVATE_HEADERS })
}
const [totalRows, reusedRows] = await Promise.all([
prisma.material.groupBy({
by: ['materialType'],
where,
_count: { _all: true },
}) as unknown as Promise<MaterialTypeCountRow[]>,
prisma.material.groupBy({
by: ['materialType'],
where: { AND: [where, { downloadCount: { gt: 0 } }] },
_count: { _all: true },
}) as unknown as Promise<MaterialTypeCountRow[]>,
])
const typeRows = buildTypeSummaries(totalRows, reusedRows)
const requestedType = normalizeMaterialTypeForUi(request.nextUrl.searchParams.get('materialType'))
const selectedSummary = typeRows.find((type) => type.key === requestedType) || typeRows[0] || null
const selectedType = selectedSummary?.key || null
const detailLimit = Math.min(Math.max(Number.parseInt(request.nextUrl.searchParams.get('limit') || '100', 10) || 100, 1), MAX_DETAIL_LIMIT)
const detailOffset = Math.max(Number.parseInt(request.nextUrl.searchParams.get('offset') || '0', 10) || 0, 0)
let materials: ValueMetricsMaterial[] = []
if (selectedSummary) {
const detailRows = await prisma.material.findMany({
where: {
AND: [where, { materialType: { in: (selectedSummary as any).rawTypes } }],
},
select: {
id: true,
reportTitle: true,
productModel: true,
taskNo: true,
materialType: true,
authorName: true,
createdAt: true,
downloadCount: true,
categoryName: true,
sceneName: true,
platformName: true,
user: { select: { realName: true, username: true } },
},
orderBy: [
{ downloadCount: 'desc' },
{ createdAt: 'desc' },
{ id: 'asc' },
],
skip: detailOffset,
take: detailLimit + 1,
})
const hasMore = detailRows.length > detailLimit
materials = detailRows.slice(0, detailLimit).map(toMaterialDetail)
const totalAssets = typeRows.reduce((sum, type) => sum + type.totalCount, 0)
const reusedAssets = typeRows.reduce((sum, type) => sum + type.reusedCount, 0)
return NextResponse.json({
success: true,
data: {
types: typeRows.map(({ rawTypes: _rawTypes, ...type }) => type),
selectedType,
selectedTypeSummary: selectedSummary
? (({ rawTypes: _rawTypes, ...summary }) => summary)(selectedSummary)
: null,
materials,
materialsTotal: selectedSummary.totalCount,
hasMore,
summary: {
totalAssets,
reusedAssets,
reuseRate: calculateReuseRate(reusedAssets, totalAssets),
},
},
}, { headers: PRIVATE_HEADERS })
}
return NextResponse.json({
success: true,
data: {
types: typeRows,
selectedType: null,
selectedTypeSummary: null,
materials: [],
materialsTotal: 0,
hasMore: false,
summary: { totalAssets: 0, reusedAssets: 0, reuseRate: null },
},
}, { headers: PRIVATE_HEADERS })
} catch (error) {
console.error('[department-dashboard] 价值指标读取失败:', error)
return NextResponse.json(
{ success: false, error: '价值指标读取失败,请稍后重试' },
{ status: 500, headers: PRIVATE_HEADERS },
)
}
}