Register 直播切片 as a video-only material type. Upload and edit flows collect required title, live date (defaults to today), optional host with user link, and live platform presets. Persist the fields on Material and skip SPU formatting for free-text titles.
143 lines
5.6 KiB
TypeScript
143 lines
5.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from '@/lib/auth';
|
|
import { updateMaterialWithSharedRules, type MaterialUpdateInput } from '@/lib/material-update';
|
|
import { isWebFileLike } from '@/lib/web-file';
|
|
import { assertBoundedMultipartHeaders, assertMultipartFiles } from '@/lib/multipart-request-guard';
|
|
import {
|
|
boundedMultipartRequestLimiter,
|
|
getBoundedMultipartFailure,
|
|
MULTIPART_ROUTE_LIMITS,
|
|
} from '@/lib/multipart-route-limits';
|
|
|
|
// PUT /api/materials/update - 更新素材(包括预览图)
|
|
export async function PUT(request: NextRequest) {
|
|
let releaseRequestSlot: (() => void) | null = null;
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: '请先登录' }, { status: 401 });
|
|
}
|
|
|
|
const contentType = request.headers.get('content-type') || '';
|
|
let id = '';
|
|
let input: MaterialUpdateInput;
|
|
let previewImage: File | null = null;
|
|
|
|
if (contentType.includes('multipart/form-data')) {
|
|
assertBoundedMultipartHeaders(request.headers, MULTIPART_ROUTE_LIMITS.materialUpdateRequestBytes);
|
|
releaseRequestSlot = await boundedMultipartRequestLimiter.acquire();
|
|
const formData = await request.formData();
|
|
assertMultipartFiles(formData, {
|
|
fields: {
|
|
previewImage: {
|
|
maxCount: 1,
|
|
maxFileBytes: MULTIPART_ROUTE_LIMITS.materialUpdatePreviewBytes,
|
|
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
|
|
},
|
|
},
|
|
maxFiles: 1,
|
|
maxTotalBytes: MULTIPART_ROUTE_LIMITS.materialUpdatePreviewBytes,
|
|
});
|
|
id = String(formData.get('id') || '');
|
|
input = {
|
|
productModel: String(formData.get('productModel') || ''),
|
|
...(formData.has('reportTitle')
|
|
? { reportTitle: formData.get('reportTitle') as string | null }
|
|
: {}),
|
|
categoryName: formData.get('categoryName') as string | null,
|
|
sceneName: formData.get('sceneName') as string | null,
|
|
platformName: formData.get('platformName') as string | null,
|
|
productCategoryName: formData.get('productCategoryName') as string | null,
|
|
materialType: String(formData.get('materialType') || ''),
|
|
renderer: formData.get('renderer') as string | null,
|
|
authorName: String(formData.get('authorName') || ''),
|
|
...(formData.has('userId')
|
|
? { userId: formData.get('userId') as string | null }
|
|
: {}),
|
|
...(formData.has('taskNo')
|
|
? { taskNo: formData.get('taskNo') as string | null }
|
|
: {}),
|
|
...(formData.has('dingtalkApprovalNo')
|
|
? { dingtalkApprovalNo: formData.get('dingtalkApprovalNo') as string | null }
|
|
: {}),
|
|
...(formData.has('taskDetailSourceType')
|
|
? { taskDetailSourceType: formData.get('taskDetailSourceType') as string | null }
|
|
: {}),
|
|
...(formData.has('taskDetailRequesterUserId')
|
|
? { taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null }
|
|
: {}),
|
|
...(formData.has('liveDate')
|
|
? { liveDate: formData.get('liveDate') as string | null }
|
|
: {}),
|
|
...(formData.has('liveHost')
|
|
? { liveHost: formData.get('liveHost') as string | null }
|
|
: {}),
|
|
...(formData.has('liveHostUserId')
|
|
? { liveHostUserId: formData.get('liveHostUserId') as string | null }
|
|
: {}),
|
|
...(formData.has('livePlatform')
|
|
? { livePlatform: formData.get('livePlatform') as string | null }
|
|
: {}),
|
|
...(formData.has('isFullSet')
|
|
? { isFullSet: formData.get('isFullSet') as string | null }
|
|
: {}),
|
|
};
|
|
const formPreview = formData.get('previewImage');
|
|
previewImage = isWebFileLike(formPreview) ? formPreview : null;
|
|
} else {
|
|
const json = await request.json();
|
|
id = String(json.id || '');
|
|
input = json;
|
|
}
|
|
|
|
if (!id) {
|
|
return NextResponse.json({ error: '缺少素材ID' }, { status: 400 });
|
|
}
|
|
|
|
const user = session.user as any;
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: id,
|
|
operatorUserId: user.id,
|
|
operatorRole: user.role,
|
|
input,
|
|
previewImage,
|
|
});
|
|
|
|
if ('error' in result) {
|
|
return NextResponse.json({ error: result.error }, { status: result.status });
|
|
}
|
|
|
|
const response = {
|
|
...result.material,
|
|
zipFileSize: result.material.zipFileSize.toString(),
|
|
isFullSet: Boolean((result.material as any).isFullSet),
|
|
taskDetailSourceType: (result.material as any).taskDetailSourceType || null,
|
|
taskDetailRequesterUserId: (result.material as any).taskDetailRequesterUserId || null,
|
|
taskDetailRequesterName: (result.material as any).taskDetailRequesterName || null,
|
|
};
|
|
|
|
return NextResponse.json(
|
|
{ message: '更新成功', material: response },
|
|
{ status: 200 }
|
|
);
|
|
} catch (error) {
|
|
console.error('Update error:', error);
|
|
const boundedFailure = getBoundedMultipartFailure(error);
|
|
if (boundedFailure) {
|
|
return NextResponse.json(
|
|
{ error: boundedFailure.message, code: boundedFailure.code },
|
|
{
|
|
status: boundedFailure.status,
|
|
...(boundedFailure.retryAfterSeconds
|
|
? { headers: { 'Retry-After': String(boundedFailure.retryAfterSeconds) } }
|
|
: {}),
|
|
},
|
|
);
|
|
}
|
|
return NextResponse.json({ error: '更新失败,请稍后再试' }, { status: 500 });
|
|
} finally {
|
|
releaseRequestSlot?.();
|
|
}
|
|
}
|