Files
order_site/apps/backend/scripts/audit-fulfillment-finance-alignment.ts
T
yml2213 2f87445ed9 fix(finance): 收敛内部 SKU 履约路由与载体状态
统一内部 SKU 与渠道主/兜底解析,禁止并列主渠道。

运营指定渠道改为先创建真实载体,失败保持明确 pending。

历史缺载体、多载体冲突和自动发货兜底统一由管道修复。

迁移人工配置与旧 sku_code 规则,补充严格预检和集成测试。
2026-11-02 15:02:49 +08:00

211 lines
7.2 KiB
TypeScript

/**
* 履约载体 ↔ 财务渠道 对齐审计(只读)。
*
* 用法:npx tsx scripts/audit-fulfillment-finance-alignment.ts
*/
import { closeDb, query } from '../src/db/client.js'
async function main() {
const pending = await query<{
total: number
with_work_order: number
with_affiliate_task: number
with_manual_task: number
no_carrier: number
}>(
`
SELECT
count(*)::int AS total,
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id))::int AS with_work_order,
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'))::int AS with_affiliate_task,
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'))::int AS with_manual_task,
count(*) FILTER (
WHERE NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
)::int AS no_carrier
FROM order_items oi
WHERE oi.fulfillment_status = 'pending'
`,
)
const channelCarrier = await query<{
channel: string
total: number
work_orders: number
affiliate_tasks: number
manual_tasks: number
config_missing: number
no_carrier: number
}>(
`
SELECT
COALESCE(NULLIF(oi.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))::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'))::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'))::int AS manual_tasks,
count(*) FILTER (
WHERE oi.fulfillment_channel = 'manual' AND oi.fulfillment_config_id IS NULL
)::int AS config_missing,
count(*) FILTER (
WHERE (
oi.fulfillment_channel = 'affiliate_dash'
AND NOT EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
)
) OR (
oi.fulfillment_channel = 'work_order'
AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
)
)::int AS no_carrier
FROM order_items oi
WHERE oi.fulfillment_status = 'assigned'
GROUP BY 1
ORDER BY 2 DESC
`,
)
const internalSku = await query<{
total: number
null_or_blank: number
numeric_only: number
with_cjk: number
}>(
`
SELECT
count(*)::int AS total,
count(*) FILTER (WHERE btrim(COALESCE(resolve_internal_sku(
manual_sku_key, sku_name, sku_code
), '')) = '')::int AS null_or_blank,
count(*) FILTER (WHERE btrim(COALESCE(resolve_internal_sku(
manual_sku_key, sku_name, sku_code
), '')) ~ '^[0-9]+$')::int AS numeric_only,
count(*) FILTER (WHERE btrim(COALESCE(resolve_internal_sku(
manual_sku_key, sku_name, sku_code
), '')) ~ '[一-龥]')::int AS with_cjk
FROM order_items
WHERE fulfillment_status = 'assigned'
`,
)
const rules = await query<{
match_key_type: string
channel: string
scope: string
total: number
}>(
`
SELECT match_key_type, channel,
CASE WHEN provider = '' AND shop_id = '' THEN 'global' ELSE 'scoped' END AS scope,
count(*)::int AS total
FROM sku_channel_rules
GROUP BY 1, 2, 3
ORDER BY 1, 2, 3
`,
)
const duplicateProducts = await query<{ duplicate_names: number; duplicate_rows: number }>(
`
SELECT count(*)::int AS duplicate_names, COALESCE(sum(total), 0)::int AS duplicate_rows
FROM (
SELECT canonical_name, count(*) AS total
FROM product_catalog
WHERE status = 'active'
GROUP BY canonical_name
HAVING count(*) > 1
) grouped
`,
)
const capabilities = await query<{ channel: string; total: number }>(
`SELECT channel, count(*)::int AS total FROM product_channel_capabilities WHERE enabled GROUP BY 1 ORDER BY 2 DESC`,
)
const internalChannelConflicts = await query<{ internal_sku: string; total: number }>(
`SELECT internal_sku, 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`,
)
const assignedCarrierConflicts = await query<{
order_item_id: number
channel: string
carrier_count: number
}>(
`SELECT oi.id AS order_item_id, oi.fulfillment_channel AS channel,
(EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'
))::int AS carrier_count
FROM order_items oi
WHERE oi.fulfillment_status = 'assigned'
AND (EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'
))::int > 1
ORDER BY oi.id
LIMIT 200`,
)
const byProvider = await query<{
provider: string
total: number
pending: number
with_carrier: number
}>(
`
SELECT o.provider,
count(*)::int AS total,
count(*) FILTER (WHERE oi.fulfillment_status = 'pending')::int AS pending,
count(*) FILTER (
WHERE EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
OR EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
)::int AS with_carrier
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE COALESCE(o.paid_at, o.created_at) >= '2026-10-31 00:00:00+08'
GROUP BY 1
ORDER BY 2 DESC
`,
)
console.log(
JSON.stringify(
{
pending: pending.rows[0],
channelCarrier: channelCarrier.rows,
internalSku: internalSku.rows[0],
rules: rules.rows,
duplicateProducts: duplicateProducts.rows[0],
capabilities: capabilities.rows,
internalChannelConflicts: internalChannelConflicts.rows,
assignedCarrierConflicts: assignedCarrierConflicts.rows,
byProvider: byProvider.rows,
},
null,
2,
),
)
}
main()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(() => closeDb())