init: 汽车零部件智能报价 MCP 服务(ERP + CRM)
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# MCP for ERP — 汽车零部件智能报价 ERP 数据服务
|
||||
|
||||
## 概述
|
||||
|
||||
为汽车零部件(小型阀体外壳)智能报价智能体提供 ERP 数据访问能力,通过 MCP 协议暴露 5 个工具。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: MCPServer(mcp 2.0.0 内置高级框架)
|
||||
- **语言**: Python 3.11+
|
||||
- **数据库**: PostgreSQL 15+
|
||||
- **传输协议**: Streamable HTTP(远程调用)
|
||||
|
||||
## 工具清单
|
||||
|
||||
| 工具名 | 功能 | 核价环节 |
|
||||
|---|---|---|
|
||||
| `query_material_master` | 按编码/参数查询物料主数据(含体积、重量、吨位) | 基础数据 |
|
||||
| `match_material_grade` | 材质牌号对照(GB/ASTM/JIS/EN),返回等效牌号+密度 | 材质确认 |
|
||||
| `get_die_casting_params` | 根据零件体积+吨位返回压铸参数(节拍、铝水重量) | **压铸成本** |
|
||||
| `get_machining_estimate` | 根据孔数/孔深/螺纹规格返回机加工工时估算 | **机加工成本** |
|
||||
| `calculate_part_cost` | 汇总核价:材料+压铸+机加工+后处理+管理费+利润 | **最终报价** |
|
||||
|
||||
## 核价逻辑
|
||||
|
||||
```
|
||||
1. 铝水重量 = 体积(cm³) × 密度(g/cm³) / 1000 × 1.05(损耗)
|
||||
2. 材料成本 = 铝水重量 × 铝锭单价
|
||||
3. 压铸成本 = (节拍秒/3600) × 机台费率 / 模穴数
|
||||
4. 机加工成本 = 总工时(小时) × 加工中心费率(60元/时)
|
||||
5. 后处理成本 = 零件重量(kg) × 后处理单价
|
||||
6. 总成本 = 材料 + 压铸 + 机加工 + 后处理
|
||||
7. 报价 = 总成本 × (1+管理费率12%) × (1+利润率15%)
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
cd src
|
||||
python seed.py # 初始化数据库(可选,数据已在共享数据库中)
|
||||
python server.py # 启动 MCP Server(端口 8001)
|
||||
```
|
||||
|
||||
## 服务地址
|
||||
|
||||
- **HTTP 端点**: `http://0.0.0.0:8001/mcp`
|
||||
- **传输协议**: Streamable HTTP
|
||||
|
||||
## 数据库配置
|
||||
|
||||
通过环境变量配置:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `ERP_DB_HOST` | 192.168.1.206 | PostgreSQL 主机 |
|
||||
| `ERP_DB_PORT` | 5432 | 端口 |
|
||||
| `ERP_DB_USER` | postgres | 用户名 |
|
||||
| `ERP_DB_PASSWORD` | digiwin | 密码 |
|
||||
| `ERP_DB_NAME` | smart_quotation_auto | 数据库名 |
|
||||
@@ -0,0 +1,3 @@
|
||||
mcp>=1.0.0
|
||||
pydantic>=2.0.0
|
||||
asyncpg>=0.30.0
|
||||
@@ -0,0 +1,234 @@
|
||||
-- ============================================================
|
||||
-- MCP for ERP — 汽车零部件智能报价 ERP 数据服务
|
||||
-- 数据库:smart_quotation_auto
|
||||
-- v3: 幂等可重跑(唯一约束+去重);扩充至 16 物料/13 牌号/9 吨位/26 节拍/10 后处理
|
||||
-- 新增 O1 material_price_history、O2 machining_rate
|
||||
-- ============================================================
|
||||
|
||||
-- 1. 物料主数据(含体积,用于算铝水重量)
|
||||
CREATE TABLE IF NOT EXISTS material_master (
|
||||
material_code VARCHAR(50) PRIMARY KEY,
|
||||
part_name VARCHAR(200) NOT NULL,
|
||||
part_category VARCHAR(50) NOT NULL,
|
||||
material_grade VARCHAR(50) NOT NULL,
|
||||
density DECIMAL(6,3) NOT NULL DEFAULT 2.700,
|
||||
volume_cc DECIMAL(10,2) NOT NULL,
|
||||
net_weight_kg DECIMAL(8,3) NOT NULL,
|
||||
die_casting_ton INTEGER NOT NULL,
|
||||
surface_treatment VARCHAR(50),
|
||||
unit VARCHAR(10) DEFAULT '件',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 2. 材质牌号对照表
|
||||
CREATE TABLE IF NOT EXISTS material_grade_mapping (
|
||||
id SERIAL PRIMARY KEY,
|
||||
grade_group VARCHAR(50) NOT NULL,
|
||||
gb_code VARCHAR(80),
|
||||
astm_code VARCHAR(80),
|
||||
jis_code VARCHAR(80),
|
||||
en_code VARCHAR(80),
|
||||
iso_code VARCHAR(80),
|
||||
internal_code VARCHAR(50) NOT NULL,
|
||||
density DECIMAL(6,3) NOT NULL,
|
||||
price_per_kg DECIMAL(10,2) NOT NULL
|
||||
);
|
||||
|
||||
-- 3. 压铸参数表(吨位 → 固定节拍)
|
||||
CREATE TABLE IF NOT EXISTS die_casting_params (
|
||||
tonnage INTEGER PRIMARY KEY,
|
||||
shot_weight_kg DECIMAL(8,3) NOT NULL,
|
||||
cycle_time_sec DECIMAL(8,2) NOT NULL,
|
||||
machine_rate DECIMAL(10,2) NOT NULL,
|
||||
mold_cavities INTEGER DEFAULT 1
|
||||
);
|
||||
|
||||
-- 4. 机加工节拍表(固化工装节拍)
|
||||
CREATE TABLE IF NOT EXISTS machining_cycle (
|
||||
id SERIAL PRIMARY KEY,
|
||||
operation_type VARCHAR(30) NOT NULL,
|
||||
spec VARCHAR(50) NOT NULL,
|
||||
cycle_time_sec DECIMAL(8,2) NOT NULL,
|
||||
tool_rate DECIMAL(8,4) DEFAULT 0,
|
||||
notes VARCHAR(200)
|
||||
);
|
||||
|
||||
-- 5. 后处理费率表(按重量计价)
|
||||
CREATE TABLE IF NOT EXISTS post_process_rate (
|
||||
id SERIAL PRIMARY KEY,
|
||||
process_type VARCHAR(50) NOT NULL,
|
||||
unit VARCHAR(10) DEFAULT 'kg',
|
||||
rate DECIMAL(10,2) NOT NULL,
|
||||
currency VARCHAR(10) DEFAULT 'CNY'
|
||||
);
|
||||
|
||||
-- 6. O1 材料价格历史表(月度参考价,核价取最新月份)
|
||||
CREATE TABLE IF NOT EXISTS material_price_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
internal_code VARCHAR(50) NOT NULL,
|
||||
price_date DATE NOT NULL,
|
||||
price_per_kg DECIMAL(10,2) NOT NULL,
|
||||
source VARCHAR(100) DEFAULT '市场参考价',
|
||||
UNIQUE (internal_code, price_date)
|
||||
);
|
||||
|
||||
-- 7. O2 机加工费率表(按材质族,替换硬编码 60 元/h)
|
||||
CREATE TABLE IF NOT EXISTS machining_rate (
|
||||
grade_group VARCHAR(50) PRIMARY KEY,
|
||||
rate_per_hour DECIMAL(10,2) NOT NULL,
|
||||
notes VARCHAR(200)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 幂等性:去重 + 唯一约束(可重复执行)
|
||||
-- ============================================================
|
||||
|
||||
DELETE FROM machining_cycle a USING machining_cycle b
|
||||
WHERE a.id > b.id AND a.operation_type = b.operation_type AND a.spec = b.spec;
|
||||
|
||||
DELETE FROM material_grade_mapping a USING material_grade_mapping b
|
||||
WHERE a.id > b.id AND a.internal_code = b.internal_code;
|
||||
|
||||
DELETE FROM post_process_rate a USING post_process_rate b
|
||||
WHERE a.id > b.id AND a.process_type = b.process_type;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE machining_cycle ADD CONSTRAINT uq_machining_cycle UNIQUE (operation_type, spec);
|
||||
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE material_grade_mapping ADD CONSTRAINT uq_grade_mapping UNIQUE (internal_code);
|
||||
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE post_process_rate ADD CONSTRAINT uq_post_process UNIQUE (process_type);
|
||||
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
|
||||
|
||||
-- ============================================================
|
||||
-- 种子数据
|
||||
-- ============================================================
|
||||
|
||||
-- 物料主数据(16 种阀体外壳:铝12/灰铁3/球铁1,吨位 160~1000T 全覆盖)
|
||||
INSERT INTO material_master (material_code, part_name, part_category, material_grade, density, volume_cc, net_weight_kg, die_casting_ton, surface_treatment) VALUES
|
||||
('VHB-AT-001', '变速箱阀体外壳', '阀体外壳', 'ADC12', 2.700, 444.44, 1.200, 400, '阳极氧化'),
|
||||
('VHB-BK-002', '制动阀体壳', '阀体外壳', 'HT250', 7.200, 388.89, 2.800, 630, '电泳涂装'),
|
||||
('VHB-SP-003', '转向器阀体壳', '阀体外壳', 'A356-T6', 2.650, 296.30, 0.785, 280, '钝化'),
|
||||
('VHB-EG-004', '发动机油路阀体壳', '阀体外壳', 'EN AC-4600', 2.700, 555.56, 1.500, 400, '阳极氧化'),
|
||||
('VHB-CL-005', '离合器控制阀体壳', '阀体外壳', 'ADC10', 2.700, 480.00, 1.300, 400, '阳极氧化'),
|
||||
('VHB-TR-006', '变速箱液压控制阀体', '阀体外壳', 'AlSi9Cu3', 2.700, 620.00, 1.670, 630, '电泳涂装'),
|
||||
('VHB-EB-007', '电子制动阀体壳', '阀体外壳', 'AlSi10MnMg', 2.650, 350.00, 0.930, 400, '钝化'),
|
||||
('VHB-QT-008', '悬架阻尼阀体壳', '阀体外壳', 'QT500-7', 7.100, 520.00, 3.690, 630, '喷涂'),
|
||||
('VHB-MN-009', '微型泵阀体壳', '阀体外壳', 'ADC12', 2.700, 96.30, 0.260, 160, '钝化'),
|
||||
('VHB-LG-010', '大型主阀体壳', '阀体外壳', 'ADC12', 2.700, 1037.00, 2.800, 800, '电泳涂装'),
|
||||
('VHB-TC-011', '涡轮增压控制阀体壳', '阀体外壳', 'ADC12', 2.700, 320.00, 0.860, 350, '钝化'),
|
||||
('VHB-EG-012', 'EGR阀体壳', '阀体外壳', 'ADC10', 2.700, 265.00, 0.720, 280, '钝化'),
|
||||
('VHB-WP-013', '水泵阀体壳', '阀体外壳', 'AlSi12', 2.650, 380.00, 1.010, 400, '阳极氧化'),
|
||||
('VHB-TM-014', '热管理阀体壳(新能源)', '阀体外壳', 'AlSi10MnMg', 2.650, 505.00, 1.340, 500, '钝化'),
|
||||
('VHB-OP-015', '机油泵阀体壳', '阀体外壳', 'ZL101', 2.650, 210.00, 0.560, 200, '钝化'),
|
||||
('VHB-HD-016', '重卡制动阀体壳', '阀体外壳', 'HT300', 7.250, 850.00, 6.160, 1000, '达克罗')
|
||||
ON CONFLICT (material_code) DO NOTHING;
|
||||
|
||||
-- 材质牌号对照表(13 条:铝合金/铸铝/灰铸铁/球墨铸铁)
|
||||
INSERT INTO material_grade_mapping (grade_group, gb_code, astm_code, jis_code, en_code, iso_code, internal_code, density, price_per_kg) VALUES
|
||||
('铝合金', 'ZAlSi10Cu(ADC12)', 'A380', 'ADC12', 'EN AC-4600', 'AlSi9Cu3', 'AL-ADC12', 2.700, 18.50),
|
||||
('铝合金', 'ZAlSi7Mg(A356)', 'A356', 'AC4C-T6', 'EN AC-4210', 'AlSi7Mg', 'AL-A356', 2.650, 20.00),
|
||||
('铝合金', 'ZAlSi9Cu(AlSi9Cu)', 'B443', 'AC4B', 'EN AC-4600', 'AlSi9Cu', 'AL-4600', 2.700, 18.00),
|
||||
('铸铝', 'ZL101', '356.0', 'AC4C', 'EN AC-4200', 'AlSi7', 'AL-ZL101', 2.650, 19.00),
|
||||
('铸铁', 'HT250', 'G2500', 'FC250', 'EN-GJL-250', 'GJL-250', 'IR-HT250', 7.200, 6.50),
|
||||
('球墨铸铁', 'QT500-7', 'D5007', 'FCD500', 'EN-GJS-500', 'GJS-500', 'IR-QT500', 7.100, 7.20),
|
||||
('铝合金', 'ZAlSi8Cu(ADC10)', 'A384', 'ADC10', 'EN AC-4650', 'AlSi8Cu', 'AL-ADC10', 2.700, 18.20),
|
||||
('铝合金', 'ZAlSi10MnMg', '—', '—', 'AlSi10MnMg', 'AlSi10MnMg', 'AL-S10MM', 2.650, 22.00),
|
||||
('铝合金', 'ZAlSi12', '413.0', 'AC3A', 'EN AC-4420', 'AlSi12', 'AL-S12', 2.650, 17.80),
|
||||
('铸铁', 'HT200', 'G3000', 'FC200', 'EN-GJL-200', 'GJL-200', 'IR-HT200', 7.200, 6.00),
|
||||
('铸铁', 'HT300', 'G3500', 'FC300', 'EN-GJL-300', 'GJL-300', 'IR-HT300', 7.250, 6.80),
|
||||
('球墨铸铁', 'QT450-10', 'D4510', 'FCD450', 'EN-GJS-450', 'GJS-450', 'IR-QT450', 7.100, 7.00),
|
||||
('球墨铸铁', 'QT600-3', 'D6003', 'FCD600', 'EN-GJS-600', 'GJS-600', 'IR-QT600', 7.100, 7.50)
|
||||
ON CONFLICT (internal_code) DO NOTHING;
|
||||
|
||||
-- 球铁牌号组别归位(历史数据曾归入"铸铁"组)
|
||||
UPDATE material_grade_mapping SET grade_group = '球墨铸铁' WHERE internal_code LIKE 'IR-QT%';
|
||||
|
||||
-- 压铸参数表(9 档吨位:160~1000T;故意不含 1250T 用于回退测试)
|
||||
INSERT INTO die_casting_params (tonnage, shot_weight_kg, cycle_time_sec, machine_rate, mold_cavities) VALUES
|
||||
( 160, 0.50, 15.00, 120.00, 2),
|
||||
( 200, 0.80, 18.00, 140.00, 2),
|
||||
( 280, 1.20, 22.00, 180.00, 1),
|
||||
( 350, 1.80, 26.00, 220.00, 1),
|
||||
( 400, 2.50, 30.00, 260.00, 1),
|
||||
( 500, 3.20, 36.00, 320.00, 1),
|
||||
( 630, 4.00, 42.00, 380.00, 1),
|
||||
( 800, 6.50, 55.00, 520.00, 1),
|
||||
(1000, 8.50, 65.00, 650.00, 1)
|
||||
ON CONFLICT (tonnage) DO NOTHING;
|
||||
|
||||
-- 机加工节拍表(26 条全规格谱系;故意不含 Φ25钻/M18攻 用于回退测试)
|
||||
INSERT INTO machining_cycle (operation_type, spec, cycle_time_sec, tool_rate, notes) VALUES
|
||||
('drill', 'Φ3', 2.00, 0.10, '通孔'),
|
||||
('drill', 'Φ4', 2.20, 0.12, '通孔'),
|
||||
('drill', 'Φ5', 2.50, 0.13, '通孔'),
|
||||
('drill', 'Φ6', 3.00, 0.15, '通孔'),
|
||||
('drill', 'Φ8', 4.00, 0.20, '通孔'),
|
||||
('drill', 'Φ10', 5.00, 0.25, '通孔'),
|
||||
('drill', 'Φ12', 6.00, 0.30, '通孔'),
|
||||
('drill', 'Φ14', 7.00, 0.35, '通孔'),
|
||||
('drill', 'Φ16', 8.00, 0.40, '通孔'),
|
||||
('drill', 'Φ20', 10.00, 0.50, '通孔'),
|
||||
('drill', 'Φ24', 12.00, 0.60, '通孔'),
|
||||
('tap', 'M4×0.7', 3.00, 0.15, '盲孔'),
|
||||
('tap', 'M5×0.8', 3.50, 0.18, '盲孔'),
|
||||
('tap', 'M6×1.0', 4.00, 0.20, '盲孔'),
|
||||
('tap', 'M8×1.25', 5.00, 0.25, '盲孔'),
|
||||
('tap', 'M10×1.5', 6.00, 0.30, '盲孔'),
|
||||
('tap', 'M12×1.75', 7.00, 0.35, '盲孔'),
|
||||
('tap', 'M14×2.0', 8.00, 0.40, '盲孔'),
|
||||
('tap', 'M16×2.0', 9.00, 0.45, '盲孔'),
|
||||
('ream', 'Φ6H7', 5.00, 0.45, '铰孔'),
|
||||
('ream', 'Φ8H7', 6.00, 0.50, '铰孔'),
|
||||
('ream', 'Φ10H7', 7.00, 0.60, '铰孔'),
|
||||
('ream', 'Φ12H7', 8.00, 0.70, '铰孔'),
|
||||
('ream', 'Φ16H7', 10.00, 0.90, '铰孔'),
|
||||
('ream', 'Φ20H7', 12.00, 1.10, '铰孔'),
|
||||
('mill', 'face', 12.00, 0.80, '铣面(单面)')
|
||||
ON CONFLICT (operation_type, spec) DO NOTHING;
|
||||
|
||||
-- 后处理费率表(10 种;故意不含"镀铬"用于回退测试)
|
||||
INSERT INTO post_process_rate (process_type, unit, rate) VALUES
|
||||
('喷砂', 'kg', 1.50),
|
||||
('钝化', 'kg', 2.00),
|
||||
('阳极氧化', 'kg', 5.00),
|
||||
('电泳涂装', 'kg', 3.50),
|
||||
('喷涂', 'kg', 2.50),
|
||||
('粉末喷涂', 'kg', 3.00),
|
||||
('磷化', 'kg', 1.80),
|
||||
('达克罗', 'kg', 4.00),
|
||||
('硬质阳极氧化', 'kg', 8.00),
|
||||
('化学镀镍', 'kg', 9.00)
|
||||
ON CONFLICT (process_type) DO NOTHING;
|
||||
|
||||
-- O2 机加工费率表(按材质族)
|
||||
INSERT INTO machining_rate (grade_group, rate_per_hour, notes) VALUES
|
||||
('铝合金', 60.00, '铝合金常规机加工费率'),
|
||||
('铸铝', 60.00, '铸铝同铝合金费率'),
|
||||
('铸铁', 75.00, '灰铸铁刀具损耗高,费率上浮'),
|
||||
('球墨铸铁', 80.00, '球铁强度高,机加工费率最高')
|
||||
ON CONFLICT (grade_group) DO NOTHING;
|
||||
|
||||
-- O1 材料价格历史表(2025-01 ~ 2026-08 共 20 个月 × 13 牌号,最新月系数=1.00 与对照表价格一致)
|
||||
WITH factors(price_date, al_factor, ir_factor) AS (VALUES
|
||||
('2025-01-01'::date, 0.92, 0.95), ('2025-02-01'::date, 0.93, 0.96),
|
||||
('2025-03-01'::date, 0.95, 0.96), ('2025-04-01'::date, 0.94, 0.97),
|
||||
('2025-05-01'::date, 0.96, 0.98), ('2025-06-01'::date, 0.98, 0.99),
|
||||
('2025-07-01'::date, 0.97, 0.98), ('2025-08-01'::date, 0.99, 1.00),
|
||||
('2025-09-01'::date, 1.01, 1.01), ('2025-10-01'::date, 1.00, 1.00),
|
||||
('2025-11-01'::date, 0.98, 0.99), ('2025-12-01'::date, 0.97, 0.98),
|
||||
('2026-01-01'::date, 0.99, 0.99), ('2026-02-01'::date, 1.02, 1.00),
|
||||
('2026-03-01'::date, 1.01, 1.01), ('2026-04-01'::date, 1.00, 1.00),
|
||||
('2026-05-01'::date, 0.99, 0.99), ('2026-06-01'::date, 1.01, 1.00),
|
||||
('2026-07-01'::date, 1.00, 1.00), ('2026-08-01'::date, 1.00, 1.00)
|
||||
)
|
||||
INSERT INTO material_price_history (internal_code, price_date, price_per_kg, source)
|
||||
SELECT m.internal_code, f.price_date,
|
||||
ROUND(m.price_per_kg * CASE WHEN m.grade_group IN ('铸铁', '球墨铸铁') THEN f.ir_factor ELSE f.al_factor END, 2),
|
||||
'市场参考价'
|
||||
FROM material_grade_mapping m CROSS JOIN factors f
|
||||
ON CONFLICT (internal_code, price_date) DO NOTHING;
|
||||
@@ -0,0 +1,28 @@
|
||||
"""PostgreSQL 连接池管理"""
|
||||
|
||||
import asyncpg
|
||||
import os
|
||||
|
||||
DB_CONFIG = {
|
||||
"host": os.getenv("ERP_DB_HOST", "192.168.1.206"),
|
||||
"port": int(os.getenv("ERP_DB_PORT", "5432")),
|
||||
"user": os.getenv("ERP_DB_USER", "postgres"),
|
||||
"password": os.getenv("ERP_DB_PASSWORD", "digiwin"),
|
||||
"database": os.getenv("ERP_DB_NAME", "smart_quotation_auto"),
|
||||
}
|
||||
|
||||
_pool: asyncpg.Pool | None = None
|
||||
|
||||
|
||||
async def get_pool() -> asyncpg.Pool:
|
||||
global _pool
|
||||
if _pool is None:
|
||||
_pool = await asyncpg.create_pool(**DB_CONFIG, min_size=2, max_size=10)
|
||||
return _pool
|
||||
|
||||
|
||||
async def close_pool():
|
||||
global _pool
|
||||
if _pool:
|
||||
await _pool.close()
|
||||
_pool = None
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""初始化 ERP 数据库表和种子数据"""
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from db import get_pool, close_pool
|
||||
|
||||
async def main():
|
||||
pool = await get_pool()
|
||||
sql_path = Path(__file__).parent.parent / "sql" / "init.sql"
|
||||
sql = sql_path.read_text(encoding="utf-8")
|
||||
await pool.execute(sql)
|
||||
print("ERP 数据库初始化完成")
|
||||
await close_pool()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
MCP for ERP — 汽车零部件智能报价 ERP 数据服务
|
||||
使用 MCPServer 框架,5 个工具:物料查询、材质对照、压铸参数、机加工估算、成本核算
|
||||
HTTP 模式,支持远程调用
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from mcp.server import MCPServer
|
||||
from db import get_pool
|
||||
|
||||
app = MCPServer(
|
||||
name="mcp-for-erp-auto",
|
||||
description="汽车零部件智能报价 ERP 数据服务",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 工具 1: query_material_master — 查询物料主数据
|
||||
# ============================================================
|
||||
|
||||
@app.tool()
|
||||
async def query_material_master(
|
||||
material_code: str = None,
|
||||
part_category: str = None,
|
||||
material_grade: str = None,
|
||||
die_casting_ton: int = None
|
||||
) -> str:
|
||||
"""查询 ERP 物料主数据。支持按物料编码精确查询,或按零件类别、材质、吨位等参数组合查询。返回物料列表,包含体积、重量、吨位等关键参数。"""
|
||||
pool = await get_pool()
|
||||
query = "SELECT * FROM material_master WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if material_code:
|
||||
query += f" AND material_code = ${len(params)+1}"
|
||||
params.append(material_code)
|
||||
if part_category:
|
||||
query += f" AND part_category = ${len(params)+1}"
|
||||
params.append(part_category)
|
||||
if material_grade:
|
||||
query += f" AND material_grade = ${len(params)+1}"
|
||||
params.append(material_grade)
|
||||
if die_casting_ton:
|
||||
query += f" AND die_casting_ton = ${len(params)+1}"
|
||||
params.append(die_casting_ton)
|
||||
|
||||
rows = await pool.fetch(query, *params)
|
||||
materials = [dict(row) for row in rows]
|
||||
|
||||
return json.dumps({"total": len(materials), "materials": materials}, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 工具 2: match_material_grade — 材质牌号对照
|
||||
# ============================================================
|
||||
|
||||
@app.tool()
|
||||
async def match_material_grade(customer_grade: str, standard: str = None) -> str:
|
||||
"""将客户指定材质牌号自动对照为企业内部等效牌号。支持 GB/ASTM/JIS/EN/ISO 标准体系,返回等效牌号、密度、铝锭参考价。"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
SELECT * FROM material_grade_mapping
|
||||
WHERE LOWER(gb_code) LIKE LOWER($1) OR LOWER(astm_code) LIKE LOWER($1)
|
||||
OR LOWER(jis_code) LIKE LOWER($1) OR LOWER(en_code) LIKE LOWER($1)
|
||||
OR LOWER(iso_code) LIKE LOWER($1) OR LOWER(internal_code) LIKE LOWER($1)
|
||||
"""
|
||||
rows = await pool.fetch(query, f"%{customer_grade}%")
|
||||
|
||||
# 牌号带热处理状态后缀(如 A356-T6 / ADC12-F)时,去后缀重试
|
||||
if not rows:
|
||||
stripped = customer_grade
|
||||
for _ in range(2):
|
||||
if "-" not in stripped:
|
||||
break
|
||||
stripped = stripped.rsplit("-", 1)[0]
|
||||
rows = await pool.fetch(query, f"%{stripped}%")
|
||||
if rows:
|
||||
break
|
||||
|
||||
if not rows:
|
||||
return json.dumps({"matched": False, "message": f"未找到材质 {customer_grade}"}, ensure_ascii=False, indent=2)
|
||||
|
||||
mappings = [dict(row) for row in rows]
|
||||
best = mappings[0]
|
||||
return json.dumps({
|
||||
"matched": True, "customer_grade": customer_grade, "internal_code": best["internal_code"],
|
||||
"density": float(best["density"]), "price_per_kg": float(best["price_per_kg"]), "all_mappings": mappings
|
||||
}, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 工具 3: get_die_casting_params — 获取压铸参数
|
||||
# ============================================================
|
||||
|
||||
@app.tool()
|
||||
async def get_die_casting_params(material_code: str, volume_cc: float = None) -> str:
|
||||
"""根据物料编码获取压铸参数。返回压铸机吨位、模次节拍、铝水重量、压铸成本。铝水重量 = 体积 × 密度。"""
|
||||
pool = await get_pool()
|
||||
|
||||
material = await pool.fetchrow("SELECT * FROM material_master WHERE material_code = $1", material_code)
|
||||
if not material:
|
||||
return json.dumps({"success": False, "message": f"未找到物料 {material_code}"}, ensure_ascii=False, indent=2)
|
||||
|
||||
vol = volume_cc or float(material["volume_cc"])
|
||||
density = float(material["density"])
|
||||
tonnage = material["die_casting_ton"]
|
||||
aluminum_weight_kg = vol * density / 1000 * 1.05
|
||||
|
||||
casting = await pool.fetchrow("SELECT * FROM die_casting_params WHERE tonnage = $1", tonnage)
|
||||
if not casting:
|
||||
return json.dumps({"success": False, "message": f"未找到 {tonnage}T 压铸机参数"}, ensure_ascii=False, indent=2)
|
||||
|
||||
cycle_hours = float(casting["cycle_time_sec"]) / 3600
|
||||
casting_cost = cycle_hours * float(casting["machine_rate"]) / casting["mold_cavities"]
|
||||
|
||||
return json.dumps({
|
||||
"success": True, "material_code": material_code, "volume_cc": vol, "density": density,
|
||||
"aluminum_weight_kg": round(aluminum_weight_kg, 3), "die_casting_ton": tonnage,
|
||||
"cycle_time_sec": float(casting["cycle_time_sec"]), "machine_rate": float(casting["machine_rate"]),
|
||||
"mold_cavities": casting["mold_cavities"], "casting_cost_per_part": round(casting_cost, 2)
|
||||
}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 工具 4: get_machining_estimate — 机加工工时估算
|
||||
# ============================================================
|
||||
|
||||
@app.tool()
|
||||
async def get_machining_estimate(
|
||||
material_code: str,
|
||||
drill_holes: list[dict] = None,
|
||||
tap_holes: list[dict] = None,
|
||||
ream_holes: list[dict] = None,
|
||||
mill_faces: int = 0
|
||||
) -> str:
|
||||
"""根据孔清单估算机加工工时。钻孔、攻丝、铰孔、铣面均有固化节拍,逐项累加得到总工时和成本。"""
|
||||
pool = await get_pool()
|
||||
drill_holes = drill_holes or []
|
||||
tap_holes = tap_holes or []
|
||||
ream_holes = ream_holes or []
|
||||
|
||||
total_time_sec = 0
|
||||
total_tool_cost = 0
|
||||
details = []
|
||||
|
||||
for hole in drill_holes:
|
||||
spec = hole.get("spec", "Φ8")
|
||||
count = hole.get("count", 1)
|
||||
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='drill' AND spec=$1", spec)
|
||||
if row:
|
||||
time_sec = float(row["cycle_time_sec"]) * count
|
||||
tool_cost = float(row["tool_rate"]) * count
|
||||
total_time_sec += time_sec
|
||||
total_tool_cost += tool_cost
|
||||
details.append({"operation": "drill", "spec": spec, "count": count, "total_time_sec": time_sec})
|
||||
|
||||
for hole in tap_holes:
|
||||
spec = hole.get("spec", "M8×1.25")
|
||||
count = hole.get("count", 1)
|
||||
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='tap' AND spec=$1", spec)
|
||||
if row:
|
||||
time_sec = float(row["cycle_time_sec"]) * count
|
||||
tool_cost = float(row["tool_rate"]) * count
|
||||
total_time_sec += time_sec
|
||||
total_tool_cost += tool_cost
|
||||
details.append({"operation": "tap", "spec": spec, "count": count, "total_time_sec": time_sec})
|
||||
|
||||
for hole in ream_holes:
|
||||
spec = hole.get("spec", "Φ8H7")
|
||||
count = hole.get("count", 1)
|
||||
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='ream' AND spec=$1", spec)
|
||||
if row:
|
||||
time_sec = float(row["cycle_time_sec"]) * count
|
||||
tool_cost = float(row["tool_rate"]) * count
|
||||
total_time_sec += time_sec
|
||||
total_tool_cost += tool_cost
|
||||
details.append({"operation": "ream", "spec": spec, "count": count, "total_time_sec": time_sec})
|
||||
|
||||
if mill_faces > 0:
|
||||
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='mill' AND spec='face'")
|
||||
if row:
|
||||
time_sec = float(row["cycle_time_sec"]) * mill_faces
|
||||
tool_cost = float(row["tool_rate"]) * mill_faces
|
||||
total_time_sec += time_sec
|
||||
total_tool_cost += tool_cost
|
||||
details.append({"operation": "mill", "spec": "face", "count": mill_faces, "total_time_sec": time_sec})
|
||||
|
||||
return json.dumps({
|
||||
"success": True, "material_code": material_code, "total_time_sec": round(total_time_sec, 2),
|
||||
"total_time_hours": round(total_time_sec / 3600, 4), "total_tool_cost": round(total_tool_cost, 2), "details": details
|
||||
}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 工具 5: calculate_part_cost — 汇总核价
|
||||
# ============================================================
|
||||
|
||||
@app.tool()
|
||||
async def calculate_part_cost(
|
||||
material_code: str,
|
||||
annual_qty: int,
|
||||
drill_holes: list[dict] = None,
|
||||
tap_holes: list[dict] = None,
|
||||
ream_holes: list[dict] = None,
|
||||
mill_faces: int = 0,
|
||||
overhead_rate: float = 0.12,
|
||||
profit_rate: float = 0.15
|
||||
) -> str:
|
||||
"""汇总核价:材料成本(铝水重量×单价)+ 压铸成本 + 机加工成本 + 后处理成本 + 管理费 + 利润。返回完整成本明细和最终报价。"""
|
||||
pool = await get_pool()
|
||||
drill_holes = drill_holes or []
|
||||
tap_holes = tap_holes or []
|
||||
ream_holes = ream_holes or []
|
||||
|
||||
material = await pool.fetchrow("SELECT * FROM material_master WHERE material_code = $1", material_code)
|
||||
if not material:
|
||||
return json.dumps({"success": False, "message": f"未找到物料 {material_code}"}, ensure_ascii=False, indent=2)
|
||||
|
||||
vol = float(material["volume_cc"])
|
||||
density = float(material["density"])
|
||||
tonnage = material["die_casting_ton"]
|
||||
surface_treatment = material["surface_treatment"]
|
||||
|
||||
grade_mapping = await pool.fetchrow(
|
||||
"SELECT * FROM material_grade_mapping WHERE internal_code = $1 OR jis_code = $1 OR gb_code = $1 OR astm_code = $1 OR en_code = $1 OR iso_code = $1 LIMIT 1",
|
||||
material["material_grade"]
|
||||
)
|
||||
aluminum_price = 18.50
|
||||
internal_code = None
|
||||
grade_group = None
|
||||
price_basis_month = None
|
||||
if grade_mapping:
|
||||
internal_code = grade_mapping["internal_code"]
|
||||
grade_group = grade_mapping["grade_group"]
|
||||
aluminum_price = float(grade_mapping["price_per_kg"])
|
||||
try:
|
||||
price_row = await pool.fetchrow(
|
||||
"SELECT price_per_kg, price_date FROM material_price_history WHERE internal_code = $1 ORDER BY price_date DESC LIMIT 1",
|
||||
internal_code
|
||||
)
|
||||
if price_row:
|
||||
aluminum_price = float(price_row["price_per_kg"])
|
||||
price_basis_month = price_row["price_date"].strftime("%Y-%m")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
aluminum_weight_kg = vol * density / 1000 * 1.05
|
||||
material_cost = aluminum_weight_kg * aluminum_price
|
||||
|
||||
casting = await pool.fetchrow("SELECT * FROM die_casting_params WHERE tonnage = $1", tonnage)
|
||||
casting_cost = (float(casting["cycle_time_sec"]) / 3600) * float(casting["machine_rate"]) / casting["mold_cavities"]
|
||||
|
||||
machining_rate_per_hour = 60.0
|
||||
if grade_group:
|
||||
try:
|
||||
rate_row = await pool.fetchrow("SELECT rate_per_hour FROM machining_rate WHERE grade_group = $1", grade_group)
|
||||
if rate_row:
|
||||
machining_rate_per_hour = float(rate_row["rate_per_hour"])
|
||||
except Exception:
|
||||
pass
|
||||
total_machining_time_sec = 0
|
||||
|
||||
for hole in drill_holes:
|
||||
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='drill' AND spec=$1", hole.get("spec", "Φ8"))
|
||||
if row:
|
||||
total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1)
|
||||
|
||||
for hole in tap_holes:
|
||||
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='tap' AND spec=$1", hole.get("spec", "M8×1.25"))
|
||||
if row:
|
||||
total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1)
|
||||
|
||||
for hole in ream_holes:
|
||||
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='ream' AND spec=$1", hole.get("spec", "Φ8H7"))
|
||||
if row:
|
||||
total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1)
|
||||
|
||||
if mill_faces > 0:
|
||||
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='mill' AND spec='face'")
|
||||
if row:
|
||||
total_machining_time_sec += float(row["cycle_time_sec"]) * mill_faces
|
||||
|
||||
machining_cost = (total_machining_time_sec / 3600) * machining_rate_per_hour
|
||||
|
||||
post_process = await pool.fetchrow("SELECT rate FROM post_process_rate WHERE process_type = $1", surface_treatment)
|
||||
post_process_cost = float(material["net_weight_kg"]) * float(post_process["rate"]) if post_process else 0
|
||||
|
||||
total_cost = material_cost + casting_cost + machining_cost + post_process_cost
|
||||
overhead = total_cost * overhead_rate
|
||||
unit_price = (total_cost + overhead) * (1 + profit_rate)
|
||||
|
||||
return json.dumps({
|
||||
"success": True, "material_code": material_code, "part_name": material["part_name"],
|
||||
"cost_breakdown": {
|
||||
"material_cost": round(material_cost, 2), "aluminum_weight_kg": round(aluminum_weight_kg, 3),
|
||||
"aluminum_price_per_kg": aluminum_price, "price_basis_month": price_basis_month,
|
||||
"internal_code": internal_code, "grade_group": grade_group,
|
||||
"casting_cost": round(casting_cost, 2),
|
||||
"die_casting_ton": tonnage, "machining_cost": round(machining_cost, 2),
|
||||
"machining_hours": round(total_machining_time_sec / 3600, 4),
|
||||
"machining_rate_per_hour": machining_rate_per_hour,
|
||||
"post_process_cost": round(post_process_cost, 2), "surface_treatment": surface_treatment
|
||||
},
|
||||
"total_cost": round(total_cost, 2), "overhead_rate": overhead_rate, "overhead": round(overhead, 2),
|
||||
"profit_rate": profit_rate, "unit_price": round(unit_price, 2), "currency": "CNY"
|
||||
}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 启动 HTTP Server
|
||||
# ============================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(app.run_streamable_http_async(host="0.0.0.0", port=8001, streamable_http_path="/mcp"))
|
||||
Reference in New Issue
Block a user