/** * 一次性全量视频重转码脚本(容器内执行) * - 直接复用 /api/materials/transcode 队列接口,无需登录 * - 自动兼容缺失 zipFilePath / deletedAt 列的旧库 * - 逐个排队并输出进度,避免误以为卡住 * * 使用方式(宿主机运行): * docker cp scripts/retranscode-once.js youyidian-3d-material:/app/ * docker exec -it youyidian-3d-material node /app/retranscode-once.js */ const { PrismaClient } = require('/app/node_modules/@prisma/client') const prisma = new PrismaClient() const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) async function hasColumn(column) { try { const rows = await prisma.$queryRaw` SELECT column_name FROM information_schema.columns WHERE table_name = 'materials' AND column_name = ${column} ` return rows.length > 0 } catch (e) { console.warn('[retranscode] column check failed:', column, e?.message || e) return false } } async function main() { console.log('========== 全量重转码开始 ==========') const hasZipFilePath = await hasColumn('zipFilePath') const hasDeletedAt = await hasColumn('deletedAt') console.log('列检测 => zipFilePath:', hasZipFilePath, 'deletedAt:', hasDeletedAt) // 使用最兼容的条件:仅基于 videoSourceUrl,避免 Prisma 客户端缺少字段类型时报错 const andConditions = [ { materialType: { in: ['主图视频', '安装视频', '种草视频', '视频', 'VIDEO'] } }, { videoSourceUrl: { not: null } }, ] if (hasDeletedAt) { andConditions.push({ deletedAt: null }) } console.log('[retranscode] 查询候选素材...') const candidates = await prisma.material.findMany({ where: { AND: andConditions }, select: { id: true, productModel: true, createdAt: true }, orderBy: { createdAt: 'asc' }, }) console.log(`[retranscode] 匹配到 ${candidates.length} 条素材,开始逐个入队...`) let baseUrl = process.env.NEXTAUTH_URL || process.env.APP_PUBLIC_URL || 'http://127.0.0.1:3000' // 避免 localhost 解析到 IPv6(::1) 导致连接失败 baseUrl = baseUrl.replace('localhost', '127.0.0.1').replace(/\/$/, '') console.log('[retranscode] Base URL =', baseUrl) let success = 0 let failed = 0 for (let i = 0; i < candidates.length; i++) { const m = candidates[i] const idx = i + 1 const prefix = `[${idx}/${candidates.length}] ${m.id} (${m.productModel || '无型号'})` try { process.stdout.write(`${prefix} -> 入队中... `) const res = await fetch(`${baseUrl}/api/materials/transcode`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ materialId: m.id, forceRetry: true }), }) if (!res.ok) { const text = await res.text().catch(() => '') console.log(`失败 ${res.status} ${text}`) failed++ } else { console.log('成功') success++ } } catch (e) { console.log(`异常 ${(e && e.message) || e}`) if (e && e.stack) { console.log(String(e.stack).split('\n')[0]) } failed++ } // 节流,避免请求过快 if (idx % 10 === 0) { await sleep(100) } } console.log('========== 全量重转码完成 ==========') console.log(`总计: ${candidates.length},入队成功: ${success},失败: ${failed}`) await prisma.$disconnect() } main().catch(async (e) => { console.error('运行出错:', e) await prisma.$disconnect() process.exit(1) })