gongxiangp2/scripts/build-history-import-retry-files.ts
2026-05-07 16:37:22 +08:00

180 lines
5.1 KiB
TypeScript

import path from 'path'
import { promises as fs } from 'fs'
import type { HistoryImportRecord } from '@/lib/history-material-import'
type FailedResult = {
fingerprint: string
spu: string
sourcePath: string
guessedStore: string
materialId: string | null
status: string
reason: string | null
}
type QueueProgress = {
summary?: {
totalCandidates?: number
processed?: number
imported?: number
skipped?: number
failed?: number
}
updatedAt?: string
results?: FailedResult[]
}
function parseArgs() {
const args = process.argv.slice(2)
const getArg = (name: string) => {
const match = args.find((arg) => arg.startsWith(`--${name}=`))
return match ? match.slice(name.length + 3) : ''
}
const input = getArg('input')
const progress = getArg('progress')
const outputDir = getArg('output-dir')
if (!input) {
throw new Error('缺少 --input=全量历史导入JSON')
}
if (!progress) {
throw new Error('缺少 --progress=队列进度JSON')
}
return {
input,
progress,
outputDir: outputDir || path.dirname(progress),
}
}
function normalizeReason(reason: string | null | undefined) {
return String(reason || '')
.replace(/\s+/g, ' ')
.trim()
}
function classifyFailure(reason: string) {
if (reason.includes('打包超时')) {
return 'retry_timeout'
}
if (reason.includes('S3上传失败')) {
return 'retry_s3'
}
if (reason.includes('Input Buffer is empty')) {
return 'manual_empty_buffer'
}
if (reason.includes('corrupt header')) {
return 'manual_corrupt_image'
}
if (reason.includes('图片处理失败')) {
return 'manual_image_processing'
}
if (reason.includes('Foreign key constraint violated')) {
return 'manual_invalid_source'
}
return 'manual_other'
}
async function main() {
const options = parseArgs()
const [inputRaw, progressRaw] = await Promise.all([
fs.readFile(options.input, 'utf8'),
fs.readFile(options.progress, 'utf8'),
])
const inputRecords = JSON.parse(inputRaw) as HistoryImportRecord[]
const progress = JSON.parse(progressRaw) as QueueProgress
const failedResults = (progress.results || []).filter((item) => item.status === 'failed')
const recordByFingerprint = new Map(inputRecords.map((record) => [record.fingerprint, record]))
const retryableRecords: HistoryImportRecord[] = []
const manualReviewItems: Array<{
fingerprint: string
spu: string
sourcePath: string
guessedStore: string
failureType: string
reason: string
}> = []
const summary = new Map<string, number>()
for (const failed of failedResults) {
const reason = normalizeReason(failed.reason)
const failureType = classifyFailure(reason)
summary.set(failureType, (summary.get(failureType) || 0) + 1)
const sourceRecord = recordByFingerprint.get(failed.fingerprint)
if (!sourceRecord) {
manualReviewItems.push({
fingerprint: failed.fingerprint,
spu: failed.spu,
sourcePath: failed.sourcePath,
guessedStore: failed.guessedStore,
failureType: 'manual_missing_source_record',
reason: reason || '未在全量输入中找到对应记录',
})
continue
}
if (failureType === 'retry_timeout' || failureType === 'retry_s3') {
retryableRecords.push(sourceRecord)
continue
}
manualReviewItems.push({
fingerprint: failed.fingerprint,
spu: failed.spu,
sourcePath: failed.sourcePath,
guessedStore: failed.guessedStore,
failureType,
reason,
})
}
await fs.mkdir(options.outputDir, { recursive: true })
const retryablePath = path.join(options.outputDir, 'history-import-retryable-failures.json')
const manualReviewPath = path.join(options.outputDir, 'history-import-manual-review-failures.json')
const summaryPath = path.join(options.outputDir, 'history-import-failure-classification.json')
await Promise.all([
fs.writeFile(retryablePath, JSON.stringify(retryableRecords, null, 2), 'utf8'),
fs.writeFile(manualReviewPath, JSON.stringify(manualReviewItems, null, 2), 'utf8'),
fs.writeFile(
summaryPath,
JSON.stringify(
{
sourceInput: options.input,
sourceProgress: options.progress,
updatedAt: progress.updatedAt || null,
totals: {
failed: failedResults.length,
retryable: retryableRecords.length,
manualReview: manualReviewItems.length,
},
buckets: Object.fromEntries(Array.from(summary.entries()).sort((a, b) => b[1] - a[1])),
},
null,
2,
),
'utf8',
),
])
console.log('历史失败记录分流完成')
console.log(`- 失败总数: ${failedResults.length}`)
console.log(`- 可自动重试: ${retryableRecords.length}`)
console.log(`- 需要人工排查: ${manualReviewItems.length}`)
console.log(`- 可重试文件: ${retryablePath}`)
console.log(`- 人工排查文件: ${manualReviewPath}`)
console.log(`- 分类汇总文件: ${summaryPath}`)
}
main().catch((error) => {
console.error('历史失败记录分流失败:', error)
process.exit(1)
})