2026-03-06 14:14:50 +08:00

238 lines
7.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { isSystemAdmin } from '@/lib/department-permission'
import { safeJsonParse } from '@/lib/safe-json'
const VALID_ACTIONS = new Set(['ALL', 'UPLOAD', 'UPDATE', 'DELETE', 'DOWNLOAD', 'RESTORE'])
function toDateStart(dateString: string): Date {
return new Date(`${dateString}T00:00:00`)
}
function toDateEnd(dateString: string): Date {
return new Date(`${dateString}T23:59:59.999`)
}
function normalizeKeyword(keyword: string | null): string {
return (keyword || '').trim()
}
function includesKeyword(value: string | null | undefined, keyword: string): boolean {
if (!value) return false
return value.toLowerCase().includes(keyword.toLowerCase())
}
function getSnapshotMaterialInfo(changes: Record<string, unknown> | null): {
productModel?: string
materialType?: string
} {
if (!changes) return {}
const snapshot = changes.materialSnapshot
if (!snapshot || typeof snapshot !== 'object') return {}
const info = snapshot as Record<string, unknown>
return {
productModel: typeof info.productModel === 'string' ? info.productModel : undefined,
materialType: typeof info.materialType === 'string' ? info.materialType : undefined,
}
}
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user || !isSystemAdmin((session.user as any).role)) {
return NextResponse.json({ error: '无权限' }, { status: 403 })
}
const { searchParams } = new URL(request.url)
const requestedAction = (searchParams.get('action') || 'ALL').toUpperCase()
const action = VALID_ACTIONS.has(requestedAction) ? requestedAction : 'ALL'
const page = Math.max(Number(searchParams.get('page') || '1'), 1)
const pageSize = Math.min(Math.max(Number(searchParams.get('pageSize') || '20'), 1), 100)
const startDate = searchParams.get('startDate')
const endDate = searchParams.get('endDate')
const keyword = normalizeKeyword(searchParams.get('keyword'))
const materialLogWhere: any = {}
const downloadLogWhere: any = {}
if (startDate || endDate) {
const timestampFilter: { gte?: Date; lte?: Date } = {}
if (startDate) {
timestampFilter.gte = toDateStart(startDate)
}
if (endDate) {
timestampFilter.lte = toDateEnd(endDate)
}
materialLogWhere.timestamp = timestampFilter
downloadLogWhere.downloadedAt = timestampFilter
}
if (action === 'ALL') {
materialLogWhere.action = { not: 'DOWNLOAD' }
} else if (action !== 'DOWNLOAD') {
materialLogWhere.action = action
}
const includeMaterialLogs = action !== 'DOWNLOAD'
const includeDownloadLogs = action === 'ALL' || action === 'DOWNLOAD'
const fetchLimit = keyword ? 5000 : page * pageSize
const [materialLogTotal, downloadLogTotal, materialLogs, downloadLogs] = await Promise.all([
includeMaterialLogs ? prisma.materialLog.count({ where: materialLogWhere }) : Promise.resolve(0),
includeDownloadLogs ? prisma.materialDownloadLog.count({ where: downloadLogWhere }) : Promise.resolve(0),
includeMaterialLogs
? prisma.materialLog.findMany({
where: materialLogWhere,
orderBy: { timestamp: 'desc' },
take: fetchLimit,
include: {
user: {
select: {
id: true,
username: true,
realName: true,
department: true,
departmentId: true,
departmentRef: {
select: {
name: true,
},
},
},
},
},
})
: Promise.resolve([]),
includeDownloadLogs
? prisma.materialDownloadLog.findMany({
where: downloadLogWhere,
orderBy: { downloadedAt: 'desc' },
take: fetchLimit,
include: {
user: {
select: {
id: true,
username: true,
realName: true,
department: true,
departmentId: true,
departmentRef: {
select: {
name: true,
},
},
},
},
},
})
: Promise.resolve([]),
])
const materialIds = Array.from(
new Set([
...materialLogs.map((log) => log.materialId),
...downloadLogs.map((log) => log.materialId),
])
)
const materials = materialIds.length
? await prisma.material.findMany({
where: { id: { in: materialIds } },
select: {
id: true,
productModel: true,
materialType: true,
},
})
: []
const materialMap = new Map(materials.map((material) => [material.id, material]))
const formattedMaterialLogs = materialLogs.map((log) => {
const parsedChanges = log.changes
? safeJsonParse<Record<string, unknown>>(log.changes, {}, false)
: null
const relatedMaterial = materialMap.get(log.materialId)
const snapshotInfo = getSnapshotMaterialInfo(parsedChanges)
return {
id: `material-${log.id}`,
source: 'material_log',
action: log.action,
timestamp: log.timestamp.toISOString(),
user: {
id: log.user.id,
username: log.user.username,
realName: log.user.realName,
departmentName: log.user.departmentRef?.name || log.user.department || '未分配部门',
},
material: {
id: log.materialId,
productModel: relatedMaterial?.productModel || snapshotInfo.productModel || '已删除素材',
materialType: relatedMaterial?.materialType || snapshotInfo.materialType || '未知类型',
},
changes: parsedChanges,
}
})
const formattedDownloadLogs = downloadLogs.map((log) => ({
id: `download-${log.id}`,
source: 'download_log',
action: 'DOWNLOAD',
timestamp: log.downloadedAt.toISOString(),
user: {
id: log.user.id,
username: log.user.username,
realName: log.user.realName,
departmentName: log.user.departmentRef?.name || log.user.department || '未分配部门',
},
material: {
id: log.materialId,
productModel: materialMap.get(log.materialId)?.productModel || '已删除素材',
materialType: materialMap.get(log.materialId)?.materialType || '未知类型',
},
changes: null,
}))
const mergedLogs = [...formattedMaterialLogs, ...formattedDownloadLogs].sort(
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
)
const filteredLogs = keyword
? mergedLogs.filter((log) => {
return (
includesKeyword(log.user.username, keyword) ||
includesKeyword(log.user.realName, keyword) ||
includesKeyword(log.user.departmentName, keyword) ||
includesKeyword(log.material.id, keyword) ||
includesKeyword(log.material.productModel, keyword) ||
includesKeyword(log.material.materialType, keyword)
)
})
: mergedLogs
const offset = (page - 1) * pageSize
const paginatedLogs = filteredLogs.slice(offset, offset + pageSize)
const total = keyword ? filteredLogs.length : materialLogTotal + downloadLogTotal
return NextResponse.json({
success: true,
data: {
logs: paginatedLogs,
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
})
} catch (error) {
console.error('[Admin Material Operation Logs] GET error:', error)
return NextResponse.json({ error: '获取素材操作日志失败' }, { status: 500 })
}
}