From 01b3798e02a71833f5e04b50e0b1310d4d9078ad Mon Sep 17 00:00:00 2001 From: dongsk Date: Wed, 2 Sep 2026 17:30:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=20crm=E3=80=81erp=20mcp?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 20 - docker-compose.yml | 46 --- mcp-for-crm/MCP工具说明文档.md | 555 ---------------------------- mcp-for-crm/README.md | 49 --- mcp-for-crm/requirements.txt | 4 - mcp-for-crm/sql/init.sql | 271 -------------- mcp-for-crm/src/auth.py | 119 ------ mcp-for-crm/src/db.py | 28 -- mcp-for-crm/src/seed.py | 16 - mcp-for-crm/src/server.py | 543 --------------------------- mcp-for-crm/数据表与种子数据说明.md | 278 -------------- mcp-for-erp/MCP工具说明文档.md | 383 ------------------- mcp-for-erp/README.md | 60 --- mcp-for-erp/requirements.txt | 4 - mcp-for-erp/sql/init.sql | 234 ------------ mcp-for-erp/src/auth.py | 119 ------ mcp-for-erp/src/db.py | 28 -- mcp-for-erp/src/seed.py | 16 - mcp-for-erp/src/server.py | 353 ------------------ mcp-for-erp/数据表与种子数据说明.md | 239 ------------ 20 files changed, 3365 deletions(-) delete mode 100644 Dockerfile delete mode 100644 docker-compose.yml delete mode 100644 mcp-for-crm/MCP工具说明文档.md delete mode 100644 mcp-for-crm/README.md delete mode 100644 mcp-for-crm/requirements.txt delete mode 100644 mcp-for-crm/sql/init.sql delete mode 100644 mcp-for-crm/src/auth.py delete mode 100644 mcp-for-crm/src/db.py delete mode 100644 mcp-for-crm/src/seed.py delete mode 100644 mcp-for-crm/src/server.py delete mode 100644 mcp-for-crm/数据表与种子数据说明.md delete mode 100644 mcp-for-erp/MCP工具说明文档.md delete mode 100644 mcp-for-erp/README.md delete mode 100644 mcp-for-erp/requirements.txt delete mode 100644 mcp-for-erp/sql/init.sql delete mode 100644 mcp-for-erp/src/auth.py delete mode 100644 mcp-for-erp/src/db.py delete mode 100644 mcp-for-erp/src/seed.py delete mode 100644 mcp-for-erp/src/server.py delete mode 100644 mcp-for-erp/数据表与种子数据说明.md diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index acb0821..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# 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"] diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 4b39650..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,46 +0,0 @@ -# 汽车零部件智能报价 — MCP 服务编排 -# 生产环境:docker compose up -d -# 默认连生产库 10.100.154.100:5432 -# 8001 = mcp-for-erp, 8002 = mcp-for-crm -# 停止:docker compose down -# 日志:docker compose logs -f -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=${ERP_DB_HOST:-10.100.154.100} - - ERP_DB_PORT=${ERP_DB_PORT:-5432} - - ERP_DB_USER=${ERP_DB_USER:-postgres} - - ERP_DB_PASSWORD=${ERP_DB_PASSWORD:-digiwin} - - ERP_DB_NAME=${ERP_DB_NAME:-smart_quotation_auto} - - MCP_PUBLIC_URL=${MCP_PUBLIC_URL_ERP:-http://10.100.154.100:8001} - - MCP_AUTH_API_URL=${MCP_AUTH_API_URL:-http://10.100.154.100:8003} - - MCP_AUTH_API_KEY=${ERP_MCP_AUTH_API_KEY:-Bb53hKPHR7RzngS2A2iDLXiENSku9Rm5hRYQqzbn2-o} - 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=${CRM_DB_HOST:-10.100.154.100} - - CRM_DB_PORT=${CRM_DB_PORT:-5432} - - CRM_DB_USER=${CRM_DB_USER:-postgres} - - CRM_DB_PASSWORD=${CRM_DB_PASSWORD:-digiwin} - - CRM_DB_NAME=${CRM_DB_NAME:-smart_quotation_auto} - - MCP_PUBLIC_URL=${MCP_PUBLIC_URL_CRM:-http://10.100.154.100:8002} - - MCP_AUTH_API_URL=${MCP_AUTH_API_URL:-http://10.100.154.100:8003} - - MCP_AUTH_API_KEY=${CRM_MCP_AUTH_API_KEY:-MrhPFY1_WJIh_AwELAhegf_DtCcROpdLsUgRx5pvU3E} - restart: unless-stopped diff --git a/mcp-for-crm/MCP工具说明文档.md b/mcp-for-crm/MCP工具说明文档.md deleted file mode 100644 index c233085..0000000 --- a/mcp-for-crm/MCP工具说明文档.md +++ /dev/null @@ -1,555 +0,0 @@ -# MCP for CRM — 工具说明文档 - -> 服务对象:汽车零部件(小型阀体外壳)智能报价智能体 -> 服务定位:CRM 侧客户/询价/报价/商机数据服务(业务写入 + 只读回读 + 受限清理) -> 版本:v1.1(2026-08-09) - ---- - -## 1 服务概览 - -| 项 | 值 | -|----|----| -| 服务名 | `mcp-for-crm-auto` | -| 框架 | MCPServer(mcp 2.0.0 内置高级框架),`@app.tool()` 装饰器注册 | -| 传输协议 | Streamable HTTP(JSON-RPC 2.0) | -| 远程端点 | `https://crm-data.dongsk.top/mcp` | -| 本地端点 | `http://0.0.0.0:8002/mcp`(`cd src && python3 server.py`) | -| 数据库 | PostgreSQL 15+,库 `smart_quotation_auto`(仅服务层内部经 `db.py` 连接池访问) | -| 工具数 | **10 个** = 6 个业务工具 + 4 个验证/管理工具 | - -### 工具一览 - -| # | 工具 | 功能 | 类型 | 报价环节 | -|---|------|------|------|----------| -| 1 | `get_customer_info` | 查询客户信用/折扣/账期 | 只读 | Step 2 客户查询 | -| 2 | `create_inquiry` | 创建询价单 | 写入 | Step 7 存档 | -| 3 | `save_quotation` | 保存报价单(自动版本+1) | 写入(事务) | Step 7 存档 | -| 4 | `get_quotation_history` | 历史报价/版本链查询 | 只读 | Step 7 前置检查 | -| 5 | `update_opportunity` | 创建/更新商机 | 写入 | Step 7 存档 | -| 6 | `get_inquiry_info` | 询价单整行回读 | 只读 | 存档后验证 | -| 7 | `get_opportunity_info` | 商机回读 | 只读 | 存档后验证 | -| 8 | `get_record_counts` | 三表行数统计 | 只读 | 数据卫生检查 | -| 9 | `purge_test_records` | 受限测试数据清理 | 写入(带防护) | 回归测试清理 | -| 10 | `save_quotation_mock` | 报价写入 CRM(Mock 模拟) | 写入(独立 mock 表) | 模拟报价写入 CRM | - -### 单号与状态约定 - -| 实体 | 单号规则 | 状态机 | -|------|----------|--------| -| 询价单 | `INQ-2026-NNNN`(自增) | `pending` → `quoted`(首次报价自动推进) | -| 报价单 | `QUO-2026-NNNN`(自增) | `draft` → `superseded`(被新版替代);`won` / `lost` 为终态,受保护 | -| 商机 | `OPP-2026-NNNN`(自增) | `lead` → `quoted` → `won` / `lost` | - ---- - -## 2 调用约定 - -### 2.1 客户端注册(mcpServers) - -```json -{ - "mcpServers": { - "crm": { "url": "https://crm-data.dongsk.top/mcp" } - } -} -``` - -### 2.2 兜底命令行客户端(纯标准库) - -```bash -python3 scripts/mcp_http_client.py crm <工具名> '' -# 示例 -python3 scripts/mcp_http_client.py crm get_customer_info '{"customer_code":"OEM-2024-003"}' -``` - -### 2.3 返回通用约定 - -- 所有工具返回 **JSON 字符串**(`ensure_ascii=False`,中文不转义)。 -- 查询类:`{"total": N, "<集合名>": [...]}`;写入/操作类:`{"success": true|false, ...}`。 -- 失败统一形态:`{"success": false, "message": "<原因>"}`。 -- 金额字段(decimal 列)在 JSON 中为字符串,如 `"180000.00"`;日期 `YYYY-MM-DD`。 -- 日期入参格式必须为 `YYYY-MM-DD`,否则返回格式错误。 - ---- - -## 3 业务工具详解(6 个) - -### 3.1 get_customer_info — 查询客户信息 - -**功能**:按客户编码精确查询,或按名称、OEM 层级模糊搜索。返回信用等级、折扣率、账期、历史订单统计,用于报价前的客户校验与商务条款确定。 - -**入参**(全部可选;全部不传 = 返回全表): - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `customer_code` | string | 否 | — | 客户编码,精确匹配,如 `OEM-2024-003` | -| `name` | string | 否 | — | 客户名称,`%模糊%` 匹配 | -| `oem_tier` | string | 否 | — | 层级精确匹配:`OEM` / `Tier1` / `Tier2` | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `total` | int | 命中条数 | -| `customers[].customer_code` | string | 客户编码 | -| `customers[].name` | string | 客户名称 | -| `customers[].oem_tier` | string | OEM / Tier1 / Tier2 | -| `customers[].credit_level` | string | 信用等级 A/B/C/D 或「无评级」 | -| `customers[].discount_rate` | decimal | 折扣率(字符串,如 `"0.040"` = 4%) | -| `customers[].payment_days` | int | 账期天数(月结 N 天) | -| `customers[].contact_person` / `phone` / `email` | string | 联系人信息 | -| `customers[].industry` / `region` | string | 行业 / 区域 | -| `customers[].total_orders` | int | 历史订单数 | -| `customers[].total_amount` | decimal | 历史订单总额 | -| `customers[].notes` | string | 备注 | - -**返回示例**: - -```json -{ - "total": 1, - "customers": [ - { - "customer_code": "OEM-2024-003", - "name": "某新能源主机厂", - "oem_tier": "OEM", - "credit_level": "A", - "discount_rate": "0.040", - "payment_days": 60, - "contact_person": "刘工", - "phone": "0551-2222-0005", - "email": "liu@oem-nev.example.com", - "industry": "汽车", - "region": "合肥", - "total_orders": 22, - "total_amount": "9800000.00", - "notes": "新能源三电系统配套,增长快", - "created_at": "2026-08-07 17:31:56.244396" - } - ] -} -``` - ---- - -### 3.2 create_inquiry — 创建询价记录 - -**功能**:客户提交询价后建档。校验客户存在性,自动生成 `INQ-2026-NNNN` 单号,初始状态 `pending`。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `customer_code` | string | **是** | — | 客户编码(不存在则失败) | -| `drawing_number` | string | 否 | null | 图纸号 | -| `part_number` | string | 否 | null | 零件号 | -| `annual_volume` | int | 否 | null | 年用量(件/年) | -| `target_price` | float | 否 | null | 客户目标价 ¥ | -| `notes` | string | 否 | null | 备注 | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | bool | 成功标记 | -| `inquiry_id` | string | 生成的询价单号 | -| `customer_name` | string | 客户名称 | -| `status` | string | 固定 `pending` | -| `message` | string | 提示信息 | - -**失败返回**:`{"success": false, "message": "未找到客户 XXX"}` - -**返回示例**: - -```json -{ - "success": true, - "inquiry_id": "INQ-2026-0028", - "customer_name": "某新能源主机厂", - "status": "pending", - "message": "询价记录 INQ-2026-0028 已创建" -} -``` - -**业务规则**:同一询价单可多次创建(每次生成新单号);「同一询价单多次报价」通过 3.3 `save_quotation` 的版本机制实现,不重复建档。 - ---- - -### 3.3 save_quotation — 保存报价单(多版本核心) - -**功能**:报价存档核心工具。同一询价单**不限报价次数**,版本号自动 MAX+1;旧版非终态自动置 `superseded`;首报自动推询价单 `pending → quoted`;`won` 终态保护(仍存档但带 warning)。事务内完成:作废旧版 → 推询价状态 → 写报价主表 → 写成本明细表。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `inquiry_id` | string | **是** | — | 询价单号(不存在则失败并提示先建档) | -| `customer_code` | string | **是** | — | 客户编码(不存在则失败) | -| `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` | string | 否 | `月结30天` | 付款条款 | -| `delivery_terms` | string | 否 | `含税含运` | 交付条款 | -| `valid_days` | int | 否 | 30 | 报价有效天数(自当日起) | -| `remarks` | string | 否 | null | 备注 | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | bool | 成功标记 | -| `quotation_id` | string | 生成的报价单号 | -| `status` | string | 固定 `draft`(草稿,待人工审核后发出) | -| `version` | int | 本次版本号(自动递增) | -| `previous_quotation_id` | string\|null | 上一版报价单号(首报为 null) | -| `price_change_pct` | float\|null | 较上版价差百分比(首报为 null) | -| `summary.mold_cost` / `unit_price` / `annual_volume` | float/int | 回显 | -| `summary.total_annual` | float | 年总额 = 单价×年用量 | -| `summary.tax_rate` | string | 税率百分比字符串,如 `"13%"` | -| `summary.tax_amount` | float | 税额 | -| `summary.total_amount` | float | 含税总额 | -| `summary.currency` | string | 固定 `CNY` | -| `valid_until` | string | 有效期至 `YYYY-MM-DD` | -| `warning` | string(可选) | 存在中标旧版时的提示 | -| `message` | string | 提示信息 | - -**返回示例**(第 2 版议价): - -```json -{ - "success": true, - "quotation_id": "QUO-2026-0029", - "status": "draft", - "version": 2, - "previous_quotation_id": "QUO-2026-0028", - "price_change_pct": -3.0, - "summary": { - "mold_cost": 180000, - "unit_price": 42.7, - "annual_volume": 80000, - "total_annual": 3416000.0, - "tax_rate": "13%", - "tax_amount": 444080.0, - "total_amount": 3860080.0, - "currency": "CNY" - }, - "valid_until": "2026-09-08", - "message": "报价单 QUO-2026-0029(第 2 版)已保存为草稿,待人工审核后发出" -} -``` - -**业务规则**: -- 版本链查询用 3.4;`won`/`lost` 终态永不被 superseded 覆盖。 -- 并发安全由数据库 `(inquiry_id, version)` 唯一约束兜底。 -- 草稿态报价单在 HTML 上带「草稿·待人工审核」水印,人工审核后才可发出。 - ---- - -### 3.4 get_quotation_history — 查询历史报价 - -**功能**:历史报价与版本链查询,用于再报价前置检查(有没有报过、上一版多少钱)与价格一致性校验。同一询价单按版本号降序返回,并逐行标记 `is_latest`。 - -**入参**(全部可选): - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `customer_code` | string | 否 | — | 按客户筛选 | -| `inquiry_id` | string | 否 | — | 按询价单筛选 | -| `start_date` | string | 否 | — | 报价日期起,`YYYY-MM-DD` | -| `end_date` | string | 否 | — | 报价日期止,`YYYY-MM-DD` | -| `status` | string | 否 | — | 状态筛选:draft/superseded/won/lost | -| `latest_only` | bool | 否 | false | true 时每个询价单仅返回最新一版 | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `total` | int | 命中条数 | -| `quotations[]` | array | 报价行(quotation 全字段 + `customer_name`) | -| `quotations[].version` | int | 版本号 | -| `quotations[].status` | string | 状态 | -| `quotations[].is_latest` | bool | 是否为该询价单最新版本(全表口径,不受筛选影响) | -| `quotations[].cost_detail` | object | 成本明细(四项成本 + 费率 + total_cost + unit_price) | - -**返回示例**(INQ-2026-0019 三版议价链,latest_only=true): - -```json -{ - "total": 1, - "quotations": [ - { - "quotation_id": "QUO-2026-0018", - "inquiry_id": "INQ-2026-0019", - "customer_code": "OEM-2024-002", - "quotation_date": "2026-08-07", - "valid_until": "2026-09-06", - "version": 3, - "status": "draft", - "mold_cost": "180000.00", - "unit_price": "41.50", - "annual_volume": 80000, - "customer_name": "某商用车主机厂", - "is_latest": true, - "cost_detail": { "...": "四项成本与费率" } - } - ] -} -``` - -**注意**:日期参数格式错误(非 `YYYY-MM-DD`)会抛出解析异常,调用方应保证格式。 - ---- - -### 3.5 update_opportunity — 创建/更新商机 - -**功能**:双模式。传 `opportunity_id` = 更新已有商机的阶段/金额/概率等字段(仅更新非 null 字段);不传 `opportunity_id` = 按 `customer_code + title` 新建商机(默认 stage=lead、probability=20、expected_amount=0,source=智能报价自动生成)。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `opportunity_id` | string | 否 | — | 商机号(传入则走更新模式) | -| `customer_code` | string | 新建时必填 | — | 客户编码 | -| `title` | string | 新建时必填 | — | 商机标题 | -| `stage` | string | 否 | 新建时 `lead` | 阶段:lead/quoted/won/lost | -| `expected_amount` | float | 否 | 新建时 0 | 预期金额 ¥ | -| `probability` | int | 否 | 新建时 20 | 成交概率 % | -| `oem_program` | string | 否 | — | OEM 项目/平台 | -| `expected_close_date` | string | 否 | — | 预期关单日期 `YYYY-MM-DD` | -| `notes` | string | 否 | — | 备注(保留参数) | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | bool | 成功标记 | -| `opportunity_id` | string | 商机号(更新时为传入值,新建时为生成值) | -| `message` | string | 「已更新」或「已创建」 | - -**失败返回**: -- `{"success": false, "message": "expected_close_date 格式错误:...,应为 YYYY-MM-DD"}` -- `{"success": false, "message": "未找到商机 OPP-2026-XXXX"}` -- `{"success": false, "message": "新建商机需要提供 customer_code 和 title"}` -- `{"success": false, "message": "未找到客户 XXX"}` - -**返回示例**(新建): - -```json -{ - "success": true, - "opportunity_id": "OPP-2026-0019", - "message": "商机 OPP-2026-0019 已创建" -} -``` - ---- - -### 3.6 save_quotation_mock — 报价写入 CRM(Mock 模拟) - -**用途**:模拟「报价写入 CRM」动作。写入独立 mock 表 `quotation_mock`,不校验客户/询价单/商机,与其他工具无任何关联,适合演示与联调场景单独调用。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `part_name` | string | ✅ | — | 零件名称 | -| `unit_price` | number | ✅ | — | 报价单价 | -| `customer_name` | string | — | null | 客户名称(自由文本,不做存在性校验) | -| `part_number` | string | — | null | 零件号 | -| `material_grade` | string | — | null | 材质牌号 | -| `mold_cost` | number | — | 0.0 | 模具费 | -| `annual_volume` | integer | — | null | 年用量(提供时自动计算 `total_annual = unit_price × annual_volume`) | -| `currency` | string | — | CNY | 币种 | -| `remarks` | string | — | null | 备注 | - -**返回示例**: - -```json -{ - "success": true, - "quotation_id": "QUO-MOCK-0001", - "status": "draft", - "summary": { - "part_name": "变速箱阀体外壳", "unit_price": 41.66, "mold_cost": 180000.0, - "annual_volume": 300000, "total_annual": 12498000.0, "currency": "CNY" - }, - "message": "报价单 QUO-MOCK-0001 已写入 CRM mock 表(quotation_mock)" -} -``` - -**关键说明**: -- 单号 `QUO-MOCK-####` 自增,独立于正式 `QUO-2026-####` 序列。 -- `quotation_mock` 表无外键,写入不影响正式三表(inquiry/quotation/opportunity),`get_record_counts` 也不统计该表。 -- 无版本管理、无状态流转:每次调用新增一行,状态固定 `draft`。 - -## 4 验证/管理工具详解(4 个) - -> 用途:**MCP 读写分离交叉验证**——写工具的返回值与只读回读工具交叉核对;测试数据清理经受限工具完成。 -> 原则:验证/清理环节禁止直连数据库;`db.py` 仅为服务层内部连接池。 - -### 4.1 get_inquiry_info — 询价单回读(只读) - -**功能**:按询价单号回读询价单整行,用于存档后验证(状态、图号、零件号、年用量、目标价、创建时间)。 - -**入参**: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `inquiry_id` | string | **是** | 询价单号 | - -**返回**:命中 `{"success": true, "inquiry": {<整行字段>}}`;未命中 `{"success": false, "message": "未找到询价单 XXX"}`。 - -**示例**: - -```json -{ - "success": true, - "inquiry": { - "inquiry_id": "INQ-2026-0025", - "customer_code": "T1-2025-002", - "inquiry_date": "2026-08-08", - "drawing_number": "DWG-VHB-MN-Rev1", - "part_number": "P-VHB-009", - "annual_volume": 5000, - "target_price": "30.00", - "status": "quoted", - "notes": "TC-05生产报价:微型泵阀体壳 ADC12 96.3cc 160T一模两腔...", - "created_at": "2026-08-08 13:34:11.945841" - } -} -``` - ---- - -### 4.2 get_opportunity_info — 商机回读(只读) - -**功能**:按商机号或客户编码回读商机,用于存档后验证(stage/probability/expected_amount/更新时间)。两参数二选一,`opportunity_id` 优先。 - -**入参**: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `opportunity_id` | string | 二选一 | 商机号(精确单条) | -| `customer_code` | string | 二选一 | 客户编码(该客户全部商机,按创建时间倒序) | - -**返回**:`{"total": N, "opportunities": [<整行字段>]}`;两参数都未提供时 `{"success": false, "message": "需提供 opportunity_id 或 customer_code"}`。 - -**示例**: - -```json -{ - "total": 1, - "opportunities": [ - { - "opportunity_id": "OPP-2026-0018", - "customer_code": "T1-2025-002", - "title": "微型泵阀体壳 ADC12(P-VHB-009)首单", - "stage": "quoted", - "expected_amount": "40300.00", - "probability": 50, - "oem_program": "大陆制动系统配套", - "expected_close_date": "2026-09-30", - "source": "智能报价自动生成", - "created_by": "AI智能报价", - "updated_at": "2026-08-08" - } - ] -} -``` - ---- - -### 4.3 get_record_counts — 三表行数统计(只读) - -**功能**:返回 inquiry/quotation/opportunity 三表行数,用于数据卫生检查(如回归前后行数一致性核对)。 - -**入参**:无。 - -**返回示例**: - -```json -{ - "success": true, - "counts": { "inquiry": 24, "quotation": 27, "opportunity": 18 } -} -``` - ---- - -### 4.4 purge_test_records — 受限测试数据清理(管理) - -**功能**:清理 TC-03 回归测试写入的数据。**带硬性防护**:仅当报价单 `remarks`、询价单 `notes` 含「TC-03回归测试」,或商机 `title` 以「TC-03」开头时才允许删除;其余一律进入 `rejected` 列表,防止误删演示/生产数据。级联删除子表(quotation_cost_detail / inquiry_item)。 - -**入参**(至少传一组;均为 ID 数组): - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `quotation_ids` | array[string] | 否 | 待清理报价单号列表 | -| `inquiry_ids` | array[string] | 否 | 待清理询价单号列表 | -| `opportunity_ids` | array[string] | 否 | 待清理商机号列表 | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | bool | 固定 true(单条拒绝不影响整体) | -| `deleted.quotations` / `deleted.inquiries` / `deleted.opportunities` | int | 实际删除条数 | -| `deleted.rejected` | array[string] | 被防护拒绝的 ID 列表(非测试标记或不存在) | - -**示例一:正常清理(回归测试自动调用)** - -```json -{ - "success": true, - "deleted": { "quotations": 3, "inquiries": 3, "opportunities": 1, "rejected": [] } -} -``` - -**示例二:防护生效(对生产数据调用)** - -```json -{ - "success": true, - "deleted": { - "quotations": 0, "inquiries": 0, "opportunities": 0, - "rejected": ["QUO-2026-0027", "INQ-2026-0025", "OPP-2026-0018"] - } -} -``` - ---- - -## 5 附录 - -### 5.1 依赖数据表 - -| 表 | 用途 | -|----|------| -| `customer` | 客户主数据(信用/折扣/账期/历史统计) | -| `inquiry` | 询价单(pending→quoted) | -| `quotation` | 报价单主表(版本链/状态机/金额) | -| `quotation_cost_detail` | 报价成本明细(四项成本+费率) | -| `opportunity` | 商机(阶段/金额/概率/关单日期) | - -### 5.2 标准存档链路(智能体 Step 7) - -``` -get_quotation_history(inquiry_id) ← 前置:查有无历史版本 - ├─ 无 → create_inquiry(...) ← 建询价单 - └─ 有 → 复用原 inquiry_id -save_quotation(...) ← 存报价(版本自动+1,事务) -update_opportunity(...) ← 商机同步最新版年总额(stage=quoted) -get_inquiry_info / get_opportunity_info ← 可选:存档后交叉验证 -``` - -### 5.3 与 ERP 服务的协作 - -核价计算(材质对照/压铸参数/机加工工时/汇总核价)由 `mcp-for-erp` 承担,见《MCP for ERP — 工具说明文档》;CRM 服务不做核价计算,只做客户与商务数据管理。 diff --git a/mcp-for-crm/README.md b/mcp-for-crm/README.md deleted file mode 100644 index 8e88b4b..0000000 --- a/mcp-for-crm/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# MCP for CRM — 汽车零部件智能报价 CRM 数据服务 - -## 概述 - -为汽车零部件(小型阀体外壳)智能报价智能体提供 CRM 数据访问能力,通过 MCP 协议暴露 10 个工具(6 个业务 + 4 个验证/管理)。 - -## 技术栈 - -- **框架**: 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) | -| `save_quotation_mock` | 报价写入 CRM(Mock 模拟,独立 mock 表,与其他工具无关联) | - -## 快速开始 - -```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` | 10.100.154.100 | PostgreSQL 主机 | -| `CRM_DB_PORT` | 5432 | 端口 | -| `CRM_DB_USER` | postgres | 用户名 | -| `CRM_DB_PASSWORD` | digiwin | 密码 | -| `CRM_DB_NAME` | smart_quotation_auto | 数据库名 | diff --git a/mcp-for-crm/requirements.txt b/mcp-for-crm/requirements.txt deleted file mode 100644 index 6b6a815..0000000 --- a/mcp-for-crm/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -mcp>=1.0.0 -pydantic>=2.0.0 -asyncpg>=0.30.0 -httpx>=0.27.0 diff --git a/mcp-for-crm/sql/init.sql b/mcp-for-crm/sql/init.sql deleted file mode 100644 index 3ad19a1..0000000 --- a/mcp-for-crm/sql/init.sql +++ /dev/null @@ -1,271 +0,0 @@ --- ============================================================ --- 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; - --- ============================================================ --- 7. 报价写入 CRM Mock 表(独立模拟场景,无外键、与其他工具无关) --- ============================================================ -CREATE TABLE IF NOT EXISTS quotation_mock ( - quotation_id VARCHAR(50) PRIMARY KEY, - customer_name VARCHAR(200), - part_name VARCHAR(200), - part_number VARCHAR(50), - material_grade VARCHAR(50), - mold_cost DECIMAL(12,2) DEFAULT 0, - unit_price DECIMAL(10,2), - annual_volume INTEGER, - total_annual DECIMAL(14,2), - currency VARCHAR(10) DEFAULT 'CNY', - quotation_date DATE NOT NULL DEFAULT CURRENT_DATE, - status VARCHAR(20) DEFAULT 'draft', - remarks TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); diff --git a/mcp-for-crm/src/auth.py b/mcp-for-crm/src/auth.py deleted file mode 100644 index 1e32864..0000000 --- a/mcp-for-crm/src/auth.py +++ /dev/null @@ -1,119 +0,0 @@ -"""动态 Bearer Token 鉴权(通过 mcp-auth 后端 API 校验,不直连鉴权库)。 - -token 校验由 MCP 服务调用 mcp-auth 后端 `POST /api/auth/verify-token` 完成, -后端统一查 mcp_auth.mcp_token 表。后端通过 per-service API Key 识别调用方服务, -MCP 服务无需自报 service。进程内 LRU 缓存 30s,减少 API 调用。 - -环境变量: - MCP_PUBLIC_URL 服务对外地址(OAuth 资源元数据),默认 http://localhost:{port} - MCP_AUTH_API_URL mcp-auth 后端地址,如 http://localhost:8000 - MCP_AUTH_API_KEY 本服务的 API Key(由 mcp-auth 后端签发,per-service 唯一) - MCP_AUTH_CACHE_TTL 缓存秒数,默认 30 -""" - -import hashlib -import os -import time - -import httpx -from mcp.server.auth.middleware.auth_context import get_access_token -from mcp.server.auth.provider import AccessToken -from mcp.server.auth.settings import AuthSettings - - -_CACHE_TTL = int(os.getenv("MCP_AUTH_CACHE_TTL", "30")) - -# httpx 异步客户端(进程级单例,复用连接池) -_http_client: httpx.AsyncClient | None = None - - -def _get_http_client() -> httpx.AsyncClient: - global _http_client - if _http_client is None: - _http_client = httpx.AsyncClient(timeout=5.0) - return _http_client - - -async def close_auth_client() -> None: - """关闭 httpx 客户端(进程退出时调用)。""" - global _http_client - if _http_client is not None: - await _http_client.aclose() - _http_client = None - - -class ApiTokenVerifier: - """通过 mcp-auth 后端 API 校验 Bearer Token。 - - 进程内 LRU 缓存(token_hash -> (valid, client_id, fetched_at)),TTL 由 MCP_AUTH_CACHE_TTL 控制。 - """ - - def __init__(self, api_url: str, api_key: str): - self._api_url = api_url.rstrip("/") - self._api_key = api_key - self._cache: dict[str, tuple[bool, str | None, float]] = {} - - def invalidate(self, token: str | None = None) -> None: - """清缓存:token=None 清全部,否则清单个。""" - if token is None: - self._cache.clear() - else: - self._cache.pop(hashlib.sha256(token.encode()).hexdigest(), None) - - async def verify_token(self, token: str) -> AccessToken | None: - token_hash = hashlib.sha256(token.encode()).hexdigest() - - # 1. 查缓存 - now = time.time() - cached = self._cache.get(token_hash) - if cached is not None and (now - cached[2]) < _CACHE_TTL: - valid, client_id, _ = cached - if not valid: - return None - return AccessToken(token=token, client_id=client_id, scopes=[], expires_at=None) - - # 2. 调用后端校验 API(后端从 X-API-Key 识别 service,无需传 service) - try: - client = _get_http_client() - resp = await client.post( - f"{self._api_url}/api/auth/verify-token", - headers={"X-API-Key": self._api_key}, - json={"token": token}, - ) - except Exception: - # 后端不可达,缓存短时间避免雪崩 - self._cache[token_hash] = (False, None, now) - return None - - if resp.status_code == 200: - data = resp.json() - valid = data.get("valid", False) - client_id = data.get("client_id") - self._cache[token_hash] = (valid, client_id, now) - if valid: - return AccessToken(token=token, client_id=client_id, scopes=[], expires_at=None) - return None - else: - # 非 200(含 401 API Key 错误),缓存避免雪崩 - self._cache[token_hash] = (False, None, now) - return None - - -def get_auth(port: int) -> tuple[AuthSettings, ApiTokenVerifier]: - """构建 MCPServer 的 (auth, token_verifier) 参数。 - - service 由后端从 API Key 自动识别,MCP 服务无需配置 MCP_AUTH_SERVICE。 - """ - url = os.getenv("MCP_PUBLIC_URL") or f"http://localhost:{port}" - api_url = os.getenv("MCP_AUTH_API_URL", "http://localhost:8000") - api_key = os.getenv("MCP_AUTH_API_KEY", "") - return ( - AuthSettings(issuer_url=url, resource_server_url=url), - ApiTokenVerifier(api_url=api_url, api_key=api_key), - ) - - -def get_caller() -> str: - """工具内获取当前调用方标识(未认证时返回 anonymous)。""" - access_token = get_access_token() - return access_token.client_id if access_token else "anonymous" diff --git a/mcp-for-crm/src/db.py b/mcp-for-crm/src/db.py deleted file mode 100644 index dbfa236..0000000 --- a/mcp-for-crm/src/db.py +++ /dev/null @@ -1,28 +0,0 @@ -"""PostgreSQL 连接池管理""" - -import asyncpg -import os - -DB_CONFIG = { - "host": os.getenv("CRM_DB_HOST", "10.100.154.100"), - "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 diff --git a/mcp-for-crm/src/seed.py b/mcp-for-crm/src/seed.py deleted file mode 100644 index e3bbd70..0000000 --- a/mcp-for-crm/src/seed.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/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()) diff --git a/mcp-for-crm/src/server.py b/mcp-for-crm/src/server.py deleted file mode 100644 index 7a15a23..0000000 --- a/mcp-for-crm/src/server.py +++ /dev/null @@ -1,543 +0,0 @@ -""" -MCP for CRM — 汽车零部件智能报价 CRM 数据服务 -使用 MCPServer 框架,5 个工具:客户查询、询价建档、报价存档、历史报价、商机管理 -HTTP 模式,支持远程调用 -""" - -import asyncio -import json -from datetime import datetime, timedelta -from mcp.server import MCPServer -from auth import get_auth -from db import get_pool - - -def _s(value): - """查询语句处的 str 归一化:None / 空串 / 纯空白 → 统一为 None,其他原值返回。 - - 只处理 str 类型,不碰 int/float/list/bool。 - 用法:在每个工具函数开头对所有 str 参数执行 `x = _s(x)`, - 确保后续 `if x:` / `if x is not None:` / LIKE 拼接 行为一致。 - """ - if isinstance(value, str): - return value if value.strip() else None - return value - - -# 鉴权:静态 Bearer Token,token → 客户端 映射来自环境变量 MCP_AUTH_TOKENS -_auth, _token_verifier = get_auth(port=8002) - -app = MCPServer( - name="mcp-for-crm-auto", - description="汽车零部件智能报价 CRM 数据服务", - version="1.0.0", - auth=_auth, - token_verifier=_token_verifier, -) - - -# ============================================================ -# 工具 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 层级模糊搜索。返回客户信用等级、折扣率、历史订单等。""" - # 查询语句处 str 归一化:空串/纯空白同 None - customer_code = _s(customer_code) - name = _s(name) - oem_tier = _s(oem_tier) - 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 中创建新的询价记录。关联客户、图纸号、零件号、年用量。客户提交询价后建档使用。""" - # 查询语句处 str 归一化:空串/纯空白同 None - customer_code = _s(customer_code) - drawing_number = _s(drawing_number) - part_number = _s(part_number) - notes = _s(notes) - 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。包含模具费、单价、年用量、成本明细。报价生成后存档使用。""" - # 查询语句处 str 归一化:空串/纯空白同 None - inquiry_id = _s(inquiry_id) - customer_code = _s(customer_code) - payment_terms = _s(payment_terms) - delivery_terms = _s(delivery_terms) - remarks = _s(remarks) - 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 时每个询价单仅返回最新版本。""" - # 查询语句处 str 归一化:空串/纯空白同 None - customer_code = _s(customer_code) - inquiry_id = _s(inquiry_id) - start_date = _s(start_date) - end_date = _s(end_date) - status = _s(status) - 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 中的商机状态。支持创建新商机或更新已有商机的阶段、金额、概率。报价后跟进使用。""" - # 查询语句处 str 归一化:空串/纯空白同 None。str 变为 None 后, - # 后面 if value is not None 会自然跳过,避免空串被当作有效值写入。 - opportunity_id = _s(opportunity_id) - customer_code = _s(customer_code) - title = _s(title) - stage = _s(stage) - oem_program = _s(oem_program) - expected_close_date = _s(expected_close_date) - notes = _s(notes) - 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) - - -# ============================================================ -# 工具 6: get_inquiry_info — 询价单回读(只读) -# ============================================================ - -@app.tool() -async def get_inquiry_info(inquiry_id: str) -> str: - """按询价单号回读询价单整行(只读)。用于存档后验证:状态、图号、零件号、年用量、目标价、创建时间。""" - # 查询语句处 str 归一化:空串/纯空白同 None - inquiry_id = _s(inquiry_id) - pool = await get_pool() - row = await pool.fetchrow("SELECT * FROM inquiry WHERE inquiry_id = $1", inquiry_id) - if not row: - return json.dumps({"success": False, "message": f"未找到询价单 {inquiry_id}"}, ensure_ascii=False, indent=2) - return json.dumps({"success": True, "inquiry": dict(row)}, ensure_ascii=False, indent=2, default=str) - - -# ============================================================ -# 工具 7: get_opportunity_info — 商机回读(只读) -# ============================================================ - -@app.tool() -async def get_opportunity_info(opportunity_id: str = None, customer_code: str = None) -> str: - """按商机号或客户编码回读商机(只读)。用于存档后验证:stage/probability/expected_amount/更新时间。""" - # 查询语句处 str 归一化:空串/纯空白同 None - opportunity_id = _s(opportunity_id) - customer_code = _s(customer_code) - pool = await get_pool() - if opportunity_id: - row = await pool.fetchrow("SELECT * FROM opportunity WHERE opportunity_id = $1", opportunity_id) - rows = [row] if row else [] - elif customer_code: - rows = await pool.fetch("SELECT * FROM opportunity WHERE customer_code = $1 ORDER BY created_at DESC", customer_code) - else: - return json.dumps({"success": False, "message": "需提供 opportunity_id 或 customer_code"}, ensure_ascii=False, indent=2) - return json.dumps({"total": len(rows), "opportunities": [dict(r) for r in rows]}, ensure_ascii=False, indent=2, default=str) - - -# ============================================================ -# 工具 8: get_record_counts — 行数统计(只读) -# ============================================================ - -@app.tool() -async def get_record_counts() -> str: - """返回 inquiry/quotation/opportunity 三表行数(只读)。用于数据卫生检查。""" - pool = await get_pool() - counts = {t: await pool.fetchval(f"SELECT COUNT(*) FROM {t}") for t in ("inquiry", "quotation", "opportunity")} - return json.dumps({"success": True, "counts": counts}, ensure_ascii=False, indent=2) - - -# ============================================================ -# 工具 9: purge_test_records — 受限测试数据清理(管理) -# ============================================================ - -@app.tool() -async def purge_test_records( - quotation_ids: list = None, - inquiry_ids: list = None, - opportunity_ids: list = None -) -> str: - """清理 TC-03 回归测试数据(受限管理工具):仅删除 remarks/notes 含「TC-03回归测试」或 title 以「TC-03」开头的记录,其余一律拒绝,防止误删演示/生产数据。""" - pool = await get_pool() - deleted = {"quotations": 0, "inquiries": 0, "opportunities": 0, "rejected": []} - # 查询语句处 str 归一化:过滤掉 list 里的空串/纯空白 ID(同 None,跳过) - qids = [qid for qid in (quotation_ids or []) if _s(qid)] - iids = [iid for iid in (inquiry_ids or []) if _s(iid)] - oids = [oid for oid in (opportunity_ids or []) if _s(oid)] - - for qid in qids: - row = await pool.fetchrow("SELECT remarks FROM quotation WHERE quotation_id = $1", qid) - if row and "TC-03回归测试" in (row["remarks"] or ""): - await pool.execute("DELETE FROM quotation_cost_detail WHERE quotation_id = $1", qid) - await pool.execute("DELETE FROM quotation WHERE quotation_id = $1", qid) - deleted["quotations"] += 1 - else: - deleted["rejected"].append(qid) - - for iid in iids: - row = await pool.fetchrow("SELECT notes FROM inquiry WHERE inquiry_id = $1", iid) - if row and "TC-03回归测试" in (row["notes"] or ""): - await pool.execute("DELETE FROM inquiry_item WHERE inquiry_id = $1", iid) - await pool.execute("DELETE FROM inquiry WHERE inquiry_id = $1", iid) - deleted["inquiries"] += 1 - else: - deleted["rejected"].append(iid) - - for oid in oids: - row = await pool.fetchrow("SELECT title FROM opportunity WHERE opportunity_id = $1", oid) - if row and (row["title"] or "").startswith("TC-03"): - await pool.execute("DELETE FROM opportunity WHERE opportunity_id = $1", oid) - deleted["opportunities"] += 1 - else: - deleted["rejected"].append(oid) - - return json.dumps({"success": True, "deleted": deleted}, ensure_ascii=False, indent=2) - - -# ============================================================ -# 工具 10: save_quotation_mock — 报价写入 CRM(Mock 模拟) -# ============================================================ - -@app.tool() -async def save_quotation_mock( - part_name: str, - unit_price: float, - customer_name: str = None, - part_number: str = None, - material_grade: str = None, - mold_cost: float = 0.0, - annual_volume: int = None, - currency: str = "CNY", - remarks: str = None -) -> str: - """报价写入 CRM(模拟场景)。写入独立 mock 表 quotation_mock:不校验客户/询价单/商机,与其他工具无关联,仅模拟报价写入 CRM 动作。""" - pool = await get_pool() - - max_seq = await pool.fetchval( - "SELECT COALESCE(MAX(CAST(SUBSTRING(quotation_id FROM '(\\d+)$') AS INTEGER)), 0) FROM quotation_mock WHERE quotation_id LIKE 'QUO-MOCK-%'" - ) - quotation_id = f"QUO-MOCK-{str(max_seq + 1).zfill(4)}" - - total_annual = round(unit_price * annual_volume, 2) if annual_volume else None - - await pool.execute( - """INSERT INTO quotation_mock (quotation_id, customer_name, part_name, part_number, material_grade, - mold_cost, unit_price, annual_volume, total_annual, currency, status, remarks) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'draft', $11)""", - quotation_id, customer_name, part_name, part_number, material_grade, - mold_cost, unit_price, annual_volume, total_annual, currency, remarks - ) - - return json.dumps({ - "success": True, - "quotation_id": quotation_id, - "status": "draft", - "summary": { - "part_name": part_name, "unit_price": unit_price, "mold_cost": mold_cost, - "annual_volume": annual_volume, "total_annual": total_annual, "currency": currency - }, - "message": f"报价单 {quotation_id} 已写入 CRM mock 表(quotation_mock)" - }, 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")) diff --git a/mcp-for-crm/数据表与种子数据说明.md b/mcp-for-crm/数据表与种子数据说明.md deleted file mode 100644 index 584f03a..0000000 --- a/mcp-for-crm/数据表与种子数据说明.md +++ /dev/null @@ -1,278 +0,0 @@ -# MCP for CRM — 数据表结构与种子数据说明文档 - -> 数据库:PostgreSQL 15+,库 `smart_quotation_auto`(与 ERP 服务共库不同表) -> 建表/种子脚本:`sql/init.sql`(v3,幂等可重跑);执行入口:`cd src && python3 seed.py` -> 版本:v1.0(2026-08-09) - ---- - -## 1 概览 - -| # | 表名 | 用途 | 主键 | 种子行数 | -|---|------|------|------|----------| -| 1 | `customer` | 客户主数据(信用/折扣/账期) | customer_code | 20 | -| 2 | `inquiry` | 询价单 | inquiry_id | 9 | -| 3 | `inquiry_item` | 询价明细行(多零件) | id(SERIAL) | 9 | -| 4 | `quotation` | 报价单(多版本链) | quotation_id | 7 | -| 5 | `quotation_cost_detail` | 报价成本明细 | id(SERIAL) | 7 | -| 6 | `opportunity` | 商机 | opportunity_id | 8 | -| 7 | `quotation_mock` | 报价写入 CRM 模拟(独立 mock 表) | quotation_id | 0(运行时写入) | - -**表关系**: - -``` -customer ──┬── inquiry ──── inquiry_item (客户 1─N 询价;询价 1─N 明细行) - ├── quotation ── quotation_cost_detail(客户 1─N 报价;报价 1─1 成本明细) - │ └── inquiry_id 关联询价(版本链:同询价 N 版报价) - └── opportunity (客户 1─N 商机) -``` - -**外键**:inquiry/inquiry_item/quotation/opportunity 均外键指向 `customer.customer_code`;inquiry_item → inquiry;quotation → inquiry(可空);quotation_cost_detail → quotation。`quotation_mock` 无任何外键,为独立模拟表。 - ---- - -## 2 表结构详解 - -### 2.1 customer — 客户 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `customer_code` | VARCHAR(50) | PRIMARY KEY | 客户编码,如 `OEM-2024-003` | -| `name` | VARCHAR(200) | NOT NULL | 客户名称 | -| `oem_tier` | VARCHAR(20) | NOT NULL | OEM / Tier1 / Tier2 | -| `credit_level` | VARCHAR(5) | DEFAULT 'B' | 信用等级:A/B/C/D/无评级 | -| `discount_rate` | DECIMAL(4,3) | DEFAULT 0 | 折扣率(0.040 = 4%) | -| `payment_days` | INTEGER | DEFAULT 30 | 账期天数(0 = 货到付款/预付) | -| `contact_person` / `phone` / `email` | VARCHAR | — | 联系人信息 | -| `industry` / `region` | VARCHAR(50) | — | 行业 / 区域 | -| `total_orders` | INTEGER | DEFAULT 0 | 历史订单数 | -| `total_amount` | DECIMAL(14,2) | DEFAULT 0 | 历史订单总额 | -| `notes` | TEXT | — | 备注(风险提示等) | -| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建档时间 | - -### 2.2 inquiry — 询价单 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `inquiry_id` | VARCHAR(50) | PRIMARY KEY | 询价单号 `INQ-YYYY-NNNN` | -| `customer_code` | VARCHAR(50) | NOT NULL,FK | 客户编码 | -| `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' | pending / quoted(首报自动推进);种子含历史态 completed/processing/lost | -| `notes` | TEXT | — | 备注 | -| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | - -### 2.3 inquiry_item — 询价明细行 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `id` | SERIAL | PRIMARY KEY | 自增主键 | -| `inquiry_id` | VARCHAR(50) | NOT NULL,FK | 所属询价单 | -| `line_no` | INTEGER | NOT NULL | 行号;UNIQUE(inquiry_id, line_no) | -| `part_name` | VARCHAR(200) | — | 零件名称 | -| `part_number` | VARCHAR(100) | — | 零件号 | -| `material_grade` | VARCHAR(50) | — | 客户材质牌号 | -| `volume_cc` | DECIMAL(10,2) | — | 体积 cm³ | -| `annual_qty` | INTEGER | — | 该行年用量 | -| `unit` | VARCHAR(10) | DEFAULT '件' | 单位 | -| `remarks` | TEXT | — | 备注 | - -### 2.4 quotation — 报价单(多版本核心表) - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `quotation_id` | VARCHAR(50) | PRIMARY KEY | 报价单号 `QUO-YYYY-NNNN` | -| `inquiry_id` | VARCHAR(50) | FK(可空) | 所属询价单;索引 idx_quotation_inquiry | -| `customer_code` | VARCHAR(50) | NOT NULL,FK | 客户编码 | -| `quotation_date` | DATE | NOT NULL DEFAULT CURRENT_DATE | 报价日期 | -| `valid_until` | DATE | — | 有效期至 | -| `version` | INTEGER | DEFAULT 1 | 版本号;**UNIQUE(inquiry_id, version)**(并发防重) | -| `status` | VARCHAR(20) | DEFAULT 'draft' | draft/sent/approved/superseded/won/lost | -| `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 | 创建时间 | - -**版本链规则**:save_quotation 事务内 `version = MAX(version)+1`(不限次数);旧版非终态批量置 `superseded`;`won`/`lost` 终态保留不覆盖。 - -### 2.5 quotation_cost_detail — 报价成本明细 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `id` | SERIAL | PRIMARY KEY | 自增主键 | -| `quotation_id` | VARCHAR(50) | NOT NULL,FK,UNIQUE(uq_quotation_cost) | 报价单号(1:1) | -| `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) | — | 单价(冗余留痕,与主表对账) | - -### 2.6 opportunity — 商机 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `opportunity_id` | VARCHAR(50) | PRIMARY KEY | 商机号 `OPP-YYYY-NNNN` | -| `customer_code` | VARCHAR(50) | NOT NULL,FK | 客户编码 | -| `title` | VARCHAR(200) | NOT NULL | 商机标题(测试数据防护标记写入处) | -| `stage` | VARCHAR(20) | DEFAULT 'lead' | lead/qualified/proposal/negotiation/quoted/won/lost | -| `expected_amount` | DECIMAL(14,2) | DEFAULT 0 | 预期金额 ¥ | -| `probability` | INTEGER | DEFAULT 20 | 成交概率 % | -| `oem_program` | VARCHAR(200) | — | OEM 项目/平台 | -| `expected_close_date` | DATE | — | 预期关单日期 | -| `source` | VARCHAR(100) | — | 来源(智能报价自动生成/客户主动询价/展会获客等) | -| `created_by` | VARCHAR(50) | — | 创建者 | -| `created_at` / `updated_at` | DATE | DEFAULT CURRENT_DATE | 创建/更新日期 | - ---- - -### 2.7 quotation_mock — 报价写入 CRM 模拟表(独立,无外键) - -| 字段 | 类型 | 说明 | -|------|------|------| -| quotation_id | VARCHAR(50) PK | 模拟报价单号,`QUO-MOCK-####` 自增 | -| customer_name | VARCHAR(200) | 客户名称(自由文本,不校验) | -| part_name | VARCHAR(200) | 零件名称 | -| part_number | VARCHAR(50) | 零件号 | -| material_grade | VARCHAR(50) | 材质牌号 | -| mold_cost | DECIMAL(12,2) | 模具费,默认 0 | -| unit_price | DECIMAL(10,2) | 报价单价 | -| annual_volume | INTEGER | 年用量 | -| total_annual | DECIMAL(14,2) | 年总额(unit_price × annual_volume) | -| currency | VARCHAR(10) | 币种,默认 CNY | -| quotation_date | DATE | 报价日期,默认当天 | -| status | VARCHAR(20) | 状态,固定 `draft` | -| remarks | TEXT | 备注 | -| created_at | TIMESTAMP | 创建时间 | - -> 由工具 `save_quotation_mock` 单独写入,与正式报价链路(inquiry/quotation/opportunity)完全隔离,用于模拟「报价写入 CRM」场景。 - -## 3 种子数据详解 - -### 3.1 customer — 20 家客户 - -覆盖设计: - -- **层级**:OEM×7 / Tier1×7 / Tier2×6 -- **信用梯度**:A×8 / B×6 / C×2 / D×1 / 无评级×3 -- **特殊样本**:零历史新客户×3(T2-2025-002、T2-2026-001、T2-2026-002,用于信用评审场景);长账期 90 天(OEM-2025-001);货到付款 C/D 级(T2-2025-001/003/004) - -| 编码 | 名称 | 层级 | 信用 | 折扣率 | 账期 | 区域 | 特征 | -|------|------|------|------|--------|------|------|------| -| OEM-2024-001 | 某日系主机厂 | OEM | A | 5.0% | 60 | 上海 | 长期合作,铝合金阀体壳 | -| OEM-2024-002 | 某德系主机厂 | OEM | A | 3.0% | 45 | 北京 | 质量要求高,偏好铸铁件 | -| OEM-2024-003 | 某新能源主机厂 | OEM | A | 4.0% | 60 | 合肥 | 三电配套,增长快 | -| OEM-2024-004 | 某美系主机厂 | OEM | A | 3.0% | 45 | 武汉 | 混动变速箱项目 | -| OEM-2025-001 | 某商用车主机厂 | OEM | B | 2.0% | 90 | 长春 | 长账期,注意现金流 | -| OEM-2025-002 | 某乘商两用主机厂 | OEM | B | 2.5% | 60 | 柳州 | 乘商双线采购 | -| OEM-2026-001 | 某智能电动新势力 | OEM | A | 5.0% | 45 | 上海 | 高折扣,战略培育 | -| T1-2025-001 | 某博世供应商 | Tier1 | B | 2.0% | 30 | 江苏苏州 | 变速箱配套 | -| T1-2025-002 | 某大陆供应商 | Tier1 | B | 0 | 30 | 广东广州 | 制动配套,新开发 | -| T1-2025-003 | 某采埃孚供应商 | Tier1 | A | 2.0% | 45 | 上海 | ZF 悬架配套 | -| T1-2025-004 | 某电装系供应商 | Tier1 | A | 2.5% | 45 | 广州 | 日系热管理 | -| T1-2025-005 | 某日立安斯泰莫供应商 | Tier1 | A | 2.0% | 45 | 大连 | 日系制动/转向 | -| T1-2026-001 | 某制动系统供应商 | Tier1 | B | 1.5% | 30 | 重庆 | 国产线控制动 | -| T1-2026-002 | 某液压系统供应商 | Tier1 | B | 1.0% | 60 | 长沙 | 液压阀体,账期长 | -| T2-2025-001 | 某售后市场贸易商 | Tier2 | C | 0 | 0 | 杭州 | 货到付款,注意风险 | -| T2-2025-002 | 某智能底盘初创公司 | Tier2 | 无评级 | 0 | 30 | 苏州 | 零订单新客户 | -| T2-2025-003 | 某非道路机械贸易商 | Tier2 | D | 0 | 0 | 济宁 | 仅接受预付款 | -| T2-2025-004 | 某改装件连锁商 | Tier2 | C | 0 | 0 | 成都 | 改装连锁,货到付款 | -| T2-2026-001 | 某机器人关节初创公司 | Tier2 | 无评级 | 0 | 30 | 深圳 | 人形机器人关节部件 | -| T2-2026-002 | 某低空经济无人机公司 | Tier2 | 无评级 | 0 | 30 | 深圳 | eVTOL 液压部件 | - -### 3.2 inquiry + inquiry_item — 9 单询价(含明细行) - -覆盖设计:2024~2025 历史闭环(completed/lost)+ 2026 在途(pending/processing);单零件行,多零件场景由运行时写入。 - -| 询价单 | 客户 | 日期 | 零件 | 年用量 | 目标价 | 状态 | -|--------|------|------|------|--------|--------|------| -| INQ-2024-9005 | OEM-2024-001 | 2024-10-20 | 发动机油路阀体壳(EN AC-4600,555.56cc) | 200,000 | 55.00 | completed | -| INQ-2025-9001 | OEM-2024-001 | 2025-09-25 | 变速箱阀体外壳(ADC12,444.44cc) | 300,000 | 96.00 | completed | -| INQ-2025-9002 | OEM-2024-002 | 2025-08-10 | 制动阀体壳(HT250,388.89cc) | 150,000 | 155.00 | completed | -| INQ-2025-9003 | T1-2025-001 | 2025-05-28 | 转向器阀体壳(A356-T6,296.30cc) | 150,000 | 25.00 | lost | -| INQ-2025-9006 | OEM-2024-002 | 2025-11-05 | 变速箱阀体外壳(ADC12,小批量) | 100,000 | 100.00 | completed | -| INQ-2026-0001 | OEM-2024-001 | 2026-08-01 | 变速箱阀体外壳(ADC12,444.44cc) | 300,000 | 95.00 | processing | -| INQ-2026-0002 | T1-2025-001 | 2026-08-03 | 转向器阀体壳(A356-T6,296.30cc) | 150,000 | 55.00 | pending | -| INQ-2026-0006 | OEM-2024-004 | 2026-08-05 | 涡轮增压控制阀体壳(ADC12,320cc) | 120,000 | 48.00 | pending | -| INQ-2026-0007 | OEM-2025-001 | 2026-08-06 | 重卡制动阀体壳(HT300,850cc,1000T) | 60,000 | 260.00 | processing | - -### 3.3 quotation + quotation_cost_detail — 7 张报价 - -覆盖设计:状态谱系 approved/won/lost/sent + 一条**两版议价链**(INQ-2025-9001:v1→v2,一致性校验基准)+ 跨客户价差样本。 - -| 报价单 | 询价单 | 客户 | 版本 | 状态 | 模具费 | 单价 | 年用量 | 含税总额 | 备注 | -|--------|--------|------|------|------|--------|------|--------|----------|------| -| QUO-2024-9005 | INQ-2024-9005 | OEM-2024-001 | 1 | approved | 180,000 | 52.30 | 200,000 | 11,819,800 | 2024 基线价 | -| QUO-2025-9001 | INQ-2025-9001 | OEM-2024-001 | 1 | approved | 0 | 42.50 | 300,000 | 14,407,500 | 模具费已摊销 | -| QUO-2025-9002 | INQ-2025-9001 | OEM-2024-001 | 2 | approved | 0 | 41.90 | 300,000 | 14,204,100 | 第二轮降价 | -| QUO-2025-9003 | INQ-2025-9002 | OEM-2024-002 | 1 | won | 250,000 | 46.80 | 150,000 | 7,932,600 | 已中标(终态保护样本) | -| QUO-2025-9004 | INQ-2025-9003 | T1-2025-001 | 1 | lost | 120,000 | 28.40 | 150,000 | 4,813,800 | 报价偏高丢单(利润率 28% 样本) | -| QUO-2025-9006 | INQ-2025-9006 | OEM-2024-002 | 1 | sent | 0 | 43.20 | 100,000 | 4,881,600 | 跨客户价差校验 | -| QUO-2026-0001 | INQ-2026-0001 | OEM-2024-001 | 1 | sent | 180,000 | 92.50 | 300,000 | 31,357,500 | 老客户优惠,含模具费 | - -成本明细与报价 1:1(四项成本 + 12% 管理费 + 15% 利润,仅 QUO-2025-9004 为 28% 利润)。 - -### 3.4 opportunity — 8 条商机 - -覆盖设计:阶段全谱系 lead/qualified/proposal/negotiation/won/lost;金额 450 万~2775 万;AI 与销售团队双来源。 - -| 商机号 | 客户 | 阶段 | 金额¥ | 概率 | 关单日期 | OEM 项目 | -|--------|------|------|-------|------|----------|----------| -| OPP-2025-0005 | OEM-2024-002 | won | 8,200,000 | 100% | 2025-09-30 | 德系新制动平台 | -| OPP-2025-0006 | T1-2025-001 | lost | 4,500,000 | 0% | 2025-07-31 | EPS 转向系统 | -| OPP-2026-0001 | OEM-2024-001 | proposal | 27,750,000 | 60% | 2026-10-31 | 日系新车型 CVT | -| OPP-2026-0002 | T1-2025-001 | lead | 8,250,000 | 30% | 2026-12-31 | EPS 转向配套 | -| OPP-2026-0003 | T1-2025-003 | lead | 5,000,000 | 20% | 2026-12-31 | 主动悬架配套 | -| OPP-2026-0004 | OEM-2024-003 | negotiation | 9,000,000 | 60% | 2026-11-30 | 纯电平台热管理+制动 | -| OPP-2026-0005 | OEM-2024-004 | qualified | 12,000,000 | 40% | 2027-03-31 | 下一代混动变速箱 | -| OPP-2026-0006 | OEM-2025-001 | lead | 6,000,000 | 15% | 2026-12-31 | 重卡制动升级 | - ---- - -## 4 运行时数据(种子之外,真实运行留痕) - -以下数据由智能体真实运行产生,**不属于 init.sql**,作为演示/生产留痕保留: - -- **版本链演示**:INQ-2026-0019 三版议价(v1→v2→v3,旧版 superseded,带 price_change_pct)。 -- **生产报价留痕**:INQ-2026-0020~0027、QUO-2026-0019~0027、OPP-2026-0012~0018(含 TC-01/02/05 生产运行,如 QUO-2026-0027 单价 ¥8.06)。 -- **数据库现状**(2026-08-09):customer 20 / inquiry 24 / quotation 27 / opportunity 18;inquiry_item 与成本明细随行数联动。 -- **回归测试数据**:TC-03 写入带「TC-03回归测试」标记,跑完经 `purge_test_records` 自清理,不累积。 - ---- - -## 5 幂等性与重跑机制 - -`init.sql` 整体可重复执行,无副作用(NF-3): - -1. **建表**:`CREATE TABLE IF NOT EXISTS`。 -2. **历史去重**:inquiry_item 按 (inquiry_id, line_no)、quotation_cost_detail 按 quotation_id 删重(保留最小 id),随后幂等补唯一约束。 -3. **版本号规范化**:对存量 quotation 按 (inquiry_id, created_at, quotation_id) 重写 version 为 1..n(幂等,已规范的行不变)。 -4. **并发防重**:`UNIQUE(inquiry_id, version)` + 索引 `idx_quotation_inquiry`。 -5. **种子写入**:全部 `ON CONFLICT (...) DO NOTHING`,重跑不产生重复客户/询价/报价/商机。 - ---- - -## 6 数据维护约定 - -| 场景 | 操作 | 说明 | -|------|------|------| -| 新客户建档 | 直接 SQL 或后续扩展工具 | customer_code 规则:OEM/T1/T2-年份-序号 | -| 报价审核 | UPDATE quotation SET status/approved_by | 当前无 MCP 写工具,人工线下完成 | -| 测试数据清理 | 仅经 MCP `purge_test_records` | 禁止直连 DELETE;非 TC-03 标记数据受防护 | -| 生产数据 | 不得清理 | 演示/运行留痕作为验收基线 | diff --git a/mcp-for-erp/MCP工具说明文档.md b/mcp-for-erp/MCP工具说明文档.md deleted file mode 100644 index cd245ad..0000000 --- a/mcp-for-erp/MCP工具说明文档.md +++ /dev/null @@ -1,383 +0,0 @@ -# MCP for ERP — 工具说明文档 - -> 服务对象:汽车零部件(小型阀体外壳)智能报价智能体 -> 服务定位:ERP 侧核价基础数据与成本计算引擎(只读 + 计算,不写库) -> 版本:v1.1(2026-08-09) - ---- - -## 1 服务概览 - -| 项 | 值 | -|----|----| -| 服务名 | `mcp-for-erp-auto` | -| 框架 | MCPServer(mcp 2.0.0 内置高级框架),`@app.tool()` 装饰器注册 | -| 传输协议 | Streamable HTTP(JSON-RPC 2.0) | -| 远程端点 | `https://erp-data.dongsk.top/mcp` | -| 本地端点 | `http://0.0.0.0:8001/mcp`(`cd src && python3 server.py`) | -| 数据库 | PostgreSQL 15+,库 `smart_quotation_auto`(仅服务层内部经 `db.py` 连接池访问) | -| 工具数 | **5 个**(全部只读/计算) | - -### 工具一览 - -| # | 工具 | 功能 | 报价环节 | -|---|------|------|----------| -| 1 | `query_material_master` | 查询物料主数据(体积/密度/吨位/后处理) | 基础数据 | -| 2 | `match_material_grade` | 客户牌号 → 内部等效牌号对照 | 材质确认 | -| 3 | `get_die_casting_params` | 压铸参数:铝水重量、节拍、铸造成本 | 压铸成本 | -| 4 | `get_machining_estimate` | 机加工工时估算(固化工装节拍) | 机加工成本 | -| 5 | `calculate_part_cost` | 汇总核价,输出最终单价 | 最终报价 | - ---- - -## 2 调用约定 - -### 2.1 客户端注册(mcpServers) - -```json -{ - "mcpServers": { - "erp": { "url": "https://erp-data.dongsk.top/mcp" } - } -} -``` - -### 2.2 JSON-RPC 调用流程 - -1. `initialize`(握手,获取 `Mcp-Session-Id`) -2. `notifications/initialized`(通知,无响应体) -3. `tools/list`(可选,列出工具) -4. `tools/call`(调用工具,`params: {name, arguments}`) - -### 2.3 兜底命令行客户端(纯标准库) - -```bash -python3 scripts/mcp_http_client.py erp <工具名> '' -# 示例 -python3 scripts/mcp_http_client.py erp calculate_part_cost '{"material_code":"VHB-AT-001","annual_qty":300000}' -``` - -### 2.4 返回通用约定 - -- 所有工具返回 **JSON 字符串**(`ensure_ascii=False`,中文不转义)。 -- 查询类工具返回 `{"total": N, "<集合名>": [...]}`;操作/计算类工具返回 `{"success": true|false, ...}`。 -- 失败统一形态:`{"success": false, "message": "<原因>"}`(查询类失败以 `total: 0` 空集表达)。 -- 金额/重量:浮点数,成本保留 2 位小数,重量保留 3 位小数;日期 `YYYY-MM-DD`,月份 `YYYY-MM`。 - ---- - -## 3 工具详解 - -### 3.1 query_material_master — 查询物料主数据 - -**功能**:按物料编码精确查询,或按零件类别、材质牌号、压铸吨位组合过滤。返回物料档案(含体积、密度、净重、吨位、表面处理),是核价的起点数据。 - -**入参**(全部可选;全部不传 = 返回全表): - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `material_code` | string | 否 | — | 物料编码,精确匹配,如 `VHB-AT-001` | -| `part_category` | string | 否 | — | 零件类别,精确匹配,如 `阀体外壳` | -| `material_grade` | string | 否 | — | 客户材质牌号,精确匹配,如 `ADC12` | -| `die_casting_ton` | int | 否 | — | 压铸机吨位,精确匹配,如 `400` | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `total` | int | 命中条数 | -| `materials[]` | array | 物料行列表 | -| `materials[].material_code` | string | 物料编码(内部主键) | -| `materials[].part_name` | string | 零件名称 | -| `materials[].part_category` | string | 零件类别 | -| `materials[].material_grade` | string | 客户材质牌号 | -| `materials[].density` | decimal | 密度 g/cm³(JSON 中为字符串) | -| `materials[].volume_cc` | decimal | 零件体积 cm³(算铝水重量的依据) | -| `materials[].net_weight_kg` | decimal | 净重 kg(算后处理成本的依据) | -| `materials[].die_casting_ton` | int | 压铸机吨位 | -| `materials[].surface_treatment` | string | 表面处理(阳极氧化/钝化/无等) | -| `materials[].unit` | string | 计量单位 | -| `materials[].created_at` | string | 建档时间 | - -**返回示例**: - -```json -{ - "total": 1, - "materials": [ - { - "material_code": "VHB-AT-001", - "part_name": "变速箱阀体外壳", - "part_category": "阀体外壳", - "material_grade": "ADC12", - "density": "2.700", - "volume_cc": "444.44", - "net_weight_kg": "1.200", - "die_casting_ton": 400, - "surface_treatment": "阳极氧化", - "unit": "件", - "created_at": "2026-08-07 12:18:51.484993" - } - ] -} -``` - ---- - -### 3.2 match_material_grade — 材质牌号对照 - -**功能**:把客户指定牌号(GB/ASTM/JIS/EN/ISO 任一体系或内部码)对照为企业内部等效牌号,返回密度与铝锭参考价。支持模糊匹配与热处理后缀自动剥离(如 `A356-T6` → `A356`,最多剥 2 段)。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `customer_grade` | string | **是** | — | 客户牌号,大小写不敏感,模糊匹配六大码系 | -| `standard` | string | 否 | — | 标准体系提示(当前实现未参与过滤,保留参数) | - -**返回字段**(命中时): - -| 字段 | 类型 | 说明 | -|------|------|------| -| `matched` | bool | 是否命中 | -| `customer_grade` | string | 原客户牌号 | -| `internal_code` | string | 内部等效牌号,如 `AL-ADC12` | -| `density` | float | 密度 g/cm³ | -| `price_per_kg` | float | 铝锭参考价 ¥/kg(兜底价,核价优先取月度价) | -| `all_mappings[]` | array | 全部命中行(含 gb/astm/jis/en/iso/internal 六码、grade_group 材质族) | - -**未命中返回**: - -```json -{ "matched": false, "message": "未找到材质 AD12" } -``` - -**返回示例**(`ADC12`): - -```json -{ - "matched": true, - "customer_grade": "ADC12", - "internal_code": "AL-ADC12", - "density": 2.7, - "price_per_kg": 18.5, - "all_mappings": [ - { - "grade_group": "铝合金", - "gb_code": "ZAlSi10Cu(ADC12)", - "astm_code": "A380", - "jis_code": "ADC12", - "en_code": "EN AC-4600", - "iso_code": "AlSi9Cu3", - "internal_code": "AL-ADC12", - "density": "2.700", - "price_per_kg": "18.50" - } - ] -} -``` - -**业务规则**:未命中时 Skill 层应回退默认铝价并产生 warning(TC-06 场景),不应中断报价。 - ---- - -### 3.3 get_die_casting_params — 获取压铸参数 - -**功能**:按物料编码取压铸参数。核心输出:铝水重量(体积×密度×1.05 损耗)、压铸机吨位、模次节拍、单件压铸成本。体现 MVP 原则「**靠体积算铝水重量、压铸机吨位固定工时**」。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `material_code` | string | **是** | — | 物料编码 | -| `volume_cc` | float | 否 | 物料档案体积 | 覆盖体积(图纸实测体积优先于档案默认值) | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | bool | 成功标记 | -| `material_code` | string | 物料编码 | -| `volume_cc` | float | 采用体积 cm³ | -| `density` | float | 密度 g/cm³ | -| `aluminum_weight_kg` | float | 铝水重量 = 体积×密度/1000×1.05,3 位小数 | -| `die_casting_ton` | int | 压铸机吨位 | -| `cycle_time_sec` | float | 模次节拍(秒/模,按吨位固化) | -| `machine_rate` | float | 机台费率 ¥/小时 | -| `mold_cavities` | int | 模穴数 | -| `casting_cost_per_part` | float | 单件压铸成本 = (节拍/3600)×费率/模穴数,2 位小数 | - -**失败返回**:`{"success": false, "message": "未找到物料 XXX"}` 或 `{"success": false, "message": "未找到 400T 压铸机参数"}` - -**返回示例**(VHB-AT-001,444.44cc,400T): - -```json -{ - "success": true, - "material_code": "VHB-AT-001", - "volume_cc": 444.44, - "density": 2.7, - "aluminum_weight_kg": 1.26, - "die_casting_ton": 400, - "cycle_time_sec": 30.0, - "machine_rate": 260.0, - "mold_cavities": 1, - "casting_cost_per_part": 2.17 -} -``` - ---- - -### 3.4 get_machining_estimate — 机加工工时估算 - -**功能**:按孔清单(钻孔/攻丝/铰孔)与铣面数量估算机加工总工时与刀具成本。所有规格节拍来自固化表 `machining_cycle`(工装节拍),逐项累加。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `material_code` | string | **是** | — | 物料编码(回显用) | -| `drill_holes` | array[object] | 否 | [] | 钻孔清单,元素 `{"spec": "Φ6", "count": 2}` | -| `tap_holes` | array[object] | 否 | [] | 攻丝清单,元素 `{"spec": "M6×1.0", "count": 2}` | -| `ream_holes` | array[object] | 否 | [] | 铰孔清单,元素 `{"spec": "Φ8H7", "count": 1}` | -| `mill_faces` | int | 否 | 0 | 铣面数量 | - -孔元素字段:`spec`(规格字符串,缺省 Φ8 / M8×1.25 / Φ8H7)、`count`(数量,缺省 1)。 - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | bool | 成功标记 | -| `material_code` | string | 物料编码 | -| `total_time_sec` | float | 总工时(秒),2 位小数 | -| `total_time_hours` | float | 总工时(小时),4 位小数 | -| `total_tool_cost` | float | 刀具成本合计 ¥ | -| `details[]` | array | 逐项明细:`operation`(drill/tap/ream/mill)、`spec`、`count`、`total_time_sec` | - -**返回示例**(钻 Φ6×2 + 攻 M6×1.0×2 + 铣面×1): - -```json -{ - "success": true, - "material_code": "VHB-AT-001", - "total_time_sec": 26.0, - "total_time_hours": 0.0072, - "total_tool_cost": 1.5, - "details": [ - { "operation": "drill", "spec": "Φ6", "count": 2, "total_time_sec": 6.0 }, - { "operation": "tap", "spec": "M6×1.0", "count": 2, "total_time_sec": 8.0 }, - { "operation": "mill", "spec": "face", "count": 1, "total_time_sec": 12.0 } - ] -} -``` - -**注意**:固化表中不存在的规格**静默跳过**(不计工时也不报错);毛坯件(无孔无面)返回总工时 0,Skill 层应产生 warning(TC-07 场景)。 - ---- - -### 3.5 calculate_part_cost — 汇总核价(最终单价) - -**功能**:一站式核价。材料成本(铝水重量×当月铝价)+ 压铸成本 + 机加工成本 + 后处理成本 → 小计 × (1+管理费率) × (1+利润率) = 最终单价。铝价优先取 `material_price_history` 最新月度价并留痕月份;机加工费率按材质族(铝合金 60 / 灰铁 75 / 球铁 80 ¥/h)取 `machining_rate`。 - -**入参**: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| `material_code` | string | **是** | — | 物料编码 | -| `annual_qty` | int | **是** | — | 年用量(件/年) | -| `drill_holes` | array[object] | 否 | [] | 同 3.4 | -| `tap_holes` | array[object] | 否 | [] | 同 3.4 | -| `ream_holes` | array[object] | 否 | [] | 同 3.4 | -| `mill_faces` | int | 否 | 0 | 铣面数量 | -| `overhead_rate` | float | 否 | 0.12 | 管理费率 | -| `profit_rate` | float | 否 | 0.15 | 利润率 | - -**返回字段**: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `success` | bool | 成功标记 | -| `material_code` / `part_name` | string | 物料编码 / 零件名称 | -| `cost_breakdown.material_cost` | float | 材料成本 = 铝水重量×铝价 | -| `cost_breakdown.aluminum_weight_kg` | float | 铝水重量(含 1.05 损耗) | -| `cost_breakdown.aluminum_price_per_kg` | float | 采用铝价 ¥/kg | -| `cost_breakdown.price_basis_month` | string | 铝价月份 `YYYY-MM`(留痕;无月度价时为 null) | -| `cost_breakdown.internal_code` | string | 内部牌号 | -| `cost_breakdown.grade_group` | string | 材质族(决定机加工费率) | -| `cost_breakdown.casting_cost` | float | 单件压铸成本 | -| `cost_breakdown.die_casting_ton` | int | 压铸机吨位 | -| `cost_breakdown.machining_cost` | float | 机加工成本 = 工时×费率 | -| `cost_breakdown.machining_hours` | float | 机加工总工时(小时) | -| `cost_breakdown.machining_rate_per_hour` | float | 机加工费率 ¥/h | -| `cost_breakdown.post_process_cost` | float | 后处理成本 = 净重×工艺费率 | -| `cost_breakdown.surface_treatment` | string | 表面处理工艺 | -| `total_cost` | float | 四项成本小计 | -| `overhead_rate` / `overhead` | float | 管理费率 / 管理费额 | -| `profit_rate` | float | 利润率 | -| `unit_price` | float | **最终单价** = (total_cost+overhead)×(1+profit_rate) | -| `currency` | string | 固定 `CNY` | - -**返回示例**(VHB-AT-001,30 万件/年,钻 Φ6×2/攻 M6×1.0×2/铰 Φ8H7×1/铣面×1): - -```json -{ - "success": true, - "material_code": "VHB-AT-001", - "part_name": "变速箱阀体外壳", - "cost_breakdown": { - "material_cost": 23.31, - "aluminum_weight_kg": 1.26, - "aluminum_price_per_kg": 18.5, - "price_basis_month": "2026-08", - "internal_code": "AL-ADC12", - "grade_group": "铝合金", - "casting_cost": 2.17, - "die_casting_ton": 400, - "machining_cost": 0.53, - "machining_hours": 0.0089, - "machining_rate_per_hour": 60.0, - "post_process_cost": 6.0, - "surface_treatment": "阳极氧化" - }, - "total_cost": 32.01, - "overhead_rate": 0.12, - "overhead": 3.84, - "profit_rate": 0.15, - "unit_price": 41.23, - "currency": "CNY" -} -``` - -**失败返回**:`{"success": false, "message": "未找到物料 XXX"}` - ---- - -## 4 附录 - -### 4.1 核心公式 - -``` -铝水重量(kg) = 体积(cm³) × 密度(g/cm³) / 1000 × 1.05(损耗系数) -材料成本 = 铝水重量 × 铝锭单价(当月月度价,缺省 18.5) -压铸成本 = (节拍秒 / 3600) × 机台费率 / 模穴数 ← 吨位固化节拍 -机加工成本 = Σ(工序固化节拍×数量) / 3600 × 材质族费率 -后处理成本 = 净重(kg) × 表面处理费率 -单价 = (四项成本小计 × (1+管理费率)) × (1+利润率) -``` - -### 4.2 依赖数据表 - -| 表 | 用途 | -|----|------| -| `material_master` | 物料主数据(体积/密度/吨位/后处理) | -| `material_grade_mapping` | 五标准体系牌号对照 + 材质族 + 参考铝价 | -| `material_price_history` | 月度铝价留痕(核价取最新月) | -| `die_casting_params` | 吨位 → 节拍/费率/模穴数 | -| `machining_cycle` | 工序规格固化节拍与刀具费率 | -| `machining_rate` | 材质族机加工费率(60/75/80 ¥/h) | -| `post_process_rate` | 表面处理费率(¥/kg) | - -### 4.3 与 CRM 服务的协作 - -ERP 服务只读不写。报价存档链路(create_inquiry → save_quotation → update_opportunity)由 `mcp-for-crm` 承担,见《MCP for CRM — 工具说明文档》。 diff --git a/mcp-for-erp/README.md b/mcp-for-erp/README.md deleted file mode 100644 index ed1891d..0000000 --- a/mcp-for-erp/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# 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` | 10.100.154.100 | PostgreSQL 主机 | -| `ERP_DB_PORT` | 5432 | 端口 | -| `ERP_DB_USER` | postgres | 用户名 | -| `ERP_DB_PASSWORD` | digiwin | 密码 | -| `ERP_DB_NAME` | smart_quotation_auto | 数据库名 | diff --git a/mcp-for-erp/requirements.txt b/mcp-for-erp/requirements.txt deleted file mode 100644 index 6b6a815..0000000 --- a/mcp-for-erp/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -mcp>=1.0.0 -pydantic>=2.0.0 -asyncpg>=0.30.0 -httpx>=0.27.0 diff --git a/mcp-for-erp/sql/init.sql b/mcp-for-erp/sql/init.sql deleted file mode 100644 index 9541da8..0000000 --- a/mcp-for-erp/sql/init.sql +++ /dev/null @@ -1,234 +0,0 @@ --- ============================================================ --- 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; diff --git a/mcp-for-erp/src/auth.py b/mcp-for-erp/src/auth.py deleted file mode 100644 index 1e32864..0000000 --- a/mcp-for-erp/src/auth.py +++ /dev/null @@ -1,119 +0,0 @@ -"""动态 Bearer Token 鉴权(通过 mcp-auth 后端 API 校验,不直连鉴权库)。 - -token 校验由 MCP 服务调用 mcp-auth 后端 `POST /api/auth/verify-token` 完成, -后端统一查 mcp_auth.mcp_token 表。后端通过 per-service API Key 识别调用方服务, -MCP 服务无需自报 service。进程内 LRU 缓存 30s,减少 API 调用。 - -环境变量: - MCP_PUBLIC_URL 服务对外地址(OAuth 资源元数据),默认 http://localhost:{port} - MCP_AUTH_API_URL mcp-auth 后端地址,如 http://localhost:8000 - MCP_AUTH_API_KEY 本服务的 API Key(由 mcp-auth 后端签发,per-service 唯一) - MCP_AUTH_CACHE_TTL 缓存秒数,默认 30 -""" - -import hashlib -import os -import time - -import httpx -from mcp.server.auth.middleware.auth_context import get_access_token -from mcp.server.auth.provider import AccessToken -from mcp.server.auth.settings import AuthSettings - - -_CACHE_TTL = int(os.getenv("MCP_AUTH_CACHE_TTL", "30")) - -# httpx 异步客户端(进程级单例,复用连接池) -_http_client: httpx.AsyncClient | None = None - - -def _get_http_client() -> httpx.AsyncClient: - global _http_client - if _http_client is None: - _http_client = httpx.AsyncClient(timeout=5.0) - return _http_client - - -async def close_auth_client() -> None: - """关闭 httpx 客户端(进程退出时调用)。""" - global _http_client - if _http_client is not None: - await _http_client.aclose() - _http_client = None - - -class ApiTokenVerifier: - """通过 mcp-auth 后端 API 校验 Bearer Token。 - - 进程内 LRU 缓存(token_hash -> (valid, client_id, fetched_at)),TTL 由 MCP_AUTH_CACHE_TTL 控制。 - """ - - def __init__(self, api_url: str, api_key: str): - self._api_url = api_url.rstrip("/") - self._api_key = api_key - self._cache: dict[str, tuple[bool, str | None, float]] = {} - - def invalidate(self, token: str | None = None) -> None: - """清缓存:token=None 清全部,否则清单个。""" - if token is None: - self._cache.clear() - else: - self._cache.pop(hashlib.sha256(token.encode()).hexdigest(), None) - - async def verify_token(self, token: str) -> AccessToken | None: - token_hash = hashlib.sha256(token.encode()).hexdigest() - - # 1. 查缓存 - now = time.time() - cached = self._cache.get(token_hash) - if cached is not None and (now - cached[2]) < _CACHE_TTL: - valid, client_id, _ = cached - if not valid: - return None - return AccessToken(token=token, client_id=client_id, scopes=[], expires_at=None) - - # 2. 调用后端校验 API(后端从 X-API-Key 识别 service,无需传 service) - try: - client = _get_http_client() - resp = await client.post( - f"{self._api_url}/api/auth/verify-token", - headers={"X-API-Key": self._api_key}, - json={"token": token}, - ) - except Exception: - # 后端不可达,缓存短时间避免雪崩 - self._cache[token_hash] = (False, None, now) - return None - - if resp.status_code == 200: - data = resp.json() - valid = data.get("valid", False) - client_id = data.get("client_id") - self._cache[token_hash] = (valid, client_id, now) - if valid: - return AccessToken(token=token, client_id=client_id, scopes=[], expires_at=None) - return None - else: - # 非 200(含 401 API Key 错误),缓存避免雪崩 - self._cache[token_hash] = (False, None, now) - return None - - -def get_auth(port: int) -> tuple[AuthSettings, ApiTokenVerifier]: - """构建 MCPServer 的 (auth, token_verifier) 参数。 - - service 由后端从 API Key 自动识别,MCP 服务无需配置 MCP_AUTH_SERVICE。 - """ - url = os.getenv("MCP_PUBLIC_URL") or f"http://localhost:{port}" - api_url = os.getenv("MCP_AUTH_API_URL", "http://localhost:8000") - api_key = os.getenv("MCP_AUTH_API_KEY", "") - return ( - AuthSettings(issuer_url=url, resource_server_url=url), - ApiTokenVerifier(api_url=api_url, api_key=api_key), - ) - - -def get_caller() -> str: - """工具内获取当前调用方标识(未认证时返回 anonymous)。""" - access_token = get_access_token() - return access_token.client_id if access_token else "anonymous" diff --git a/mcp-for-erp/src/db.py b/mcp-for-erp/src/db.py deleted file mode 100644 index d7ef0f4..0000000 --- a/mcp-for-erp/src/db.py +++ /dev/null @@ -1,28 +0,0 @@ -"""PostgreSQL 连接池管理""" - -import asyncpg -import os - -DB_CONFIG = { - "host": os.getenv("ERP_DB_HOST", "10.100.154.100"), - "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 diff --git a/mcp-for-erp/src/seed.py b/mcp-for-erp/src/seed.py deleted file mode 100644 index 316848d..0000000 --- a/mcp-for-erp/src/seed.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/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()) diff --git a/mcp-for-erp/src/server.py b/mcp-for-erp/src/server.py deleted file mode 100644 index 8eda01f..0000000 --- a/mcp-for-erp/src/server.py +++ /dev/null @@ -1,353 +0,0 @@ -""" -MCP for ERP — 汽车零部件智能报价 ERP 数据服务 -使用 MCPServer 框架,5 个工具:物料查询、材质对照、压铸参数、机加工估算、成本核算 -HTTP 模式,支持远程调用 -""" - -import asyncio -import json -from mcp.server import MCPServer -from auth import get_auth -from db import get_pool - - -def _s(value): - """查询语句处的 str 归一化:None / 空串 / 纯空白 → 统一为 None,其他原值返回。 - - 只处理 str 类型,不碰 int/float/list/bool。 - 用法:在每个工具函数开头对所有 str 参数执行 `x = _s(x)`, - 确保后续 `if x:` / `if x is not None:` / LIKE 拼接 行为一致。 - """ - if isinstance(value, str): - return value if value.strip() else None - return value - - -# 鉴权:静态 Bearer Token,token → 客户端 映射来自环境变量 MCP_AUTH_TOKENS -_auth, _token_verifier = get_auth(port=8001) - -app = MCPServer( - name="mcp-for-erp-auto", - description="汽车零部件智能报价 ERP 数据服务", - version="1.0.0", - auth=_auth, - token_verifier=_token_verifier, -) - - -# ============================================================ -# 工具 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 物料主数据。支持按物料编码精确查询,或按零件类别、材质、吨位等参数组合查询。返回物料列表,包含体积、重量、吨位等关键参数。""" - # 查询语句处 str 归一化:空串/纯空白同 None - material_code = _s(material_code) - part_category = _s(part_category) - material_grade = _s(material_grade) - 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 标准体系,返回等效牌号、密度、铝锭参考价。""" - # 查询语句处 str 归一化:空串/纯空白同 None - customer_grade = _s(customer_grade) - standard = _s(standard) - if customer_grade is None: - return json.dumps({"matched": False, "message": "需提供 customer_grade(材质牌号)"}, ensure_ascii=False, indent=2) - 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: - """根据物料编码获取压铸参数。返回压铸机吨位、模次节拍、铝水重量、压铸成本。铝水重量 = 体积 × 密度。""" - # 查询语句处 str 归一化:空串/纯空白同 None - material_code = _s(material_code) - 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: - """根据孔清单估算机加工工时。钻孔、攻丝、铰孔、铣面均有固化节拍,逐项累加得到总工时和成本。""" - # 查询语句处 str 归一化:空串/纯空白同 None - material_code = _s(material_code) - 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 = _s(hole.get("spec", "Φ8")) or "Φ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 = _s(hole.get("spec", "M8×1.25")) or "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 = _s(hole.get("spec", "Φ8H7")) or "Φ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: - """汇总核价:材料成本(铝水重量×单价)+ 压铸成本 + 机加工成本 + 后处理成本 + 管理费 + 利润。返回完整成本明细和最终报价。""" - # 查询语句处 str 归一化:空串/纯空白同 None - material_code = _s(material_code) - 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: - spec = _s(hole.get("spec", "Φ8")) or "Φ8" - row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='drill' AND spec=$1", spec) - if row: - total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1) - - for hole in tap_holes: - spec = _s(hole.get("spec", "M8×1.25")) or "M8×1.25" - row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='tap' AND spec=$1", spec) - if row: - total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1) - - for hole in ream_holes: - spec = _s(hole.get("spec", "Φ8H7")) or "Φ8H7" - row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='ream' AND spec=$1", spec) - 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")) diff --git a/mcp-for-erp/数据表与种子数据说明.md b/mcp-for-erp/数据表与种子数据说明.md deleted file mode 100644 index c0fd211..0000000 --- a/mcp-for-erp/数据表与种子数据说明.md +++ /dev/null @@ -1,239 +0,0 @@ -# MCP for ERP — 数据表结构与种子数据说明文档 - -> 数据库:PostgreSQL 15+,库 `smart_quotation_auto`(与 CRM 服务共库不同表) -> 建表/种子脚本:`sql/init.sql`(v3,幂等可重跑);执行入口:`cd src && python3 seed.py` -> 版本:v1.0(2026-08-09) - ---- - -## 1 概览 - -| # | 表名 | 用途 | 主键 | 种子行数 | -|---|------|------|------|----------| -| 1 | `material_master` | 物料主数据(体积/密度/吨位/后处理) | material_code | 16 | -| 2 | `material_grade_mapping` | 五标准体系牌号对照 | id(SERIAL) | 13 | -| 3 | `die_casting_params` | 压铸参数(吨位→固化节拍/费率/模穴) | tonnage | 9 | -| 4 | `machining_cycle` | 机加工固化工装节拍 | id(SERIAL) | 26 | -| 5 | `post_process_rate` | 后处理费率(¥/kg) | id(SERIAL) | 10 | -| 6 | `material_price_history` | 月度材料价格留痕 | id(SERIAL) | 260(13 牌号×20 月) | -| 7 | `machining_rate` | 材质族机加工费率 | grade_group | 4 | - -**表关系**:`material_master.material_grade` →(经 match_material_grade 模糊对照)→ `material_grade_mapping`(internal_code/grade_group)→ `material_price_history`(按 internal_code 取最新月价)、`machining_rate`(按 grade_group 取费率);`material_master.die_casting_ton` → `die_casting_params.tonnage`;机加工工时查 `machining_cycle`(operation_type+spec);后处理查 `post_process_rate`(process_type = material_master.surface_treatment)。 - ---- - -## 2 表结构详解 - -### 2.1 material_master — 物料主数据 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `material_code` | VARCHAR(50) | PRIMARY KEY | 物料编码,如 `VHB-AT-001` | -| `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 | 密度 g/cm³ | -| `volume_cc` | DECIMAL(10,2) | NOT NULL | 零件体积 cm³(**算铝水重量的依据**) | -| `net_weight_kg` | DECIMAL(8,3) | NOT NULL | 净重 kg(**算后处理成本的依据**) | -| `die_casting_ton` | INTEGER | NOT NULL | 压铸机吨位(关联 die_casting_params) | -| `surface_treatment` | VARCHAR(50) | — | 表面处理(关联 post_process_rate) | -| `unit` | VARCHAR(10) | DEFAULT '件' | 计量单位 | -| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建档时间 | - -### 2.2 material_grade_mapping — 材质牌号对照表 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `id` | SERIAL | PRIMARY KEY | 自增主键 | -| `grade_group` | VARCHAR(50) | NOT NULL | 材质族:铝合金/铸铝/铸铁/球墨铸铁(决定机加工费率) | -| `gb_code` | VARCHAR(80) | — | GB 牌号 | -| `astm_code` | VARCHAR(80) | — | ASTM 牌号 | -| `jis_code` | VARCHAR(80) | — | JIS 牌号 | -| `en_code` | VARCHAR(80) | — | EN 牌号 | -| `iso_code` | VARCHAR(80) | — | ISO 牌号 | -| `internal_code` | VARCHAR(50) | NOT NULL,UNIQUE(uq_grade_mapping) | 内部等效牌号 | -| `density` | DECIMAL(6,3) | NOT NULL | 密度 g/cm³ | -| `price_per_kg` | DECIMAL(10,2) | NOT NULL | 铝锭/材料参考价 ¥/kg(兜底价) | - -> 对照匹配对六列码值做大小写不敏感 LIKE 模糊匹配;带热处理后缀(如 A356-T6)自动剥后缀重试。 - -### 2.3 die_casting_params — 压铸参数表 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `tonnage` | INTEGER | PRIMARY KEY | 压铸机吨位 | -| `shot_weight_kg` | DECIMAL(8,3) | NOT NULL | 射出重量 kg(参考值) | -| `cycle_time_sec` | DECIMAL(8,2) | NOT NULL | 模次节拍 秒/模(**吨位固化**) | -| `machine_rate` | DECIMAL(10,2) | NOT NULL | 机台费率 ¥/小时 | -| `mold_cavities` | INTEGER | DEFAULT 1 | 模穴数(压铸成本按穴分摊) | - -### 2.4 machining_cycle — 机加工节拍表(固化工装节拍) - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `id` | SERIAL | PRIMARY KEY | 自增主键 | -| `operation_type` | VARCHAR(30) | NOT NULL | drill 钻孔 / tap 攻丝 / ream 铰孔 / mill 铣面 | -| `spec` | VARCHAR(50) | NOT NULL | 规格:Φ6、M6×1.0、Φ8H7、face;UNIQUE(operation_type, spec) | -| `cycle_time_sec` | DECIMAL(8,2) | NOT NULL | 单件固化节拍(秒) | -| `tool_rate` | DECIMAL(8,4) | DEFAULT 0 | 单件刀具成本 ¥ | -| `notes` | VARCHAR(200) | — | 备注(通孔/盲孔等) | - -### 2.5 post_process_rate — 后处理费率表 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `id` | SERIAL | PRIMARY KEY | 自增主键 | -| `process_type` | VARCHAR(50) | NOT NULL,UNIQUE(uq_post_process) | 工艺名(与物料 surface_treatment 对应) | -| `unit` | VARCHAR(10) | DEFAULT 'kg' | 计价单位 | -| `rate` | DECIMAL(10,2) | NOT NULL | 费率 ¥/kg | -| `currency` | VARCHAR(10) | DEFAULT 'CNY' | 币种 | - -### 2.6 material_price_history — 材料价格历史表 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `id` | SERIAL | PRIMARY KEY | 自增主键 | -| `internal_code` | VARCHAR(50) | NOT NULL | 内部牌号;UNIQUE(internal_code, price_date) | -| `price_date` | DATE | NOT NULL | 价格月份(每月 1 日) | -| `price_per_kg` | DECIMAL(10,2) | NOT NULL | 当月参考价 ¥/kg | -| `source` | VARCHAR(100) | DEFAULT '市场参考价' | 数据来源留痕 | - -> 核价取法:`ORDER BY price_date DESC LIMIT 1`(最新月),并在返回中带 `price_basis_month` 留痕。 - -### 2.7 machining_rate — 机加工费率表 - -| 列 | 类型 | 约束/默认 | 说明 | -|----|------|-----------|------| -| `grade_group` | VARCHAR(50) | PRIMARY KEY | 材质族 | -| `rate_per_hour` | DECIMAL(10,2) | NOT NULL | 费率 ¥/小时 | -| `notes` | VARCHAR(200) | — | 备注 | - ---- - -## 3 种子数据详解 - -### 3.1 material_master — 16 种阀体外壳 - -覆盖设计:铝合金 12 / 灰铸铁 2(HT250、HT300)/ 球墨铸铁 1(QT500-7)/ 铸铝 1(ZL101);吨位 160~1000T 全谱系;体积 96.3~1037 cm³。 - -| 物料编码 | 零件名称 | 牌号 | 密度 | 体积cm³ | 净重kg | 吨位 | 后处理 | -|----------|----------|------|------|---------|--------|------|--------| -| 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 | 达克罗 | - -### 3.2 material_grade_mapping — 13 条牌号对照 - -覆盖设计:铝合金 7 / 铸铝 1 / 铸铁 3 / 球墨铸铁 2;每条六码齐全(GB/ASTM/JIS/EN/ISO/内部码)。 - -| 材质族 | GB | ASTM | JIS | EN | ISO | 内部码 | 密度 | 参考价¥/kg | -|--------|----|------|-----|----|----|--------|------|-----------| -| 铝合金 | 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 | - -> 脚本含修正语句:`IR-QT%` 组别归位「球墨铸铁」(历史数据曾误归「铸铁」)。 - -### 3.3 die_casting_params — 9 档吨位 - -| 吨位 | 射出重量kg | 节拍秒/模 | 机台费率¥/h | 模穴数 | -|------|-----------|-----------|-------------|--------| -| 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 | - -> **故意不含 1250T**:用于回退测试(未命中吨位场景)。 - -### 3.4 machining_cycle — 26 条固化节拍 - -| 工序 | 规格谱系 | 条数 | 节拍范围(秒) | 刀具费范围(¥) | -|------|----------|------|--------------|----------------| -| drill 钻孔 | Φ3 / Φ4 / Φ5 / Φ6 / Φ8 / Φ10 / Φ12 / Φ14 / Φ16 / Φ20 / Φ24 | 11 | 2.00~12.00 | 0.10~0.60 | -| tap 攻丝 | M4×0.7 / M5×0.8 / M6×1.0 / M8×1.25 / M10×1.5 / M12×1.75 / M14×2.0 / M16×2.0 | 8 | 3.00~9.00 | 0.15~0.45 | -| ream 铰孔 | Φ6H7 / Φ8H7 / Φ10H7 / Φ12H7 / Φ16H7 / Φ20H7 | 6 | 5.00~12.00 | 0.45~1.10 | -| mill 铣面 | face(单面) | 1 | 12.00 | 0.80 | - -> **故意不含 Φ25 钻孔 / M18 攻丝**:用于回退测试(未知规格静默跳过场景)。 - -### 3.5 post_process_rate — 10 种后处理 - -| 工艺 | 费率¥/kg | 工艺 | 费率¥/kg | -|------|----------|------|----------| -| 喷砂 | 1.50 | 粉末喷涂 | 3.00 | -| 钝化 | 2.00 | 磷化 | 1.80 | -| 阳极氧化 | 5.00 | 达克罗 | 4.00 | -| 电泳涂装 | 3.50 | 硬质阳极氧化 | 8.00 | -| 喷涂 | 2.50 | 化学镀镍 | 9.00 | - -> **故意不含「镀铬」**:用于回退测试(未命中后处理场景,成本计 0)。 - -### 3.6 material_price_history — 260 行月度价格(2025-01 ~ 2026-08) - -生成规则(脚本内 CTE 批量生成,非手工录入): - -- 13 个内部牌号 × 20 个月 = 260 行,`source='市场参考价'`。 -- 每月价格 = 对照表参考价 × 月份系数;铝系用 `al_factor`(0.92~1.02 波动),铁系(铸铁/球墨铸铁)用 `ir_factor`(0.95~1.01 波动)。 -- **最新月(2026-08)系数 = 1.00**,即与对照表 `price_per_kg` 完全一致——保证核价结果与静态对照表口径可对账(如 AL-ADC12 当月价 18.50)。 - -### 3.7 machining_rate — 4 条材质族费率 - -| 材质族 | 费率¥/h | 备注 | -|--------|---------|------| -| 铝合金 | 60.00 | 常规机加工费率 | -| 铸铝 | 60.00 | 同铝合金 | -| 铸铁 | 75.00 | 刀具损耗高,费率上浮 | -| 球墨铸铁 | 80.00 | 强度高,费率最高 | - ---- - -## 4 幂等性与重跑机制 - -`init.sql` 整体可重复执行,无副作用(NF-3): - -1. **建表**:`CREATE TABLE IF NOT EXISTS`。 -2. **历史去重**:对 SERIAL 表先按业务键删除重复行(保留最小 id)。 -3. **补唯一约束**:`DO $$ ... ADD CONSTRAINT ... EXCEPTION WHEN duplicate_table` 幂等加约束(uq_machining_cycle / uq_grade_mapping / uq_post_process)。 -4. **种子写入**:全部 `ON CONFLICT (...) DO NOTHING`。 -5. **月度价格**:`UNIQUE(internal_code, price_date)` + `ON CONFLICT DO NOTHING`,重跑不翻倍。 - ---- - -## 5 数据维护约定 - -| 场景 | 操作 | 约束 | -|------|------|------| -| 月度铝价更新 | `material_price_history` 追加新月行 | 核价自动取最新月并留痕 `price_basis_month` | -| 新增机加工规格 | `machining_cycle` 增行 | 不改旧行(历史报价可复算) | -| 新增吨位 | `die_casting_params` 增行 | 同上 | -| 新增物料 | `material_master` 增行 | material_grade 须能命中对照表,否则走回退 | - -> ERP 侧全部为只读基础数据:无 MCP 写工具,数据变更仅通过 SQL/脚本进行。