205 lines
7.5 KiB
TypeScript
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 },
|
|
)
|
|
}
|
|
}
|