gongxiangp2/scripts/performance-survey-test-data.ts
2026-07-23 09:25:50 +08:00

153 lines
5.6 KiB
TypeScript

import { createHash } from 'crypto'
import { mkdir, readFile, writeFile } from 'fs/promises'
import path from 'path'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const mode = readArg('mode') || 'audit'
const outputPath = readArg('output') || path.join(
'.performance-survey-audits',
`test-surveys-${new Date().toISOString().replace(/[:.]/g, '-')}.json`,
)
const planPath = readArg('plan')
const confirmed = process.argv.includes('--confirm-all-current-surveys-are-test-data')
const expectedSurveyCount = Number(readArg('expected-survey-count'))
function readArg(name: string) {
const prefix = `--${name}=`
const inline = process.argv.find((item) => item.startsWith(prefix))
if (inline) return inline.slice(prefix.length)
const index = process.argv.indexOf(`--${name}`)
return index >= 0 ? process.argv[index + 1] || '' : ''
}
function hashIds(ids: string[]) {
return createHash('sha256').update([...ids].sort().join('\n')).digest('hex')
}
function getDatabaseIdentity() {
const raw = process.env.DATABASE_URL
if (!raw) throw new Error('缺少 DATABASE_URL')
const url = new URL(raw)
return {
provider: url.protocol.replace(':', ''),
host: url.hostname,
port: url.port || '5432',
database: url.pathname.replace(/^\//, ''),
schema: url.searchParams.get('schema') || 'public',
}
}
async function collectSnapshot() {
const surveys = await prisma.performanceSurvey.findMany({
orderBy: { createdAt: 'asc' },
select: {
id: true,
title: true,
templateCode: true,
status: true,
createdAt: true,
_count: {
select: {
targets: true,
assignments: true,
responses: true,
workItems: true,
workItemScores: true,
notifications: true,
},
},
},
})
const surveyIds = surveys.map((survey) => survey.id)
const totals = surveys.reduce((result, survey) => {
result.targets += survey._count.targets
result.assignments += survey._count.assignments
result.responses += survey._count.responses
result.workItems += survey._count.workItems
result.workItemScores += survey._count.workItemScores
result.notifications += survey._count.notifications
return result
}, { targets: 0, assignments: 0, responses: 0, workItems: 0, workItemScores: 0, notifications: 0 })
return {
database: getDatabaseIdentity(),
surveyIds,
surveyIdsHash: hashIds(surveyIds),
counts: { surveys: surveys.length, ...totals },
surveys: surveys.map((survey) => ({
id: survey.id,
title: survey.title,
templateCode: survey.templateCode,
status: survey.status,
createdAt: survey.createdAt.toISOString(),
counts: survey._count,
})),
}
}
async function audit() {
const snapshot = await collectSnapshot()
const manifest = {
manifestVersion: 1,
purpose: 'performance-survey-test-data-clearance',
createdAt: new Date().toISOString(),
...snapshot,
}
const absoluteOutput = path.resolve(outputPath)
await mkdir(path.dirname(absoluteOutput), { recursive: true })
await writeFile(absoluteOutput, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
console.log(JSON.stringify({ output: absoluteOutput, database: manifest.database, counts: manifest.counts }, null, 2))
}
async function clear() {
if (!planPath) throw new Error('清理模式必须传入 --plan <审计清单>')
if (!confirmed) throw new Error('清理模式必须显式传入 --confirm-all-current-surveys-are-test-data')
if (!Number.isInteger(expectedSurveyCount) || expectedSurveyCount < 0) {
throw new Error('清理模式必须传入 --expected-survey-count <审计确认的调研数量>')
}
const manifest = JSON.parse(await readFile(path.resolve(planPath), 'utf8')) as {
manifestVersion: number
purpose: string
database: ReturnType<typeof getDatabaseIdentity>
surveyIds: string[]
surveyIdsHash: string
counts: { surveys: number }
}
if (manifest.manifestVersion !== 1 || manifest.purpose !== 'performance-survey-test-data-clearance') {
throw new Error('审计清单类型或版本无效')
}
if (JSON.stringify(manifest.database) !== JSON.stringify(getDatabaseIdentity())) {
throw new Error('审计清单数据库身份与当前连接不一致,已拒绝清理')
}
if (hashIds(manifest.surveyIds) !== manifest.surveyIdsHash) throw new Error('审计清单校验失败')
if (manifest.counts.surveys !== expectedSurveyCount) {
throw new Error(`审计清单包含 ${manifest.counts.surveys} 条调研,与明确确认数量 ${expectedSurveyCount} 不一致`)
}
const current = await collectSnapshot()
if (current.surveyIdsHash !== manifest.surveyIdsHash || current.counts.surveys !== manifest.counts.surveys) {
throw new Error('审计后调研数据已经变化,请重新生成审计清单')
}
const result = await prisma.performanceSurvey.deleteMany({ where: { id: { in: manifest.surveyIds } } })
const remaining = await prisma.performanceSurvey.count()
if (result.count !== manifest.surveyIds.length || remaining !== 0) {
throw new Error(`清理结果不一致:计划 ${manifest.surveyIds.length},已删除 ${result.count},剩余 ${remaining}`)
}
console.log(JSON.stringify({ deletedSurveys: result.count, remainingSurveys: remaining }, null, 2))
}
async function main() {
if (mode === 'audit') return audit()
if (mode === 'clear') return clear()
throw new Error(`未知模式:${mode}`)
}
main()
.catch((error) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
.finally(async () => {
await prisma.$disconnect()
})