diff --git a/app/api/materials/[id]/route.ts b/app/api/materials/[id]/route.ts index 3062d8f..de63b76 100644 --- a/app/api/materials/[id]/route.ts +++ b/app/api/materials/[id]/route.ts @@ -165,6 +165,8 @@ export async function GET( materialType: true, isAiGenerated: true, taskDetailSourceType: true, + taskDetailRequesterUserId: true, + taskDetailRequesterName: true, renderer: true, authorName: true, previewUrl: true, @@ -234,6 +236,8 @@ export async function GET( materialType: true, isAiGenerated: true, taskDetailSourceType: true, + taskDetailRequesterUserId: true, + taskDetailRequesterName: true, renderer: true, authorName: true, previewUrl: true, @@ -392,6 +396,8 @@ export async function GET( materialType: materialWithUser.materialType, isAiGenerated: Boolean(materialWithUser.isAiGenerated), taskDetailSourceType: materialWithUser.taskDetailSourceType || null, + taskDetailRequesterUserId: materialWithUser.taskDetailRequesterUserId || null, + taskDetailRequesterName: materialWithUser.taskDetailRequesterName || null, renderer: materialWithUser.renderer, authorName: displayAuthorName, // 使用处理后的作者名 previewUrl: materialWithUser.previewUrl, @@ -502,6 +508,7 @@ export async function PUT( userId: formData.get('userId') as string | null, taskNo: formData.get('taskNo') as string | null, taskDetailSourceType: formData.get('taskDetailSourceType') as string | null, + taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null, isFullSet: formData.get('isFullSet') as string | null, }; const formPreview = formData.get('previewImage'); @@ -539,6 +546,8 @@ export async function PUT( authorName: result.material.authorName, taskNo: result.material.taskNo, taskDetailSourceType: (result.material as any).taskDetailSourceType || null, + taskDetailRequesterUserId: (result.material as any).taskDetailRequesterUserId || null, + taskDetailRequesterName: (result.material as any).taskDetailRequesterName || null, taskStatus: result.material.taskStatus, taskFetchedAt: result.material.taskFetchedAt, isFullSet: Boolean((result.material as any).isFullSet), diff --git a/app/api/materials/route.ts b/app/api/materials/route.ts index dbed30d..d7ff18f 100644 --- a/app/api/materials/route.ts +++ b/app/api/materials/route.ts @@ -118,6 +118,8 @@ export async function GET(request: NextRequest) { materialType: true, isAiGenerated: true, taskDetailSourceType: true, + taskDetailRequesterUserId: true, + taskDetailRequesterName: true, renderer: true, authorName: true, zipFileName: true, @@ -341,6 +343,8 @@ export async function GET(request: NextRequest) { materialType: true, isAiGenerated: true, taskDetailSourceType: true, + taskDetailRequesterUserId: true, + taskDetailRequesterName: true, renderer: true, authorName: true, zipFileName: true, @@ -444,6 +448,8 @@ export async function GET(request: NextRequest) { materialType: true, isAiGenerated: true, taskDetailSourceType: true, + taskDetailRequesterUserId: true, + taskDetailRequesterName: true, renderer: true, authorName: true, zipFileName: true, @@ -506,6 +512,8 @@ export async function GET(request: NextRequest) { materialType: true, isAiGenerated: true, taskDetailSourceType: true, + taskDetailRequesterUserId: true, + taskDetailRequesterName: true, renderer: true, authorName: true, zipFileName: true, @@ -574,6 +582,8 @@ export async function GET(request: NextRequest) { materialType: true, isAiGenerated: true, taskDetailSourceType: true, + taskDetailRequesterUserId: true, + taskDetailRequesterName: true, renderer: true, authorName: true, zipFileName: true, diff --git a/app/api/materials/update/route.ts b/app/api/materials/update/route.ts index 9bef1ec..de4f904 100644 --- a/app/api/materials/update/route.ts +++ b/app/api/materials/update/route.ts @@ -33,6 +33,7 @@ export async function PUT(request: NextRequest) { userId: formData.get('userId') as string | null, taskNo: formData.get('taskNo') as string | null, taskDetailSourceType: formData.get('taskDetailSourceType') as string | null, + taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null, isFullSet: formData.get('isFullSet') as string | null, }; const formPreview = formData.get('previewImage'); @@ -65,6 +66,8 @@ export async function PUT(request: NextRequest) { 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( diff --git a/app/api/materials/upload/product-package/complete/route.ts b/app/api/materials/upload/product-package/complete/route.ts index 5731e69..71128bd 100644 --- a/app/api/materials/upload/product-package/complete/route.ts +++ b/app/api/materials/upload/product-package/complete/route.ts @@ -52,6 +52,8 @@ export async function POST(request: NextRequest) { select: { id: true, userId: true, + materialType: true, + taskDetailRequesterUserId: true, }, }) @@ -69,6 +71,20 @@ export async function POST(request: NextRequest) { ) } + if (material.materialType !== '任务-详情') { + return NextResponse.json( + { success: false, error: '当前素材不是任务-详情,不能使用此完成接口' }, + { status: 400 }, + ) + } + + if (!material.taskDetailRequesterUserId) { + return NextResponse.json( + { success: false, error: '当前素材缺少需求运营人员,不能完成上传' }, + { status: 400 }, + ) + } + const result = await completeProductPackageUpload({ materialId: body.materialId, taskNo: body.taskNo, diff --git a/app/api/materials/upload/product-package/init/route.ts b/app/api/materials/upload/product-package/init/route.ts index 0bff593..2d2982e 100644 --- a/app/api/materials/upload/product-package/init/route.ts +++ b/app/api/materials/upload/product-package/init/route.ts @@ -22,9 +22,17 @@ export async function POST(request: NextRequest) { } const body = await request.json(); - const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body; + const { + taskNo, + productModel, + categoryName, + sceneName, + platformName, + productCategoryName, + } = body; const rawTaskDetailSourceType = body?.taskDetailSourceType; const taskDetailSourceType = normalizeTaskDetailSourceType(rawTaskDetailSourceType); + const taskDetailRequesterUserId = String(body?.taskDetailRequesterUserId || '').trim(); if (rawTaskDetailSourceType != null && String(rawTaskDetailSourceType).trim() && !taskDetailSourceType) { return NextResponse.json( @@ -41,6 +49,33 @@ export async function POST(request: NextRequest) { ); } + if (!taskDetailRequesterUserId) { + return NextResponse.json( + { success: false, error: '请选择需求运营人员' }, + { status: 400 } + ); + } + + const taskDetailRequester = await prisma.user.findFirst({ + where: { + id: taskDetailRequesterUserId, + isActive: true, + status: 'active', + }, + select: { + id: true, + username: true, + realName: true, + }, + }); + + if (!taskDetailRequester) { + return NextResponse.json( + { success: false, error: '所选需求运营人员不存在或已停用' }, + { status: 400 } + ); + } + // 如果提供了任务号,验证格式和唯一性 if (taskNo && taskNo.trim()) { // 验证任务号格式 @@ -90,6 +125,8 @@ export async function POST(request: NextRequest) { renderer: '', // 任务-详情不需要渲染器 isFullSet: false, taskDetailSourceType, + taskDetailRequesterUserId: taskDetailRequester.id, + taskDetailRequesterName: taskDetailRequester.realName || taskDetailRequester.username, } as any, }); diff --git a/app/api/materials/upload/route.ts b/app/api/materials/upload/route.ts index 7ce30bb..a861cd3 100644 --- a/app/api/materials/upload/route.ts +++ b/app/api/materials/upload/route.ts @@ -225,6 +225,14 @@ export async function POST(request: NextRequest) { const productCategoryName = (formData.get('productCategoryName') as string) || ''; const materialTypeRaw = formData.get('materialType') as string | null; const materialType = normalizeMaterialType(materialTypeRaw); + + if (materialType === '任务-详情') { + return NextResponse.json( + { error: '任务-详情必须使用专用上传入口,并选择需求运营人员' }, + { status: 400 } + ); + } + const isTaskOtherMaterial = materialType === '任务-其他'; const rendererParam = formData.get('renderer') as string | null; // 统一处理renderer:3D场景必须有renderer,其他类型可选 diff --git a/app/edit/[id]/page.tsx b/app/edit/[id]/page.tsx index bbf93a9..1cc2c8c 100644 --- a/app/edit/[id]/page.tsx +++ b/app/edit/[id]/page.tsx @@ -33,6 +33,8 @@ interface Material { taskStatus?: string | null taskFetchedAt?: string | null isFullSet?: boolean + taskDetailRequesterUserId?: string | null + taskDetailRequesterName?: string | null } export default function EditMaterialPage({ params }: { params: { id: string } }) { @@ -59,6 +61,8 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) const [taskStatus, setTaskStatus] = useState('') const [taskFetchedAt, setTaskFetchedAt] = useState('') const [isFullSet, setIsFullSet] = useState(false) + const [taskDetailRequesterUserId, setTaskDetailRequesterUserId] = useState('') + const [taskDetailRequesterName, setTaskDetailRequesterName] = useState('') const [categories, setCategories] = useState>([]) const [scenes, setScenes] = useState>([]) @@ -73,6 +77,8 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) const allowPreviewReplacement = canManuallyReplacePreview(materialType) const isFetchedTask = taskStatus === 'fetched' const materialTypeOptions = getSelectableMaterialTypeOptions(materialType) + const canKeepRequesterBlank = + material?.materialType === '任务-详情' && !material.taskDetailRequesterUserId const loadInitialData = useCallback(async () => { try { @@ -133,6 +139,8 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) setTaskStatus(data.taskStatus || '') setTaskFetchedAt(data.taskFetchedAt || '') setIsFullSet(Boolean(data.isFullSet)) + setTaskDetailRequesterUserId(data.taskDetailRequesterUserId || '') + setTaskDetailRequesterName(data.taskDetailRequesterName || '') if (categoriesResponse.ok) { setCategories(await categoriesResponse.json()) @@ -147,7 +155,19 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) setProductCategories(await productCategoriesResponse.json()) } if (usersResponse.ok) { - setUsers(await usersResponse.json()) + const activeUsers = await usersResponse.json() + const normalizedUsers = Array.isArray(activeUsers) ? activeUsers : [] + if ( + data.taskDetailRequesterUserId && + !normalizedUsers.some((user) => user.id === data.taskDetailRequesterUserId) + ) { + normalizedUsers.push({ + id: data.taskDetailRequesterUserId, + username: '已停用', + displayName: data.taskDetailRequesterName || '原需求运营', + }) + } + setUsers(normalizedUsers) } } catch (loadError: any) { console.error('加载编辑页失败:', loadError) @@ -211,6 +231,11 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) return } + if (materialType === '任务-详情' && !taskDetailRequesterUserId && !canKeepRequesterBlank) { + setError('请选择需求运营人员') + return + } + if (isFetchedTask) { const confirmed = window.confirm('这条任务素材已经被外部系统获取过,继续保存可能影响对方使用,确认继续吗?') if (!confirmed) { @@ -232,6 +257,10 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) formData.append('authorName', authorName) formData.append('userId', userId) formData.append('taskNo', showTaskNo ? taskNo : '') + formData.append( + 'taskDetailRequesterUserId', + materialType === '任务-详情' ? taskDetailRequesterUserId : '', + ) if (previewImage && allowPreviewReplacement) { formData.append('previewImage', previewImage) @@ -463,6 +492,32 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) )} + {materialType === '任务-详情' && ( +
+ + { + setTaskDetailRequesterUserId(nextUserId) + setTaskDetailRequesterName(displayName) + }} + placeholder="搜索发起本次出图需求的运营人员" + required={!canKeepRequesterBlank} + clearable={canKeepRequesterBlank} + /> +

+ {canKeepRequesterBlank + ? taskDetailRequesterName + ? `原记录为“${taskDetailRequesterName}”,对应账号已不可选;可保持原记录或重新选择。` + : '这是一条历史素材,原来没有需求运营数据,可暂时留空。' + : '记录发起本次出图需求的运营人员,与素材所有者可以不同。'} +

+
+ )} + {showIsFullSet && (