196 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 任务-详情上传完成接口(改造版)
* POST - 打包文件、生成预览图、保存资产信息
*/
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import {
parsePublicProductPackageAssets,
} from '@/lib/product-package-completion'
import {
createTaskDetailUploadJob,
getLatestTaskDetailUploadJobStatus,
} from '@/lib/task-detail-upload-jobs'
import { normalizeTaskDetailSourceType } from '@/lib/task-detail-source-type'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export const maxDuration = 60
interface CompleteRequestBody {
materialId: string
taskNo?: string
dingtalkApprovalNo?: string
noBarcodes?: boolean
allowServerPreviewFallback?: boolean
assets: unknown
}
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user || !(session.user as any).id) {
return NextResponse.json(
{ success: false, error: '未登录' },
{ status: 401 },
)
}
const body: CompleteRequestBody = await request.json()
let assets
try {
assets = parsePublicProductPackageAssets(body.assets)
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : '资产参数无效',
},
{ status: 400 },
)
}
const material = await prisma.material.findUnique({
where: { id: body.materialId },
select: {
id: true,
userId: true,
materialType: true,
taskDetailRequesterUserId: true,
taskDetailSourceType: true,
zipFilePath: true,
_count: { select: { assets: true } },
},
})
if (!material) {
return NextResponse.json(
{ success: false, error: '素材不存在' },
{ status: 404 },
)
}
if (material.userId !== (session.user as any).id) {
return NextResponse.json(
{ success: false, error: '无权限操作此素材' },
{ status: 403 },
)
}
if (material.materialType !== '任务-详情') {
return NextResponse.json(
{ success: false, error: '当前素材不是任务-详情,不能使用此完成接口' },
{ status: 400 },
)
}
if (!material.taskDetailRequesterUserId) {
return NextResponse.json(
{ success: false, error: '当前素材缺少需求运营人员,不能完成上传' },
{ status: 400 },
)
}
if (!normalizeTaskDetailSourceType(material.taskDetailSourceType)) {
return NextResponse.json(
{ success: false, error: '任务-详情必须选择出图方式(实拍、渲染或 AI)' },
{ status: 400 },
)
}
// 初始化素材的 zip 路径必须为空。已有路径说明上一次后台处理已经写入了
// 部分持久化结果,禁止再次入队覆盖,避免重复资产和悬空文件。
if (material.zipFilePath || material._count?.assets > 0) {
return NextResponse.json(
{ success: false, error: '该素材已有后台处理结果,请刷新素材状态后再操作' },
{ status: 409 },
)
}
const existingJob = await getLatestTaskDetailUploadJobStatus(
body.materialId,
(session.user as any).id,
)
if (existingJob && ['queued', 'processing', 'completed'].includes(existingJob.status)) {
return NextResponse.json({
success: true,
data: {
jobId: existingJob.jobId,
materialId: existingJob.materialId,
status: existingJob.status,
stage: existingJob.stage,
progress: existingJob.progress,
message: existingJob.status === 'completed'
? '后台处理已完成'
: '文件已接收,正在后台处理',
},
}, { status: 202 })
}
const imageAssets = [
...(assets.main || []),
...(assets.sku || []),
...(assets.detail || []),
]
if (imageAssets.length === 0) {
return NextResponse.json(
{ success: false, error: '请至少上传一张图片' },
{ status: 400 },
)
}
const missingClientPreviewCount = imageAssets.filter(
(asset) => !Array.isArray(asset.previewAssets) || asset.previewAssets.length === 0,
).length
if (missingClientPreviewCount > 0) {
if (body.allowServerPreviewFallback !== true) {
return NextResponse.json(
{ success: false, error: '客户端预览图缺失,请重新处理图片后再提交' },
{ status: 400 },
)
}
// 浏览器预处理属于性能优化,不应成为提交门槛。Edge 的 worker、图片编解码
// 或超大 EXIF 头部出现兼容性问题时,保留原图并交给后台唯一 worker 生成预览;
// 该 worker 仍受像素、文件大小和全局并发限制保护。
console.warn('[Product Package Complete] 客户端预览缺失,转服务器兜底处理', {
materialId: body.materialId,
count: missingClientPreviewCount,
})
}
const job = await createTaskDetailUploadJob({
materialId: body.materialId,
taskNo: body.taskNo,
dingtalkApprovalNo: body.dingtalkApprovalNo,
assets,
operatorUserId: (session.user as any).id,
options: {
allowSkuWithoutBarcodes: body.noBarcodes === true,
},
})
return NextResponse.json({
success: true,
data: {
jobId: job.jobId,
materialId: job.materialId,
status: job.status,
stage: job.stage,
progress: job.progress,
message: '文件已接收,正在后台处理',
},
}, { status: 202 })
} catch (error) {
console.error('[Product Package Complete] Error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : '服务器错误',
},
{ status: 500 },
)
}
}