237 lines
7.9 KiB
TypeScript
237 lines
7.9 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* 履约状态只读审计。
|
|
*
|
|
* 用法:
|
|
* npm run audit:fulfillment
|
|
* npm run audit:fulfillment -- --strict
|
|
*
|
|
* --strict 会在关键异常大于 0 时返回失败,适合部署后和生产巡检。
|
|
*/
|
|
import { closeDb, query } from '../src/db/client.js'
|
|
|
|
const strict = process.argv.includes('--strict')
|
|
|
|
type CountRow = {
|
|
label: string
|
|
count: number
|
|
}
|
|
|
|
async function loadCounts(): Promise<CountRow[]> {
|
|
const result = await query<{
|
|
pending_items: number
|
|
classification: number
|
|
configuration: number
|
|
carrier: number
|
|
manual_review: number
|
|
after_sales_pending: number
|
|
state_inconsistent: number
|
|
assigned_missing_carrier: number
|
|
}>(
|
|
`
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE issue_code <> '')::int AS pending_items,
|
|
COUNT(*) FILTER (WHERE issue_category = 'classification')::int
|
|
AS classification,
|
|
COUNT(*) FILTER (WHERE issue_category = 'configuration')::int
|
|
AS configuration,
|
|
COUNT(*) FILTER (WHERE issue_category = 'carrier')::int AS carrier,
|
|
COUNT(*) FILTER (WHERE issue_category = 'manual_review')::int
|
|
AS manual_review,
|
|
COUNT(*) FILTER (
|
|
WHERE issue_category = 'after_sales' AND fulfillment_status = 'pending'
|
|
)::int AS after_sales_pending,
|
|
COUNT(*) FILTER (WHERE issue_code = 'state_inconsistent')::int
|
|
AS state_inconsistent,
|
|
COUNT(*) FILTER (
|
|
WHERE fulfillment_status = 'assigned' AND carrier_status = 'missing'
|
|
)::int AS assigned_missing_carrier
|
|
FROM v_fulfillment_item_state
|
|
`,
|
|
)
|
|
const row = result.rows[0]
|
|
return [
|
|
{ label: '待处理订单项', count: Number(row?.pending_items || 0) },
|
|
{ label: '未分类', count: Number(row?.classification || 0) },
|
|
{ label: '配置待补', count: Number(row?.configuration || 0) },
|
|
{ label: '载体异常', count: Number(row?.carrier || 0) },
|
|
{ label: '人工复核', count: Number(row?.manual_review || 0) },
|
|
{ label: '售后关闭仍进入履约队列', count: Number(row?.after_sales_pending || 0) },
|
|
{ label: '状态不一致', count: Number(row?.state_inconsistent || 0) },
|
|
{ label: '已分配但无有效载体', count: Number(row?.assigned_missing_carrier || 0) },
|
|
]
|
|
}
|
|
|
|
async function loadIssueBreakdown(): Promise<CountRow[]> {
|
|
const result = await query<{ label: string; count: number }>(
|
|
`
|
|
SELECT issue_code AS label, COUNT(*)::int AS count
|
|
FROM v_fulfillment_item_state
|
|
WHERE issue_code <> ''
|
|
GROUP BY issue_code
|
|
ORDER BY count DESC, issue_code
|
|
`,
|
|
)
|
|
return result.rows
|
|
}
|
|
|
|
async function loadRecentDuplicateAttempts(): Promise<CountRow[]> {
|
|
const result = await query<{ label: string; count: number }>(
|
|
`
|
|
SELECT
|
|
'近 24 小时重复尝试订单项' AS label,
|
|
COUNT(*)::int AS count
|
|
FROM (
|
|
SELECT order_item_id, unit_index
|
|
FROM fulfillment_tasks
|
|
WHERE created_at >= NOW() - INTERVAL '24 hours'
|
|
GROUP BY order_item_id, unit_index
|
|
HAVING COUNT(*) > 1
|
|
) duplicate
|
|
`,
|
|
)
|
|
return result.rows
|
|
}
|
|
|
|
async function loadShadowModelDiffs(): Promise<CountRow[]> {
|
|
const result = await query<{
|
|
missing_fulfillments: number
|
|
fulfillment_state_diffs: number
|
|
missing_attempts: number
|
|
missing_open_issues: number
|
|
unlinked_events: number
|
|
}>(
|
|
`
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE fulfillment.id IS NULL)::int AS missing_fulfillments,
|
|
COUNT(*) FILTER (
|
|
WHERE fulfillment.id IS NOT NULL
|
|
AND ROW(
|
|
fulfillment.route_status,
|
|
fulfillment.channel,
|
|
fulfillment.carrier_status,
|
|
fulfillment.current_attempt_type,
|
|
fulfillment.current_attempt_source_id,
|
|
fulfillment.issue_code,
|
|
fulfillment.issue_category
|
|
) IS DISTINCT FROM ROW(
|
|
CASE WHEN state.route_status = 'configured' THEN 'configured' ELSE 'unrouted' END,
|
|
state.intended_channel,
|
|
state.carrier_status,
|
|
state.current_attempt_type,
|
|
NULLIF(state.current_attempt_id, 0),
|
|
state.issue_code,
|
|
state.issue_category
|
|
)
|
|
)::int AS fulfillment_state_diffs,
|
|
(
|
|
SELECT COUNT(*)::int
|
|
FROM (
|
|
SELECT task.order_item_id
|
|
FROM fulfillment_tasks task
|
|
JOIN fulfillments fulfillment ON fulfillment.order_item_id = task.order_item_id
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM fulfillment_attempts attempt
|
|
WHERE attempt.attempt_type = 'task'
|
|
AND attempt.source_id = task.id
|
|
)
|
|
UNION ALL
|
|
SELECT work_order.order_item_id
|
|
FROM work_orders work_order
|
|
JOIN fulfillments fulfillment ON fulfillment.order_item_id = work_order.order_item_id
|
|
WHERE work_order.order_item_id IS NOT NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM fulfillment_attempts attempt
|
|
WHERE attempt.attempt_type = 'work_order'
|
|
AND attempt.source_id = work_order.id
|
|
)
|
|
) missing
|
|
)::int AS missing_attempts,
|
|
COUNT(*) FILTER (
|
|
WHERE state.issue_code <> ''
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM fulfillment_issues issue
|
|
WHERE issue.fulfillment_id = fulfillment.id
|
|
AND issue.issue_code = state.issue_code
|
|
AND issue.status <> 'resolved'
|
|
)
|
|
)::int AS missing_open_issues,
|
|
(
|
|
SELECT COUNT(*)::int
|
|
FROM fulfillment_state_events event
|
|
JOIN v_fulfillment_item_state active_state
|
|
ON active_state.order_item_id = event.order_item_id
|
|
WHERE event.fulfillment_id IS NULL
|
|
) AS unlinked_events
|
|
FROM v_fulfillment_item_state state
|
|
LEFT JOIN fulfillments fulfillment
|
|
ON fulfillment.order_item_id = state.order_item_id
|
|
`,
|
|
)
|
|
const row = result.rows[0]
|
|
return [
|
|
{ label: '影子履约缺失', count: Number(row?.missing_fulfillments || 0) },
|
|
{ label: '影子履约状态差异', count: Number(row?.fulfillment_state_diffs || 0) },
|
|
{ label: '影子执行尝试缺失', count: Number(row?.missing_attempts || 0) },
|
|
{ label: '影子开放异常缺失', count: Number(row?.missing_open_issues || 0) },
|
|
{ label: '履约事件未关联影子记录', count: Number(row?.unlinked_events || 0) },
|
|
]
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const [counts, issues, duplicates, shadowDiffs] = await Promise.all([
|
|
loadCounts(),
|
|
loadIssueBreakdown(),
|
|
loadRecentDuplicateAttempts(),
|
|
loadShadowModelDiffs(),
|
|
])
|
|
|
|
console.info('[fulfillment-audit] 总览')
|
|
for (const row of counts) {
|
|
console.info(` ${row.label}: ${row.count}`)
|
|
}
|
|
|
|
console.info('[fulfillment-audit] 异常分布')
|
|
if (issues.length === 0) {
|
|
console.info(' 无')
|
|
} else {
|
|
for (const row of issues) {
|
|
console.info(` ${row.label}: ${row.count}`)
|
|
}
|
|
}
|
|
|
|
console.info('[fulfillment-audit] 尝试审计')
|
|
for (const row of duplicates) {
|
|
console.info(` ${row.label}: ${row.count}`)
|
|
}
|
|
|
|
console.info('[fulfillment-audit] 影子模型差异')
|
|
for (const row of shadowDiffs) {
|
|
console.info(` ${row.label}: ${row.count}`)
|
|
}
|
|
|
|
const critical = new Map(counts.map((row) => [row.label, Number(row.count || 0)]))
|
|
const duplicateCount = Number(duplicates[0]?.count || 0)
|
|
const shadowDiffCount = shadowDiffs.reduce((sum, row) => sum + Number(row.count || 0), 0)
|
|
const hasCritical =
|
|
(critical.get('状态不一致') || 0) > 0 ||
|
|
(critical.get('售后关闭仍进入履约队列') || 0) > 0 ||
|
|
(critical.get('已分配但无有效载体') || 0) > 0 ||
|
|
duplicateCount > 0 ||
|
|
shadowDiffCount > 0
|
|
|
|
if (strict && hasCritical) {
|
|
process.exitCode = 1
|
|
console.error('[fulfillment-audit] strict 模式检测到关键异常')
|
|
}
|
|
}
|
|
|
|
try {
|
|
await main()
|
|
} finally {
|
|
await closeDb()
|
|
}
|