Files
order_site/apps/backend/scripts/audit-product-routing-alignment.ts
T

208 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env tsx
/**
* 商品来源、内部 SKU 与履约路由只读审计。
*
* 用法:
* npm run audit:product-routing
* npm run audit:product-routing -- --strict
*/
import { closeDb, query } from '../src/db/client.js'
const strict = process.argv.includes('--strict')
async function main() {
const [
sourceChannel,
routeCounts,
manualConfigRoutes,
orphanManualProducts,
multiplePrimaryRoutes,
missingInternalSku,
unroutedProducts,
invalidSourceKinds,
] = await Promise.all([
query<{ source_kind: string; primary_channel: string | null; total: number }>(
`
SELECT source_kind, active_primary_channel AS primary_channel, COUNT(*)::int AS total
FROM v_product_current
WHERE status = 'active'
GROUP BY 1, 2
ORDER BY 1, 2 NULLS FIRST
`,
),
query<{ channel: string; route_role: string; total: number }>(
`
SELECT channel, route_role, COUNT(*)::int AS total
FROM sku_channel_routes
WHERE enabled = TRUE
GROUP BY 1, 2
ORDER BY 1, 2
`,
),
query<{
route_state: string
total: number
}>(
`
SELECT
CASE
WHEN primary_route.channel = 'manual' THEN 'primary_manual'
WHEN primary_route.channel IS NOT NULL
AND manual_route.route_role = 'fallback' THEN 'manual_fallback'
WHEN manual_route.route_role = 'fallback' THEN 'fallback_without_primary'
ELSE 'missing_manual_route'
END AS route_state,
COUNT(*)::int AS total
FROM manual_fulfillment_configs config
LEFT JOIN LATERAL (
SELECT route.channel
FROM sku_channel_routes route
WHERE route.internal_sku_id = config.internal_sku_id
AND route.enabled = TRUE
AND route.route_role = 'primary'
ORDER BY route.priority, route.channel
LIMIT 1
) primary_route ON TRUE
LEFT JOIN sku_channel_routes manual_route
ON manual_route.internal_sku_id = config.internal_sku_id
AND manual_route.channel = 'manual'
AND manual_route.enabled = TRUE
WHERE config.enabled = TRUE
GROUP BY 1
ORDER BY 1
`,
),
query<{
product_id: number
canonical_sku: string
canonical_name: string
}>(
`
SELECT product.id AS product_id,
product.canonical_sku,
product.canonical_name
FROM product_catalog product
WHERE product.status = 'active'
AND product.source_kind = 'manual'
AND NOT EXISTS (
SELECT 1
FROM sku_channel_routes route
WHERE route.internal_sku_id = product.internal_sku_id
AND route.enabled = TRUE
AND route.route_role = 'primary'
)
AND NOT EXISTS (
SELECT 1
FROM manual_fulfillment_configs config
WHERE config.enabled = TRUE
AND (
config.canonical_product_id = product.id
OR config.internal_sku_id = product.internal_sku_id
OR canonicalize_internal_sku(config.sku_key)
= canonicalize_internal_sku(substring(product.canonical_sku from 5))
)
)
ORDER BY product.id
`,
),
query<{ internal_sku_id: number; total: number }>(
`
SELECT internal_sku_id, COUNT(*)::int AS total
FROM sku_channel_routes
WHERE enabled = TRUE AND route_role = 'primary'
GROUP BY 1
HAVING COUNT(*) > 1
ORDER BY 2 DESC, 1
`,
),
query<{ source_kind: string; total: number }>(
`
SELECT source_kind, COUNT(*)::int AS total
FROM product_catalog
WHERE status = 'active'
AND source_kind IN ('manual', 'third_party_voucher')
AND internal_sku_id IS NULL
GROUP BY 1
ORDER BY 1
`,
),
query<{
product_id: number
canonical_sku: string
canonical_name: string
source_kind: string
}>(
`
SELECT id AS product_id, canonical_sku, canonical_name, source_kind
FROM v_product_current
WHERE status = 'active' AND active_primary_channel IS NULL
ORDER BY source_kind, id
`,
),
query<{ source_kind: string; total: number }>(
`
SELECT source_kind, COUNT(*)::int AS total
FROM product_catalog
WHERE status = 'active'
AND source_kind NOT IN (
'affiliate_dash',
'work_order',
'third_party_voucher',
'manual',
'unclassified'
)
GROUP BY 1
ORDER BY 1
`,
),
])
const critical = {
orphanManualProducts: orphanManualProducts.rows.length,
multiplePrimaryRoutes: multiplePrimaryRoutes.rows.length,
missingInternalSku: missingInternalSku.rows.reduce((sum, row) => sum + Number(row.total), 0),
invalidSourceKinds: invalidSourceKinds.rows.reduce((sum, row) => sum + Number(row.total), 0),
invalidManualConfigRoutes: manualConfigRoutes.rows
.filter(
(row) => row.route_state !== 'primary_manual' && row.route_state !== 'manual_fallback',
)
.reduce((sum, row) => sum + Number(row.total), 0),
}
console.info('[product-routing-audit] 商品来源 × 当前渠道')
console.table(sourceChannel.rows)
console.info('[product-routing-audit] 路由分布')
console.table(routeCounts.rows)
console.info('[product-routing-audit] 人工配置路由状态')
console.table(manualConfigRoutes.rows)
console.info('[product-routing-audit] 无主路由且无人工配置的 man 商品')
console.table(orphanManualProducts.rows)
console.info('[product-routing-audit] 多主路由内部 SKU')
console.table(multiplePrimaryRoutes.rows)
console.info('[product-routing-audit] 缺少内部 SKU 的商品')
console.table(missingInternalSku.rows)
console.info('[product-routing-audit] 非法商品来源')
console.table(invalidSourceKinds.rows)
console.info('[product-routing-audit] 未路由活动商品(待人工分类)')
console.table(unroutedProducts.rows)
console.info('[product-routing-audit] 关键异常', critical)
if (strict && Object.values(critical).some((count) => count > 0)) {
process.exitCode = 1
}
}
main()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(() => closeDb())