refactor(finance)!: 强制渠道模型,移除旧履约方式与同步链路
- 删除 fulfillment_mode 相关的旧同步/回填链路与定时任务 - 删除 cost_finance_sync、成本快照同步、人工商品归一等遗留服务 - 迁移 113 直接删除 order_items.fulfillment_mode 及其索引约束 - 断代脚本自带归档步骤,不再依赖独立回填脚本 - 退款/事实/总账统一走渠道版销售事实
This commit is contained in:
@@ -16,9 +16,10 @@ import { closeDb, query } from '../src/db/client.js'
|
||||
* sku_channel_rules(配置)
|
||||
*
|
||||
* 前置:
|
||||
* 1. 迁移 112 已执行;
|
||||
* 2. `npm run fulfillment:backfill-channel` 已完成(不允许有 assigned 但无渠道的行);
|
||||
* 3. 已做数据库备份。
|
||||
* 1. 迁移 112/113 已执行;
|
||||
* 2. 已做数据库备份。
|
||||
*
|
||||
* 断代本身包含两步:先把断代前的订单项批量置为 archived,再清理派生数据。
|
||||
*
|
||||
* 默认 dry-run,只打印将删除的行数;加 `--execute --confirm=2026-10-31` 才真正执行。
|
||||
* 不可重复执行:成功后会写入 finance_cutovers,第二次会被拦下。
|
||||
@@ -77,6 +78,8 @@ async function main() {
|
||||
|
||||
await assertNotExecuted()
|
||||
|
||||
const archived = await archiveHistoricalItems()
|
||||
|
||||
const deleted: Record<string, number> = {}
|
||||
for (const item of [...DERIVED_TABLES].sort((a, b) => a.order - b.order)) {
|
||||
const result = await query(
|
||||
@@ -110,7 +113,13 @@ async function main() {
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ mode: 'execute', cutoverDate: CUTOVER_DATE, deleted, durationMs: Date.now() - startedAt },
|
||||
{
|
||||
mode: 'execute',
|
||||
cutoverDate: CUTOVER_DATE,
|
||||
archivedItems: archived.updated,
|
||||
deleted,
|
||||
durationMs: Date.now() - startedAt,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
@@ -181,21 +190,48 @@ async function assertPreconditions() {
|
||||
`有 ${bad} 行 status=assigned 但 channel 为空;请先执行 fulfillment:backfill-channel`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pendingBackfill = await query<{ count: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS count
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date < $1::date
|
||||
AND oi.fulfillment_status <> 'archived'
|
||||
`,
|
||||
[CUTOVER_DATE],
|
||||
)
|
||||
const notArchived = pendingBackfill.rows[0]?.count || 0
|
||||
if (notArchived > 0) {
|
||||
throw new Error(`有 ${notArchived} 行断代前订单项未归档;请先执行 fulfillment:backfill-channel`)
|
||||
/**
|
||||
* 断代前的订单项统一置为 archived(分批,避免 42 万行整表更新撞超时)。
|
||||
*/
|
||||
async function archiveHistoricalItems(): Promise<{ updated: number }> {
|
||||
let cursor = 0
|
||||
let updated = 0
|
||||
for (let batch = 0; batch < 500; batch += 1) {
|
||||
const rows = await query<{ id: string }>(
|
||||
`
|
||||
SELECT oi.id::text
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date < $1::date
|
||||
AND oi.fulfillment_status <> 'archived'
|
||||
AND oi.id > $2
|
||||
ORDER BY oi.id
|
||||
LIMIT 20000
|
||||
`,
|
||||
[CUTOVER_DATE, cursor],
|
||||
)
|
||||
if (rows.rows.length === 0) break
|
||||
cursor = Number(rows.rows[rows.rows.length - 1]!.id)
|
||||
|
||||
const result = await query(
|
||||
`
|
||||
UPDATE order_items
|
||||
SET fulfillment_status = 'archived',
|
||||
fulfillment_channel = NULL,
|
||||
fulfillment_source = 'cutover_archive',
|
||||
fulfillment_decided_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = ANY($1::bigint[])
|
||||
AND fulfillment_status <> 'archived'
|
||||
`,
|
||||
[rows.rows.map((row) => Number(row.id))],
|
||||
)
|
||||
updated += result.rowCount || 0
|
||||
process.stderr.write(`归档 ${result.rowCount || 0} 行(游标 ${cursor})\n`)
|
||||
}
|
||||
return { updated }
|
||||
}
|
||||
|
||||
async function assertNotExecuted() {
|
||||
|
||||
Reference in New Issue
Block a user