184 lines
5.5 KiB
TypeScript
184 lines
5.5 KiB
TypeScript
#!/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,
|
||
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 manual_route.route_role = 'fallback' THEN 'manual_fallback'
|
||
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<{ 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),
|
||
}
|
||
|
||
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] 关键异常', critical)
|
||
if (strict && Object.values(critical).some((count) => count > 0)) {
|
||
process.exitCode = 1
|
||
}
|
||
}
|
||
|
||
main()
|
||
.catch((error) => {
|
||
console.error(error)
|
||
process.exitCode = 1
|
||
})
|
||
.finally(() => closeDb())
|