/** * 手动归档已处理的成本异常(日常由 data-retention 定时任务自动执行)。 * * 默认窗口与线上配置一致(60 天),可通过 --days 覆盖;open 行永远不归档。 * * 用法: * npm run cost-exceptions:archive -- --dry-run * npm run cost-exceptions:archive -- --days=90 * npm run cost-exceptions:archive -- --days=60 --batch-size=2000 * npm run cost-exceptions:archive -- --days=0 # 一次性清理:归档全部已处理行 */ import process from 'node:process' import { closeDb } from '../src/db/client.js' import { runtimeConfig } from '../src/config/runtime.js' import { archiveResolvedCostExceptions, countArchivableCostExceptions, } from '../src/services/cost-center/cost-exception-archive-service.js' const options = parseOptions(process.argv.slice(2)) const defaultDays = Number(runtimeConfig.retention?.costExceptionDays) || 60 // 0 表示不按时间过滤:一次性把已处理的历史异常全部归档(日常任务永远 >= 1 天)。 const retentionDays = normalizeInteger(options.days, defaultDays, 0, 3650) const batchSize = normalizeInteger(options.batchSize, 1000, 1, 10_000) const dryRun = options.dryRun === 'true' async function main() { const startedAt = Date.now() const archivable = await countArchivableCostExceptions(retentionDays) if (retentionDays === 0) { console.warn('[archive] days=0:一次性清理,将归档全部已处理异常(open 行不受影响)') } if (dryRun) { console.log( JSON.stringify( { dryRun: true, retentionDays, archivableRows: archivable, durationMs: Date.now() - startedAt, }, null, 2, ), ) return } const result = await archiveResolvedCostExceptions({ retentionDays, batchSize }) console.log( JSON.stringify( { dryRun: false, ...result, batchSize, durationMs: Date.now() - startedAt }, null, 2, ), ) if (result.remainingArchivableRows > 0) { console.error(`仍有 ${result.remainingArchivableRows} 行待归档;请重跑(幂等)。`) process.exitCode = 2 } } main() .catch((error) => { console.error(error instanceof Error ? error.message : error) process.exitCode = 1 }) .finally(async () => { await closeDb() }) function parseOptions(args: string[]) { const result: Record = {} for (const arg of args) { if (!arg.startsWith('--')) continue const body = arg.slice(2) const index = body.indexOf('=') const key = index === -1 ? body : body.slice(0, index) const value = index === -1 ? 'true' : body.slice(index + 1) result[key] = value result[toCamelCase(key)] = value } return result } function toCamelCase(value: string) { return value.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase()) } function normalizeInteger(value: unknown, fallback: number, min: number, max: number) { const parsed = Number(value) if (!Number.isInteger(parsed)) return fallback return Math.min(max, Math.max(min, parsed)) }