238 lines
7.9 KiB
TypeScript
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 })
|
|
}
|
|
}
|