419 lines
12 KiB
TypeScript
419 lines
12 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getServerSession } from 'next-auth'
|
||
import { authOptions } from '@/lib/auth'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { createMaterialLog } from '@/lib/material-log'
|
||
import {
|
||
checkMetadataSize,
|
||
compressMetadata,
|
||
parseMetadata,
|
||
prepareMaterialData,
|
||
readAllFieldValues,
|
||
} from '@/lib/field-data-storage'
|
||
import { validateSubmittedTemplateFields } from '@/lib/template-field-validation'
|
||
import { canManageMaterial, getMaterialPermissionActor } from '@/lib/material-permissions'
|
||
|
||
// GET /api/materials/[id] - 获取单个素材详情
|
||
export async function GET(
|
||
request: NextRequest,
|
||
{ params }: { params: { id: string } }
|
||
) {
|
||
try {
|
||
const session = await getServerSession(authOptions)
|
||
if (!session?.user) {
|
||
return NextResponse.json(
|
||
{ error: '请先登录' },
|
||
{ status: 401 }
|
||
)
|
||
}
|
||
|
||
const actor = await getMaterialPermissionActor(session)
|
||
if (!actor) {
|
||
return NextResponse.json(
|
||
{ error: '账号不可用,请重新登录或联系管理员' },
|
||
{ status: 403 }
|
||
)
|
||
}
|
||
|
||
const material = await prisma.material.findFirst({
|
||
where: {
|
||
id: params.id,
|
||
deletedAt: null,
|
||
},
|
||
select: {
|
||
id: true,
|
||
userId: true,
|
||
productModel: true,
|
||
materialKind: true,
|
||
brandName: true,
|
||
seriesName: true,
|
||
skuCode: true,
|
||
materialTexture: true,
|
||
categoryName: true,
|
||
sceneName: true,
|
||
platformName: true,
|
||
materialType: true,
|
||
renderer: true,
|
||
renderTime: true,
|
||
imageResolution: true,
|
||
colorMode: true,
|
||
authorName: true,
|
||
previewUrl: true,
|
||
videoCoverUrl: true,
|
||
videoPreview540Url: true,
|
||
videoPreviewClipUrl: true,
|
||
videoProcessingStatus: true,
|
||
videoDuration: true,
|
||
zipFileName: true,
|
||
zipFileSize: true,
|
||
downloadCount: true,
|
||
likeCount: true,
|
||
displayImages: true,
|
||
metadata: true,
|
||
createdAt: true,
|
||
templateId: true,
|
||
// 音频相关字段
|
||
// @ts-ignore
|
||
audioDuration: true,
|
||
// @ts-ignore
|
||
audioCategory: true,
|
||
// @ts-ignore
|
||
audioScene: true,
|
||
// @ts-ignore
|
||
audioUrl: true,
|
||
user: {
|
||
select: {
|
||
id: true,
|
||
username: true,
|
||
avatar: true,
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
if (!material) {
|
||
return NextResponse.json(
|
||
{ error: '素材不存在' },
|
||
{ status: 404 }
|
||
)
|
||
}
|
||
|
||
const parsedMetadata = parseMetadata(material.metadata)
|
||
|
||
// 返回素材信息(转换BigInt为字符串)
|
||
return NextResponse.json({
|
||
id: material.id,
|
||
userId: material.userId,
|
||
productModel: material.productModel,
|
||
brandName: material.brandName,
|
||
seriesName: material.seriesName,
|
||
skuCode: material.skuCode,
|
||
materialTexture: material.materialTexture,
|
||
categoryName: material.categoryName,
|
||
sceneName: material.sceneName,
|
||
platformName: material.platformName,
|
||
materialType: material.materialType,
|
||
renderer: material.renderer,
|
||
renderTime: material.renderTime,
|
||
imageResolution: material.imageResolution,
|
||
colorMode: material.colorMode,
|
||
authorName: material.authorName,
|
||
previewUrl: material.previewUrl,
|
||
materialKind:
|
||
(material.materialKind || '').trim().toUpperCase() ||
|
||
(material.materialType === '视频' ? 'VIDEO' : material.materialType === '音频' ? 'AUDIO' : 'IMAGE'),
|
||
videoCoverUrl: material.videoCoverUrl,
|
||
videoPreview540Url: material.videoPreview540Url,
|
||
videoPreviewClipUrl: material.videoPreviewClipUrl,
|
||
videoProcessingStatus: material.videoProcessingStatus,
|
||
videoDuration: material.videoDuration,
|
||
zipFileName: material.zipFileName,
|
||
zipFileSize: material.zipFileSize.toString(),
|
||
downloadCount: material.downloadCount,
|
||
likeCount: material.likeCount,
|
||
displayImages: material.displayImages,
|
||
createdAt: material.createdAt,
|
||
templateId: material.templateId,
|
||
templateFields: parsedMetadata.templateFields,
|
||
// 音频相关字段
|
||
audioDuration: (material as any).audioDuration,
|
||
audioCategory: (material as any).audioCategory,
|
||
audioScene: (material as any).audioScene,
|
||
audioUrl: (material as any).audioUrl,
|
||
user: {
|
||
username: material.user.username,
|
||
avatar: material.user.avatar
|
||
}
|
||
})
|
||
} catch (error: any) {
|
||
console.error('[Materials] GET [id] error:', error)
|
||
return NextResponse.json(
|
||
{ error: '获取素材失败' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
|
||
// PUT /api/materials/[id] - 更新素材信息
|
||
export async function PUT(
|
||
request: NextRequest,
|
||
{ params }: { params: { id: string } }
|
||
) {
|
||
try {
|
||
const session = await getServerSession(authOptions)
|
||
|
||
if (!session?.user) {
|
||
return NextResponse.json(
|
||
{ error: '请先登录' },
|
||
{ status: 401 }
|
||
)
|
||
}
|
||
|
||
const body = await request.json()
|
||
const {
|
||
productModel,
|
||
categoryName,
|
||
sceneName,
|
||
materialType,
|
||
renderer,
|
||
authorName,
|
||
brandName,
|
||
seriesName,
|
||
skuCode,
|
||
materialTexture,
|
||
renderTime,
|
||
imageResolution,
|
||
colorMode,
|
||
templateId,
|
||
templateFields = {},
|
||
} = body
|
||
|
||
if (
|
||
templateFields === null ||
|
||
typeof templateFields !== 'object' ||
|
||
Array.isArray(templateFields)
|
||
) {
|
||
return NextResponse.json(
|
||
{ error: '模板字段格式无效', errorCode: 'INVALID_TEMPLATE_FIELDS' },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
// 验证必填字段
|
||
if (!productModel || !categoryName || !sceneName || !materialType || !renderer || !authorName) {
|
||
return NextResponse.json(
|
||
{ error: '缺少必填字段' },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
// 获取素材信息
|
||
const material = await prisma.material.findFirst({
|
||
where: {
|
||
id: params.id,
|
||
deletedAt: null,
|
||
},
|
||
include: {
|
||
user: {
|
||
select: {
|
||
id: true,
|
||
department: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!material) {
|
||
return NextResponse.json(
|
||
{ error: '素材不存在' },
|
||
{ status: 404 }
|
||
)
|
||
}
|
||
|
||
// 权限检查:本人、最高权限账号,或管理部门范围内的管理员可以编辑
|
||
const actor = await getMaterialPermissionActor(session)
|
||
if (!(await canManageMaterial(material, actor))) {
|
||
return NextResponse.json(
|
||
{ error: '您没有权限编辑此素材' },
|
||
{ status: 403 }
|
||
)
|
||
}
|
||
|
||
// 记录修改前的值,用于日志
|
||
const oldValues: Record<string, any> = {
|
||
productModel: material.productModel,
|
||
categoryName: material.categoryName,
|
||
sceneName: material.sceneName,
|
||
materialType: material.materialType,
|
||
renderer: material.renderer,
|
||
authorName: material.authorName,
|
||
brandName: material.brandName,
|
||
seriesName: material.seriesName,
|
||
skuCode: material.skuCode,
|
||
materialTexture: material.materialTexture,
|
||
renderTime: material.renderTime,
|
||
imageResolution: material.imageResolution,
|
||
colorMode: material.colorMode,
|
||
}
|
||
|
||
// 更新素材信息
|
||
const updateData: any = {
|
||
productModel,
|
||
categoryName,
|
||
sceneName,
|
||
materialType,
|
||
renderer,
|
||
authorName,
|
||
updatedAt: new Date()
|
||
}
|
||
|
||
const assignOptional = (key: string, value: any) => {
|
||
if (value !== undefined) {
|
||
updateData[key] = value === null || value === '' ? null : value
|
||
}
|
||
}
|
||
|
||
assignOptional('brandName', brandName)
|
||
assignOptional('seriesName', seriesName)
|
||
assignOptional('skuCode', skuCode)
|
||
assignOptional('materialTexture', materialTexture)
|
||
assignOptional('renderTime', renderTime)
|
||
assignOptional('imageResolution', imageResolution)
|
||
assignOptional('colorMode', colorMode)
|
||
|
||
const effectiveTemplateId = templateId ?? material.templateId ?? null
|
||
if (effectiveTemplateId) {
|
||
const mergedTemplateFields = {
|
||
...readAllFieldValues(material as Record<string, unknown>),
|
||
...updateData,
|
||
...templateFields,
|
||
}
|
||
|
||
const templateFieldsValidation = await validateSubmittedTemplateFields({
|
||
templateId: effectiveTemplateId,
|
||
templateFields: mergedTemplateFields,
|
||
allowUnknownKeys: true,
|
||
})
|
||
|
||
if (!templateFieldsValidation.valid) {
|
||
return NextResponse.json(
|
||
{
|
||
error: templateFieldsValidation.error,
|
||
errorCode: templateFieldsValidation.errorCode,
|
||
details: templateFieldsValidation.details,
|
||
},
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
const { metadata: rawMetadata } = prepareMaterialData(
|
||
templateFieldsValidation.templateFields as Record<string, any>,
|
||
effectiveTemplateId,
|
||
undefined,
|
||
material.metadata
|
||
)
|
||
const metadata = compressMetadata(rawMetadata)
|
||
const metadataSize = checkMetadataSize(metadata)
|
||
if (!metadataSize.ok) {
|
||
return NextResponse.json(
|
||
{
|
||
error: `自定义字段数据过大,当前约 ${metadataSize.size} 字节,最大允许 ${metadataSize.maxSize} 字节。请减少字段内容后重试。`,
|
||
errorCode: 'METADATA_TOO_LARGE',
|
||
},
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
updateData.templateId = effectiveTemplateId
|
||
updateData.metadata = metadata
|
||
}
|
||
|
||
const updatedMaterial = await prisma.material.update({
|
||
where: { id: params.id },
|
||
data: updateData
|
||
})
|
||
|
||
// 记录修改详情到日志(用于监控仪表板)
|
||
const changes: string[] = []
|
||
const fieldNames: Record<string, string> = {
|
||
productModel: '产品型号',
|
||
categoryName: '一级分类',
|
||
sceneName: '二级分类',
|
||
materialType: '素材类型',
|
||
renderer: '渲染器',
|
||
authorName: '作者花名',
|
||
brandName: '品牌名称',
|
||
seriesName: '系列/产品线',
|
||
skuCode: 'SKU编码',
|
||
materialTexture: '材质/工艺',
|
||
renderTime: '渲染耗时',
|
||
imageResolution: '输出分辨率',
|
||
colorMode: '颜色模式',
|
||
}
|
||
|
||
Object.keys(updateData).forEach((key) => {
|
||
if (key === 'updatedAt') return
|
||
const oldVal = oldValues[key] ?? null
|
||
const newVal = updateData[key] ?? null
|
||
if (String(oldVal) !== String(newVal)) {
|
||
const fieldName = fieldNames[key] || key
|
||
changes.push(`${fieldName}: "${oldVal ?? '(空)'}" → "${newVal ?? '(空)'}"`)
|
||
}
|
||
})
|
||
|
||
if (changes.length > 0) {
|
||
console.log(`[MaterialUpdate] Material ${params.id} updated by ${actor!.id}:`, changes.join('; '))
|
||
|
||
// 记录详细修改日志到数据库
|
||
const changesObj: Record<string, { oldValue: any; newValue: any }> = {}
|
||
Object.keys(updateData).forEach((key) => {
|
||
if (key === 'updatedAt') return
|
||
const oldVal = oldValues[key] ?? null
|
||
const newVal = updateData[key] ?? null
|
||
if (String(oldVal) !== String(newVal)) {
|
||
changesObj[key] = {
|
||
oldValue: oldVal ?? null,
|
||
newValue: newVal ?? null,
|
||
}
|
||
}
|
||
})
|
||
|
||
if (Object.keys(changesObj).length > 0) {
|
||
await createMaterialLog({
|
||
materialId: params.id,
|
||
userId: actor!.id,
|
||
action: 'UPDATE',
|
||
changes: changesObj,
|
||
})
|
||
}
|
||
}
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
message: '素材信息更新成功',
|
||
material: {
|
||
id: updatedMaterial.id,
|
||
productModel: updatedMaterial.productModel,
|
||
brandName: updatedMaterial.brandName,
|
||
seriesName: updatedMaterial.seriesName,
|
||
skuCode: updatedMaterial.skuCode,
|
||
materialTexture: updatedMaterial.materialTexture,
|
||
categoryName: updatedMaterial.categoryName,
|
||
sceneName: updatedMaterial.sceneName,
|
||
materialType: updatedMaterial.materialType,
|
||
renderer: updatedMaterial.renderer,
|
||
renderTime: updatedMaterial.renderTime,
|
||
imageResolution: updatedMaterial.imageResolution,
|
||
colorMode: updatedMaterial.colorMode,
|
||
authorName: updatedMaterial.authorName
|
||
}
|
||
})
|
||
} catch (error: any) {
|
||
console.error('[Materials] PUT [id] error:', error)
|
||
return NextResponse.json(
|
||
{ error: '更新素材失败' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|