重写 120/121 迁移,统一内部 SKU、渠道 id、config_json 和旧规则转换。 后台渠道配置改读统一渠道表,卡券与人工配置不再写旧规则。 增加安全渠道切换,并排除已关闭、失败或过期的历史任务载体。 按生产 119 基线重跑迁移、完整履约财务链路和严格预检。
216 lines
8.4 KiB
TypeScript
216 lines
8.4 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 AND wo.status <> 'cancelled'))::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' AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled')))::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' AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled')))::int AS with_manual_task,
|
|
count(*) FILTER (
|
|
WHERE 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)
|
|
)::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 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,
|
|
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'
|
|
AND ft.task_status NOT IN ('closed','expired','failed','manual_review','retry_pending','cancelled')
|
|
)
|
|
) OR (
|
|
oi.fulfillment_channel = 'work_order'
|
|
AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id AND wo.status <> 'cancelled')
|
|
)
|
|
)::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 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 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 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
|
|
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 AND wo.status <> 'cancelled')
|
|
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())
|