386 lines
9.8 KiB
TypeScript
386 lines
9.8 KiB
TypeScript
/**
|
|
* 素材资产管理接口
|
|
* GET - 获取资产列表
|
|
* POST - 添加资产
|
|
* DELETE - 删除资产
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from '@/lib/auth';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { getAssetsByMaterialId, addAsset } from '@/lib/product-package';
|
|
import { deleteFileIfUnreferenced } from '@/lib/file-cleanup';
|
|
import { createMaterialLog } from '@/lib/material-log';
|
|
import { normalizeDisplayImages } from '@/lib/material-cleanup';
|
|
import {
|
|
canUserViewMaterial,
|
|
isSuperAdmin,
|
|
isSystemAdmin,
|
|
} from '@/lib/department-permission';
|
|
|
|
type AssetRouteSessionUser = {
|
|
id: string;
|
|
role?: string | null;
|
|
department?: string | null;
|
|
managedDepartment?: string | null;
|
|
isSuperAdmin?: boolean | null;
|
|
};
|
|
|
|
async function canManageMaterial(user: AssetRouteSessionUser, materialUserId: string) {
|
|
if (materialUserId === user.id || isSystemAdmin(user.role) || user.isSuperAdmin === true) {
|
|
return true;
|
|
}
|
|
|
|
return isSuperAdmin(user.id);
|
|
}
|
|
|
|
async function canViewMaterial(
|
|
user: AssetRouteSessionUser,
|
|
material: { userId: string; materialType: string },
|
|
) {
|
|
if (material.userId === user.id) {
|
|
return true;
|
|
}
|
|
|
|
return canUserViewMaterial(
|
|
user.department,
|
|
user.role || 'USER',
|
|
user.managedDepartment,
|
|
material.materialType,
|
|
user.id,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* GET - 获取资产列表
|
|
*/
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user || !(session.user as any).id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '未登录' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const materialId = params.id;
|
|
|
|
// 验证素材是否存在
|
|
const material = await prisma.material.findUnique({
|
|
where: { id: materialId, deletedAt: null },
|
|
select: {
|
|
id: true,
|
|
userId: true,
|
|
materialType: true,
|
|
previewUrl: true,
|
|
zipFilePath: true,
|
|
zipFileName: true,
|
|
zipFileSize: true,
|
|
displayImages: true,
|
|
},
|
|
});
|
|
|
|
if (!material) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '素材不存在' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
if (!(await canViewMaterial(session.user as AssetRouteSessionUser, material))) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '无权限查看此素材' },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// 获取资产列表
|
|
const assets = await getAssetsByMaterialId(materialId);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: assets,
|
|
});
|
|
} catch (error) {
|
|
console.error('[materials/assets] GET error:', error);
|
|
return NextResponse.json(
|
|
{ success: false, error: '服务器错误' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST - 添加资产
|
|
*/
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user || !(session.user as any).id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '未登录' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const userId = (session.user as any).id;
|
|
const materialId = params.id;
|
|
const body = await request.json();
|
|
const { assetType, fileUrl, fileName, fileSize, sortOrder } = body;
|
|
|
|
// 验证必填字段
|
|
if (!assetType || !fileUrl) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '缺少必填参数' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 验证素材是否存在且属于当前用户
|
|
const material = await prisma.material.findUnique({
|
|
where: { id: materialId, deletedAt: null },
|
|
select: { id: true, userId: true },
|
|
});
|
|
|
|
if (!material) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '素材不存在' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
if (!(await canManageMaterial(session.user as AssetRouteSessionUser, material.userId))) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '无权限操作此素材' },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// 添加资产
|
|
const asset = await addAsset({
|
|
materialId,
|
|
assetType,
|
|
fileUrl,
|
|
fileName,
|
|
fileSize: fileSize ? BigInt(fileSize) : undefined,
|
|
sortOrder,
|
|
});
|
|
|
|
await createMaterialLog({
|
|
materialId,
|
|
userId,
|
|
action: 'UPDATE',
|
|
changes: {
|
|
assetOperation: {
|
|
oldValue: null,
|
|
newValue: 'ADD',
|
|
},
|
|
assetType: {
|
|
oldValue: null,
|
|
newValue: asset.assetType,
|
|
},
|
|
assetFileName: {
|
|
oldValue: null,
|
|
newValue: asset.fileName || null,
|
|
},
|
|
assetFileUrl: {
|
|
oldValue: null,
|
|
newValue: asset.fileUrl,
|
|
},
|
|
},
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: asset,
|
|
});
|
|
} catch (error) {
|
|
console.error('[materials/assets] POST error:', error);
|
|
return NextResponse.json(
|
|
{ success: false, error: '服务器错误' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE - 删除资产
|
|
*/
|
|
export async function DELETE(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user || !(session.user as any).id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '未登录' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const userId = (session.user as any).id;
|
|
const materialId = params.id;
|
|
const { searchParams } = new URL(request.url);
|
|
const assetId = searchParams.get('assetId');
|
|
|
|
if (!assetId) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '缺少 assetId 参数' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 验证素材是否存在且属于当前用户
|
|
const material = await prisma.material.findUnique({
|
|
where: { id: materialId, deletedAt: null },
|
|
select: {
|
|
id: true,
|
|
userId: true,
|
|
previewUrl: true,
|
|
zipFilePath: true,
|
|
zipFileName: true,
|
|
zipFileSize: true,
|
|
displayImages: true,
|
|
},
|
|
});
|
|
|
|
if (!material) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '素材不存在' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
if (!(await canManageMaterial(session.user as AssetRouteSessionUser, material.userId))) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '无权限操作此素材' },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// 验证资产是否属于该素材
|
|
const asset = await prisma.materialAsset.findUnique({
|
|
where: { id: assetId },
|
|
select: {
|
|
id: true,
|
|
materialId: true,
|
|
assetType: true,
|
|
fileUrl: true,
|
|
fileName: true,
|
|
},
|
|
});
|
|
|
|
if (!asset || asset.materialId !== materialId) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '资产不存在或不属于该素材' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const previewAssetTypes = ['preview', 'main-preview', 'sku-preview', 'detail-preview'];
|
|
const currentDisplayImages = normalizeDisplayImages((material as any).displayImages);
|
|
const displayImages = currentDisplayImages.filter((url) => url !== asset.fileUrl);
|
|
const materialUpdateData: Record<string, unknown> = {};
|
|
|
|
if ((material as any).previewUrl === asset.fileUrl) {
|
|
const nextPreviewAsset = await prisma.materialAsset.findFirst({
|
|
where: {
|
|
materialId,
|
|
id: { not: assetId },
|
|
assetType: { in: previewAssetTypes },
|
|
},
|
|
orderBy: [
|
|
{ sortOrder: 'asc' },
|
|
{ createdAt: 'asc' },
|
|
],
|
|
select: { fileUrl: true },
|
|
});
|
|
materialUpdateData.previewUrl = nextPreviewAsset?.fileUrl || '';
|
|
}
|
|
|
|
if ((material as any).zipFilePath === asset.fileUrl) {
|
|
materialUpdateData.zipFilePath = '';
|
|
materialUpdateData.zipFileName = '';
|
|
materialUpdateData.zipFileSize = BigInt(0);
|
|
}
|
|
|
|
if (displayImages.length !== currentDisplayImages.length) {
|
|
materialUpdateData.displayImages = displayImages;
|
|
}
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.materialAsset.delete({
|
|
where: { id: assetId },
|
|
});
|
|
|
|
if (Object.keys(materialUpdateData).length > 0) {
|
|
await tx.material.update({
|
|
where: { id: materialId },
|
|
data: materialUpdateData as any,
|
|
});
|
|
}
|
|
});
|
|
|
|
const cleanupResult = await deleteFileIfUnreferenced(asset.fileUrl, {
|
|
excludeAssetIds: [assetId],
|
|
});
|
|
|
|
await createMaterialLog({
|
|
materialId,
|
|
userId,
|
|
action: 'UPDATE',
|
|
changes: {
|
|
assetOperation: {
|
|
oldValue: null,
|
|
newValue: 'DELETE',
|
|
},
|
|
fileCleanup: {
|
|
oldValue: null,
|
|
newValue: cleanupResult.skipped ? 'skipped' : cleanupResult.success ? 'deleted' : 'failed',
|
|
},
|
|
assetId: {
|
|
oldValue: asset.id,
|
|
newValue: asset.id,
|
|
},
|
|
assetType: {
|
|
oldValue: asset.assetType,
|
|
newValue: asset.assetType,
|
|
},
|
|
assetFileName: {
|
|
oldValue: asset.fileName || null,
|
|
newValue: null,
|
|
},
|
|
assetFileUrl: {
|
|
oldValue: asset.fileUrl,
|
|
newValue: null,
|
|
},
|
|
},
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: '删除成功',
|
|
details: {
|
|
fileDeleted: !cleanupResult.skipped && cleanupResult.success,
|
|
fileSkipped: !!cleanupResult.skipped,
|
|
fileCleanupError: cleanupResult.success ? null : cleanupResult.error || null,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('[materials/assets] DELETE error:', error);
|
|
return NextResponse.json(
|
|
{ success: false, error: '服务器错误' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|