Files
lzwcai-mcp-server-package/lzwcai_mcpskills_mfg_data_agent/sql/供应链风险预警.sql
yuanzhipeng 3ea772c3be feat(mfg-data-agent): 添加HTML可视化仪表盘和优化项目配置
- 新增6个HTML可视化仪表盘组件用于数据展示
* 人效产值损耗三维模型仪表盘
* 指标趋势分析与拐点预警仪表盘
* 一页式决策简报仪表盘
* 订单延迟预警分析仪表盘
* 供应链风险预警仪表盘
* 工单执行进度与异常节点仪表盘
- 添加VSCode工作区配置文件
- 更新businessQueries.json业务查询配置
- 优化api_client.py API客户端实现
- 更新pyproject.toml项目依赖版本
- 重组SQL查询文件结构
- 删除v2版本冗余文档配置
- 添加v2版本技能清单文档
- 更新日志文件记录
2026-01-14 11:56:43 +08:00

417 lines
15 KiB
SQL
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.
-- =====================================================
-- 供应链风险预警SQL
-- Supply Chain Risk Warning
-- 数据库: PostgreSQL
-- =====================================================
-- =====================================================
-- 1. 供应商历史交期表现分析
-- =====================================================
WITH supplier_delivery AS (
SELECT
po.supplier_id,
COUNT(*) AS order_count,
COUNT(pr.purchase_receipt_id) AS receipt_count,
AVG(
CASE WHEN pr.doc_date_utc IS NOT NULL AND po.doc_date_utc IS NOT NULL
THEN EXTRACT(DAY FROM pr.doc_date_utc::timestamp - po.doc_date_utc::timestamp)
ELSE NULL END
) AS avg_delivery_days,
MAX(
CASE WHEN pr.doc_date_utc IS NOT NULL AND po.doc_date_utc IS NOT NULL
THEN EXTRACT(DAY FROM pr.doc_date_utc::timestamp - po.doc_date_utc::timestamp)
ELSE NULL END
) AS max_delivery_days,
STDDEV(
CASE WHEN pr.doc_date_utc IS NOT NULL AND po.doc_date_utc IS NOT NULL
THEN EXTRACT(DAY FROM pr.doc_date_utc::timestamp - po.doc_date_utc::timestamp)
ELSE NULL END
) AS stddev_delivery_days
FROM fact_purchase_order po
LEFT JOIN fact_purchase_receipt pr ON po.supplier_id = pr.supplier_id
GROUP BY po.supplier_id
),
supplier_quality AS (
SELECT
pr.supplier_id,
COUNT(pr.purchase_receipt_id) AS total_receipts,
SUM(pr.receipt_qty_total) AS total_qty,
SUM(pr.amount) AS total_amount
FROM fact_purchase_receipt pr
GROUP BY pr.supplier_id
),
supplier_returns AS (
SELECT
pret.supplier_id,
COUNT(*) AS return_count,
SUM(CASE WHEN pret.return_reason = '损坏' THEN 1 ELSE 0 END) AS damage_count
FROM fact_purchase_return pret
GROUP BY pret.supplier_id
),
supplier_quality_rate AS (
SELECT
sq.supplier_id,
sq.total_receipts,
sq.total_qty,
sq.total_amount,
COALESCE(sr.return_count, 0) AS return_count,
COALESCE(sr.damage_count, 0) AS damage_count,
CASE WHEN sq.total_receipts > 0
THEN (sq.total_receipts - COALESCE(sr.return_count, 0)) * 100.0 / sq.total_receipts
ELSE 100 END AS quality_rate
FROM supplier_quality sq
LEFT JOIN supplier_returns sr ON sq.supplier_id = sr.supplier_id
),
supplier_risk AS (
SELECT
s.supplier_id,
s.supplier_name,
s.supplier_category,
COALESCE(sd.order_count, 0) AS order_count,
COALESCE(sd.receipt_count, 0) AS receipt_count,
ROUND(COALESCE(sd.avg_delivery_days, 0), 1) AS avg_delivery_days,
ROUND(COALESCE(sd.max_delivery_days, 0), 1) AS max_delivery_days,
ROUND(COALESCE(sd.stddev_delivery_days, 0), 1) AS delivery_volatility,
COALESCE(sqr.total_receipts, 0) AS total_receipts,
COALESCE(sqr.return_count, 0) AS return_count,
ROUND(COALESCE(sqr.quality_rate, 100), 1) AS quality_rate,
CASE
WHEN COALESCE(sd.avg_delivery_days, 0) > 60 THEN 40
WHEN COALESCE(sd.avg_delivery_days, 0) > 45 THEN 30
WHEN COALESCE(sd.avg_delivery_days, 0) > 30 THEN 20
ELSE 10
END +
CASE
WHEN COALESCE(sd.stddev_delivery_days, 0) > 20 THEN 30
WHEN COALESCE(sd.stddev_delivery_days, 0) > 10 THEN 20
ELSE 10
END AS delivery_risk_score,
CASE
WHEN COALESCE(sqr.quality_rate, 100) < 80 THEN 50
WHEN COALESCE(sqr.quality_rate, 100) < 90 THEN 30
WHEN COALESCE(sqr.quality_rate, 100) < 95 THEN 15
ELSE 5
END AS quality_risk_score
FROM dim_supplier s
LEFT JOIN supplier_delivery sd ON s.supplier_id = sd.supplier_id
LEFT JOIN supplier_quality_rate sqr ON s.supplier_id = sqr.supplier_id
WHERE s.is_current = 't'
),
supplier_risk_level AS (
SELECT
*,
delivery_risk_score + quality_risk_score AS total_risk_score,
CASE
WHEN delivery_risk_score + quality_risk_score >= 80 THEN 'HIGH'
WHEN delivery_risk_score + quality_risk_score >= 50 THEN 'MEDIUM'
ELSE 'LOW'
END AS risk_level,
CASE
WHEN delivery_risk_score >= 50 AND quality_risk_score >= 30 THEN 'DELIVERY_AND_QUALITY'
WHEN delivery_risk_score >= 50 THEN 'DELIVERY_ISSUE'
WHEN quality_risk_score >= 30 THEN 'QUALITY_ISSUE'
ELSE 'NORMAL'
END AS risk_pattern
FROM supplier_risk
)
SELECT
supplier_name,
supplier_category,
order_count,
receipt_count,
avg_delivery_days,
max_delivery_days,
delivery_volatility,
total_receipts,
return_count,
quality_rate,
delivery_risk_score,
quality_risk_score,
total_risk_score,
risk_level,
risk_pattern
FROM supplier_risk_level
ORDER BY total_risk_score DESC, supplier_name;
-- =====================================================
-- 补充查询1高风险订单预警清单
-- =====================================================
WITH supplier_risk_info AS (
SELECT
s.supplier_id,
s.supplier_name,
s.supplier_category,
COALESCE(
(SELECT AVG(EXTRACT(DAY FROM pr.doc_date_utc::timestamp - po2.doc_date_utc::timestamp))
FROM fact_purchase_order po2
LEFT JOIN fact_purchase_receipt pr ON po2.supplier_id = pr.supplier_id
WHERE po2.supplier_id = s.supplier_id
AND pr.doc_date_utc IS NOT NULL), 0
) AS avg_delivery_days,
COALESCE(
(SELECT COUNT(*) FROM fact_purchase_return pret WHERE pret.supplier_id = s.supplier_id), 0
) AS return_count,
COALESCE(
(SELECT COUNT(*) FROM fact_purchase_receipt pr WHERE pr.supplier_id = s.supplier_id), 0
) AS receipt_count
FROM dim_supplier s
WHERE s.is_current = 't'
),
order_risk AS (
SELECT
po.purchase_order_number,
po.doc_date_utc::date AS order_date,
sri.supplier_name,
sri.supplier_category,
ROUND(sri.avg_delivery_days, 1) AS supplier_avg_days,
sri.return_count,
sri.receipt_count,
CASE WHEN sri.receipt_count > 0
THEN ROUND((sri.receipt_count - sri.return_count) * 100.0 / sri.receipt_count, 1)
ELSE 100 END AS quality_rate,
EXTRACT(DAY FROM CURRENT_TIMESTAMP - po.doc_date_utc::timestamp) AS days_since_order,
CASE
WHEN sri.avg_delivery_days > 45 AND sri.return_count > 0 THEN 'HIGH'
WHEN sri.avg_delivery_days > 45 OR sri.return_count > 0 THEN 'MEDIUM'
ELSE 'LOW'
END AS risk_level
FROM fact_purchase_order po
JOIN supplier_risk_info sri ON po.supplier_id = sri.supplier_id
)
SELECT
purchase_order_number,
order_date,
supplier_name,
supplier_category,
supplier_avg_days,
quality_rate,
days_since_order,
risk_level,
CASE
WHEN risk_level = 'HIGH' THEN 'IMMEDIATE_FOLLOWUP'
WHEN risk_level = 'MEDIUM' THEN 'MONITOR'
ELSE 'NORMAL'
END AS action_required
FROM order_risk
WHERE risk_level IN ('HIGH', 'MEDIUM')
ORDER BY
CASE risk_level WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 ELSE 3 END,
days_since_order DESC;
-- =====================================================
-- 补充查询2供应商类别风险分布
-- =====================================================
WITH supplier_stats AS (
SELECT
s.supplier_category,
COUNT(DISTINCT s.supplier_id) AS supplier_count,
COUNT(DISTINCT po.purchase_order_id) AS order_count,
COUNT(DISTINCT pr.purchase_receipt_id) AS receipt_count,
COUNT(DISTINCT pret.purchase_return_id) AS return_count,
SUM(pr.amount) AS total_amount
FROM dim_supplier s
LEFT JOIN fact_purchase_order po ON s.supplier_id = po.supplier_id
LEFT JOIN fact_purchase_receipt pr ON s.supplier_id = pr.supplier_id
LEFT JOIN fact_purchase_return pret ON s.supplier_id = pret.supplier_id
WHERE s.is_current = 't'
GROUP BY s.supplier_category
)
SELECT
supplier_category,
supplier_count,
order_count,
receipt_count,
return_count,
CASE WHEN receipt_count > 0
THEN ROUND((receipt_count - return_count) * 100.0 / receipt_count, 1)
ELSE 100 END AS quality_rate,
ROUND(COALESCE(total_amount, 0), 2) AS purchase_amount,
CASE
WHEN receipt_count > 0 AND return_count * 100.0 / receipt_count > 10 THEN 'HIGH'
WHEN receipt_count > 0 AND return_count * 100.0 / receipt_count > 5 THEN 'MEDIUM'
ELSE 'LOW'
END AS category_risk_level
FROM supplier_stats
ORDER BY return_count DESC;
-- =====================================================
-- 补充查询3近期交期异常趋势
-- =====================================================
WITH monthly_delivery AS (
SELECT
DATE_TRUNC('month', pr.doc_date_utc::timestamp)::date AS month_start,
COUNT(*) AS receipt_count,
AVG(EXTRACT(DAY FROM pr.doc_date_utc::timestamp - po.doc_date_utc::timestamp)) AS avg_delivery_days
FROM fact_purchase_receipt pr
JOIN fact_purchase_order po ON pr.supplier_id = po.supplier_id
WHERE pr.doc_date_utc IS NOT NULL AND po.doc_date_utc IS NOT NULL
GROUP BY DATE_TRUNC('month', pr.doc_date_utc::timestamp)
),
monthly_returns AS (
SELECT
DATE_TRUNC('month', pret.doc_date_utc::timestamp)::date AS month_start,
COUNT(*) AS return_count
FROM fact_purchase_return pret
GROUP BY DATE_TRUNC('month', pret.doc_date_utc::timestamp)
),
monthly_combined AS (
SELECT
md.month_start,
md.receipt_count,
ROUND(md.avg_delivery_days, 1) AS avg_delivery_days,
COALESCE(mr.return_count, 0) AS return_count
FROM monthly_delivery md
LEFT JOIN monthly_returns mr ON md.month_start = mr.month_start
),
with_trend AS (
SELECT
*,
LAG(avg_delivery_days, 1) OVER (ORDER BY month_start) AS prev_avg_days,
LAG(return_count, 1) OVER (ORDER BY month_start) AS prev_return_count
FROM monthly_combined
)
SELECT
month_start,
receipt_count,
avg_delivery_days,
return_count,
CASE WHEN receipt_count > 0
THEN ROUND(return_count * 100.0 / receipt_count, 1)
ELSE 0 END AS return_rate,
CASE
WHEN prev_avg_days IS NULL THEN 'NONE'
WHEN avg_delivery_days > prev_avg_days * 1.1 THEN 'INCREASING'
WHEN avg_delivery_days < prev_avg_days * 0.9 THEN 'DECREASING'
ELSE 'STABLE'
END AS delivery_trend,
CASE
WHEN prev_return_count IS NULL THEN 'NONE'
WHEN return_count > prev_return_count THEN 'INCREASING'
WHEN return_count < prev_return_count THEN 'DECREASING'
ELSE 'STABLE'
END AS return_trend
FROM with_trend
ORDER BY month_start DESC;
-- =====================================================
-- 补充查询4风险预警汇总看板
-- =====================================================
WITH risk_summary AS (
SELECT
s.supplier_id,
s.supplier_name,
COALESCE(
(SELECT COUNT(*) FROM fact_purchase_return pret WHERE pret.supplier_id = s.supplier_id), 0
) AS return_count,
COALESCE(
(SELECT COUNT(*) FROM fact_purchase_receipt pr WHERE pr.supplier_id = s.supplier_id), 0
) AS receipt_count
FROM dim_supplier s
WHERE s.is_current = 't'
),
risk_counts AS (
SELECT
SUM(CASE WHEN receipt_count > 0 AND return_count * 100.0 / receipt_count > 10 THEN 1 ELSE 0 END) AS high_risk_suppliers,
SUM(CASE WHEN receipt_count > 0 AND return_count * 100.0 / receipt_count BETWEEN 5 AND 10 THEN 1 ELSE 0 END) AS medium_risk_suppliers,
SUM(CASE WHEN receipt_count = 0 OR return_count * 100.0 / receipt_count < 5 THEN 1 ELSE 0 END) AS low_risk_suppliers,
COUNT(*) AS total_suppliers
FROM risk_summary
),
order_stats AS (
SELECT
COUNT(*) AS pending_orders,
COUNT(CASE WHEN EXTRACT(DAY FROM CURRENT_TIMESTAMP - doc_date_utc::timestamp) > 30 THEN 1 END) AS overdue_orders
FROM fact_purchase_order
),
return_stats AS (
SELECT
COUNT(*) AS total_returns,
COUNT(CASE WHEN doc_date_utc::timestamp >= CURRENT_DATE - INTERVAL '30 days' THEN 1 END) AS recent_returns
FROM fact_purchase_return
)
SELECT
'high_risk_suppliers' AS metric_name,
rc.high_risk_suppliers::text AS metric_value,
CASE WHEN rc.high_risk_suppliers > 0 THEN 'ATTENTION_NEEDED' ELSE 'NORMAL' END AS status
FROM risk_counts rc
UNION ALL
SELECT 'medium_risk_suppliers', rc.medium_risk_suppliers::text,
CASE WHEN rc.medium_risk_suppliers > 2 THEN 'MONITOR' ELSE 'NORMAL' END
FROM risk_counts rc
UNION ALL
SELECT 'low_risk_suppliers', rc.low_risk_suppliers::text, 'NORMAL'
FROM risk_counts rc
UNION ALL
SELECT 'pending_orders', os.pending_orders::text,
CASE WHEN os.pending_orders > 20 THEN 'BACKLOG' ELSE 'NORMAL' END
FROM order_stats os
UNION ALL
SELECT 'overdue_orders_30d', os.overdue_orders::text,
CASE WHEN os.overdue_orders > 5 THEN 'DELIVERY_WARNING' ELSE 'NORMAL' END
FROM order_stats os
UNION ALL
SELECT 'recent_returns_30d', rs.recent_returns::text,
CASE WHEN rs.recent_returns > 3 THEN 'QUALITY_WARNING' ELSE 'NORMAL' END
FROM return_stats rs;
-- =====================================================
-- 补充查询5供应商风险排行榜
-- =====================================================
WITH supplier_metrics AS (
SELECT
s.supplier_id,
s.supplier_name,
s.supplier_category,
COUNT(DISTINCT po.purchase_order_id) AS order_count,
COUNT(DISTINCT pr.purchase_receipt_id) AS receipt_count,
COUNT(DISTINCT pret.purchase_return_id) AS return_count,
SUM(pr.amount) AS total_amount
FROM dim_supplier s
LEFT JOIN fact_purchase_order po ON s.supplier_id = po.supplier_id
LEFT JOIN fact_purchase_receipt pr ON s.supplier_id = pr.supplier_id
LEFT JOIN fact_purchase_return pret ON s.supplier_id = pret.supplier_id
WHERE s.is_current = 't'
GROUP BY s.supplier_id, s.supplier_name, s.supplier_category
),
ranked_suppliers AS (
SELECT
*,
CASE WHEN receipt_count > 0
THEN ROUND(return_count * 100.0 / receipt_count, 1)
ELSE 0 END AS return_rate,
ROW_NUMBER() OVER (ORDER BY
CASE WHEN receipt_count > 0 THEN return_count * 1.0 / receipt_count ELSE 0 END DESC,
return_count DESC
) AS risk_rank
FROM supplier_metrics
)
SELECT
risk_rank,
supplier_name,
supplier_category,
order_count,
receipt_count,
return_count,
return_rate,
ROUND(COALESCE(total_amount, 0), 2) AS purchase_amount,
CASE
WHEN return_rate > 10 THEN 'HIGH_RISK'
WHEN return_rate > 5 THEN 'MEDIUM_RISK'
WHEN return_count > 0 THEN 'LOW_RISK'
ELSE 'EXCELLENT'
END AS risk_assessment
FROM ranked_suppliers
ORDER BY risk_rank
LIMIT 20;