153 lines
5.6 KiB
TypeScript
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()
|
|
})
|