WuYongJie d8d843e6ad feat: add live clip material type with date host and platform
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.
2026-09-22 18:59:05 +08:00

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?.();
}
}