/** * 履约/财务统一分流部署后核对(只读)。 * * 用法: * npx tsx scripts/preflight-fulfillment-routing.ts * npx tsx scripts/preflight-fulfillment-routing.ts --strict */ import { closeDb, query } from '../src/db/client.js' type CountRow = { label: string; total: number } async function main() { const strict = process.argv.includes('--strict') const includeDemo = process.argv.includes('--include-demo') const checks: Record = {} checks.migrations = ( await query<{ name: string }>(`SELECT name FROM schema_migrations ORDER BY id DESC LIMIT 5`) ).rows.map((row) => row.name) checks.pendingReasons = ( await query( `SELECT COALESCE(fulfillment_pending_reason, '(null)') AS label, count(*)::int AS total FROM order_items WHERE fulfillment_status = 'pending' GROUP BY 1 ORDER BY 2 DESC`, ) ).rows checks.renameCardChannels = ( await query( `SELECT COALESCE(fulfillment_channel, '(pending)') AS label, count(*)::int AS total FROM order_items WHERE sku_name = '11月改名卡' GROUP BY 1 ORDER BY 2 DESC`, ) ).rows checks.rules = ( await query( `SELECT match_key_type || '/' || channel || '/' || CASE WHEN enabled THEN 'on' ELSE 'off' END AS label, count(*)::int AS total FROM sku_channel_rules GROUP BY 1 ORDER BY 2 DESC`, ) ).rows checks.internalChannels = ( await query( `SELECT channel AS label, count(*)::int AS total FROM internal_sku_channels WHERE enabled GROUP BY 1 ORDER BY 2 DESC`, ) ).rows checks.pendingWithoutChannel = ( await query( `SELECT 'pending_no_internal_channel' AS label, count(*)::int AS total FROM order_items oi WHERE oi.fulfillment_status = 'pending' AND btrim(COALESCE(resolve_internal_sku( oi.manual_sku_key, oi.sku_name, oi.sku_code ), '')) <> '' AND NOT EXISTS ( SELECT 1 FROM internal_sku_channels c WHERE c.enabled AND btrim(c.internal_sku) = resolve_internal_sku( oi.manual_sku_key, oi.sku_name, oi.sku_code ) )`, ) ).rows[0] checks.assignedWithoutCarrier = ( await query( `SELECT 'assigned_without_carrier' AS label, count(*)::int AS total FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE oi.fulfillment_status = 'assigned' AND oi.fulfillment_channel IN ('affiliate_dash', 'work_order') AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%') AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id AND wo.status <> 'cancelled') AND NOT EXISTS ( SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash' AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled') )`, [includeDemo], ) ).rows[0] checks.assignedWithMultipleCarriers = ( await query( `SELECT 'assigned_with_multiple_carriers' AS label, count(*)::int AS total FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE oi.fulfillment_status = 'assigned' AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%') AND (EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id AND wo.status <> 'cancelled'))::int + (EXISTS ( SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash' AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled') ))::int + (EXISTS ( SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch' AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled') ))::int > 1`, [includeDemo], ) ).rows[0] checks.internalChannelConflicts = ( await query( `SELECT internal_sku AS label, count(*)::int AS total FROM internal_sku_channels WHERE enabled = TRUE AND is_fallback = FALSE GROUP BY internal_sku HAVING count(*) > 1 ORDER BY count(*) DESC, internal_sku`, ) ).rows checks.canonicalChannelDuplicates = ( await query( `SELECT canonicalize_internal_sku(internal_sku) AS label, count(*)::int AS total FROM internal_sku_channels WHERE enabled = TRUE GROUP BY canonicalize_internal_sku(internal_sku), channel HAVING count(*) > 1 ORDER BY count(*) DESC, label`, ) ).rows checks.incompleteCarrierConfigs = ( await query( `SELECT channel AS label, count(*)::int AS total FROM internal_sku_channels WHERE enabled = TRUE AND ( ( channel = 'work_order' AND ( COALESCE(config_json->>'workRuleKey', '') = '' OR NOT EXISTS ( SELECT 1 FROM work_product_rules rule WHERE rule.rule_key = config_json->>'workRuleKey' AND rule.enabled = TRUE ) ) ) OR ( channel = 'manual' AND NOT EXISTS ( SELECT 1 FROM manual_fulfillment_configs cfg WHERE cfg.enabled = TRUE AND canonicalize_internal_sku(cfg.sku_key) = canonicalize_internal_sku(internal_sku_channels.internal_sku) ) ) ) GROUP BY channel ORDER BY count(*) DESC`, ) ).rows checks.ambiguousCarrierConfigs = ( await query( `SELECT canonicalize_internal_sku(canonical_name) || '/' || channel AS label, count(DISTINCT canonical_sku)::int AS total FROM ( SELECT canonical_name, canonical_sku, CASE WHEN canonical_sku LIKE 'aff:%' THEN 'affiliate_dash' WHEN canonical_sku LIKE 'tpl:%' OR canonical_sku LIKE 'wo:%' THEN 'work_order' WHEN canonical_sku LIKE 'tpv:%' THEN 'third_party_voucher' END AS channel FROM product_catalog WHERE status = 'active' AND (canonical_sku LIKE 'aff:%' OR canonical_sku LIKE 'tpl:%' OR canonical_sku LIKE 'wo:%' OR canonical_sku LIKE 'tpv:%') ) carrier WHERE channel IS NOT NULL GROUP BY canonicalize_internal_sku(canonical_name), channel HAVING count(DISTINCT canonical_sku) > 1 ORDER BY count(DISTINCT canonical_sku) DESC, label`, ) ).rows checks.ambiguousManualConfigs = ( await query( `SELECT canonicalize_internal_sku(sku_key) AS label, count(*)::int AS total FROM manual_fulfillment_configs WHERE enabled = TRUE GROUP BY canonicalize_internal_sku(sku_key) HAVING count(*) > 1 ORDER BY count(*) DESC, label`, ) ).rows checks.ambiguousInternalSkuMappings = ( await query( `SELECT provider || '/' || shop_id || '/' || match_type || '/' || canonicalize_internal_sku(match_value) AS label, count(*)::int AS total FROM internal_sku_mappings WHERE enabled = TRUE GROUP BY provider, shop_id, match_type, canonicalize_internal_sku(match_value) HAVING count(*) > 1 ORDER BY count(*) DESC, label LIMIT 200`, ) ).rows checks.pendingWithoutChineseInternalSku = ( await query( `SELECT 'pending_without_chinese_internal_sku' AS label, count(*)::int AS total FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE oi.fulfillment_status = 'pending' AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%') AND COALESCE(resolve_internal_sku( oi.manual_sku_key, oi.sku_name, oi.sku_code ), '') = ''`, [includeDemo], ) ).rows[0] checks.enabledLegacyRules = ( await query( `SELECT match_key_type || '/' || channel AS label, count(*)::int AS total FROM sku_channel_rules WHERE enabled = TRUE AND match_key_type = 'sku_code' GROUP BY 1 ORDER BY 2 DESC`, ) ).rows checks.demoPending = ( await query( `SELECT 'demo_pending' AS label, count(*)::int AS total FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE oi.fulfillment_status = 'pending' AND o.shop_id LIKE 'sim-%'`, ) ).rows[0] checks.pendingWithoutReason = ( await query( `SELECT 'pending_without_reason' AS label, count(*)::int AS total FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE oi.fulfillment_status = 'pending' AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%') AND oi.fulfillment_pending_reason IS NULL`, [includeDemo], ) ).rows[0] checks.channelCarrier = ( await query<{ channel: string total: number work_orders: number affiliate_tasks: number manual_tasks: number }>( `SELECT COALESCE(fulfillment_channel, '(pending)') AS channel, count(*)::int AS total, count(*) FILTER (WHERE EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id AND wo.status <> 'cancelled'))::int AS work_orders, count(*) FILTER (WHERE EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash' AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled')))::int AS affiliate_tasks, count(*) FILTER (WHERE EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch' AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled')))::int AS manual_tasks FROM order_items oi WHERE oi.fulfillment_status = 'assigned' GROUP BY 1 ORDER BY 2 DESC`, ) ).rows const failed: string[] = [] const rename = checks.renameCardChannels as CountRow[] if (rename.some((row) => row.label === 'pending')) failed.push('11月改名卡仍有 pending') if (!includeDemo && (checks.demoPending as CountRow).total > 0) { checks.excludedDemoPending = (checks.demoPending as CountRow).total } if ((checks.pendingWithoutChannel as CountRow).total > 0 && !includeDemo) { // 本地演示店铺故意保留 pending;严格模式默认只校验非演示数据。 const productionPending = await query( `SELECT 'pending_no_internal_channel' AS label, count(*)::int AS total FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE oi.fulfillment_status = 'pending' AND o.shop_id NOT LIKE 'sim-%' AND btrim(COALESCE(resolve_internal_sku( oi.manual_sku_key, oi.sku_name, oi.sku_code ), '')) <> '' AND NOT EXISTS ( SELECT 1 FROM internal_sku_channels c WHERE c.enabled AND btrim(c.internal_sku) = resolve_internal_sku( oi.manual_sku_key, oi.sku_name, oi.sku_code ) )`, ) if (productionPending.rows[0]!.total > 0) failed.push('存在有内部 SKU 但无渠道能力的非演示 pending') } else if ((checks.pendingWithoutChannel as CountRow).total > 0) { failed.push('存在有内部 SKU 但无渠道能力的 pending') } if ((checks.assignedWithoutCarrier as CountRow).total > 0) failed.push('存在已分配自动发货/接单渠道但没有真实载体的订单项') if ((checks.assignedWithMultipleCarriers as CountRow).total > 0) failed.push('存在同时挂载多个真实履约载体的已分配订单项') if ((checks.internalChannelConflicts as CountRow[]).length > 0) failed.push('存在多个非兜底主渠道') if ((checks.canonicalChannelDuplicates as CountRow[]).length > 0) failed.push('存在规范化后重复的内部 SKU 渠道配置') if ((checks.incompleteCarrierConfigs as CountRow[]).length > 0) failed.push('存在缺少 workRuleKey 或人工履约配置的启用渠道') if ((checks.ambiguousCarrierConfigs as CountRow[]).length > 0) failed.push('存在同一中文内部 SKU 的多个载体配置') if ((checks.ambiguousManualConfigs as CountRow[]).length > 0) failed.push('存在规范化后重复的人工履约配置') if ((checks.ambiguousInternalSkuMappings as CountRow[]).length > 0) failed.push('存在规范化后重复的内部 SKU 映射') if ((checks.pendingWithoutChineseInternalSku as CountRow).total > 0) failed.push('存在无法归一到中文内部 SKU 的 pending 订单项') if ((checks.enabledLegacyRules as CountRow[]).length > 0) failed.push('仍有启用的 sku_code 兼容规则,未完全收敛到内部 SKU') if ((checks.pendingWithoutReason as CountRow).total > 0) failed.push('存在未写入 pending 原因的订单项,请先执行履约/财务管道') console.log(JSON.stringify({ strict, failed, ...checks }, null, 2)) if (strict && failed.length > 0) process.exitCode = 2 } main() .catch((error) => { console.error(error) process.exitCode = 1 }) .finally(() => closeDb())