init: 汽车零部件智能报价 MCP 服务(ERP + CRM)

This commit is contained in:
dongsk
2026-08-08 20:11:23 +08:00
commit ae992f45a1
16 changed files with 1502 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
**/.DS_Store
**/__pycache__
**/*.pyc
**/*.pyo
.git
.gitignore
*.md
.pg-init
+79
View File
@@ -0,0 +1,79 @@
# ===== Python =====
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg
*.egg-info/
.eggs/
build/
dist/
sdist/
wheels/
pip-wheel-metadata/
share/python-wheels/
installs/
# 测试 / 类型检查 / lint 缓存
.pytest_cache/
.mypy_cache/
.ruff_cache/
.pyre/
.pytype/
.dmypy.json
dmypy.json
# 虚拟环境
.venv/
venv/
env/
ENV/
.python-version
# ===== 环境变量与密钥(含数据库密码等,切勿入库)=====
.env
.env.*
!.env.example
*.env
secrets/
*.pem
*.key
# ===== IDE / 编辑器 =====
.vscode/
.idea/
*.swp
*.swo
*~
.project
.settings/
# ===== 操作系统 =====
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
desktop.ini
# ===== Docker(镜像/卷/构建产物,不入库)=====
# 容器运行时数据
.docker/
# 本地 PostgreSQL 数据与初始化副本(历史遗留)
.pg-init/
*.sql.gz
# ===== 日志与临时文件 =====
*.log
logs/
tmp/
*.tmp
*.bak
# ===== Jupyter =====
.ipynb_checkpoints/
# ===== 项目本地脚本/快照 =====
*.local
+20
View File
@@ -0,0 +1,20 @@
# syntax=docker/dockerfile:1
# 两个 MCP 服务共用此 Dockerfile,通过构建参数 SERVICE 选择
# docker build --build-arg SERVICE=mcp-for-erp -t mcp-for-erp .
# docker build --build-arg SERVICE=mcp-for-crm -t mcp-for-crm .
FROM python:3.12-slim
ARG SERVICE
WORKDIR /app
# 依赖单独成层,利用缓存
COPY ${SERVICE}/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# 拷贝服务源码与 SQL(SQL 供 seed.py 使用,远程库已初始化时无需运行)
COPY ${SERVICE}/src /app/src
COPY ${SERVICE}/sql /app/sql
WORKDIR /app/src
EXPOSE 8000
CMD ["python", "server.py"]
+41
View File
@@ -0,0 +1,41 @@
# 汽车零部件智能报价 — 两个 MCP 服务编排
# 数据库为外部 PostgreSQL(192.168.1.206:5432),不在本编排内
# 启动:docker compose up -d
# 停止:docker compose down
# 日志:docker compose logs -f
# 网络模式 host:容器直接共享宿主网络栈,绕开 Docker 桥接 FORWARD 链,
# 便于局域网其他机器经 192.168.1.119:8001/8002 直接访问(无需 ports 映射)
services:
mcp-for-erp:
build:
context: .
dockerfile: Dockerfile
args:
SERVICE: mcp-for-erp
image: mcp-for-erp:latest
container_name: mcp-for-erp
network_mode: host
environment:
- ERP_DB_HOST=192.168.1.206
- ERP_DB_PORT=5432
- ERP_DB_USER=postgres
- ERP_DB_PASSWORD=digiwin
- ERP_DB_NAME=smart_quotation_auto
restart: unless-stopped
mcp-for-crm:
build:
context: .
dockerfile: Dockerfile
args:
SERVICE: mcp-for-crm
image: mcp-for-crm:latest
container_name: mcp-for-crm
network_mode: host
environment:
- CRM_DB_HOST=192.168.1.206
- CRM_DB_PORT=5432
- CRM_DB_USER=postgres
- CRM_DB_PASSWORD=digiwin
- CRM_DB_NAME=smart_quotation_auto
restart: unless-stopped
+48
View File
@@ -0,0 +1,48 @@
# MCP for CRM — 汽车零部件智能报价 CRM 数据服务
## 概述
为汽车零部件(小型阀体外壳)智能报价智能体提供 CRM 数据访问能力,通过 MCP 协议暴露 5 个工具。
## 技术栈
- **框架**: MCPServer(mcp 2.0.0 内置高级框架)
- **语言**: Python 3.11+
- **数据库**: PostgreSQL 15+
- **传输协议**: Streamable HTTP(远程调用)
## 工具清单
| 工具名 | 功能 |
|---|---|
| `get_customer_info` | 查询客户信息(OEM/Tier、信用、折扣率) |
| `create_inquiry` | 创建询价记录(关联图纸号、零件号、年用量) |
| `save_quotation` | 保存报价单(含模具费、单价、成本明细、版本管理) |
| `get_quotation_history` | 查询历史报价(按客户/零件号/日期) |
| `update_opportunity` | 商机状态管理(lead→quoted→won/lost) |
## 快速开始
```bash
pip install -r requirements.txt
cd src
python seed.py # 初始化数据库(可选,数据已在共享数据库中)
python server.py # 启动 MCP Server(端口 8002)
```
## 服务地址
- **HTTP 端点**: `http://0.0.0.0:8002/mcp`
- **传输协议**: Streamable HTTP
## 数据库配置
通过环境变量配置:
| 变量 | 默认值 | 说明 |
|---|---|---|
| `CRM_DB_HOST` | 192.168.1.206 | PostgreSQL 主机 |
| `CRM_DB_PORT` | 5432 | 端口 |
| `CRM_DB_USER` | postgres | 用户名 |
| `CRM_DB_PASSWORD` | digiwin | 密码 |
| `CRM_DB_NAME` | smart_quotation_auto | 数据库名 |
+3
View File
@@ -0,0 +1,3 @@
mcp>=1.0.0
pydantic>=2.0.0
asyncpg>=0.30.0
+251
View File
@@ -0,0 +1,251 @@
-- ============================================================
-- MCP for CRM — 汽车零部件智能报价 CRM 数据服务
-- 数据库:smart_quotation_auto
-- v3: 幂等可重跑(唯一约束+去重);扩充至 20 客户 + 历史询价/报价/商机全谱系
-- ============================================================
-- 1. 客户
CREATE TABLE IF NOT EXISTS customer (
customer_code VARCHAR(50) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
oem_tier VARCHAR(20) NOT NULL,
credit_level VARCHAR(5) DEFAULT 'B',
discount_rate DECIMAL(4,3) DEFAULT 0,
payment_days INTEGER DEFAULT 30,
contact_person VARCHAR(50),
phone VARCHAR(30),
email VARCHAR(100),
industry VARCHAR(50),
region VARCHAR(50),
total_orders INTEGER DEFAULT 0,
total_amount DECIMAL(14,2) DEFAULT 0,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 询价记录
CREATE TABLE IF NOT EXISTS inquiry (
inquiry_id VARCHAR(50) PRIMARY KEY,
customer_code VARCHAR(50) NOT NULL REFERENCES customer(customer_code),
inquiry_date DATE NOT NULL DEFAULT CURRENT_DATE,
drawing_number VARCHAR(100),
part_number VARCHAR(100),
annual_volume INTEGER,
target_price DECIMAL(10,2),
status VARCHAR(20) DEFAULT 'pending',
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. 询价明细行
CREATE TABLE IF NOT EXISTS inquiry_item (
id SERIAL PRIMARY KEY,
inquiry_id VARCHAR(50) NOT NULL REFERENCES inquiry(inquiry_id),
line_no INTEGER NOT NULL,
part_name VARCHAR(200),
part_number VARCHAR(100),
material_grade VARCHAR(50),
volume_cc DECIMAL(10,2),
annual_qty INTEGER,
unit VARCHAR(10) DEFAULT '件',
remarks TEXT
);
-- 4. 报价单
CREATE TABLE IF NOT EXISTS quotation (
quotation_id VARCHAR(50) PRIMARY KEY,
inquiry_id VARCHAR(50) REFERENCES inquiry(inquiry_id),
customer_code VARCHAR(50) NOT NULL REFERENCES customer(customer_code),
quotation_date DATE NOT NULL DEFAULT CURRENT_DATE,
valid_until DATE,
version INTEGER DEFAULT 1,
status VARCHAR(20) DEFAULT 'draft',
mold_cost DECIMAL(12,2) DEFAULT 0,
unit_price DECIMAL(10,2),
annual_volume INTEGER,
total_annual DECIMAL(14,2),
subtotal DECIMAL(14,2),
tax_rate DECIMAL(4,3) DEFAULT 0.130,
tax_amount DECIMAL(14,2),
total_amount DECIMAL(14,2),
currency VARCHAR(10) DEFAULT 'CNY',
payment_terms VARCHAR(200) DEFAULT '月结30天',
delivery_terms VARCHAR(200) DEFAULT '含税含运',
created_by VARCHAR(50) DEFAULT 'AI智能报价',
approved_by VARCHAR(50),
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. 报价成本明细
CREATE TABLE IF NOT EXISTS quotation_cost_detail (
id SERIAL PRIMARY KEY,
quotation_id VARCHAR(50) NOT NULL REFERENCES quotation(quotation_id),
material_cost DECIMAL(10,4),
casting_cost DECIMAL(10,4),
machining_cost DECIMAL(10,4),
post_process_cost DECIMAL(10,4),
overhead_rate DECIMAL(4,3),
profit_rate DECIMAL(4,3),
total_cost DECIMAL(10,4),
unit_price DECIMAL(10,4)
);
-- 6. 商机
CREATE TABLE IF NOT EXISTS opportunity (
opportunity_id VARCHAR(50) PRIMARY KEY,
customer_code VARCHAR(50) NOT NULL REFERENCES customer(customer_code),
title VARCHAR(200) NOT NULL,
stage VARCHAR(20) DEFAULT 'lead',
expected_amount DECIMAL(14,2) DEFAULT 0,
probability INTEGER DEFAULT 20,
oem_program VARCHAR(200),
expected_close_date DATE,
source VARCHAR(100),
created_by VARCHAR(50),
created_at DATE DEFAULT CURRENT_DATE,
updated_at DATE DEFAULT CURRENT_DATE
);
-- ============================================================
-- 幂等性:去重 + 唯一约束(可重复执行)
-- ============================================================
DELETE FROM inquiry_item a USING inquiry_item b
WHERE a.id > b.id AND a.inquiry_id = b.inquiry_id AND a.line_no = b.line_no;
DELETE FROM quotation_cost_detail a USING quotation_cost_detail b
WHERE a.id > b.id AND a.quotation_id = b.quotation_id;
DO $$ BEGIN
ALTER TABLE inquiry_item ADD CONSTRAINT uq_inquiry_item UNIQUE (inquiry_id, line_no);
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE quotation_cost_detail ADD CONSTRAINT uq_quotation_cost UNIQUE (quotation_id);
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
-- 多版本报价支持:同一询价单不限报价次数
-- 1) 规范化历史版本号:同一询价单下按时间序重写为 1..n(幂等,已是 1..n 的行不变)
WITH ranked AS (
SELECT quotation_id,
ROW_NUMBER() OVER (PARTITION BY inquiry_id ORDER BY created_at, quotation_id) AS rn
FROM quotation
WHERE inquiry_id IS NOT NULL
)
UPDATE quotation q
SET version = r.rn
FROM ranked r
WHERE q.quotation_id = r.quotation_id AND q.version <> r.rn;
-- 2) 同一询价单内版本号唯一(并发防重)
DO $$ BEGIN
ALTER TABLE quotation ADD CONSTRAINT uq_quotation_version UNIQUE (inquiry_id, version);
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
-- 3) 版本链查询索引
CREATE INDEX IF NOT EXISTS idx_quotation_inquiry ON quotation(inquiry_id);
-- ============================================================
-- 种子数据
-- ============================================================
-- 客户(20 家:OEM×7 / Tier1×7 / Tier2×6;信用 A×8 / B×6 / C×2 / D×1 / 无评级×3)
INSERT INTO customer (customer_code, name, oem_tier, credit_level, discount_rate, payment_days, contact_person, phone, email, industry, region, total_orders, total_amount, notes) VALUES
('OEM-2024-001', '某日系主机厂', 'OEM', 'A', 0.050, 60, '张工', '021-5555-0001', 'zhang@oem-jp.example.com', '汽车', '上海', 45, 28000000, '长期合作,主要采购铝合金阀体壳'),
('OEM-2024-002', '某德系主机厂', 'OEM', 'A', 0.030, 45, '李总', '010-8888-0002', 'li@oem-de.example.com', '汽车', '北京', 30, 15000000, '质量要求高,偏好铸铁件'),
('OEM-2024-003', '某新能源主机厂', 'OEM', 'A', 0.040, 60, '刘工', '0551-2222-0005', 'liu@oem-nev.example.com', '汽车', '合肥', 22, 9800000, '新能源三电系统配套,增长快'),
('OEM-2024-004', '某美系主机厂', 'OEM', 'A', 0.030, 45, '孙总', '027-3333-0006', 'sun@oem-us.example.com', '汽车', '武汉', 26, 11200000, '混合动力变速箱项目'),
('OEM-2025-001', '某商用车主机厂', 'OEM', 'B', 0.020, 90, '赵工', '0431-4444-0007', 'zhao@oem-cv.example.com', '商用车', '长春', 12, 4600000, '长账期客户,注意现金流'),
('OEM-2025-002', '某乘商两用主机厂', 'OEM', 'B', 0.025, 60, '周工', '0772-5555-0008', 'zhou@oem-mpv.example.com', '汽车', '柳州', 9, 3200000, '乘用车+商用车双线采购'),
('OEM-2026-001', '某智能电动新势力', 'OEM', 'A', 0.050, 45, '吴总', '021-6666-0009', 'wu@oem-ev.example.com', '新能源', '上海', 3, 850000, '高折扣新OEM,战略培育客户'),
('T1-2025-001', '某博世供应商', 'Tier1', 'B', 0.020, 30, '王经理','0512-6666-0003', 'wang@tier1-bs.example.com', '汽车零部件', '江苏苏州', 18, 5600000, '变速箱配套阀体'),
('T1-2025-002', '某大陆供应商', 'Tier1', 'B', 0.000, 30, '陈工', '020-7777-0004', 'chen@tier1-cr.example.com', '汽车零部件', '广东广州', 8, 1200000, '制动系统配套,新开发客户'),
('T1-2025-003', '某采埃孚供应商', 'Tier1', 'A', 0.020, 45, '郑总', '021-7777-0010', 'zheng@tier1-zf.example.com', '汽车零部件', '上海', 15, 4800000, 'ZF悬架系统配套'),
('T1-2025-004', '某电装系供应商', 'Tier1', 'A', 0.025, 45, '林总', '020-8888-0011', 'lin@tier1-ds.example.com', '汽车零部件', '广州', 20, 6300000, '日系热管理系统配套'),
('T1-2025-005', '某日立安斯泰莫供应商', 'Tier1', 'A', 0.020, 45, '徐总', '0411-9999-0012', 'xu@tier1-ha.example.com', '汽车零部件', '大连', 14, 4100000, '日系制动/转向配套'),
('T1-2026-001', '某制动系统供应商', 'Tier1', 'B', 0.015, 30, '何工', '023-1111-0013', 'he@tier1-bk.example.com', '汽车零部件', '重庆', 6, 1500000, '国产线控制动项目'),
('T1-2026-002', '某液压系统供应商', 'Tier1', 'B', 0.010, 60, '高工', '0731-2222-0014', 'gao@tier1-hy.example.com', '工程机械', '长沙', 5, 980000, '液压阀体,账期较长'),
('T2-2025-001', '某售后市场贸易商', 'Tier2', 'C', 0.000, 0, '罗经理','0571-3333-0015', 'luo@am-trade.example.com', '售后市场', '杭州', 3, 210000, '货到付款,信用C级,注意风险'),
('T2-2025-002', '某智能底盘初创公司', 'Tier2', '无评级', 0.000, 30, '宋工', '0512-4444-0016', 'song@chassis.example.com', '智能底盘', '苏州', 0, 0, '零订单历史新客户,需信用评审'),
('T2-2025-003', '某非道路机械贸易商', 'Tier2', 'D', 0.000, 0, '蒋经理','0537-5555-0017', 'jiang@offroad.example.com', '非道路机械', '济宁', 1, 45000, '信用D级,仅接受预付款订单'),
('T2-2025-004', '某改装件连锁商', 'Tier2', 'C', 0.000, 0, '邓总', '028-5555-0018', 'deng@mod-chain.example.com', '售后市场', '成都', 5, 320000, '改装市场连锁,货到付款'),
('T2-2026-001', '某机器人关节初创公司', 'Tier2', '无评级', 0.000, 30, '冯工', '0755-6666-0019', 'feng@robot-joint.example.com','机器人', '深圳', 0, 0, '零历史,人形机器人关节部件'),
('T2-2026-002', '某低空经济无人机公司', 'Tier2', '无评级', 0.000, 30, '曹工', '0755-7777-0020', 'cao@uav.example.com', '低空经济', '深圳', 0, 0, '新兴行业,eVTOL液压部件询价')
ON CONFLICT (customer_code) DO NOTHING;
-- 历史询价(2024-2025 闭环 + 2026 在途)
INSERT INTO inquiry (inquiry_id, customer_code, inquiry_date, drawing_number, part_number, annual_volume, target_price, status, notes) VALUES
('INQ-2024-9005', 'OEM-2024-001', '2024-10-20', 'DWG-VHB-EG-Rev1', 'P-VHB-004', 200000, 55.00, 'completed', '2024年发动机油路阀体壳询价,已成交'),
('INQ-2025-9001', 'OEM-2024-001', '2025-09-25', 'DWG-VBH-AT-Rev2', 'P-VBH-001', 300000, 96.00, 'completed', '2025年变速箱阀体外壳年价谈判'),
('INQ-2025-9002', 'OEM-2024-002', '2025-08-10', 'DWG-VHB-BK-Rev2', 'P-VHB-002', 150000, 155.00, 'completed', '2025年制动阀体壳询价,已中标'),
('INQ-2025-9003', 'T1-2025-001', '2025-05-28', 'DWG-VHB-SP-Rev1', 'P-VHB-003', 150000, 25.00, 'lost', '转向器阀体壳,因报价偏高丢单'),
('INQ-2025-9006', 'OEM-2024-002', '2025-11-05', 'DWG-VBH-AT-Rev2', 'P-VBH-001', 100000, 100.00, 'completed', '德系小批量采购变速箱阀体外壳'),
('INQ-2026-0006', 'OEM-2024-004', '2026-08-05', 'DWG-VHB-TC-Rev1', 'P-VHB-011', 120000, 48.00, 'pending', '涡轮增压控制阀体壳新询价'),
('INQ-2026-0007', 'OEM-2025-001', '2026-08-06', 'DWG-VHB-HD-Rev1', 'P-VHB-016', 60000, 260.00, 'processing', '重卡制动阀体壳,1000T大件')
ON CONFLICT (inquiry_id) DO NOTHING;
-- 询价明细行(与询价单一一对应)
INSERT INTO inquiry_item (inquiry_id, line_no, part_name, part_number, material_grade, volume_cc, annual_qty, remarks) VALUES
('INQ-2024-9005', 1, '发动机油路阀体壳', 'P-VHB-004', 'EN AC-4600', 555.56, 200000, '400T压铸,阳极氧化'),
('INQ-2025-9001', 1, '变速箱阀体外壳', 'P-VBH-001', 'ADC12', 444.44, 300000, '400T压铸,阳极氧化'),
('INQ-2025-9002', 1, '制动阀体壳', 'P-VHB-002', 'HT250', 388.89, 150000, '630T压铸,电泳涂装'),
('INQ-2025-9003', 1, '转向器阀体壳', 'P-VHB-003', 'A356-T6', 296.30, 150000, '280T压铸,钝化'),
('INQ-2025-9006', 1, '变速箱阀体外壳', 'P-VBH-001', 'ADC12', 444.44, 100000, '小批量'),
('INQ-2026-0006', 1, '涡轮增压控制阀体壳', 'P-VHB-011', 'ADC12', 320.00, 120000, '350T压铸,钝化'),
('INQ-2026-0007', 1, '重卡制动阀体壳', 'P-VHB-016', 'HT300', 850.00, 60000, '1000T压铸,达克罗')
ON CONFLICT (inquiry_id, line_no) DO NOTHING;
-- 基础询价(保留原种子)
INSERT INTO inquiry (inquiry_id, customer_code, inquiry_date, drawing_number, part_number, annual_volume, target_price, status, notes) VALUES
('INQ-2026-0001', 'OEM-2024-001', '2026-08-01', 'DWG-VBH-AT-Rev3', 'P-VBH-001', 300000, 95.00, 'processing', '新车型配套阀体壳,要求阳极氧化'),
('INQ-2026-0002', 'T1-2025-001', '2026-08-03', 'DWG-VHB-SP-Rev1', 'P-VHB-003', 150000, 55.00, 'pending', '转向器阀体壳,钝化处理')
ON CONFLICT (inquiry_id) DO NOTHING;
INSERT INTO inquiry_item (inquiry_id, line_no, part_name, part_number, material_grade, volume_cc, annual_qty, remarks) VALUES
('INQ-2026-0001', 1, '变速箱阀体外壳', 'P-VBH-001', 'ADC12', 444.44, 300000, '体积444.44cm³,400T压铸'),
('INQ-2026-0002', 1, '转向器阀体壳', 'P-VHB-003', 'A356-T6', 296.30, 150000, '体积296.30cm³,280T压铸')
ON CONFLICT (inquiry_id, line_no) DO NOTHING;
-- 历史报价(2024Q4-2025,含 won/lost,用于价格趋势与一致性校验)
INSERT INTO quotation (quotation_id, inquiry_id, customer_code, quotation_date, valid_until, version, status, mold_cost, unit_price, annual_volume, total_annual, subtotal, tax_rate, tax_amount, total_amount, payment_terms, delivery_terms, created_by, approved_by, remarks) VALUES
('QUO-2024-9005', 'INQ-2024-9005', 'OEM-2024-001', '2024-11-08', '2024-12-08', 1, 'approved', 180000, 52.30, 200000, 10460000, 10460000, 0.130, 1359800, 11819800, '月结60天', '含税含运', 'AI智能报价', '销售经理-赵总', '2024年基线价'),
('QUO-2025-9001', 'INQ-2025-9001', 'OEM-2024-001', '2025-10-15', '2025-11-15', 1, 'approved', 0, 42.50, 300000, 12750000, 12750000, 0.130, 1657500, 14407500, '月结60天', '含税含运', 'AI智能报价', '销售经理-赵总', '2025年年价,模具费已摊销'),
('QUO-2025-9002', 'INQ-2025-9001', 'OEM-2024-001', '2025-12-20', '2026-01-20', 2, 'approved', 0, 41.90, 300000, 12570000, 12570000, 0.130, 1634100, 14204100, '月结60天', '含税含运', 'AI智能报价', '销售经理-赵总', '第二轮降价,一致性校验基准'),
('QUO-2025-9003', 'INQ-2025-9002', 'OEM-2024-002', '2025-09-05', '2025-10-05', 1, 'won', 250000, 46.80, 150000, 7020000, 7020000, 0.130, 912600, 7932600, '月结45天', '含税含运,北京交货', 'AI智能报价', '销售总监-钱总', '已中标'),
('QUO-2025-9004', 'INQ-2025-9003', 'T1-2025-001', '2025-06-18', '2025-07-18', 1, 'lost', 120000, 28.40, 150000, 4260000, 4260000, 0.130, 553800, 4813800, '月结30天', '含税含运', 'AI智能报价', NULL, '报价偏高丢单,客户目标价25元'),
('QUO-2025-9006', 'INQ-2025-9006', 'OEM-2024-002', '2025-11-30', '2025-12-30', 1, 'sent', 0, 43.20, 100000, 4320000, 4320000, 0.130, 561600, 4881600, '月结45天', '含税含运', 'AI智能报价', '销售经理-赵总', '跨客户价差校验样本')
ON CONFLICT (quotation_id) DO NOTHING;
-- 历史报价成本明细
INSERT INTO quotation_cost_detail (quotation_id, material_cost, casting_cost, machining_cost, post_process_cost, overhead_rate, profit_rate, total_cost, unit_price) VALUES
('QUO-2024-9005', 28.50, 2.17, 2.45, 7.50, 0.120, 0.150, 40.62, 52.30),
('QUO-2025-9001', 23.90, 2.17, 0.87, 6.00, 0.120, 0.150, 32.94, 42.50),
('QUO-2025-9002', 23.50, 2.17, 0.87, 6.00, 0.120, 0.150, 32.54, 41.90),
('QUO-2025-9003', 20.20, 4.43, 1.91, 9.80, 0.120, 0.150, 36.34, 46.80),
('QUO-2025-9004', 16.50, 1.10, 2.88, 1.57, 0.120, 0.280, 22.05, 28.40),
('QUO-2025-9006', 24.50, 2.17, 0.87, 6.00, 0.120, 0.150, 33.54, 43.20)
ON CONFLICT (quotation_id) DO NOTHING;
-- 基础报价(保留原种子)
INSERT INTO quotation (quotation_id, inquiry_id, customer_code, quotation_date, valid_until, version, status, mold_cost, unit_price, annual_volume, total_annual, subtotal, tax_rate, tax_amount, total_amount, payment_terms, delivery_terms, created_by, approved_by, remarks) VALUES
('QUO-2026-0001', 'INQ-2026-0001', 'OEM-2024-001', '2026-07-15', '2026-08-15', 1, 'sent',
180000, 92.50, 300000, 27750000, 27750000, 0.130, 3607500, 31357500,
'月结60天', '含税含运,上海交货', 'AI智能报价', '销售经理-赵总', '老客户优惠价,含模具费18万')
ON CONFLICT (quotation_id) DO NOTHING;
INSERT INTO quotation_cost_detail (quotation_id, material_cost, casting_cost, machining_cost, post_process_cost, overhead_rate, profit_rate, total_cost, unit_price) VALUES
('QUO-2026-0001', 24.50, 8.20, 35.60, 6.00, 0.120, 0.150, 74.30, 92.50)
ON CONFLICT (quotation_id) DO NOTHING;
-- 商机(完整阶段谱系:lead/qualified/proposal/negotiation/quoted/won/lost)
INSERT INTO opportunity (opportunity_id, customer_code, title, stage, expected_amount, probability, oem_program, expected_close_date, source, created_by) VALUES
('OPP-2025-0005', 'OEM-2024-002', '2025年制动阀体壳定点', 'won', 8200000, 100, '德系新制动系统平台', '2025-09-30', '客户主动询价', '销售团队'),
('OPP-2025-0006', 'T1-2025-001', '转向器阀体壳年度供应', 'lost', 4500000, 0, 'EPS转向系统', '2025-07-31', '展会获客', '销售团队'),
('OPP-2026-0003', 'T1-2025-003', '采埃孚悬架阀体项目', 'lead', 5000000, 20, '主动悬架系统配套', '2026-12-31', '客户推荐', 'AI智能报价'),
('OPP-2026-0004', 'OEM-2024-003', '新能源平台阀体定点', 'negotiation', 9000000, 60, '纯电平台热管理+制动', '2026-11-30', '战略客户开发', '销售团队'),
('OPP-2026-0005', 'OEM-2024-004', '美系OEM新项目询价', 'qualified', 12000000, 40, '下一代混动变速箱', '2027-03-31', '客户主动询价', 'AI智能报价'),
('OPP-2026-0006', 'OEM-2025-001', '商用车平台重卡阀体', 'lead', 6000000, 15, '重卡制动系统升级', '2026-12-31', '展会获客', '销售团队'),
('OPP-2026-0001', 'OEM-2024-001', '2026年新车型阀体壳定点', 'proposal', 27750000, 60, '某日系新车型CVT配套', '2026-10-31', '客户主动询价', 'AI智能报价'),
('OPP-2026-0002', 'T1-2025-001', '转向器阀体壳年度采购', 'lead', 8250000, 30, 'EPS转向系统配套', '2026-12-31', '展会获客', 'AI智能报价')
ON CONFLICT (opportunity_id) DO NOTHING;
+28
View File
@@ -0,0 +1,28 @@
"""PostgreSQL 连接池管理"""
import asyncpg
import os
DB_CONFIG = {
"host": os.getenv("CRM_DB_HOST", "192.168.1.206"),
"port": int(os.getenv("CRM_DB_PORT", "5432")),
"user": os.getenv("CRM_DB_USER", "postgres"),
"password": os.getenv("CRM_DB_PASSWORD", "digiwin"),
"database": os.getenv("CRM_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
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""初始化 CRM 数据库表和种子数据"""
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("CRM 数据库初始化完成")
await close_pool()
if __name__ == "__main__":
asyncio.run(main())
+351
View File
@@ -0,0 +1,351 @@
"""
MCP for CRM — 汽车零部件智能报价 CRM 数据服务
使用 MCPServer 框架,5 个工具:客户查询、询价建档、报价存档、历史报价、商机管理
HTTP 模式,支持远程调用
"""
import asyncio
import json
from datetime import datetime, timedelta
from mcp.server import MCPServer
from db import get_pool
app = MCPServer(
name="mcp-for-crm-auto",
description="汽车零部件智能报价 CRM 数据服务",
version="1.0.0"
)
# ============================================================
# 工具 1: get_customer_info — 查询客户信息
# ============================================================
@app.tool()
async def get_customer_info(
customer_code: str = None,
name: str = None,
oem_tier: str = None
) -> str:
"""查询 CRM 客户信息。支持按客户编码精确查询或按名称、OEM 层级模糊搜索。返回客户信用等级、折扣率、历史订单等。"""
pool = await get_pool()
query = "SELECT * FROM customer WHERE 1=1"
params = []
if customer_code:
query += f" AND customer_code = ${len(params)+1}"
params.append(customer_code)
if name:
query += f" AND name LIKE ${len(params)+1}"
params.append(f"%{name}%")
if oem_tier:
query += f" AND oem_tier = ${len(params)+1}"
params.append(oem_tier)
rows = await pool.fetch(query, *params)
customers = [dict(row) for row in rows]
return json.dumps({"total": len(customers), "customers": customers}, ensure_ascii=False, indent=2, default=str)
# ============================================================
# 工具 2: create_inquiry — 创建询价记录
# ============================================================
@app.tool()
async def create_inquiry(
customer_code: str,
drawing_number: str = None,
part_number: str = None,
annual_volume: int = None,
target_price: float = None,
notes: str = None
) -> str:
"""在 CRM 中创建新的询价记录。关联客户、图纸号、零件号、年用量。客户提交询价后建档使用。"""
pool = await get_pool()
customer = await pool.fetchrow("SELECT name FROM customer WHERE customer_code = $1", customer_code)
if not customer:
return json.dumps({"success": False, "message": f"未找到客户 {customer_code}"}, ensure_ascii=False, indent=2)
max_seq = await pool.fetchval(
"SELECT COALESCE(MAX(CAST(SUBSTRING(inquiry_id FROM '(\\d+)$') AS INTEGER)), 0) FROM inquiry WHERE inquiry_id LIKE 'INQ-2026-%'"
)
inquiry_id = f"INQ-2026-{str(max_seq + 1).zfill(4)}"
await pool.execute(
"""INSERT INTO inquiry (inquiry_id, customer_code, inquiry_date, drawing_number, part_number, annual_volume, target_price, status, notes)
VALUES ($1, $2, CURRENT_DATE, $3, $4, $5, $6, 'pending', $7)""",
inquiry_id, customer_code, drawing_number, part_number,
annual_volume, target_price, notes
)
return json.dumps({
"success": True, "inquiry_id": inquiry_id, "customer_name": customer["name"],
"status": "pending", "message": f"询价记录 {inquiry_id} 已创建"
}, ensure_ascii=False, indent=2)
# ============================================================
# 工具 3: save_quotation — 保存报价单
# ============================================================
@app.tool()
async def save_quotation(
inquiry_id: str,
customer_code: str,
mold_cost: float,
unit_price: float,
annual_volume: int,
material_cost: float,
casting_cost: float,
machining_cost: float,
post_process_cost: float,
overhead_rate: float = 0.12,
profit_rate: float = 0.15,
tax_rate: float = 0.13,
payment_terms: str = "月结30天",
delivery_terms: str = "含税含运",
valid_days: int = 30,
remarks: str = None
) -> str:
"""保存报价单到 CRM。包含模具费、单价、年用量、成本明细。报价生成后存档使用。"""
pool = await get_pool()
customer = await pool.fetchrow("SELECT name FROM customer WHERE customer_code = $1", customer_code)
if not customer:
return json.dumps({"success": False, "message": f"未找到客户 {customer_code}"}, ensure_ascii=False, indent=2)
inquiry = await pool.fetchrow("SELECT inquiry_id, status FROM inquiry WHERE inquiry_id = $1", inquiry_id)
if not inquiry:
return json.dumps({"success": False, "message": f"未找到询价单 {inquiry_id},请先调用 create_inquiry"}, ensure_ascii=False, indent=2)
# 多版本报价:同一询价单不限报价次数,版本号自动递增
prev = await pool.fetchrow(
"SELECT quotation_id, version, unit_price, status FROM quotation WHERE inquiry_id = $1 ORDER BY version DESC LIMIT 1",
inquiry_id
)
new_version = (prev["version"] + 1) if prev else 1
price_change_pct = None
if prev and prev["unit_price"]:
price_change_pct = round((unit_price - float(prev["unit_price"])) / float(prev["unit_price"]) * 100, 2)
warning = None
if prev and prev["status"] == "won":
warning = f"该询价单已有中标报价 {prev['quotation_id']},本次作为补充报价存档"
max_seq = await pool.fetchval(
"SELECT COALESCE(MAX(CAST(SUBSTRING(quotation_id FROM '(\\d+)$') AS INTEGER)), 0) FROM quotation WHERE quotation_id LIKE 'QUO-2026-%'"
)
quotation_id = f"QUO-2026-{str(max_seq + 1).zfill(4)}"
today = datetime.now()
valid_until = today + timedelta(days=valid_days)
total_annual = unit_price * annual_volume
subtotal = total_annual
tax_amount = subtotal * tax_rate
total_amount = subtotal + tax_amount
total_cost = material_cost + casting_cost + machining_cost + post_process_cost
async with pool.acquire() as conn:
async with conn.transaction():
# 旧版本自动作废(won/lost 终态保留)
if prev:
await conn.execute(
"""UPDATE quotation SET status = 'superseded'
WHERE inquiry_id = $1 AND version < $2 AND status NOT IN ('won', 'lost', 'superseded')""",
inquiry_id, new_version
)
# 首次报价推动询价单状态 pending → quoted
await conn.execute(
"UPDATE inquiry SET status = 'quoted' WHERE inquiry_id = $1 AND status = 'pending'",
inquiry_id
)
await conn.execute(
"""INSERT INTO quotation (quotation_id, inquiry_id, customer_code, quotation_date, valid_until, version, status,
mold_cost, unit_price, annual_volume, total_annual, subtotal, tax_rate, tax_amount, total_amount,
payment_terms, delivery_terms, remarks, created_by)
VALUES ($1, $2, $3, CURRENT_DATE, $4, $5, 'draft', $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, 'AI智能报价')""",
quotation_id, inquiry_id, customer_code, valid_until.date(), new_version, mold_cost, unit_price,
annual_volume, total_annual, subtotal, tax_rate, tax_amount, total_amount,
payment_terms, delivery_terms, remarks
)
await conn.execute(
"""INSERT INTO quotation_cost_detail (quotation_id, material_cost, casting_cost, machining_cost, post_process_cost,
overhead_rate, profit_rate, total_cost, unit_price)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
quotation_id, material_cost, casting_cost, machining_cost,
post_process_cost, overhead_rate, profit_rate, total_cost, unit_price
)
result = {
"success": True, "quotation_id": quotation_id, "status": "draft",
"version": new_version,
"previous_quotation_id": prev["quotation_id"] if prev else None,
"price_change_pct": price_change_pct,
"summary": {
"mold_cost": mold_cost, "unit_price": unit_price, "annual_volume": annual_volume,
"total_annual": total_annual, "tax_rate": f"{tax_rate*100:.0f}%", "tax_amount": tax_amount,
"total_amount": total_amount, "currency": "CNY"
},
"valid_until": valid_until.strftime("%Y-%m-%d"),
"message": f"报价单 {quotation_id}(第 {new_version} 版)已保存为草稿,待人工审核后发出"
}
if warning:
result["warning"] = warning
return json.dumps(result, ensure_ascii=False, indent=2)
# ============================================================
# 工具 4: get_quotation_history — 查询历史报价
# ============================================================
@app.tool()
async def get_quotation_history(
customer_code: str = None,
inquiry_id: str = None,
start_date: str = None,
end_date: str = None,
status: str = None,
latest_only: bool = False
) -> str:
"""查询历史报价记录。用于核价参考和价格一致性校验。支持按客户、询价编号、日期范围筛选。latest_only=true 时每个询价单仅返回最新版本。"""
pool = await get_pool()
select_clause = (
"SELECT DISTINCT ON (q.inquiry_id) q.*, c.name as customer_name"
if latest_only
else "SELECT q.*, c.name as customer_name"
)
query = select_clause + " FROM quotation q JOIN customer c ON q.customer_code = c.customer_code WHERE 1=1"
params = []
if customer_code:
query += f" AND q.customer_code = ${len(params)+1}"
params.append(customer_code)
if inquiry_id:
query += f" AND q.inquiry_id = ${len(params)+1}"
params.append(inquiry_id)
if start_date:
query += f" AND q.quotation_date >= ${len(params)+1}"
params.append(datetime.strptime(start_date, "%Y-%m-%d").date())
if end_date:
query += f" AND q.quotation_date <= ${len(params)+1}"
params.append(datetime.strptime(end_date, "%Y-%m-%d").date())
if status:
query += f" AND q.status = ${len(params)+1}"
params.append(status)
# 版本链视图:同一询价单按版本号降序排列(DISTINCT ON 要求 ORDER BY 以 inquiry_id 开头)
query += " ORDER BY q.inquiry_id, q.version DESC, q.quotation_date DESC"
rows = await pool.fetch(query, *params)
quotations = [dict(row) for row in rows]
# 标记每条报价是否为其询价单的最新版本(全表口径,不受筛选影响)
inq_ids = list({q["inquiry_id"] for q in quotations if q.get("inquiry_id")})
max_versions = {}
if inq_ids:
mv_rows = await pool.fetch(
"SELECT inquiry_id, MAX(version) AS mv FROM quotation WHERE inquiry_id = ANY($1) GROUP BY inquiry_id",
inq_ids
)
max_versions = {r["inquiry_id"]: r["mv"] for r in mv_rows}
for q in quotations:
q["is_latest"] = bool(q.get("inquiry_id")) and q["version"] == max_versions.get(q["inquiry_id"])
for q in quotations:
cost_detail = await pool.fetchrow("SELECT * FROM quotation_cost_detail WHERE quotation_id = $1", q["quotation_id"])
if cost_detail:
q["cost_detail"] = dict(cost_detail)
return json.dumps({"total": len(quotations), "quotations": quotations}, ensure_ascii=False, indent=2, default=str)
# ============================================================
# 工具 5: update_opportunity — 更新商机状态
# ============================================================
@app.tool()
async def update_opportunity(
opportunity_id: str = None,
customer_code: str = None,
title: str = None,
stage: str = None,
expected_amount: float = None,
probability: int = None,
oem_program: str = None,
expected_close_date: str = None,
notes: str = None
) -> str:
"""更新 CRM 中的商机状态。支持创建新商机或更新已有商机的阶段、金额、概率。报价后跟进使用。"""
pool = await get_pool()
close_date = None
if expected_close_date:
try:
close_date = datetime.strptime(expected_close_date, "%Y-%m-%d").date()
except ValueError:
return json.dumps({"success": False, "message": f"expected_close_date 格式错误:{expected_close_date},应为 YYYY-MM-DD"}, ensure_ascii=False, indent=2)
if opportunity_id:
opp = await pool.fetchrow("SELECT * FROM opportunity WHERE opportunity_id = $1", opportunity_id)
if not opp:
return json.dumps({"success": False, "message": f"未找到商机 {opportunity_id}"}, ensure_ascii=False, indent=2)
field_values = {
"stage": stage,
"expected_amount": expected_amount,
"probability": probability,
"oem_program": oem_program,
"expected_close_date": close_date,
"title": title,
}
updates = []
params = []
for field, value in field_values.items():
if value is not None:
updates.append(f"{field} = ${len(params)+1}")
params.append(value)
if updates:
updates.append("updated_at = CURRENT_DATE")
params.append(opportunity_id)
await pool.execute(f"UPDATE opportunity SET {', '.join(updates)} WHERE opportunity_id = ${len(params)}", *params)
return json.dumps({
"success": True, "opportunity_id": opportunity_id, "message": f"商机 {opportunity_id} 已更新"
}, ensure_ascii=False, indent=2)
if not customer_code or not title:
return json.dumps({"success": False, "message": "新建商机需要提供 customer_code 和 title"}, ensure_ascii=False, indent=2)
customer = await pool.fetchrow("SELECT name FROM customer WHERE customer_code = $1", customer_code)
if not customer:
return json.dumps({"success": False, "message": f"未找到客户 {customer_code}"}, ensure_ascii=False, indent=2)
max_seq = await pool.fetchval(
"SELECT COALESCE(MAX(CAST(SUBSTRING(opportunity_id FROM '(\\d+)$') AS INTEGER)), 0) FROM opportunity WHERE opportunity_id LIKE 'OPP-2026-%'"
)
opp_id = f"OPP-2026-{str(max_seq + 1).zfill(4)}"
await pool.execute(
"""INSERT INTO opportunity (opportunity_id, customer_code, title, stage, expected_amount, probability,
oem_program, expected_close_date, source, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '智能报价自动生成', 'AI智能报价')""",
opp_id, customer_code, title, stage or "lead", expected_amount or 0,
probability or 20, oem_program, close_date
)
return json.dumps({
"success": True, "opportunity_id": opp_id, "message": f"商机 {opp_id} 已创建"
}, ensure_ascii=False, indent=2)
# ============================================================
# 启动 HTTP Server
# ============================================================
if __name__ == "__main__":
asyncio.run(app.run_streamable_http_async(host="0.0.0.0", port=8002, streamable_http_path="/mcp"))
+60
View File
@@ -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 | 数据库名 |
+3
View File
@@ -0,0 +1,3 @@
mcp>=1.0.0
pydantic>=2.0.0
asyncpg>=0.30.0
+234
View File
@@ -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;
+28
View File
@@ -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
+16
View File
@@ -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())
+316
View File
@@ -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"))