Files
order_site/apps/backend/scripts/rebuild-after-cutover.ts
T
yml2213 855f2efcff feat(admin): 履约渠道接口与断代运维脚本
- 新增 /admin/fulfillment 待分类、指定渠道、规则 CRUD、问题队列、管道触发
- backfill-fulfillment-channel:438k 行分批回填,幂等可续跑
- finance-cutover-v2:默认 dry-run,--execute 才清理派生数据,禁止重复执行
- rebuild-after-cutover:按断代日重跑三条管道并生成期初分录
2026-10-31 18:12:25 +08:00

112 lines
3.8 KiB
TypeScript

import process from 'node:process'
import { closeDb, query } from '../src/db/client.js'
import { assignPendingOrderItemChannels } from '../src/services/fulfillment/channel-assign-service.js'
import { normalizeOrderItemProducts } from '../src/services/cost-center/product-normalize-service.js'
import { syncChannelCostSnapshots } from '../src/services/cost-center/cost-snapshot-service.js'
import { rebuildChannelSalesDailyFacts } from '../src/services/finance/sales-facts-channel-service.js'
import { syncFinanceLedgerFromFacts } from '../src/services/finance/finance-ledger-service.js'
import { formatShanghaiDate } from '../src/utils/finance-date-range.js'
/**
* 断代后重建:从 cutover 日起重跑三条管道,生成成本快照、销售事实与期初分录。
*
* 幂等:渠道判定/商品归一/成本快照都按「缺什么补什么」推进,事实按日先删后插,
* 总账按「event_type + 日期」幂等。可以反复执行。
*
* 用法:
* npm run finance:rebuild-cutover -- --date-from=2026-10-31 --date-to=2026-10-31
* npm run finance:rebuild-cutover -- --max-batches=200
*/
const options = parseOptions(process.argv.slice(2))
const dateFrom = String(options.dateFrom || '2026-10-31').slice(0, 10)
const dateTo = String(options.dateTo || formatShanghaiDate(new Date())).slice(0, 10)
const maxBatches = clampInt(options.maxBatches, 200, 1, 2000)
const batchSize = clampInt(options.batchSize, 2000, 1, 5000)
async function main() {
await assertSchema()
const startedAt = Date.now()
const channel = await assignPendingOrderItemChannels({ batchSize, maxBatches })
const product = await normalizeOrderItemProducts({ batchSize, maxBatches })
const cost = await syncChannelCostSnapshots({ batchSize, maxBatches })
const facts = await rebuildChannelSalesDailyFacts({ dateFrom, dateTo })
const ledger = await syncFinanceLedgerFromFacts({ dateFrom, dateTo })
const summary = await query<{
assigned: number
pending: number
archived: number
facts: number
snapshots: number
}>(
`
SELECT
COUNT(*) FILTER (WHERE fulfillment_status = 'assigned')::int AS assigned,
COUNT(*) FILTER (WHERE fulfillment_status = 'pending')::int AS pending,
COUNT(*) FILTER (WHERE fulfillment_status = 'archived')::int AS archived,
(SELECT COUNT(*)::int FROM sales_daily_facts) AS facts,
(SELECT COUNT(*)::int FROM cost_snapshots WHERE superseded_at IS NULL) AS snapshots
FROM order_items
`,
)
console.log(
JSON.stringify(
{
dateFrom,
dateTo,
channel,
product,
cost,
facts,
ledger,
totals: summary.rows[0],
durationMs: Date.now() - startedAt,
},
null,
2,
),
)
}
main()
.catch((error) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
.finally(async () => {
await closeDb()
})
async function assertSchema() {
const ready = await query<{ ready: boolean }>(
`SELECT to_regclass('public.sku_channel_rules') IS NOT NULL AS ready`,
)
if (!ready.rows[0]?.ready) {
throw new Error('缺少 112 迁移产物;请先执行 npm run db:migrate')
}
}
function parseOptions(args: string[]) {
const result: Record<string, string> = {}
for (const arg of args) {
if (!arg.startsWith('--')) continue
const body = arg.slice(2)
const index = body.indexOf('=')
const rawKey = index === -1 ? body : body.slice(0, index)
const value = index === -1 ? 'true' : body.slice(index + 1)
result[rawKey] = value
result[rawKey.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase())] = value
}
return result
}
function clampInt(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))
}