249 lines
7.8 KiB
TypeScript
249 lines
7.8 KiB
TypeScript
/**
|
||
* 媒体迁移校验脚本
|
||
*
|
||
* 校验内容:
|
||
* 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)
|
||
})
|
||
|
||
|