- 新增 /admin/fulfillment 待分类、指定渠道、规则 CRUD、问题队列、管道触发 - backfill-fulfillment-channel:438k 行分批回填,幂等可续跑 - finance-cutover-v2:默认 dry-run,--execute 才清理派生数据,禁止重复执行 - rebuild-after-cutover:按断代日重跑三条管道并生成期初分录
112 lines
3.8 KiB
TypeScript
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))
|
|
}
|