149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
import 'dotenv/config'
|
|
import crypto from 'node:crypto'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
const CONFIRM_TOKEN = 'backfill-sku-structural-data'
|
|
|
|
function readOption(name: string): string {
|
|
const prefix = `${name}=`
|
|
const inline = process.argv.find((arg) => arg.startsWith(prefix))
|
|
if (inline) return inline.slice(prefix.length)
|
|
const index = process.argv.indexOf(name)
|
|
if (index === -1) return ''
|
|
const value = process.argv[index + 1] || ''
|
|
return value.startsWith('--') ? '' : value
|
|
}
|
|
|
|
function databaseIdentity(databaseUrl: string): string {
|
|
const parsed = new URL(databaseUrl)
|
|
return [
|
|
parsed.protocol.replace(/:$/, ''),
|
|
decodeURIComponent(parsed.username || ''),
|
|
parsed.hostname,
|
|
parsed.port || '',
|
|
parsed.pathname.replace(/^\//, ''),
|
|
parsed.searchParams.get('schema') || '',
|
|
].join('|')
|
|
}
|
|
|
|
function backupSha256(): string {
|
|
const backupPath = process.env.DATABASE_BACKUP_FILE || process.env.DB_BACKUP_FILE || ''
|
|
if (!backupPath) return ''
|
|
const absolutePath = path.resolve(process.cwd(), backupPath)
|
|
if (!fs.existsSync(absolutePath)) return ''
|
|
return crypto.createHash('sha256').update(fs.readFileSync(absolutePath)).digest('hex')
|
|
}
|
|
|
|
function assertApplyGuards() {
|
|
if (!process.argv.includes('--apply')) return
|
|
|
|
if (readOption('--confirm') !== CONFIRM_TOKEN) {
|
|
throw new Error(`SKU 结构数据回填真实写库必须显式传入 --confirm=${CONFIRM_TOKEN}。`)
|
|
}
|
|
const databaseUrl = process.env.DATABASE_URL || ''
|
|
if (!databaseUrl) {
|
|
throw new Error('SKU 结构数据回填真实写库必须先配置 DATABASE_URL。')
|
|
}
|
|
const expectedDatabaseIdentity = readOption('--expected-database-identity')
|
|
if (!expectedDatabaseIdentity) {
|
|
throw new Error('SKU 结构数据回填真实写库必须显式传入 --expected-database-identity。')
|
|
}
|
|
if (expectedDatabaseIdentity !== databaseIdentity(databaseUrl)) {
|
|
throw new Error('SKU 结构数据回填目标库身份和当前 DATABASE_URL 不一致,禁止写库。')
|
|
}
|
|
const expectedBackupSha256 = readOption('--expected-backup-sha256')
|
|
if (!expectedBackupSha256) {
|
|
throw new Error('SKU 结构数据回填真实写库必须显式传入 --expected-backup-sha256。')
|
|
}
|
|
if (expectedBackupSha256 !== backupSha256()) {
|
|
throw new Error('SKU 结构数据回填备份 sha256 和当前 DATABASE_BACKUP_FILE 不一致,禁止写库。')
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
assertApplyGuards()
|
|
const apply = process.argv.includes('--apply')
|
|
const [{ prisma }, { readSkuCadDataFromAttributes }, { ensureSkuStructuralData }] =
|
|
await Promise.all([
|
|
import('../src/lib/prisma'),
|
|
import('../src/lib/sku-cad-data'),
|
|
import('../src/server/services/sku-cad-import.helper'),
|
|
])
|
|
const skus = await prisma.productSKU.findMany({
|
|
where: { deletedAt: null },
|
|
select: {
|
|
id: true,
|
|
productId: true,
|
|
skuCode: true,
|
|
attributes: true,
|
|
},
|
|
orderBy: { createdAt: 'asc' },
|
|
})
|
|
|
|
let scanned = 0
|
|
let candidates = 0
|
|
let filled = 0
|
|
let skipped = 0
|
|
|
|
try {
|
|
for (const sku of skus) {
|
|
scanned += 1
|
|
const cadData = readSkuCadDataFromAttributes(
|
|
(sku.attributes ?? null) as Record<string, unknown> | null
|
|
)
|
|
|
|
if (!cadData) {
|
|
skipped += 1
|
|
continue
|
|
}
|
|
|
|
const existingPanelsCount = await prisma.panel.count({
|
|
where: { skuId: sku.id },
|
|
})
|
|
|
|
if (existingPanelsCount > 0) {
|
|
skipped += 1
|
|
continue
|
|
}
|
|
|
|
candidates += 1
|
|
if (!apply) {
|
|
console.log(`[backfill] dry-run candidate ${sku.skuCode}`)
|
|
continue
|
|
}
|
|
|
|
await ensureSkuStructuralData({
|
|
productId: sku.productId,
|
|
skuId: sku.id,
|
|
cadSourceData: cadData,
|
|
})
|
|
|
|
filled += 1
|
|
console.log(`[backfill] filled ${sku.skuCode}`)
|
|
}
|
|
} finally {
|
|
await prisma.$disconnect()
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
apply,
|
|
scanned,
|
|
candidates,
|
|
filled,
|
|
skipped,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
}
|
|
|
|
main()
|
|
.catch((error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|