gongxiangp2/scripts/verify-media-migration.ts

249 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

/**
* 媒体迁移校验脚本
*
* 校验内容:
* 1. 视频类素材(主图视频 / 安装视频 / 种草视频):
* - 不应再有 videoSourceUrl 指向 S3_PUBLIC_DOMAIN(老 MinIO URL)
* - videoSourceUrl 如为 /uploads/video-source-*.mp4,则对应的本地文件必须存在
*
* 2. 音乐类素材:
* - 不应再有 zipFileName 指向 S3_PUBLIC_DOMAIN(老 MinIO URL)
* - zipFileName 如为 audio-source-*.mp3,则对应的本地文件必须存在
*
* 说明:
* - 不会修改任何数据,只做读取 + 文件存在性检查
* - 结果打印到控制台,可用于人工核对
*/
import { PrismaClient } from '@prisma/client'
import { access } from 'fs/promises'
import { constants as fsConstants } from 'fs'
import path from 'path'
const prisma = new PrismaClient()
const S3_PUBLIC_DOMAIN = process.env.S3_PUBLIC_DOMAIN || ''
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'
const VIDEO_TYPES = ['主图视频', '安装视频', '种草视频']
async function fileExists(fullPath: string): Promise<boolean> {
try {
await access(fullPath, fsConstants.F_OK)
return true
} catch {
return false
}
}
async function verifyVideos() {
console.log('==== 校验视频类素材 ====')
// 0) 先整体统计一下视频类素材的数量分布
const allVideos = await prisma.material.findMany({
where: {
deletedAt: null,
materialType: { in: VIDEO_TYPES } as any,
},
select: {
id: true,
materialType: true,
videoSourceUrl: true,
},
})
const total = allVideos.length
const withSource = allVideos.filter((m) => !!m.videoSourceUrl)
const withoutSource = allVideos.filter((m) => !m.videoSourceUrl)
const localPattern = withSource.filter((m) => (m.videoSourceUrl as any as string).startsWith('/uploads/video-source-'))
const otherPattern = withSource.filter(
(m) => !!m.videoSourceUrl && !(m.videoSourceUrl as any as string).startsWith('/uploads/video-source-')
)
console.log(
`视频素材总数:${total} 条,其中有 videoSourceUrl 的 ${withSource.length} 条,无 videoSourceUrl 的 ${withoutSource.length} 条。`
)
console.log(` 其中 videoSourceUrl 以 /uploads/video-source- 开头的有 ${localPattern.length} 条,其它形式的有 ${otherPattern.length} 条。`)
if (otherPattern.length > 0) {
console.log(' 👉 示例(最多列出前 20 条其它形式的 videoSourceUrl):')
otherPattern.slice(0, 20).forEach((m) => {
console.log(` - [${m.id}] ${m.materialType} => ${m.videoSourceUrl}`)
})
}
// 1) 是否还有 videoSourceUrl 指向 S3
if (!S3_PUBLIC_DOMAIN) {
console.warn('⚠️ 未配置 S3_PUBLIC_DOMAIN,无法精确检查 S3 URL 残留')
} else {
const s3Host = S3_PUBLIC_DOMAIN.replace(/^https?:\/\//, '')
const legacyVideos = await prisma.material.findMany({
where: {
deletedAt: null,
materialType: { in: VIDEO_TYPES } as any,
videoSourceUrl: {
not: null,
contains: s3Host,
} as any,
},
select: {
id: true,
materialType: true,
videoSourceUrl: true,
},
})
if (legacyVideos.length === 0) {
console.log('✅ 无残留 S3 视频源 URL(videoSourceUrl 不再包含 S3_PUBLIC_DOMAIN)')
} else {
console.log(`❌ 仍有 ${legacyVideos.length} 条视频素材的 videoSourceUrl 指向 S3:`)
legacyVideos.forEach((m) => {
console.log(` - [${m.id}] ${m.materialType} => ${m.videoSourceUrl}`)
})
}
}
// 2) 校验 /uploads/video-source-*.mp4 对应本地文件存在
const localVideos = localPattern
if (localVideos.length === 0) {
console.log('ℹ️ 未找到任何 videoSourceUrl 为 /uploads/video-source-* 的记录(可能历史上没有需要迁移的视频)')
return
}
console.log(`共找到 ${localVideos.length} 条指向本地 /uploads/video-source-* 的视频素材,逐条检查本地文件存在性:`)
let okCount = 0
let missingCount = 0
for (const m of localVideos) {
const rel = m.videoSourceUrl || ''
const fileName = rel.startsWith('/uploads/') ? rel.replace('/uploads/', '') : rel
const fullPath = path.isAbsolute(UPLOAD_DIR)
? path.join(UPLOAD_DIR, fileName)
: path.join(process.cwd(), UPLOAD_DIR, fileName)
const exists = await fileExists(fullPath)
if (exists) {
okCount++
console.log(` ✅ [${m.id}] ${m.materialType} -> ${rel} (${fullPath}) 存在`)
} else {
missingCount++
console.log(` ❌ [${m.id}] ${m.materialType} -> ${rel} (${fullPath}) 不存在`)
}
}
console.log(`视频本地文件校验结果:存在 ${okCount} 条,缺失 ${missingCount} 条`)
}
async function verifyAudios() {
console.log('\n==== 校验音乐类素材 ====')
// 1) 是否还有 zipFileName 指向 S3
if (!S3_PUBLIC_DOMAIN) {
console.warn('⚠️ 未配置 S3_PUBLIC_DOMAIN,无法精确检查 S3 URL 残留')
} else {
const s3Host = S3_PUBLIC_DOMAIN.replace(/^https?:\/\//, '')
// 先查出所有音乐素材,在内存中过滤 zipFileName 中是否包含 S3 域名
const allAudios = await prisma.material.findMany({
where: {
deletedAt: null,
materialType: '音乐' as any,
},
select: {
id: true,
zipFileName: true,
},
})
const legacyAudios = allAudios.filter(
(m) => !!m.zipFileName && (m.zipFileName as any as string).includes(s3Host)
)
if (legacyAudios.length === 0) {
console.log('✅ 无残留 S3 音乐源 URL(zipFileName 不再包含 S3_PUBLIC_DOMAIN)')
} else {
console.log(`❌ 仍有 ${legacyAudios.length} 条音乐素材的 zipFileName 指向 S3:`)
legacyAudios.forEach((m) => {
console.log(` - [${m.id}] => ${m.zipFileName}`)
})
}
}
// 2) 校验 zipFileName 为 audio-source-* 的本地文件存在
const localAudios = await prisma.material.findMany({
where: {
deletedAt: null,
materialType: '音乐' as any,
zipFileName: {
startsWith: 'audio-source-',
} as any,
},
select: {
id: true,
zipFileName: true,
zipFilePath: true,
},
})
if (localAudios.length === 0) {
console.log('ℹ️ 未找到任何 zipFileName 为 audio-source-* 的音乐记录(可能历史上没有需要迁移的音乐)')
return
}
console.log(`共找到 ${localAudios.length} 条指向本地 audio-source-* 的音乐素材,逐条检查本地文件存在性:`)
let okCount = 0
let missingCount = 0
for (const m of localAudios) {
const fileName = m.zipFileName || ''
const preferredPath = m.zipFilePath && m.zipFilePath.trim().length > 0 ? m.zipFilePath : ''
let candidates: string[] = []
if (preferredPath) {
candidates.push(preferredPath)
}
// 兼容相对/绝对 UPLOAD_DIR
const fromUploadDir = path.isAbsolute(UPLOAD_DIR)
? path.join(UPLOAD_DIR, fileName)
: path.join(process.cwd(), UPLOAD_DIR, fileName)
candidates.push(fromUploadDir)
let exists = false
let usedPath = ''
for (const p of candidates) {
if (!p) continue
if (await fileExists(p)) {
exists = true
usedPath = p
break
}
}
if (exists) {
okCount++
console.log(` ✅ [${m.id}] ${fileName} -> ${usedPath} 存在`)
} else {
missingCount++
console.log(` ❌ [${m.id}] ${fileName} 对应的本地文件不存在(候选路径:${candidates.join(' , ')})`)
}
}
console.log(`音乐本地文件校验结果:存在 ${okCount} 条,缺失 ${missingCount} 条`)
}
async function main() {
try {
await verifyVideos()
await verifyAudios()
} finally {
await prisma.$disconnect()
}
}
main().catch((err) => {
console.error('❌ 校验脚本执行出错:', err)
process.exit(1)
})