Files
incudal/server/scripts/fix-migrated-instance-billing.ts
2026-06-20 14:26:29 +08:00

123 lines
3.6 KiB
TypeScript

import 'dotenv/config'
import { prisma } from '../src/db/prisma.js'
import { getInstanceBillingLineageIds, reassignInstanceBillingRecords } from '../src/db/billing-records.js'
type MigrationMessageData = {
instanceId?: unknown
}
function isInteger(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value)
}
async function countBillingRecords(instanceId: number): Promise<number> {
return prisma.instanceBillingRecord.count({
where: { instanceId }
})
}
async function main(): Promise<void> {
const apply = process.argv.includes('--apply')
const limitArg = process.argv.find(arg => arg.startsWith('--limit='))
const limit = limitArg ? Number(limitArg.split('=')[1]) : null
if (limitArg && (!Number.isInteger(limit) || Number(limit) <= 0)) {
throw new Error('参数 --limit 必须是正整数')
}
const migrationMessages = await prisma.inboxMessage.findMany({
where: {
eventType: 'instance_migrated'
},
orderBy: { createdAt: 'asc' },
...(limit ? { take: limit } : {})
})
let scanned = 0
let eligible = 0
let movedRecords = 0
let unresolved = 0
let skipped = 0
const processedTargetIds = new Set<number>()
console.log(`[FixMigratedInstanceBilling] mode=${apply ? 'apply' : 'dry-run'}, messages=${migrationMessages.length}`)
for (const message of migrationMessages) {
scanned += 1
const data = (message.data || {}) as MigrationMessageData
if (!isInteger(data.instanceId)) {
skipped += 1
console.log(`[Skip] inboxMessage=${message.id} 原始数据缺失 newInstanceId`)
continue
}
if (processedTargetIds.has(data.instanceId)) {
skipped += 1
console.log(`[Skip] inboxMessage=${message.id} 目标实例 ${data.instanceId} 已处理过`)
continue
}
const newInstance = await prisma.instance.findFirst({
where: {
id: data.instanceId,
userId: message.userId
},
select: { id: true }
})
if (!newInstance) {
skipped += 1
console.log(`[Skip] inboxMessage=${message.id} 找不到目标实例 newInstanceId=${data.instanceId}`)
continue
}
processedTargetIds.add(newInstance.id)
const lineage = await getInstanceBillingLineageIds(newInstance.id)
const sourceInstanceIds = lineage.slice(0, -1)
if (sourceInstanceIds.length === 0) {
unresolved += 1
console.log(`[Unresolved] inboxMessage=${message.id} newInstance=${newInstance.id} 未解析到历史实例`)
continue
}
const sourceSummaries = await Promise.all(sourceInstanceIds.map(async sourceInstanceId => ({
sourceInstanceId,
billingRecordCount: await countBillingRecords(sourceInstanceId)
})))
eligible += 1
console.log(
`[Match] inboxMessage=${message.id} lineage=${lineage.join(' -> ')} ` +
`sources=${sourceSummaries.map(item => `${item.sourceInstanceId}(records=${item.billingRecordCount})`).join(', ')}`
)
if (!apply) {
continue
}
const moved = await prisma.$transaction(async tx => {
let totalMoved = 0
for (const sourceInstanceId of sourceInstanceIds) {
totalMoved += await reassignInstanceBillingRecords(sourceInstanceId, newInstance.id, tx)
}
return totalMoved
})
movedRecords += moved
}
console.log(`[Summary] scanned=${scanned}, eligible=${eligible}, movedRecords=${movedRecords}, unresolved=${unresolved}, skipped=${skipped}`)
await prisma.$disconnect()
}
void main().catch(async error => {
console.error('[FixMigratedInstanceBilling] failed:', error)
await prisma.$disconnect()
process.exit(1)
})