# 产品库技术设计文档（最终架构版）

> **文档版本**: v3.1-draft
> **编写部门**: 项目部
> **编写日期**: 2026-04-17
> **文档状态**: 草稿（协作编写中）
> **技术框架**: yudao-cloud

---

## 目录

1. [概述与设计目标](#1-概述与设计目标)
2. [模块划分与服务边界](#2-模块划分与服务边界)
3. [数据模型设计](#3-数据模型设计)
4. [服务调用关系](#4-服务调用关系)
5. [API接口设计概要](#5-api接口设计概要)
6. [与yudao-cloud集成说明](#6-与yudao-cloud集成说明)

---

## 1. 概述与设计目标

### 1.1 文档背景

本文档是《烘焙企业信息化系统微服务架构方案提案》的**第一阶段实施技术设计文档**，聚焦于**产品库模块**的开发。

产品库模块是整个信息化系统的**数据基石**，为后续的生产排程、仓储管理、采购管理、成本核算等模块提供核心主数据支撑。

本项目基于 **yudao-cloud** 微服务框架进行开发。yudao-cloud 是一套基于 Spring Cloud Alibaba 的成熟开源框架，内置了用户管理、权限认证、文件上传、数据字典、代码生成等大量开箱即用的基础能力。经过架构评审，我们决定**最大化复用 yudao-cloud 自带模块**，仅自建 2 个业务模块（masterdata + recipe），避免重复造轮子。

### 1.2 架构决策说明

#### 1.2.1 yudao-cloud 自带模块 vs 自建模块

| 能力域 | yudao自带模块 | 自建模块 | 说明 |
|--------|-------------|---------|------|
| API网关、路由、认证、限流 | `ims-gateway` | - | 直接使用 |
| 用户管理、角色权限、组织架构 | `system-service` | - | 直接使用 |
| 数据字典、租户管理 | `system-service` | - | 直接使用 |
| 认证授权、站内消息、邮件、短信 | `system-service` | - | 直接使用 |
| 操作日志 | `system-service` | - | 直接使用 |
| 文件上传/管理、配置参数 | `infra-service` | - | 直接使用 |
| 代码生成、API日志 | `infra-service` | - | 直接使用 |
| 报表（积木报表+GoView大屏） | `ims-module-report` | - | 按需启用，SQL数据集+拖拽设计 |
| 原料管理、产品管理、客户管理、仓库管理 | - | `ims-module-masterdata` | 表前缀 `md_`，共11张表 |
| 客户合同、回款跟踪、IC卡管理 | - | `ims-module-crm（暂不开发）` | 表前缀 `crm_`，共3张表，架构预留 |
| 配方组管理、配方管理、工艺管理 | - | `ims-module-recipe` | 表前缀 `recipe_` / `process_`，共10张表 |

#### 1.2.2 原方案服务替代关系

| 原方案服务 | 最终决策 | 替代方案 |
|-----------|---------|---------|
| `auth-service` | **删除** | 由 `system-service` 提供 |
| `gateway-service` | **删除** | 由 `ims-gateway` 提供 |
| `file-service` | **删除** | 由 `infra-service` 提供 |
| `dict-service` | **删除** | 由 `system-service` 提供 |
| `message-service` | **删除** | 由 `system-service` 提供 |
| `report-service` | **删除** | 由 `ims-module-report` 提供 |
| `masterdata-service` | **保留** | 重构为 `ims-module-masterdata` |
| `recipe-service` | **保留** | 重构为 `ims-module-recipe` |
| `product-service` | **延后** | 不在本次文档范围，后续扩展为 `ims-module-product` |

### 1.3 为何保留 masterdata-service

原料和产品是**跨服务共享的基础主数据**，不属于任何单一业务域：

- **原料**被配方管理、采购管理、仓储管理、品管等多个服务引用
- **产品**被配方管理、工艺管理、订单管理、生产排程等多个服务引用

如果将原料/产品拆分到配方服务或其他业务服务中，会导致严重的循环依赖。因此，将它们独立为 `ims-module-masterdata` 主数据服务是合理的领域划分。

### 1.4 模块范围

```mermaid
flowchart LR
    subgraph YUDAO["yudao-cloud 自带模块"]
        GW[ims-gateway<br/>网关/认证/限流]
        SYS[system-service<br/>用户/权限/字典/租户]
        INFRA[infra-service<br/>文件/配置/代码生成]
        RPT[ims-module-report<br/>积木报表/GoView]
    end

    subgraph SELF["自建业务模块"]
        subgraph MD["ims-module-masterdata（主数据服务 · 11张表）"]
            M1[原料管理<br/>4张表]
            M3[产品管理<br/>2张表]
            M8[客户管理<br/>4张表（阶段3）]
            M9[仓库管理<br/>1张表（阶段3）"]
        end

        subgraph CRM["ims-module-crm（客户关系管理 · 3张表）"]
            M6[合同/回款管理<br/>2张表（暂不开发）]
            M7[IC卡管理<br/>1张表（暂不开发）"]
        end

        subgraph RECIPE["ims-module-recipe（配方与工艺服务 · 10张表）"]
            M2[配方组管理<br/>2张表]
            M4[配方管理<br/>4张表]
            M5[工艺管理<br/>4张表]
        end
    end

    GW --> SYS
    GW --> MD
    GW --> CRM
    GW --> RECIPE

    M1 -->|"原料数据"| M4
    M2 -->|"配方组定义"| M3
    M3 -->|"产品信息"| M4
    M4 -->|"配方关联"| M5

    M4 -.->|"配方BOM快照"| OUT1[生产排程]
    M4 -.->|"配方成本"| OUT2[成本核算]
    M5 -.->|"工艺路线"| OUT3[生产执行]

    style YUDAO fill:#f3e5f5
    style SELF fill:#e8f5e9
    style MD fill:#e3f2fd
    style CRM fill:#fff8e1
    style RECIPE fill:#fce4ec
```

### 1.5 设计目标

| 目标 | 说明 | 验收标准 |
|------|------|---------|
| **数据一体化** | 建立统一的产品/原料/配方数据源 | 所有下游服务通过API获取数据，无重复录入 |
| **版本可追溯** | 配方和工艺支持版本管理 | 可查看任意历史版本，支持版本对比 |
| **扩展性** | 支持新产品类型、新工艺步骤扩展 | 新增类型无需修改表结构 |
| **yudao-cloud兼容** | 完全适配yudao-cloud框架规范 | 可直接集成到yudao-cloud工程 |
| **最大化复用** | 充分利用yudao-cloud自带能力 | 不重复开发网关、认证、文件、字典、报表等基础模块 |
| **多级BOM支持** | 配方支持多级嵌套（半成品引用） | 半成品配方可被其他配方引用，最多6级嵌套 |
| **实验配方** | 支持实验配方独立管理 | 实验配方不关联产品，确认后转为成品配方 |
| **成本延迟计算** | 原料价格变更不立即重算成本 | 通过成本标记机制实现按需批量重算 |

### 1.6 术语定义

| 术语 | 定义 | 示例 |
|------|------|------|
| **配方组（Recipe Group）** | 按基础面团/面糊类型划分的多级树形分组概念，用于生产排程时按面团类型快速分组计算。支持多级树结构（一级面团组、二级产品组等），产品与配方组为多对多关系（一个产品可属于多个配方组，一个配方组可包含多个产品）。与"产品分类"（面包/蛋糕等，用于业务管理）是不同维度。对应SAP IS-Bakery中的Recipe Group概念 | 甜面团、法棍面团、丹麦面团 |
| **产品** | 最终销售或生产的SKU，可关联多个配方组。一个产品可以有多个配方版本，通过 `current_recipe_id` 指向当前生效的配方 | 甜面包-红豆包、法棍-原味 |
| **配方** | 产品的原料组成及用量配比。配方是完整的BOM，包含面团、馅料、包装材料。支持多级嵌套（半成品引用），最多6级。分为实验配方、成品配方、半成品配方三种类型 | 甜面团配方：面粉1000g、糖150g、酵母10g... |
| **实验配方** | 不关联产品的实验性配方，用于研发阶段。确认后可转为成品配方（填充product_id） | 新品研发中的红豆包实验配方 |
| **半成品配方** | 可被其他配方引用的中间产品配方（如甜面团、奶油馅），对应 `recipe_type=3` | 甜面团（被红豆包、菠萝包等配方引用） |
| **BOM快照** | 配方生效时展开的多级BOM扁平化缓存表，所有BOM查询、成本计算、物料需求均从快照读取，避免递归查询 | `recipe_bom_snapshot` 表 |
| **工艺路线** | 产品生产所需的工序流程，与产品1:1对应。支持模板功能，可从模板复制 | 搅拌→发酵→分割→成型→醒发→烘烤→冷却 |
| **工艺步骤** | 工艺路线中的单个工序，支持人工/半自动/全自动模式标记 | 搅拌（低速3分钟+高速5分钟） |
| **原料** | 生产所需的基础物料，类型包括原料、包材、辅料、半成品 | 面粉、糖、酵母、黄油等 |
| **聚合API** | 一次Feign调用返回关联的完整数据（如配方+原料） | `batchGetRecipesByProductIds` |
| **成本标记** | 原料价格变更时标记配方成本为过期（cost_stale=1），不立即重算，支持按需批量重算 | 原料涨价后，相关配方成本标记为需重算 |
| **客户合同** | 与客户签订的业务合同，记录合同金额、有效期、付款条件等。支持年度框架合同、单次采购合同、代工合同等类型。**归属 `ims-module-crm`（暂不开发）** | 年度框架合同、代工合同 |
| **回款跟踪** | 跟踪客户按合同的回款情况，记录回款金额、回款日期、回款方式等。**归属 `ims-module-crm`（暂不开发）** | 银行转账、现金、支票 |
| **IC卡** | 不记名预付卡，客户购买后可在门店消费。需管理发卡、充值、余额、挂失等。**归属 `ims-module-crm`（暂不开发）** | 门店售卖的预付IC卡 |

---

## 2. 模块划分与服务边界

### 2.1 基于yudao-cloud的模块结构

yudao-cloud采用**多模块Maven工程**结构，本次产品库拆分为 **2个** 独立的业务模块。

> **模块命名说明：** 所有模块名（Maven artifactId和目录名）统一使用 `ims-` 前缀（包括yudao自带的模块，如 `ims-gateway`、`ims-module-report` 等），Java包路径统一使用 `cn.lucky.ims.module.xxx`。

```
ims-module-masterdata/                    # 主数据服务（新建）
├── ims-module-masterdata-api/            # API接口层（DTO、VO、常量、Feign接口）
│   └── src/main/java/.../masterdata/api/
│       ├── dto/
│       │   ├── MaterialDTO.java            # 原料DTO
│       │   ├── ProductDTO.java             # 产品DTO
│       │   ├── MaterialCategoryDTO.java    # 原料分类DTO
│       │   └── MaterialUnitConversionDTO.java  # 原料单位换算DTO
│       ├── vo/
│       │   ├── MaterialVO.java             # 原料VO
│       │   ├── ProductVO.java              # 产品VO
│       │   └── ...
│       └── enums/
│           ├── MaterialTypeEnum.java       # 原料类型枚举（1原料 2包材 3辅料 4半成品）
│           ├── StorageConditionTypeEnum.java  # 存储条件枚举（1常温 2冷藏 3冷冻 4阴凉干燥）
│           └── ProductStatusEnum.java      # 产品状态枚举
│
└── ims-module-masterdata-biz/            # 业务实现层
    └── src/main/java/.../masterdata/
        ├── controller/                     # Controller层
        │   ├── admin/
        │   │   ├── MaterialController.java
        │   │   ├── MaterialCategoryController.java
        │   │   ├── MaterialUnitController.java
        │   │   ├── MaterialUnitConversionController.java
        │   │   ├── ProductCategoryController.java
        │   │   └── ProductController.java
        │   └── app/                        # 移动端/外部接口
        ├── service/                        # Service层
        │   ├── material/
        │   │   ├── MaterialService.java
        │   │   └── MaterialServiceImpl.java
        │   ├── product/
        │   │   ├── ProductService.java
        │   │   └── ProductServiceImpl.java
        │   └── conversion/
        │       ├── MaterialUnitConversionService.java
        │       └── MaterialUnitConversionServiceImpl.java
        ├── dal/                            # 数据访问层
        │   ├── dataobject/
        │   │   ├── MaterialDO.java
        │   │   ├── MaterialCategoryDO.java
        │   │   ├── MaterialUnitDO.java
        │   │   ├── MaterialUnitConversionDO.java
        │   │   ├── ProductCategoryDO.java
        │   │   └── ProductDO.java
        │   └── mysql/
        │       ├── MaterialMapper.java
        │       ├── MaterialCategoryMapper.java
        │       ├── MaterialUnitMapper.java
        │       ├── MaterialUnitConversionMapper.java
        │       ├── ProductCategoryMapper.java
        │       └── ProductMapper.java
        └── convert/                       # 对象转换
            ├── MaterialConvert.java
            └── ProductConvert.java

ims-module-recipe/                        # 配方与工艺服务（新建）
├── ims-module-recipe-api/                # API接口层（DTO、VO、常量、Feign接口）
│   └── src/main/java/.../recipe/api/
│       ├── dto/
│       │   ├── RecipeDTO.java              # 配方DTO
│       │   ├── RecipeGroupDTO.java         # 配方组DTO
│       │   └── ProcessDTO.java             # 工艺DTO
│       └── vo/
│           ├── RecipeVO.java               # 配方VO
│           ├── RecipeGroupVO.java          # 配方组VO
│           └── ProcessVO.java              # 工艺VO
│
└── ims-module-recipe-biz/                # 业务实现层
    └── src/main/java/.../recipe/
        ├── controller/
        │   ├── admin/
        │   │   ├── RecipeGroupController.java
        │   │   ├── RecipeController.java
        │   │   └── ProcessController.java
        │   └── rpc/
        │       └── RecipeCostController.java    # 成本相关RPC接口
        ├── service/
        │   ├── recipegroup/
        │   │   ├── RecipeGroupService.java
        │   │   └── RecipeGroupServiceImpl.java
        │   ├── recipe/
        │   │   ├── RecipeService.java
        │   │   └── RecipeServiceImpl.java
        │   └── process/
        │       ├── ProcessService.java
        │       └── ProcessServiceImpl.java
        ├── dal/
        │   ├── dataobject/
        │   │   ├── RecipeGroupDO.java
        │   │   ├── ProductRecipeGroupDO.java
        │   │   ├── RecipeDO.java
        │   │   ├── RecipeItemDO.java
        │   │   ├── RecipeVersionDO.java
        │   │   ├── RecipeBomSnapshotDO.java
        │   │   ├── ProcessRouteDO.java
        │   │   ├── ProcessStepDO.java
        │   │   ├── ProcessParamDO.java
        │   │   └── ProcessVersionDO.java
        │   └── mysql/
        │       ├── RecipeGroupMapper.java
        │       ├── ProductRecipeGroupMapper.java
        │       ├── RecipeMapper.java
        │       ├── RecipeItemMapper.java
        │       ├── RecipeVersionMapper.java
        │       ├── RecipeBomSnapshotMapper.java
        │       ├── ProcessRouteMapper.java
        │       ├── ProcessStepMapper.java
        │       ├── ProcessParamMapper.java
        │       └── ProcessVersionMapper.java
        └── convert/
            ├── RecipeGroupConvert.java
            ├── RecipeConvert.java
            └── ProcessConvert.java
```

### 2.2 两大模块职责划分

```mermaid
flowchart TB
    subgraph MD["ims-module-masterdata（主数据服务）"]
        subgraph M1[原料管理]
            M1_1[原料主数据]
            M1_2[原料分类]
            M1_3[原料单位]
            M1_4[原料单位换算]
        end

        subgraph M3[产品管理]
            M3_1[产品基础信息]
            M3_2[产品分类]
        end
    end

    subgraph RECIPE["ims-module-recipe（配方与工艺服务）"]
        subgraph M2[配方组管理]
            M2_1[配方组定义]
            M2_2[产品关联]
        end

        subgraph M4[配方管理]
            M4_1[配方头信息]
            M4_2[配方明细BOM]
            M4_3[配方版本]
            M4_4[BOM快照]
        end

        subgraph M5[工艺管理]
            M5_1[工艺路线]
            M5_2[工艺步骤]
            M5_3[工艺参数]
            M5_4[工艺版本]
        end
    end

    M1 -->|"提供原料/包材"| M4
    M2 -->|"关联"| M3
    M3 -->|"关联"| M4
    M4 -->|"关联"| M5
    M4 -->|"半成品引用"| M4

    style MD fill:#e3f2fd
    style RECIPE fill:#fce4ec
```

### 2.3 各模块详细职责

#### 2.3.1 ims-module-masterdata（主数据服务）

主数据服务负责管理原料、产品的基础主数据，表名前缀为 `md_`。

**原料管理（Material）：**

| 功能 | 说明 | 对应表 |
|------|------|--------|
| 原料主数据 | 原料编码、名称、规格、单位、存储条件类型、状态等基础信息 | `md_material` |
| 原料分类 | 原料分类树（如粉类、糖类、油脂类、乳制品等） | `md_material_category` |
| 原料单位 | 原料计量单位（kg、g、L、ml、个等） | `md_material_unit` |
| 原料单位换算 | 原料/包材的单位换算规则（如1箱=50卷） | `md_material_unit_conversion` |

**边界说明：**
- 原料的**库存信息**归属 `wms-service`，本模块仅管理主数据
- 原料的**供应商信息**归属 `purchase-service`，本模块仅存储供应商ID引用
- 原料的**质检标准**归属 `quality-service`，本模块仅存储质检标准ID引用
- 原料类型分为：1原料、2包材、3辅料、4半成品
- 存储条件使用字典枚举：1常温、2冷藏0-4℃、3冷冻-18℃、4阴凉干燥
- 原料（type=1）的kg↔g换算为固定1000倍，无需存储在换算表中
- 包材/辅料（type=2/3）需要自定义换算规则，存储在 `md_material_unit_conversion` 表中

**产品管理（Product）：**

| 功能 | 说明 | 对应表 |
|------|------|--------|
| 产品基础信息 | 产品编码、名称、规格、单位、保质期、存储条件、产品图片等 | `md_product` |
| 产品分类 | 产品分类树（如面包类、蛋糕类、半成品等） | `md_product_category` |

**边界说明：**
- 产品的**SKU管理**归属后续扩展的 `ims-module-product`，本模块仅存储产品基础信息
- 产品的**价格信息**归属后续扩展的 `ims-module-product`
- 产品的**库存信息**归属 `wms-service`
- 产品与配方是**一对多**关系（一个产品可有多个配方版本，`current_recipe_id` 指向当前生效配方）
- 产品类型分为：1成品面包、2蛋糕、3半成品、4原料

#### 2.3.2 ims-module-recipe（配方与工艺服务）

配方与工艺服务负责管理配方组、配方和工艺路线，表名前缀为 `recipe_` 和 `process_`。

**配方组管理（Recipe Group）：**

| 功能 | 说明 | 对应表 |
|------|------|--------|
| 配方组定义 | 配方组编码、名称、层级（一级面团组/二级产品组）、面团类型，支持多级树结构 | `recipe_group` |
| 产品关联 | 产品与配方组多对多关系，支持设置主配方组 | `recipe_product_group` |

**边界说明：**
- 配方组是**生产维度的分类概念**，用于生产排程时按面团类型快速分组计算
- 配方组支持**多级树结构**（parent_id自引用），一级为面团组（如甜面团、丹麦面团），二级为产品组
- 配方组与产品分类（面包/蛋糕等）是不同维度：产品分类用于业务管理，配方组用于生产排程
- 配方组与产品是**多对多**关系（通过 `recipe_product_group` 关联表实现）
- 对应SAP IS-Bakery中的Recipe Group概念

**配方管理（Recipe）：**

| 功能 | 说明 | 对应表 |
|------|------|--------|
| 配方头信息 | 配方编码、名称、产品ID（可为NULL）、版本号、状态、配方类型 | `recipe` |
| 配方明细BOM | 配方的原料/半成品组成、用量、损耗率，支持多级嵌套 | `recipe_item` |
| 配方版本 | 配方的历史版本记录 | `recipe_version` |
| BOM快照 | 配方生效时展开的多级BOM扁平化缓存 | `recipe_bom_snapshot` |

**边界说明：**
- 配方是**产品的完整BOM**，包含面团、馅料、包装材料
- 配方支持**版本管理**，可查看历史版本、回滚版本
- 配方状态简化为三态：0草稿、1已生效、2已失效（无审批流程）
- 配方支持**多级嵌套**：半成品配方（type=3）可被其他配方引用，成品配方（type=2）不可被引用
- 最大嵌套深度：6级，系统必须检测循环引用
- 配方明细使用 `source_type` 区分来源：1=原料/包材，2=半成品配方
- 配方明细可关联工艺步骤（`process_step_id`），实现按工序拆分物料
- 实验配方（type=1）：`product_id=NULL`，不可被其他配方引用，不可用于订单
- 实验配方确认后可转为成品配方（填充 `product_id`）
- 成本计算采用**延迟标记机制**：原料价格变更时标记 `cost_stale=1`，不立即重算
- 配方明细变更时立即重算成本，`cost_stale=0`
- 支持批量重算成本的管理接口
- 配方引用的原料信息需通过Feign调用主数据服务获取
- **重点**：提供聚合API，按产品ID批量查询配方+原料完整信息，一次Feign调用搞定
- 所有BOM查询、成本计算、物料需求均从 `recipe_bom_snapshot` 快照表读取

**工艺管理（Process）：**

| 功能 | 说明 | 对应表 |
|------|------|--------|
| 工艺路线 | 工序流程定义、工序顺序，与产品1:1对应，支持模板 | `process_route` |
| 工艺步骤 | 单个工序的定义（搅拌、发酵、烘烤等），支持人工/半自动/全自动模式 | `process_step` |
| 工艺参数 | 每个步骤的参数（温度、时间、湿度等） | `process_param` |
| 工艺版本 | 工艺的历史版本记录 | `process_version` |

**边界说明：**
- 工艺路线与产品**1:1对应**（`product_id NOT NULL`），每个产品只有一条工艺路线
- 工艺路线支持**模板功能**：`is_template=1` 时为模板，可被其他产品复制
- 工艺步骤支持**参数化配置**，不同产品可配置不同参数
- 工艺步骤类型使用字典枚举：1搅拌、2发酵、3分割、4成型、5醒发、6烘烤、7冷却、8包装
- 工艺步骤支持**人机模式**标记：1人工、2半自动、3全自动
- 工艺步骤预留设备关联字段（`equipment_id`），供后续设备管理模块使用
- 工艺状态简化为三态：0草稿、1已生效、2已失效（无审批流程）

### 2.4 模块间数据流向

```mermaid
flowchart LR
    subgraph 数据写入
        A1[原料录入] --> M1[（md_原料表）]
        A2[配方组定义] --> M2[（recipe_配方组表）]
        A3[产品录入] --> M3[（md_产品表）]
        A4[配方录入] --> M4[（recipe_配方表）]
        A5[工艺录入] --> M5[（process_工艺表）]
        A6[BOM快照生成] --> M6[（recipe_bom_snapshot）]
    end

    subgraph 数据消费
        M1 --> C1[配方BOM引用原料/包材]
        M2 --> C2[产品关联配方组]
        M3 --> C3[配方关联产品]
        M6 --> C4[生产排程计算原料需求]
        M6 --> C5[成本核算读取BOM成本]
        M5 --> C6[生产执行获取工艺参数]
    end

    subgraph 跨模块Feign调用
        M4 -.->|"Feign:查询原料"| M1
        M4 -.->|"Feign:查询产品"| M3
        M5 -.->|"Feign:查询产品"| M3
    end
```

---

## 3. 数据模型设计

### 3.1 ER模型总览

```mermaid
erDiagram
    %% ========== 原料管理 ==========
    MATERIAL_CATEGORY ||--o{ MATERIAL : "包含"
    MATERIAL_UNIT ||--o{ MATERIAL : "计量"
    MATERIAL ||--o{ MATERIAL_UNIT_CONVERSION : "换算规则"

    %% ========== 产品管理 ==========
    PRODUCT_CATEGORY ||--o{ PRODUCT : "包含"
    PRODUCT ||--o{ RECIPE : "拥有多个配方"
    PRODUCT ||--o| PROCESS_ROUTE : "关联工艺路线"

    %% ========== 配方组（多对多） ==========
    RECIPE_GROUP ||--o{ RECIPE_GROUP : "父级"
    PRODUCT }o--o{ RECIPE_GROUP : "多对多"
    PRODUCT_RECIPE_GROUP }o--|| PRODUCT : "产品"
    PRODUCT_RECIPE_GROUP }o--|| RECIPE_GROUP : "配方组"

    %% ========== 配方管理 ==========
    RECIPE ||--o{ RECIPE_ITEM : "包含"
    RECIPE ||--o{ RECIPE_VERSION : "版本历史"
    RECIPE ||--o{ RECIPE_BOM_SNAPSHOT : "BOM快照"
    RECIPE_ITEM }o--o| MATERIAL : "引用原料/包材"
    RECIPE_ITEM }o--o| RECIPE : "引用半成品配方"
    RECIPE_ITEM }o--o| PROCESS_STEP : "关联工艺步骤"

    %% ========== 工艺管理 ==========
    PROCESS_ROUTE ||--o{ PROCESS_STEP : "包含"
    PROCESS_ROUTE ||--o{ PROCESS_VERSION : "版本历史"
    PROCESS_STEP ||--o{ PROCESS_PARAM : "包含参数"

    %% ========== 客户管理（阶段3，masterdata） ==========
    CUSTOMER_CATEGORY ||--o{ CUSTOMER : "分类"
    CUSTOMER ||--o{ CUSTOMER_PRODUCT : "客户产品映射"
    CUSTOMER ||--o{ CUSTOMER_PRICE : "客户价格"

    %% ========== CRM（暂不开发，ims-module-crm） ==========
    CUSTOMER ||--o{ CRM_CONTRACT : "签订合同"
    CRM_CONTRACT ||--o{ CRM_PAYMENT : "回款记录"
    CRM_IC_CARD }o--o| CUSTOMER : "持卡人（记名卡）"

    %% ========== 仓库管理（阶段3，masterdata） ==========
    WAREHOUSE
```

### 3.2 表清单

| 序号 | 表名 | 说明 | 所属模块 | 子模块 | 阶段 |
|------|------|------|---------|--------|------|
| 1 | `md_material_category` | 原料分类表 | ims-module-masterdata | 原料管理 | 阶段1 |
| 2 | `md_material_unit` | 原料单位表 | ims-module-masterdata | 原料管理 | 阶段1 |
| 3 | `md_material_unit_conversion` | 原料单位换算表 | ims-module-masterdata | 原料管理 | 阶段1 |
| 4 | `md_material` | 原料主表 | ims-module-masterdata | 原料管理 | 阶段1 |
| 5 | `md_product_category` | 产品分类表 | ims-module-masterdata | 产品管理 | 阶段1 |
| 6 | `md_product` | 产品主表 | ims-module-masterdata | 产品管理 | 阶段1 |
| 7 | `md_customer_category` | 客户分类表 | ims-module-masterdata | 客户管理 | 阶段3 |
| 8 | `md_customer` | 客户主表 | ims-module-masterdata | 客户管理 | 阶段3 |
| 9 | `md_customer_product` | 客户产品映射表 | ims-module-masterdata | 客户管理 | 阶段3 |
| 10 | `md_customer_price` | 客户价格表 | ims-module-masterdata | 客户管理 | 阶段3 |
| 11 | `md_warehouse` | 仓库表 | ims-module-masterdata | 仓库管理 | 阶段3 |
| 12 | `crm_contract` | 客户合同表 | ims-module-crm | 合同管理 | 预留 |
| 13 | `crm_payment` | 客户回款表 | ims-module-crm | 回款管理 | 预留 |
| 14 | `crm_ic_card` | IC卡管理表 | ims-module-crm | IC卡管理 | 预留 |
| 15 | `recipe_group` | 配方组表 | ims-module-recipe | 配方组管理 | 阶段2 |
| 16 | `recipe_product_group` | 产品与配方组关联表 | ims-module-recipe | 配方组管理 | 阶段2 |
| 17 | `recipe` | 配方头表 | ims-module-recipe | 配方管理 | 阶段2 |
| 18 | `recipe_item` | 配方明细表 | ims-module-recipe | 配方管理 | 阶段2 |
| 19 | `recipe_version` | 配方版本表 | ims-module-recipe | 配方管理 | 阶段2 |
| 20 | `recipe_bom_snapshot` | 配方BOM快照表 | ims-module-recipe | 配方管理 | 阶段2 |
| 21 | `process_route` | 工艺路线表 | ims-module-recipe | 工艺管理 | 阶段2 |
| 22 | `process_step` | 工艺步骤表 | ims-module-recipe | 工艺管理 | 阶段2 |
| 23 | `process_param` | 工艺参数表 | ims-module-recipe | 工艺管理 | 阶段2 |
| 24 | `process_version` | 工艺版本表 | ims-module-recipe | 工艺管理 | 阶段2 |

**说明：** 本次设计共 24 张表，其中 masterdata 模块 11 张（阶段1开发6张、阶段3开发5张），recipe 模块 10 张，crm 模块 3 张（暂不开发，架构预留）。所有表共用一个数据库，通过表名前缀（`md_`、`recipe_`、`process_`、`crm_`）区分模块归属。

> **架构说明：** `ims-module-crm`（客户关系管理）为架构预留模块，包含合同管理、回款跟踪、IC卡管理三张表。合同和回款属于业务交易数据（有状态流转），IC卡属于客户资产管理，与 masterdata 的"参考数据"定位不同，因此独立为 CRM 模块。客户基础信息（`md_customer`、`md_customer_category`）保留在 masterdata 中作为共享主数据。CRM 模块暂不开发，待业务需求明确后启动，届时通过 Feign 调用 masterdata 获取客户基础信息。预留表的详细DDL见 [OMS合并计划](oms-merge-plan.md)。

### 3.3 原料管理表结构

#### 3.3.1 原料分类表 `md_material_category`

```sql
CREATE TABLE `md_material_category` (
    `id`            BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID',
    `parent_id`     BIGINT DEFAULT 0 COMMENT '父分类ID',
    `name`          VARCHAR(64) NOT NULL COMMENT '分类名称',
    `code`          VARCHAR(32) COMMENT '分类编码',
    `sort`          INT DEFAULT 0 COMMENT '排序',
    `status`        TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用',
    `remark`        VARCHAR(256) COMMENT '备注',
    `creator`       VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`   DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`       VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`       BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`     BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_parent_id` (`parent_id`),
    UNIQUE KEY `uk_code` (`code`, `tenant_id`)
) ENGINE=InnoDB COMMENT='原料分类表';
```

#### 3.3.2 原料单位表 `md_material_unit`

```sql
CREATE TABLE `md_material_unit` (
    `id`            BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '单位ID',
    `name`          VARCHAR(32) NOT NULL COMMENT '单位名称',
    `code`          VARCHAR(16) NOT NULL COMMENT '单位编码（kg/g/L/ml/个）',
    `type`          TINYINT DEFAULT 1 COMMENT '类型:1重量 2体积 3数量',
    `status`        TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用',
    `creator`       VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`   DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`       VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`       BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`     BIGINT DEFAULT 0 COMMENT '租户ID',
    UNIQUE KEY `uk_code` (`code`, `tenant_id`)
) ENGINE=InnoDB COMMENT='原料单位表';
```

#### 3.3.3 原料单位换算表 `md_material_unit_conversion`

```sql
CREATE TABLE `md_material_unit_conversion` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '换算ID',
    `material_id`       BIGINT NOT NULL COMMENT '原料ID',
    `from_unit_id`      BIGINT NOT NULL COMMENT '源单位ID',
    `to_unit_id`        BIGINT NOT NULL COMMENT '目标单位ID',
    `conversion_rate`   DECIMAL(14,6) NOT NULL COMMENT '换算率（1源单位=N目标单位）',
    `is_base`           BIT DEFAULT 0 COMMENT '是否基准换算:1是 0否',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_material` (`material_id`),
    UNIQUE KEY `uk_material_from_to` (`material_id`, `from_unit_id`, `to_unit_id`, `tenant_id`)
) ENGINE=InnoDB COMMENT='原料单位换算表';
```

> **设计说明：** 原料（material_type=1）的kg↔g换算为固定1000倍，无需存储在换算表中，系统内置处理。包材/辅料（material_type=2/3）需要自定义换算规则（如1箱=50卷），存储在本表中。`recipe_item.quantity` 对于原料始终以g为单位，对于包材使用其自身单位。

#### 3.3.4 原料主表 `md_material`

```sql
CREATE TABLE `md_material` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '原料ID',
    `material_code`     VARCHAR(32) NOT NULL COMMENT '原料编码',
    `material_name`     VARCHAR(128) NOT NULL COMMENT '原料名称',
    `category_id`       BIGINT COMMENT '分类ID',
    `category_name`     VARCHAR(64) COMMENT '分类名称（冗余）',
    `spec`              VARCHAR(128) COMMENT '规格描述',
    `base_unit_id`      BIGINT NOT NULL COMMENT '基本单位ID',
    `base_unit_code`    VARCHAR(16) NOT NULL COMMENT '基本单位编码',
    `base_unit_name`    VARCHAR(32) NOT NULL COMMENT '基本单位名称',
    `material_type`     TINYINT DEFAULT 1 COMMENT '原料类型:1原料 2包材 3辅料 4半成品',
    `storage_condition_type` TINYINT COMMENT '存储条件类型:1常温 2冷藏0-4℃ 3冷冻-18℃ 4阴凉干燥',
    `supplier_id`       BIGINT COMMENT '默认供应商ID（引用purchase-service）',
    `quality_standard_id` BIGINT COMMENT '质检标准ID（引用quality-service）',
    `safety_stock`      DECIMAL(12,2) DEFAULT 0 COMMENT '安全库存',
    `shelf_life`        INT COMMENT '保质期（天）',
    `status`            TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用',
    `remark`            VARCHAR(512) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    UNIQUE KEY `uk_code` (`material_code`, `tenant_id`),
    INDEX `idx_category` (`category_id`),
    INDEX `idx_name` (`material_name`),
    INDEX `idx_supplier` (`supplier_id`),
    INDEX `idx_material_type` (`material_type`)
) ENGINE=InnoDB COMMENT='原料主表';
```

> **变更说明：** `storage_condition VARCHAR(128)` 改为 `storage_condition_type TINYINT`，使用字典枚举（1常温 2冷藏0-4℃ 3冷冻-18℃ 4阴凉干燥）。`material_type` 新增值 4=半成品。

### 3.4 配方组管理表结构（ims-module-recipe）

#### 3.4.1 配方组表 `recipe_group`

```sql
CREATE TABLE `recipe_group` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '配方组ID',
    `parent_id`         BIGINT DEFAULT 0 COMMENT '父组ID（0表示一级组）',
    `group_code`        VARCHAR(32) NOT NULL COMMENT '配方组编码',
    `group_name`        VARCHAR(128) NOT NULL COMMENT '配方组名称',
    `group_level`       TINYINT DEFAULT 1 COMMENT '层级:1-一级面团组 2-二级产品组',
    `dough_type`        VARCHAR(64) COMMENT '面团/面糊类型（甜面团/丹麦面团/法棍面团/泡芙面糊等）',
    `sort`              INT DEFAULT 0 COMMENT '排序',
    `status`            TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用',
    `remark`            VARCHAR(256) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    UNIQUE KEY `uk_code` (`group_code`, `tenant_id`),
    INDEX `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB COMMENT='配方组表（多级树结构，参考SAP IS-Bakery Recipe Group）';
```

#### 3.4.2 产品与配方组关联表 `recipe_product_group`

```sql
CREATE TABLE `recipe_product_group` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID',
    `product_id`        BIGINT NOT NULL COMMENT '产品ID',
    `recipe_group_id`   BIGINT NOT NULL COMMENT '配方组ID',
    `is_primary`        TINYINT DEFAULT 1 COMMENT '是否主配方组:1是 0否',
    `sort`              INT DEFAULT 0 COMMENT '排序',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    UNIQUE KEY `uk_product_group` (`product_id`, `recipe_group_id`, `tenant_id`),
    INDEX `idx_product` (`product_id`),
    INDEX `idx_group` (`recipe_group_id`)
) ENGINE=InnoDB COMMENT='产品与配方组关联表（多对多）';
```

### 3.5 产品管理表结构

#### 3.5.1 产品分类表 `md_product_category`

```sql
CREATE TABLE `md_product_category` (
    `id`            BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID',
    `parent_id`     BIGINT DEFAULT 0 COMMENT '父分类ID',
    `name`          VARCHAR(64) NOT NULL COMMENT '分类名称',
    `code`          VARCHAR(32) COMMENT '分类编码',
    `sort`          INT DEFAULT 0 COMMENT '排序',
    `status`        TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用',
    `remark`        VARCHAR(256) COMMENT '备注',
    `creator`       VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`   DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`       VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`       BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`     BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_parent_id` (`parent_id`),
    UNIQUE KEY `uk_code` (`code`, `tenant_id`)
) ENGINE=InnoDB COMMENT='产品分类表';
```

#### 3.5.2 产品主表 `md_product`

```sql
CREATE TABLE `md_product` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '产品ID',
    `product_code`      VARCHAR(32) NOT NULL COMMENT '产品编码',
    `product_name`      VARCHAR(128) NOT NULL COMMENT '产品名称',
    `short_name`        VARCHAR(64) COMMENT '简称',
    `category_id`       BIGINT NOT NULL COMMENT '分类ID',
    `category_name`     VARCHAR(64) COMMENT '分类名称（冗余）',
    `product_type`      TINYINT NOT NULL COMMENT '产品类型:1成品面包 2蛋糕 3半成品 4原料',
    `spec`              VARCHAR(128) COMMENT '规格描述',
    `base_unit_id`      BIGINT NOT NULL COMMENT '基本单位ID',
    `base_unit_code`    VARCHAR(16) NOT NULL COMMENT '基本单位编码',
    `base_unit_name`    VARCHAR(32) NOT NULL COMMENT '基本单位名称',
    `barcode`           VARCHAR(32) COMMENT '条形码',
    `weight`            DECIMAL(10,2) COMMENT '重量（g）',
    `shelf_life`        INT COMMENT '保质期（小时）',
    `shelf_life_unit`   TINYINT COMMENT '保质期单位:1小时 2天',
    `storage_condition_type` TINYINT COMMENT '存储条件类型:1常温 2冷藏0-4℃ 3冷冻-18℃ 4阴凉干燥',
    `pic_url`           VARCHAR(512) COMMENT '产品主图URL',
    `images`            VARCHAR(1024) COMMENT '产品图片列表（逗号分隔URL）',
    `current_recipe_id` BIGINT COMMENT '当前生效配方ID',
    `current_recipe_ver` INT COMMENT '当前配方版本号',
    `current_process_id` BIGINT COMMENT '当前生效工艺ID',
    `current_process_ver` INT COMMENT '当前工艺版本号',
    `status`            TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用',
    `remark`            VARCHAR(256) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    UNIQUE KEY `uk_code` (`product_code`, `tenant_id`),
    INDEX `idx_category` (`category_id`),
    INDEX `idx_name` (`product_name`),
    INDEX `idx_product_type` (`product_type`)
) ENGINE=InnoDB COMMENT='产品主表';
```

> **变更说明：** 新增 `storage_condition_type`（存储条件类型，字典枚举同原料）、`pic_url`（产品主图）、`images`（产品图片列表）。`product_type` 新增值 4=原料。

### 3.6 配方管理表结构（ims-module-recipe）

#### 3.6.1 配方头表 `recipe`

```sql
CREATE TABLE `recipe` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '配方ID',
    `recipe_code`       VARCHAR(32) NOT NULL COMMENT '配方编码',
    `recipe_name`       VARCHAR(128) NOT NULL COMMENT '配方名称',
    `recipe_type`       TINYINT NOT NULL DEFAULT 2 COMMENT '配方类型:1实验配方 2成品配方 3半成品配方',
    `product_id`        BIGINT COMMENT '产品ID（实验配方为NULL）',
    `product_code`      VARCHAR(32) COMMENT '产品编码（冗余）',
    `product_name`      VARCHAR(128) COMMENT '产品名称（冗余）',
    `version`           INT DEFAULT 1 COMMENT '版本号',
    `total_weight`      DECIMAL(12,2) COMMENT '总重量（g）',
    `yield_qty`         DECIMAL(12,2) DEFAULT NULL COMMENT '产出数量（个），NULL表示未设定',
    `batch_size`        DECIMAL(12,2) COMMENT '批量大小（半成品配方的面团批量）',
    `batch_unit`        VARCHAR(16) DEFAULT 'g' COMMENT '批量单位',
    `unit_cost`         DECIMAL(14,6) COMMENT '单位成本（元）',
    `total_cost`        DECIMAL(14,6) COMMENT '总成本（元）',
    `cost_stale`        TINYINT DEFAULT 0 COMMENT '成本是否过期:0最新 1需重算',
    `is_sellable`       BIT DEFAULT 0 COMMENT '是否可销售:1可销售 0不可销售',
    `description`       VARCHAR(512) COMMENT '描述',
    `status`            TINYINT DEFAULT 0 COMMENT '状态:0草稿 1已生效 2已失效',
    `effective_from`    DATE COMMENT '生效日期',
    `effective_to`      DATE COMMENT '失效日期',
    `version_lock`      INT DEFAULT 0 COMMENT '乐观锁版本号',
    `remark`            VARCHAR(256) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    UNIQUE KEY `uk_code` (`recipe_code`, `tenant_id`),
    INDEX `idx_product` (`product_id`),
    INDEX `idx_status` (`status`),
    INDEX `idx_recipe_type` (`recipe_type`)
) ENGINE=InnoDB COMMENT='配方头表';
```

> **变更说明：**
> - 新增 `recipe_type`：1实验配方、2成品配方、3半成品配方
> - `product_id` 改为可空（实验配方不关联产品），移除 NOT NULL 约束
> - `product_code`、`product_name` 改为可空
> - `yield_qty` 默认值改为 NULL（原为 NOT NULL）
> - 新增 `batch_size`、`batch_unit`（半成品配方批量大小）
> - 新增 `is_sellable`（是否可销售标志）
> - `unit_cost`、`total_cost` 精度改为 DECIMAL(14,6)
> - 移除 `cost_calculated_at`，新增 `cost_stale`（成本延迟标记机制）
> - `status` 简化为三态：0草稿、1已生效、2已失效（移除"待审批"）
> - 移除 `uk_product_version` 唯一索引（一个产品可有多个配方版本）

#### 3.6.2 配方明细表 `recipe_item`

```sql
CREATE TABLE `recipe_item` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '明细ID',
    `recipe_id`         BIGINT NOT NULL COMMENT '配方ID',
    `source_type`       TINYINT NOT NULL COMMENT '来源类型:1原料/包材 2半成品配方',
    `source_id`         BIGINT NOT NULL COMMENT '来源ID（原料ID或半成品配方ID）',
    `source_code`       VARCHAR(32) NOT NULL COMMENT '来源编码',
    `source_name`       VARCHAR(128) NOT NULL COMMENT '来源名称',
    `unit_id`           BIGINT NOT NULL COMMENT '单位ID',
    `unit_code`         VARCHAR(16) NOT NULL COMMENT '单位编码',
    `quantity`          DECIMAL(14,6) NOT NULL COMMENT '用量',
    `loss_rate`         DECIMAL(5,2) DEFAULT 0 COMMENT '损耗率（%）',
    `process_step_id`   BIGINT COMMENT '关联工艺步骤ID（NULL表示不按工序拆分）',
    `sort`              INT DEFAULT 0 COMMENT '排序',
    `remark`            VARCHAR(256) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_recipe` (`recipe_id`),
    INDEX `idx_source` (`source_type`, `source_id`),
    INDEX `idx_process_step` (`process_step_id`)
) ENGINE=InnoDB COMMENT='配方明细表';
```

> **变更说明：**
> - `material_id`/`material_code`/`material_name` 替换为 `source_type`/`source_id`/`source_code`/`source_name`，支持引用原料/包材（type=1）和半成品配方（type=2）
> - `quantity` 精度改为 DECIMAL(14,6)
> - 移除 `percentage`（占比由前端计算）
> - 移除 `is_main`（主要原料标志，不再使用）
> - 新增 `process_step_id`（关联工艺步骤，实现按工序拆分物料）
> - 新增 `idx_source` 索引（按来源类型和ID查询）
> - 新增 `idx_process_step` 索引（按工艺步骤查询）

#### 3.6.3 配方版本表 `recipe_version`

```sql
CREATE TABLE `recipe_version` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '版本ID',
    `recipe_id`         BIGINT NOT NULL COMMENT '配方ID',
    `version`           INT NOT NULL COMMENT '版本号',
    `change_type`       TINYINT NOT NULL COMMENT '变更类型:1新增 2修改 3生效 4失效',
    `change_reason`     VARCHAR(512) COMMENT '变更原因',
    `snapshot`          JSON COMMENT '配方快照（JSON格式）',
    `operator`          VARCHAR(64) COMMENT '操作人',
    `operate_time`      DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_recipe` (`recipe_id`),
    INDEX `idx_version` (`recipe_id`, `version`)
) ENGINE=InnoDB COMMENT='配方版本表';
```

#### 3.6.4 配方BOM快照表 `recipe_bom_snapshot`

```sql
CREATE TABLE `recipe_bom_snapshot` (
    `id`                    BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '快照ID',
    `recipe_id`             BIGINT NOT NULL COMMENT '配方ID',
    `recipe_version`        INT NOT NULL COMMENT '配方版本号',
    `leaf_material_id`      BIGINT NOT NULL COMMENT '叶子节点原料ID',
    `leaf_material_code`    VARCHAR(32) NOT NULL COMMENT '叶子节点原料编码',
    `leaf_material_name`    VARCHAR(128) NOT NULL COMMENT '叶子节点原料名称',
    `leaf_unit_code`        VARCHAR(16) NOT NULL COMMENT '叶子节点单位编码',
    `leaf_material_type`    TINYINT NOT NULL COMMENT '叶子节点原料类型:1原料 2包材 3辅料 4半成品',
    `total_quantity`        DECIMAL(14,6) NOT NULL COMMENT '总用量（递归展开后）',
    `bom_path`              VARCHAR(512) COMMENT 'BOM路径（如：配方A/半成品B/原料C）',
    `nesting_level`         INT NOT NULL DEFAULT 1 COMMENT '嵌套层级',
    `leaf_unit_price`       DECIMAL(14,6) COMMENT '叶子节点单价（元）',
    `leaf_total_cost`       DECIMAL(14,6) COMMENT '叶子节点总成本（元）',
    `creator`               VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`           DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`               VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`           DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`               BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`             BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_recipe` (`recipe_id`),
    INDEX `idx_recipe_version` (`recipe_id`, `recipe_version`),
    INDEX `idx_leaf_material` (`leaf_material_id`),
    INDEX `idx_nesting_level` (`recipe_id`, `nesting_level`)
) ENGINE=InnoDB COMMENT='配方BOM快照表（递归展开后的扁平化缓存）';
```

> **设计说明：** BOM快照在配方生效（status→1）时生成，将多级BOM递归展开为叶子节点的扁平化记录。所有BOM查询、成本计算、物料需求均从快照表读取，避免递归查询带来的性能问题。`bom_path` 记录完整的BOM路径，便于溯源。`nesting_level` 记录嵌套深度，系统限制最大6级。生成快照时系统必须检测循环引用。

### 3.7 工艺管理表结构（ims-module-recipe）

#### 3.7.1 工艺路线表 `process_route`

```sql
CREATE TABLE `process_route` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '工艺路线ID',
    `route_code`        VARCHAR(32) NOT NULL COMMENT '工艺路线编码',
    `route_name`        VARCHAR(128) NOT NULL COMMENT '工艺路线名称',
    `product_id`        BIGINT NOT NULL COMMENT '产品ID',
    `product_code`      VARCHAR(32) NOT NULL COMMENT '产品编码',
    `product_name`      VARCHAR(128) NOT NULL COMMENT '产品名称（冗余）',
    `is_template`       TINYINT DEFAULT 0 COMMENT '是否模板:1是 0否',
    `template_name`     VARCHAR(64) COMMENT '模板名称（is_template=1时填写）',
    `version`           INT DEFAULT 1 COMMENT '版本号',
    `total_duration`    INT COMMENT '总工时（分钟）',
    `description`       VARCHAR(512) COMMENT '描述',
    `status`            TINYINT DEFAULT 0 COMMENT '状态:0草稿 1已生效 2已失效',
    `effective_from`    DATE COMMENT '生效日期',
    `effective_to`      DATE COMMENT '失效日期',
    `version_lock`      INT DEFAULT 0 COMMENT '乐观锁版本号',
    `remark`            VARCHAR(256) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    UNIQUE KEY `uk_code` (`route_code`, `tenant_id`),
    UNIQUE KEY `uk_product_version` (`product_id`, `version`, `tenant_id`),
    INDEX `idx_product` (`product_id`),
    INDEX `idx_is_template` (`is_template`)
) ENGINE=InnoDB COMMENT='工艺路线表';
```

> **变更说明：**
> - `product_id` 保持 NOT NULL（工艺路线与产品1:1对应）
> - 新增 `is_template`（是否模板，支持"从模板复制"功能）
> - 新增 `template_name`（模板名称）
> - `status` 简化为三态：0草稿、1已生效、2已失效（移除"待审批"）

#### 3.7.2 工艺步骤表 `process_step`

```sql
CREATE TABLE `process_step` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '步骤ID',
    `route_id`          BIGINT NOT NULL COMMENT '工艺路线ID',
    `step_code`         VARCHAR(32) NOT NULL COMMENT '步骤编码',
    `step_desc`         VARCHAR(256) COMMENT '步骤描述（不填时显示工序类型名称）',
    `step_type`         TINYINT NOT NULL COMMENT '步骤类型:1搅拌 2发酵 3分割 4成型 5醒发 6烘烤 7冷却 8包装',
    `step_mode`         TINYINT DEFAULT 1 COMMENT '工序模式:1人工 2半自动 3全自动',
    `equipment_id`      BIGINT COMMENT '设备ID（预留，供设备管理模块使用）',
    `sequence`          INT NOT NULL COMMENT '工序顺序',
    `duration`          INT COMMENT '工时（分钟）',
    `is_key_step`       TINYINT DEFAULT 0 COMMENT '是否关键工序:1是 0否',
    `qc_required`       TINYINT DEFAULT 0 COMMENT '是否需要质检:1是 0否',
    `remark`            VARCHAR(256) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_route` (`route_id`),
    INDEX `idx_sequence` (`route_id`, `sequence`),
    INDEX `idx_step_type` (`step_type`)
) ENGINE=InnoDB COMMENT='工艺步骤表';
```

> **变更说明：**
> - `step_name` 改为 `step_desc` VARCHAR(256)，语义更明确（步骤描述，不填时显示工序类型名称）
> - `step_type` 从 VARCHAR(32) 改为 TINYINT，使用字典枚举（1搅拌 2发酵 3分割 4成型 5醒发 6烘烤 7冷却 8包装）
> - 新增 `step_mode`（人机模式：1人工 2半自动 3全自动）
> - 新增 `equipment_id`（设备ID，预留字段）
> - 移除 `description`（功能合并到 `step_desc`）

#### 3.7.3 工艺参数表 `process_param`

```sql
CREATE TABLE `process_param` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '参数ID',
    `step_id`           BIGINT NOT NULL COMMENT '步骤ID',
    `param_code`        VARCHAR(32) NOT NULL COMMENT '参数编码',
    `param_name`        VARCHAR(64) NOT NULL COMMENT '参数名称',
    `param_type`        VARCHAR(32) COMMENT '参数类型（温度/时间/湿度/速度）',
    `unit`              VARCHAR(16) COMMENT '单位（℃/分钟/%/rpm）',
    `value_type`        TINYINT DEFAULT 1 COMMENT '值类型:1固定值 2范围值',
    `value`             VARCHAR(64) COMMENT '参数值（固定值时）',
    `value_min`         DECIMAL(10,2) COMMENT '最小值（范围值时）',
    `value_max`         DECIMAL(10,2) COMMENT '最大值（范围值时）',
    `is_required`       TINYINT DEFAULT 1 COMMENT '是否必填:1是 0否',
    `sort`              INT DEFAULT 0 COMMENT '排序',
    `remark`            VARCHAR(256) COMMENT '备注',
    `creator`           VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time`       DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           VARCHAR(64) DEFAULT '' COMMENT '更新者',
    `update_time`       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           BIT DEFAULT 0 COMMENT '是否删除',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_step` (`step_id`)
) ENGINE=InnoDB COMMENT='工艺参数表';
```

#### 3.7.4 工艺版本表 `process_version`

```sql
CREATE TABLE `process_version` (
    `id`                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '版本ID',
    `route_id`          BIGINT NOT NULL COMMENT '工艺路线ID',
    `version`           INT NOT NULL COMMENT '版本号',
    `change_type`       TINYINT NOT NULL COMMENT '变更类型:1新增 2修改 3生效 4失效',
    `change_reason`     VARCHAR(512) COMMENT '变更原因',
    `snapshot`          JSON COMMENT '工艺快照（JSON格式）',
    `operator`          VARCHAR(64) COMMENT '操作人',
    `operate_time`      DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
    `tenant_id`         BIGINT DEFAULT 0 COMMENT '租户ID',
    INDEX `idx_route` (`route_id`),
    INDEX `idx_version` (`route_id`, `version`)
) ENGINE=InnoDB COMMENT='工艺版本表';
```

### 3.8 yudao-cloud标准字段说明

以上表结构遵循yudao-cloud的表设计规范，包含以下标准字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `creator` | VARCHAR(64) | 创建者用户名 |
| `create_time` | DATETIME | 创建时间 |
| `updater` | VARCHAR(64) | 更新者用户名 |
| `update_time` | DATETIME | 更新时间 |
| `deleted` | BIT | 逻辑删除标识（0未删除 1已删除） |
| `tenant_id` | BIGINT | 租户ID（多租户隔离） |

**对应的DO基类：** `cn.lucky.ims.framework.mybatis.core.dataobject.BaseDO`

### 3.9 数据字典枚举汇总

以下枚举值建议在 yudao-cloud 的数据字典（`system_dict_data`）中配置，前端通过字典API获取显示文本。

| 字典类型 | 字典编码 | 枚举值 | 说明 |
|---------|---------|--------|------|
| 存储条件类型 | `storage_condition_type` | 1=常温, 2=冷藏0-4℃, 3=冷冻-18℃, 4=阴凉干燥 | 用于 `md_material`、`md_product` |
| 原料类型 | `material_type` | 1=原料, 2=包材, 3=辅料, 4=半成品 | 用于 `md_material` |
| 产品类型 | `product_type` | 1=成品面包, 2=蛋糕, 3=半成品, 4=原料 | 用于 `md_product` |
| 配方类型 | `recipe_type` | 1=实验配方, 2=成品配方, 3=半成品配方 | 用于 `recipe` |
| 配方状态 | `recipe_status` | 0=草稿, 1=已生效, 2=已失效 | 用于 `recipe`、`process_route` |
| 配方明细来源类型 | `recipe_item_source_type` | 1=原料/包材, 2=半成品配方 | 用于 `recipe_item` |
| 工艺步骤类型 | `process_step_type` | 1=搅拌, 2=发酵, 3=分割, 4=成型, 5=醒发, 6=烘烤, 7=冷却, 8=包装 | 用于 `process_step` |
| 工序模式 | `process_step_mode` | 1=人工, 2=半自动, 3=全自动 | 用于 `process_step` |
| 成本标记 | `cost_stale` | 0=最新, 1=需重算 | 用于 `recipe` |

### 3.10 多级BOM嵌套设计说明

#### 3.10.1 嵌套规则

```
成品配方（recipe_type=2）
├── 原料（source_type=1）：面粉、糖、酵母...
├── 半成品配方A（source_type=2）：甜面团
│   ├── 原料（source_type=1）：面粉、水、酵母...
│   └── 半成品配方B（source_type=2）：老面
│       └── 原料（source_type=1）：面粉、水...
├── 半成品配方C（source_type=2）：奶油馅
│   └── 原料（source_type=1）：黄油、糖、奶油...
└── 包材（source_type=1）：包装袋、标签...

实验配方（recipe_type=1）
├── 原料（source_type=1）：面粉、糖...
└── 不可引用其他配方（product_id=NULL时不可被引用）
```

#### 3.10.2 引用约束

| 配方类型 | 可引用原料/包材 | 可引用半成品配方 | 可被其他配方引用 | 可用于订单 |
|---------|:---:|:---:|:---:|:---:|
| 实验配方（type=1） | 是 | 否 | 否 | 否 |
| 成品配方（type=2） | 是 | 是 | 否 | 是 |
| 半成品配方（type=3） | 是 | 是 | 是 | 否 |

#### 3.10.3 循环引用检测

系统在保存配方明细时必须检测循环引用，规则如下：

1. 当 `source_type=2`（引用半成品配方）时，获取被引用配方的 `recipe_id`
2. 递归检查被引用配方的BOM中是否包含当前配方，若存在则拒绝保存
3. 检查嵌套深度是否超过6级，超过则拒绝保存

### 3.11 成本延迟计算机制说明

#### 3.11.1 设计原则

- **原料价格变更** → 标记相关配方 `cost_stale=1`（不立即重算，避免批量价格导入时性能问题）
- **配方明细变更** → 立即重算成本 → `cost_stale=0`
- **用户批量重算** → 管理后台触发 → `cost_stale=0`

#### 3.11.2 相关API

| API | 路径 | 说明 |
|-----|------|------|
| RPC通知价格变更 | `POST /rpc-api/recipe/recipe/cost/notify-price-change` | 采购模块调用，传入变更的原料ID列表，标记相关配方cost_stale=1 |
| 管理端批量重算 | `POST /admin-api/recipe/recipe/recalculate-cost` | 管理员手动触发，支持按产品/配方/全部重算 |

#### 3.11.3 成本计算流程

```mermaid
flowchart TD
    A[原料价格变更] --> B[RPC通知价格变更]
    B --> C[查找引用该原料的配方]
    C --> D[标记cost_stale=1]
    D --> E[前端显示"成本待更新"标识]

    F[配方明细变更] --> G[立即重算成本]
    G --> H[递归展开BOM]
    H --> I[计算总成本]
    I --> J[cost_stale=0]

    K[管理员批量重算] --> L[筛选cost_stale=1的配方]
    L --> M[批量重算成本]
    M --> N[cost_stale=0]

    style A fill:#fff3e0
    style F fill:#e8f5e9
    style K fill:#e3f2fd
```

### 3.12 工艺路线模板设计说明

#### 3.12.1 模板类型

| 模板来源 | is_template | product_id | 说明 |
|---------|:-----------:|:----------:|------|
| 产品型模板 | 1 | NOT NULL | 从现有产品的工艺路线创建模板 |
| 独立模板 | 1 | NULL | 独立创建的工艺模板 |

> **注意：** 由于 `process_route.product_id` 为 NOT NULL，独立模板需要使用一个虚拟产品ID或特殊标记。实际实现中建议 `product_id` 在模板场景下允许为 NULL，或使用约定值（如0）表示模板。

#### 3.12.2 复制操作

```mermaid
flowchart LR
    subgraph 模板来源
        T1[产品型模板<br/>is_template=1]
        T2[现有产品工艺<br/>is_template=0]
    end

    subgraph 复制操作
        C1["复制为新产品工艺<br/>product_id=新产品ID"]
    end

    T1 --> C1
    T2 -->|"先创建模板<br/>再复制"| C1

    style T1 fill:#fff3e0
    style T2 fill:#e8f5e9
    style C1 fill:#e3f2fd
```

复制操作包括：工艺路线基本信息 + 所有工艺步骤 + 所有工艺参数。复制后 `is_template=0`，`product_id` 设为目标产品ID，版本号重置为1。

---
## 4. 服务调用关系

### 4.1 模块内部调用层级

```mermaid
flowchart TB
    subgraph MD_Controller["masterdata Controller层"]
        C1[MaterialController]
        C3[ProductController]
    end

    subgraph MD_Service["masterdata Service层"]
        S1[MaterialService]
        S3[ProductService]
    end

    subgraph MD_Mapper["masterdata Mapper层"]
        M1[MaterialMapper]
        M3[ProductMapper]
    end

    subgraph RECIPE_Controller["recipe Controller层"]
        C2[RecipeGroupController]
        C4[RecipeController]
        C5[ProcessController]
    end

    subgraph RECIPE_Service["recipe Service层"]
        S2[RecipeGroupService]
        S4[RecipeService]
        S5[ProcessService]
    end

    subgraph RECIPE_Mapper["recipe Mapper层"]
        M2[RecipeGroupMapper]
        M4[RecipeMapper]
        M5[ProcessMapper]
    end

    subgraph 数据库
        DB[（共用数据库<br/>ruoyi-vue-pro）]
    end

    C1 --> S1 --> M1 --> DB
    C2 --> S2 --> M2 --> DB
    C3 --> S3 --> M3 --> DB
    C4 --> S4 --> M4 --> DB
    C5 --> S5 --> M5 --> DB

    S4 -.->|"Feign:查询原料"| S1
    S2 -.->|"Feign:查询产品"| S3
    S4 -.->|"Feign:查询产品"| S3
    S5 -.->|"Feign:查询产品"| S3
```

### 4.2 模块间调用关系（纯Feign，无MQ）

```mermaid
sequenceDiagram
    participant Client as 前端/外部系统
    participant MD as masterdata主数据服务
    participant RECIPE as recipe配方与工艺服务

    Note over Client,RECIPE: 场景1：创建新产品（含配方和工艺）
    Client->>MD: 1. 创建产品
    MD-->>Client: 返回产品ID

    Client->>RECIPE: 2. 创建配方（关联产品ID，可为NULL）
    RECIPE->>MD: 3. Feign校验原料ID列表
    RECIPE-->>Client: 返回配方ID

    Client->>RECIPE: 4. 创建工艺（关联产品ID）
    RECIPE->>MD: 5. Feign校验产品ID
    RECIPE-->>Client: 返回工艺ID

    Note over Client,RECIPE: 场景2：配方生效流程
    Client->>RECIPE: 1. 启用配方（enable）
    RECIPE->>RECIPE: 2. 更新状态为"已生效"
    RECIPE->>RECIPE: 3. 生成BOM快照
    RECIPE->>MD: 4. Feign更新产品current_recipe_id
    RECIPE->>RECIPE: 5. 记录版本历史
    RECIPE-->>Client: 操作成功

    Note over Client,RECIPE: 场景3：原料价格变更触发成本重算
    participant PURCHASE as purchase采购服务
    PURCHASE->>RECIPE: 1. RPC通知原料价格变更
    RECIPE->>RECIPE: 2. 标记相关配方cost_stale=1
    RECIPE-->>PURCHASE: 返回受影响配方数

    Client->>RECIPE: 3. 批量重算成本（Admin API）
    RECIPE->>RECIPE: 4. 遍历stale配方，重算成本
    RECIPE->>RECIPE: 5. 更新cost_stale=0
    RECIPE-->>Client: 返回重算结果

    Note over Client,RECIPE: 场景4：下游服务查询配方（聚合API）
    Client->>RECIPE: 1. 按产品ID查询配方+原料
    RECIPE->>MD: 2. Feign批量查询原料详情
    RECIPE-->>Client: 返回配方+原料完整信息
```

### 4.3 与下游服务的调用关系

```mermaid
flowchart LR
    subgraph MD["ims-module-masterdata"]
        M1[原料管理]
        M2[产品管理]
    end

    subgraph RECIPE["ims-module-recipe"]
        M3[配方管理]
        M4[工艺管理]
    end

    subgraph 下游服务
        WMS[WMS仓储服务]
        PMS[PMS排程服务]
        PRODUCTION[Production生产服务]
        FIN[Finance财务服务]
        QC[Quality品管服务]
        PURCHASE[Purchase采购服务]
    end

    M1 -->|"Feign:原料查询"| WMS
    M1 -->|"Feign:原料查询"| PMS

    M2 -->|"Feign:产品查询"| WMS
    M2 -->|"Feign:产品查询"| PMS

    M3 -->|"Feign:配方查询（聚合API）"| PMS
    M3 -->|"Feign:配方成本"| FIN
    M3 -->|"Feign:BOM快照"| PMS

    M4 -->|"Feign:工艺查询"| PRODUCTION
    M4 -->|"Feign:工艺查询"| QC

    PURCHASE -->|"RPC:通知价格变更"| M3

    style MD fill:#e3f2fd
    style RECIPE fill:#fce4ec
    style 下游服务 fill:#fff3e0
```

### 4.4 核心调用链路说明

| 场景 | 调用链路 | 说明 |
|------|---------|------|
| **创建产品** | 前端 -> masterdata:ProductController -> ProductService -> ProductMapper | 在主数据服务创建产品基础信息 |
| **创建配方** | 前端 -> recipe:RecipeController -> RecipeService -> Feign:masterdata:MaterialApi（校验原料） -> RecipeMapper | 在配方服务创建配方，跨模块校验原料。product_id可为NULL（实验配方） |
| **配方生效** | 前端 -> recipe:RecipeController -> RecipeService.enableRecipe -> Feign:masterdata:ProductApi（更新产品current_recipe_id） -> RecipeService.generateBomSnapshot -> RecipeVersionMapper（记录版本） | 启用配方时通过Feign更新主数据服务的产品表，并自动生成BOM快照 |
| **配方失效** | 前端 -> recipe:RecipeController -> RecipeService.disableRecipe -> RecipeVersionMapper（记录版本） | 失效配方，不再作为产品当前配方 |
| **创建工艺** | 前端 -> recipe:ProcessController -> ProcessService -> Feign:masterdata:ProductApi（校验产品） -> ProcessMapper | 在配方服务创建工艺，跨模块校验产品 |
| **工艺生效** | 前端 -> recipe:ProcessController -> ProcessService.enableRoute -> Feign:masterdata:ProductApi（更新产品） -> ProcessVersionMapper（记录版本） | 启用工艺时通过Feign更新主数据服务的产品表 |
| **工艺失效** | 前端 -> recipe:ProcessController -> ProcessService.disableRoute -> ProcessVersionMapper（记录版本） | 失效工艺 |
| **配方BOM展开** | PMS -> Feign:recipe:RecipeApi.getBomSnapshot -> recipe_bom_snapshot表 | 排程服务通过Feign调用配方服务获取BOM快照，避免实时递归展开 |
| **工艺参数获取** | Production -> Feign:recipe:ProcessApi -> ProcessStepMapper -> ProcessParamMapper | 生产服务通过Feign调用配方服务获取参数 |
| **原料价格变更** | Purchase -> Feign:recipe:RecipeApi.notifyPriceChange -> RecipeService标记cost_stale=1 | 采购服务通知原料价格变更，配方服务标记受影响配方为成本过期 |
| **批量成本重算** | Admin -> recipe:RecipeController.recalculateCost -> RecipeService遍历stale配方重算成本 | 管理后台手动触发批量成本重算 |

### 4.5 Service层接口设计

#### 4.5.1 ims-module-masterdata（主数据服务）

```java
// ========== 原料服务 ==========
package cn.lucky.ims.module.masterdata.service.material;

public interface MaterialService {
    // 基础CRUD
    Long createMaterial(MaterialSaveReqVO createReqVO);
    void updateMaterial(MaterialSaveReqVO updateReqVO);
    void deleteMaterial(Long id);
    MaterialDO getMaterial(Long id);
    PageResult<MaterialDO> getMaterialPage(MaterialPageReqVO pageReqVO);

    // 批量查询（供其他服务调用）
    List<MaterialDO> getMaterialList(Collection<Long> ids);
    Map<Long, MaterialDO> getMaterialMap(Collection<Long> ids);

    // 校验方法
    void validateMaterialExists(Long id);
    void validateMaterialListExists(Collection<Long> ids);
}

// ========== 产品服务 ==========
package cn.lucky.ims.module.masterdata.service.product;

public interface ProductService {
    // 基础CRUD
    Long createProduct(ProductSaveReqVO createReqVO);
    void updateProduct(ProductSaveReqVO updateReqVO);
    void deleteProduct(Long id);
    ProductDO getProduct(Long id);
    PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO);

    // 配方/工艺关联更新（供recipe模块通过Feign调用）
    void updateCurrentRecipe(Long productId, Long recipeId, Integer version);
    void updateCurrentProcess(Long productId, Long processId, Integer version);

    // 批量查询（供其他服务调用）
    List<ProductDO> getProductList(Collection<Long> ids);
    Map<Long, ProductDO> getProductMap(Collection<Long> ids);
}
```

#### 4.5.2 ims-module-recipe（配方与工艺服务）

```java
// ========== 配方组服务 ==========
package cn.lucky.ims.module.recipe.service.recipegroup;

public interface RecipeGroupService {
    // 基础CRUD
    Long createRecipeGroup(RecipeGroupSaveReqVO createReqVO);
    void updateRecipeGroup(RecipeGroupSaveReqVO updateReqVO);
    void deleteRecipeGroup(Long id);
    RecipeGroupDO getRecipeGroup(Long id);
    PageResult<RecipeGroupDO> getRecipeGroupPage(RecipeGroupPageReqVO pageReqVO);

    // 树结构
    List<RecipeGroupDO> getRecipeGroupTree();

    // 产品关联
    List<RecipeGroupDO> getGroupsByProductId(Long productId);
    List<ProductDO> getProductsByGroupId(Long groupId);
    void saveProductRelation(Long productId, List<Long> groupIds);
}

// ========== 配方服务 ==========
package cn.lucky.ims.module.recipe.service.recipe;

public interface RecipeService {
    // 基础CRUD
    Long createRecipe(RecipeSaveReqVO createReqVO);
    void updateRecipe(RecipeSaveReqVO updateReqVO);
    void deleteRecipe(Long id);
    RecipeDO getRecipe(Long id);
    PageResult<RecipeDO> getRecipePage(RecipePageReqVO pageReqVO);

    // 配方明细
    List<RecipeItemDO> getRecipeItems(Long recipeId);

    // 启用/失效（替代审批流程）
    void enableRecipe(Long id);                  // 启用配方（草稿->已生效），自动生成BOM快照，更新产品current_recipe_id
    void disableRecipe(Long id);                 // 失效配方（已生效->已失效）

    // 版本管理
    List<RecipeVersionDO> getVersionHistory(Long recipeId);
    RecipeDO getVersionSnapshot(Long recipeId, Integer version);

    // 成本管理
    void recalculateCost(List<Long> recipeIds);  // 批量重算成本
    List<RecipeDO> getStaleCostList();           // 获取成本过期的配方列表
    void notifyPriceChange(Long materialId, BigDecimal oldPrice, BigDecimal newPrice); // 原料价格变更通知

    // BOM快照
    void generateBomSnapshot(Long recipeId);     // 生成BOM快照（启用时自动调用，也可手动触发）

    // ========== 聚合API（重点） ==========
    /**
     * 按产品ID批量查询配方+原料完整信息（含BOM快照）
     * 一次Feign调用搞定，内部聚合recipe表 + recipe_item表 + md_material表 + recipe_bom_snapshot表数据
     *
     * @param productIds 产品ID列表
     * @return Map<productId, RecipeWithMaterialsVO>  产品配方+原料完整信息
     */
    Map<Long, RecipeWithMaterialsVO> batchGetRecipesByProductIds(List<Long> productIds);

    /**
     * 按单个产品ID查询配方+原料完整信息
     *
     * @param productId 产品ID
     * @return RecipeWithMaterialsVO 产品配方+原料完整信息
     */
    RecipeWithMaterialsVO getRecipeByProductId(Long productId);
}

// ========== 工艺服务 ==========
package cn.lucky.ims.module.recipe.service.process;

public interface ProcessService {
    // 基础CRUD
    Long createProcessRoute(ProcessRouteSaveReqVO createReqVO);
    void updateProcessRoute(ProcessRouteSaveReqVO updateReqVO);
    void deleteProcessRoute(Long id);
    ProcessRouteDO getProcessRoute(Long id);
    PageResult<ProcessRouteDO> getProcessRoutePage(ProcessRoutePageReqVO pageReqVO);

    // 工艺步骤
    List<ProcessStepDO> getProcessSteps(Long routeId);

    // 工艺参数
    List<ProcessParamDO> getStepParams(Long stepId);

    // 启用/失效（替代审批流程）
    void enableRoute(Long id);                   // 启用工艺路线（草稿->已生效）
    void disableRoute(Long id);                  // 失效工艺路线（已生效->已失效）

    // 模板功能
    void copyFromTemplate(Long templateId, Long productId); // 从模板复制工艺路线

    // 版本管理
    List<ProcessVersionDO> getVersionHistory(Long routeId);
    ProcessRouteDO getVersionSnapshot(Long routeId, Integer version);

    // 工艺获取（供生产服务调用）
    ProcessRouteWithStepsVO getProcessWithSteps(Long productId);
}
```

**聚合API返回值结构（RecipeWithMaterialsVO）：**

```java
package cn.lucky.ims.module.recipe.service.vo;

import lombok.Data;
import java.util.List;

/**
 * 配方+原料完整信息（聚合API返回值）
 */
@Data
public class RecipeWithMaterialsVO {
    /** 配方头信息 */
    private RecipeVO recipe;
    /** 配方明细列表（含原料完整信息） */
    private List<RecipeItemWithMaterialVO> items;
    /** BOM快照信息（如已生成） */
    private RecipeBomSnapshotVO bomSnapshot;
}

@Data
public class RecipeItemWithMaterialVO {
    /** 配方明细信息 */
    private Long materialId;
    private String materialCode;
    private String materialName;
    private Long unitId;
    private String unitCode;
    private BigDecimal quantity;       // DECIMAL（14,6）
    private BigDecimal percentage;
    private BigDecimal lossRate;
    private Boolean isMain;
    /** 来源类型: 1=原料/包材, 2=半成品配方 */
    private Integer sourceType;
    /** 来源ID: sourceType=1时为原料ID, sourceType=2时为半成品配方ID */
    private Long sourceId;
    /** 原料详细信息（从masterdata服务获取，sourceType=1时有值） */
    private MaterialVO material;
    /** 半成品配方信息（sourceType=2时有值） */
    private RecipeVO subRecipe;
}

@Data
public class RecipeBomSnapshotVO {
    /** 快照ID */
    private Long id;
    /** 配方ID */
    private Long recipeId;
    /** BOM层级明细（已展开为平级原料列表） */
    private List<BomItemVO> bomItems;
    /** 总成本 */
    private BigDecimal totalCost;      // DECIMAL（14,6）
    /** 快照生成时间 */
    private Date snapshotTime;
}

@Data
public class BomItemVO {
    /** 层级（0=顶层, 1=一级子项, 2=二级子项...） */
    private Integer level;
    /** 原料ID（最终叶子节点） */
    private Long materialId;
    private String materialCode;
    private String materialName;
    private String unitCode;
    private BigDecimal quantity;       // DECIMAL（14,6）展开后的总用量
    private BigDecimal unitPrice;      // DECIMAL（14,6）单价
    private BigDecimal totalCost;      // DECIMAL（14,6）小计成本
    /** 来源配方ID（用于追溯） */
    private Long sourceRecipeId;
    private String sourceRecipeName;
}
```

---

## 5. API接口设计概要

### 5.1 接口命名规范

遵循yudao-cloud的RESTful API规范，按模块划分路径前缀：

| 操作 | HTTP方法 | masterdata URL格式 | recipe URL格式 |
|------|---------|-------------------|---------------|
| 创建 | POST | `/admin-api/masterdata/xxx/create` | `/admin-api/recipe/xxx/create` |
| 更新 | PUT | `/admin-api/masterdata/xxx/update` | `/admin-api/recipe/xxx/update` |
| 删除 | DELETE | `/admin-api/masterdata/xxx/delete?id=` | `/admin-api/recipe/xxx/delete?id=` |
| 获取详情 | GET | `/admin-api/masterdata/xxx/get?id=` | `/admin-api/recipe/xxx/get?id=` |
| 分页查询 | GET | `/admin-api/masterdata/xxx/page` | `/admin-api/recipe/xxx/page` |
| 列表查询 | GET | `/admin-api/masterdata/xxx/list` | `/admin-api/recipe/xxx/list` |

### 5.2 原料管理API（masterdata模块）

| 接口 | 方法 | URL | 说明 |
|------|------|-----|------|
| 创建原料 | POST | `/admin-api/masterdata/material/create` | 创建原料 |
| 更新原料 | PUT | `/admin-api/masterdata/material/update` | 更新原料 |
| 删除原料 | DELETE | `/admin-api/masterdata/material/delete?id=` | 删除原料 |
| 获取原料详情 | GET | `/admin-api/masterdata/material/get?id=` | 获取原料详情 |
| 分页查询原料 | GET | `/admin-api/masterdata/material/page` | 分页查询原料列表 |
| 获取原料分类树 | GET | `/admin-api/masterdata/material/category/list` | 获取原料分类树 |
| 获取单位列表 | GET | `/admin-api/masterdata/material/unit/list` | 获取计量单位列表 |

**创建原料请求示例：**

```json
{
    "materialCode": "M001",
    "materialName": "高筋面粉",
    "categoryId": 1,
    "spec": "25kg/袋",
    "baseUnitId": 1,
    "materialType": 1,
    "safetyStock": 500.000000,
    "shelfLife": 180,
    "storageCondition": "常温干燥保存",
    "remark": "主要原料"
}
```

### 5.3 配方组管理API（recipe模块）

| 接口 | 方法 | URL | 说明 |
|------|------|-----|------|
| 创建配方组 | POST | `/admin-api/recipe/recipe-group/create` | 创建配方组 |
| 更新配方组 | PUT | `/admin-api/recipe/recipe-group/update` | 更新配方组 |
| 删除配方组 | DELETE | `/admin-api/recipe/recipe-group/delete?id=` | 删除配方组 |
| 获取配方组详情 | GET | `/admin-api/recipe/recipe-group/get?id=` | 获取配方组详情 |
| 分页查询配方组 | GET | `/admin-api/recipe/recipe-group/page` | 分页查询配方组列表 |
| 获取配方组树 | GET | `/admin-api/recipe/recipe-group/tree` | 获取配方组多级树结构 |
| 获取产品关联的配方组 | GET | `/admin-api/recipe/recipe-group/product-groups?productId=` | 获取产品关联的配方组 |
| 保存产品与配方组关联 | POST | `/admin-api/recipe/recipe-group/product-relation/save` | 保存产品与配方组关联 |

### 5.4 产品管理API（masterdata模块）

| 接口 | 方法 | URL | 说明 |
|------|------|-----|------|
| 创建产品 | POST | `/admin-api/masterdata/product/create` | 创建产品 |
| 更新产品 | PUT | `/admin-api/masterdata/product/update` | 更新产品 |
| 删除产品 | DELETE | `/admin-api/masterdata/product/delete?id=` | 删除产品 |
| 获取产品详情 | GET | `/admin-api/masterdata/product/get?id=` | 获取产品详情 |
| 分页查询产品 | GET | `/admin-api/masterdata/product/page` | 分页查询产品列表 |
| 获取产品分类树 | GET | `/admin-api/masterdata/product/category/list` | 获取产品分类树 |
| 获取产品当前配方 | GET | `/admin-api/masterdata/product/current-recipe/get?id=` | 获取产品当前生效配方 |
| 获取产品当前工艺 | GET | `/admin-api/masterdata/product/current-process/get?id=` | 获取产品当前生效工艺 |

### 5.5 配方管理API（recipe模块）

| 接口 | 方法 | URL | 说明 |
|------|------|-----|------|
| 创建配方 | POST | `/admin-api/recipe/recipe/create` | 创建配方（草稿状态） |
| 更新配方 | PUT | `/admin-api/recipe/recipe/update` | 更新配方（仅草稿可修改） |
| 删除配方 | DELETE | `/admin-api/recipe/recipe/delete?id=` | 删除配方（仅草稿可删除） |
| 获取配方详情 | GET | `/admin-api/recipe/recipe/get?id=` | 获取配方详情 |
| 分页查询配方 | GET | `/admin-api/recipe/recipe/page` | 分页查询配方列表 |
| 获取配方明细 | GET | `/admin-api/recipe/recipe/item/list?recipeId=` | 获取配方BOM明细 |
| 启用配方 | POST | `/admin-api/recipe/recipe/enable?id=` | 启用配方（草稿->已生效），自动生成BOM快照 |
| 失效配方 | POST | `/admin-api/recipe/recipe/disable?id=` | 失效配方（已生效->已失效） |
| 获取版本历史 | GET | `/admin-api/recipe/recipe/version/list?recipeId=` | 获取配方版本历史 |
| 获取历史版本快照 | GET | `/admin-api/recipe/recipe/version/snapshot?recipeId=&version=` | 获取指定版本快照 |
| 批量重算成本 | POST | `/admin-api/recipe/recipe/recalculate-cost` | 批量重算配方成本 |
| 获取成本过期列表 | GET | `/admin-api/recipe/recipe/cost/stale-list` | 获取成本过期的配方列表 |
| 手动生成BOM快照 | POST | `/admin-api/recipe/recipe/generate-bom-snapshot` | 手动触发BOM快照生成 |

**创建配方请求示例：**

```json
{
    "recipeCode": "R-2026-001",
    "recipeName": "甜面包配方V1",
    "productId": 1,
    "productCode": "P001",
    "recipeType": 2,
    "totalWeight": 5000.000000,
    "yieldQty": null,
    "description": "基础甜面包配方",
    "items": [
        {
            "sourceType": 1,
            "sourceId": 1,
            "materialId": 1,
            "materialCode": "M001",
            "materialName": "高筋面粉",
            "unitId": 1,
            "unitCode": "kg",
            "quantity": 2.500000,
            "percentage": 50.0000,
            "lossRate": 2.00,
            "isMain": 1,
            "sort": 1
        },
        {
            "sourceType": 1,
            "sourceId": 2,
            "materialId": 2,
            "materialCode": "M002",
            "materialName": "白砂糖",
            "unitId": 1,
            "unitCode": "kg",
            "quantity": 0.500000,
            "percentage": 10.0000,
            "lossRate": 1.00,
            "isMain": 0,
            "sort": 2
        },
        {
            "sourceType": 2,
            "sourceId": 100,
            "materialCode": "SF-001",
            "materialName": "酥油面团（半成品）",
            "unitId": 1,
            "unitCode": "kg",
            "quantity": 0.300000,
            "percentage": 6.0000,
            "lossRate": 0.00,
            "isMain": 0,
            "sort": 3
        }
    ]
}
```

**批量重算成本请求示例：**

```json
{
    "recipeIds": [101, 102, 103]
}
```

**成本过期列表响应示例：**

```json
{
    "code": 0,
    "data": [
        {
            "id": 101,
            "recipeCode": "R-2026-001",
            "recipeName": "甜面包配方V1",
            "productId": 1,
            "productCode": "P001",
            "productName": "甜面包",
            "totalCost": 12.500000,
            "costStale": 1,
            "updateTime": "2026-04-15 10:30:00"
        }
    ]
}
```

### 5.6 工艺管理API（recipe模块）

| 接口 | 方法 | URL | 说明 |
|------|------|-----|------|
| 创建工艺路线 | POST | `/admin-api/recipe/process/create` | 创建工艺路线（草稿状态） |
| 更新工艺路线 | PUT | `/admin-api/recipe/process/update` | 更新工艺路线（仅草稿可修改） |
| 删除工艺路线 | DELETE | `/admin-api/recipe/process/delete?id=` | 删除工艺路线 |
| 获取工艺路线详情 | GET | `/admin-api/recipe/process/get?id=` | 获取工艺路线详情 |
| 分页查询工艺路线 | GET | `/admin-api/recipe/process/page` | 分页查询工艺路线列表 |
| 获取工艺步骤 | GET | `/admin-api/recipe/process/step/list?routeId=` | 获取工艺步骤列表 |
| 获取步骤参数 | GET | `/admin-api/recipe/process/param/list?stepId=` | 获取步骤参数列表 |
| 启用工艺路线 | POST | `/admin-api/recipe/process/enable?id=` | 启用工艺路线（草稿->已生效） |
| 失效工艺路线 | POST | `/admin-api/recipe/process/disable?id=` | 失效工艺路线（已生效->已失效） |
| 从模板复制 | POST | `/admin-api/recipe/process/copy-from-template` | 从工艺模板复制为新工艺路线 |
| 获取版本历史 | GET | `/admin-api/recipe/process/version/list?routeId=` | 获取工艺版本历史 |
| 获取历史版本快照 | GET | `/admin-api/recipe/process/version/snapshot?routeId=&version=` | 获取指定版本快照 |

**创建工艺路线请求示例：**

```json
{
    "routeCode": "PR-2026-001",
    "routeName": "甜面包工艺路线V1",
    "productId": 1,
    "productCode": "P001",
    "isTemplate": 0,
    "templateName": null,
    "totalDuration": 180,
    "description": "基础甜面包工艺",
    "steps": [
        {
            "stepCode": "STEP01",
            "stepName": "搅拌",
            "stepType": "搅拌",
            "sequence": 1,
            "duration": 15,
            "description": "低速搅拌3分钟，高速搅拌12分钟",
            "isKeyStep": 1,
            "qcRequired": 0,
            "params": [
                {
                    "paramCode": "TEMP",
                    "paramName": "面团温度",
                    "paramType": "温度",
                    "unit": "\u2103",
                    "valueType": 2,
                    "valueMin": 24.00,
                    "valueMax": 26.00,
                    "isRequired": 1
                },
                {
                    "paramCode": "SPEED",
                    "paramName": "搅拌速度",
                    "paramType": "速度",
                    "unit": "rpm",
                    "valueType": 1,
                    "value": "低速/高速",
                    "isRequired": 1
                }
            ]
        },
        {
            "stepCode": "STEP02",
            "stepName": "发酵",
            "stepType": "发酵",
            "sequence": 2,
            "duration": 60,
            "description": "基础发酵60分钟",
            "isKeyStep": 1,
            "qcRequired": 1,
            "params": [
                {
                    "paramCode": "TEMP",
                    "paramName": "发酵温度",
                    "paramType": "温度",
                    "unit": "\u2103",
                    "valueType": 2,
                    "valueMin": 28.00,
                    "valueMax": 32.00,
                    "isRequired": 1
                },
                {
                    "paramCode": "HUMIDITY",
                    "paramName": "湿度",
                    "paramType": "湿度",
                    "unit": "%",
                    "valueType": 2,
                    "valueMin": 75.00,
                    "valueMax": 85.00,
                    "isRequired": 1
                }
            ]
        }
    ]
}
```

**从模板复制请求示例：**

```json
{
    "templateId": 50,
    "productId": 1,
    "routeName": "甜面包工艺路线V1（从模板复制）"
}
```

### 5.7 供外部服务调用的RPC API

以下API供其他微服务调用（通过Feign），按模块划分：

| 服务 | 接口 | URL | 所属模块 |
|------|------|-----|---------|
| **PMS排程服务** | 批量获取产品配方+原料（聚合API） | POST `/rpc-api/recipe/recipe/batch-by-products` | recipe |
| **PMS排程服务** | 获取单个产品配方+原料（聚合API） | GET `/rpc-api/recipe/recipe/by-product/get?productId=` | recipe |
| **PMS排程服务** | 获取配方BOM快照 | GET `/rpc-api/recipe/recipe/bom/get?recipeId=` | recipe |
| **PMS排程服务** | 计算原料需求 | POST `/rpc-api/recipe/recipe/material-requirement/calculate` | recipe |
| **Production生产服务** | 获取产品工艺路线 | GET `/rpc-api/recipe/process/route/get?productId=` | recipe |
| **Finance财务服务** | 获取配方成本 | GET `/rpc-api/recipe/recipe/cost/get?recipeId=` | recipe |
| **Purchase采购服务** | 通知原料价格变更 | POST `/rpc-api/recipe/recipe/cost/notify-price-change` | recipe |
| **WMS仓储服务** | 批量获取原料信息 | POST `/rpc-api/masterdata/material/batch-get` | masterdata |
| **WMS仓储服务** | 批量获取产品信息 | POST `/rpc-api/masterdata/product/batch-get` | masterdata |

**聚合API请求/响应示例：**

```
POST /rpc-api/recipe/recipe/batch-by-products
Content-Type: application/json

请求体：
[1, 2, 3]  // 产品ID列表

响应体：
{
    "code": 0,
    "data": {
        "1": {
            "recipe": {
                "id": 101,
                "recipeCode": "R-2026-001",
                "recipeName": "甜面包配方V1",
                "productId": 1,
                "recipeType": 2,
                "version": 1,
                "status": 1,
                "totalWeight": 5000.000000,
                "costStale": 0,
                "totalCost": 12.500000
            },
            "items": [
                {
                    "sourceType": 1,
                    "sourceId": 1,
                    "materialId": 1,
                    "materialCode": "M001",
                    "materialName": "高筋面粉",
                    "unitCode": "kg",
                    "quantity": 2.500000,
                    "percentage": 50.0000,
                    "lossRate": 2.00,
                    "isMain": true,
                    "material": {
                        "id": 1,
                        "materialCode": "M001",
                        "materialName": "高筋面粉",
                        "spec": "25kg/袋",
                        "materialType": 1,
                        "status": 1
                    }
                }
            ],
            "bomSnapshot": {
                "id": 1001,
                "recipeId": 101,
                "snapshotTime": "2026-04-16 14:30:00",
                "totalCost": 12.500000,
                "bomItems": [
                    {
                        "level": 0,
                        "materialId": 1,
                        "materialCode": "M001",
                        "materialName": "高筋面粉",
                        "unitCode": "kg",
                        "quantity": 2.500000,
                        "unitPrice": 3.200000,
                        "totalCost": 8.000000,
                        "sourceRecipeId": 101,
                        "sourceRecipeName": "甜面包配方V1"
                    }
                ]
            }
        },
        "2": { ... }
    }
}
```

**原料价格变更通知请求示例：**

```
POST /rpc-api/recipe/recipe/cost/notify-price-change
Content-Type: application/json

请求体：
{
    "materialId": 1,
    "oldPrice": 3.200000,
    "newPrice": 3.500000
}

响应体：
{
    "code": 0,
    "data": 5,
    "msg": "已标记5个配方成本为过期"
}
```

**BOM快照查询响应示例：**

```
GET /rpc-api/recipe/recipe/bom/get?recipeId=101

响应体：
{
    "code": 0,
    "data": {
        "id": 1001,
        "recipeId": 101,
        "snapshotTime": "2026-04-16 14:30:00",
        "totalCost": 12.500000,
        "bomItems": [
            {
                "level": 0,
                "materialId": 1,
                "materialCode": "M001",
                "materialName": "高筋面粉",
                "unitCode": "kg",
                "quantity": 2.500000,
                "unitPrice": 3.500000,
                "totalCost": 8.750000,
                "sourceRecipeId": 101,
                "sourceRecipeName": "甜面包配方V1"
            },
            {
                "level": 0,
                "materialId": 2,
                "materialCode": "M002",
                "materialName": "白砂糖",
                "unitCode": "kg",
                "quantity": 0.500000,
                "unitPrice": 5.000000,
                "totalCost": 2.500000,
                "sourceRecipeId": 101,
                "sourceRecipeName": "甜面包配方V1"
            },
            {
                "level": 1,
                "materialId": 10,
                "materialCode": "M010",
                "materialName": "低筋面粉",
                "unitCode": "kg",
                "quantity": 0.200000,
                "unitPrice": 4.000000,
                "totalCost": 0.800000,
                "sourceRecipeId": 100,
                "sourceRecipeName": "酥油面团（半成品）"
            },
            {
                "level": 1,
                "materialId": 11,
                "materialCode": "M011",
                "materialName": "黄油",
                "unitCode": "kg",
                "quantity": 0.100000,
                "unitPrice": 25.000000,
                "totalCost": 2.500000,
                "sourceRecipeId": 100,
                "sourceRecipeName": "酥油面团（半成品）"
            }
        ]
    }
}
```

---

## 6. 与yudao-cloud集成说明

### 6.1 yudao-cloud框架概述

yudao-cloud 是一套基于 Spring Cloud Alibaba 的微服务快速开发框架，具有以下特点：

- **开箱即用**：内置用户、权限、字典、文件、消息等基础模块
- **多租户支持**：完善的租户隔离机制
- **代码生成**：支持单表、树表、主子表的代码生成
- **权限控制**：基于 RBAC 的细粒度权限控制
- **操作日志**：自动记录操作日志
- **报表能力**：内置积木报表（SQL数据集+拖拽设计）和 GoView 大屏

**本次项目直接使用的 yudao-cloud 自带模块：**

| 模块 | 提供的能力 | 使用方式 |
|------|-----------|---------|
| `ims-gateway` | API网关、路由、认证、限流 | 直接配置路由规则 |
| `system-service` | 用户管理、角色权限、组织架构、数据字典、租户管理、认证授权、站内消息、邮件、短信、操作日志 | 直接使用，无需开发 |
| `infra-service` | 文件上传/管理、配置参数、代码生成、API日志 | 直接使用，无需开发 |
| `ims-module-report` | 积木报表（SQL数据集+拖拽设计）+ GoView大屏 | 按需启用，配置SQL数据集 |

### 6.2 模块集成步骤

#### 步骤1：创建两个模块目录

在 yudao-cloud 项目根目录下创建两个业务模块：

```bash
# 在 yudao-module 同级目录创建
mkdir -p ims-module-masterdata/ims-module-masterdata-api
mkdir -p ims-module-masterdata/ims-module-masterdata-biz
mkdir -p ims-module-recipe/ims-module-recipe-api
mkdir -p ims-module-recipe/ims-module-recipe-biz
```

#### 步骤2：配置 pom.xml

**父模块 pom.xml 添加：**

```xml
<modules>
    <!-- 其他模块 -->
    <module>ims-module-masterdata</module>
    <module>ims-module-recipe</module>
</modules>
```

**ims-module-masterdata/pom.xml：**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <artifactId>ims-module-masterdata</artifactId>
    <name>${project.artifactId}</name>

    <modules>
        <module>ims-module-masterdata-api</module>
        <module>ims-module-masterdata-biz</module>
    </modules>
</project>
```

**ims-module-recipe/pom.xml：**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <artifactId>ims-module-recipe</artifactId>
    <name>${project.artifactId}</name>

    <modules>
        <module>ims-module-recipe-api</module>
        <module>ims-module-recipe-biz</module>
    </modules>
</project>
```

#### 步骤3：配置数据库

两个模块**共用一个数据库**（`ruoyi-vue-pro`），通过表名前缀区分模块归属：

```yaml
spring:
  datasource:
    dynamic:
      datasource:
        # 两个模块共用同一个数据库
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
          username: root
          password: password
```

> **设计决策说明：** 共用一个数据库是当前阶段的最佳选择。所有表通过前缀（`md_`、`recipe_`、`process_`）区分模块归属，报表模块可直接SQL查询（无需跨库），简化开发和部署。后续如需拆分数据库，report模块支持多数据源，可平滑过渡。

#### 步骤4：执行SQL脚本

将本文档第3章的DDL脚本导入数据库。

#### 步骤5：配置菜单权限

在 `system_menu` 表中添加两个模块的菜单：

```sql
-- 主数据服务一级菜单
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component)
VALUES ('主数据', '', 1, 50, 0, '/masterdata', 'database', NULL);

-- 原料管理菜单
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component)
VALUES ('原料管理', 'masterdata:material:query', 2, 1, {上级ID}, 'material', '', 'masterdata/material/index');

-- 产品管理菜单
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component)
VALUES ('产品管理', 'masterdata:product:query', 2, 2, {上级ID}, 'product', '', 'masterdata/product/index');

-- 配方服务一级菜单
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component)
VALUES ('配方工艺', '', 1, 52, 0, '/recipe', 'edit', NULL);

-- 配方组管理菜单
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component)
VALUES ('配方组管理', 'recipe:recipe-group:query', 2, 1, {上级ID}, 'recipe-group', '', 'recipe/recipe-group/index');

-- 配方管理菜单
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component)
VALUES ('配方管理', 'recipe:recipe:query', 2, 1, {上级ID}, 'recipe', '', 'recipe/recipe/index');

-- 工艺管理菜单
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component)
VALUES ('工艺管理', 'recipe:process:query', 2, 2, {上级ID}, 'process', '', 'recipe/process/index');

-- 其他菜单类似...
```

### 6.3 核心类继承关系

```mermaid
classDiagram
    class BaseDO {
        +Long id
        +String creator
        +Date createTime
        +String updater
        +Date updateTime
        +Boolean deleted
        +Long tenantId
    }

    class MaterialDO {
        +String materialCode
        +String materialName
        +Long categoryId
        +String spec
        +Long baseUnitId
        ...
    }

    class ProductDO {
        +String productCode
        +String productName
        +Long categoryId
        +Long currentRecipeId
        +Long currentProcessId
        ...
    }

    class RecipeGroupDO {
        +String groupCode
        +String groupName
        +Long parentId
        +Integer groupLevel
        +String doughType
        ...
    }

    class RecipeDO {
        +String recipeCode
        +String recipeName
        +Long productId
        +Integer recipeType
        +Integer version
        +Integer status
        +Integer costStale
        +BigDecimal totalCost
        ...
    }

    class RecipeItemDO {
        +Long recipeId
        +Long sourceId
        +Integer sourceType
        +BigDecimal quantity
        ...
    }

    class RecipeBomSnapshotDO {
        +Long recipeId
        +BigDecimal totalCost
        +Date snapshotTime
        ...
    }

    class RecipeBomSnapshotItemDO {
        +Long snapshotId
        +Integer level
        +Long materialId
        +BigDecimal quantity
        +BigDecimal unitPrice
        +BigDecimal totalCost
        ...
    }

    class ProcessRouteDO {
        +String routeCode
        +String routeName
        +Long productId
        +Integer version
        +Integer status
        +Boolean isTemplate
        +String templateName
        ...
    }

    BaseDO <|-- MaterialDO
    BaseDO <|-- ProductDO
    BaseDO <|-- RecipeGroupDO : "recipe模块"
    BaseDO <|-- RecipeDO
    BaseDO <|-- RecipeItemDO
    BaseDO <|-- RecipeBomSnapshotDO
    BaseDO <|-- RecipeBomSnapshotItemDO
    BaseDO <|-- ProcessRouteDO

    RecipeDO "1" --> "*" RecipeItemDO : "包含"
    RecipeDO "1" --> "*" RecipeBomSnapshotDO : "BOM快照"
    RecipeBomSnapshotDO "1" --> "*" RecipeBomSnapshotItemDO : "快照明细"
```

### 6.4 关键配置项

| 配置项 | 说明 | masterdata示例值 | recipe示例值 |
|--------|------|-----------------|-------------|
| `yudao.info.base-package` | 扫描包路径 | `cn.lucky.ims.module.masterdata` | `cn.lucky.ims.module.recipe` |
| `yudao.swagger.title` | API文档标题 | `主数据服务` | `配方与工艺服务` |
| `yudao.swagger.description` | API文档描述 | `原料、产品管理接口` | `配方组、配方、工艺管理接口` |

### 6.5 代码生成配置

使用 yudao-cloud 的代码生成功能，配置如下：

| 配置项 | masterdata模块值 | recipe模块值 |
|--------|-----------------|-------------|
| 生成模板 | 单表（standard）/ 主子表（master-detail） | 主子表（master-detail） |
| 前端类型 | Vue3 + Element Plus | Vue3 + Element Plus |
| 上级菜单 | 主数据 | 配方工艺 |
| 前端路径 | `masterdata/xxx` | `recipe/xxx` |
| 后端包路径 | `cn.lucky.ims.module.masterdata` | `cn.lucky.ims.module.recipe` |

**主子表代码生成说明：**

产品与配方组关联、配方管理（配方头+配方明细）、工艺管理（工艺路线+工艺步骤+工艺参数）应使用**主子表模板**：

| 主表 | 子表 | 关联字段 | 所属模块 |
|------|------|---------|---------|
| `recipe_group` | `recipe_product_group` | `recipe_group_id` | recipe |
| `recipe` | `recipe_item` | `recipe_id` | recipe |
| `recipe_bom_snapshot` | `recipe_bom_snapshot_item` | `snapshot_id` | recipe |
| `process_route` | `process_step` | `route_id` | recipe |
| `process_step` | `process_param` | `step_id` | recipe |

### 6.6 Feign客户端配置

按模块分别定义Feign接口：

#### 6.6.1 masterdata模块提供的Feign接口

在 `ims-module-masterdata-api` 模块中定义：

```java
package cn.lucky.ims.module.masterdata.api;

import cn.lucky.ims.framework.common.pojo.CommonResult;
import cn.lucky.ims.module.masterdata.api.dto.*;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@FeignClient(value = "masterdata-server", contextId = "materialApi")
public interface MaterialApi {

    /**
     * 批量获取原料信息
     */
    @PostMapping("/rpc-api/masterdata/material/batch-get")
    CommonResult<List<MaterialRespDTO>> batchGetMaterials(@RequestBody List<Long> ids);

    /**
     * 校验原料是否存在
     */
    @PostMapping("/rpc-api/masterdata/material/validate")
    CommonResult<Boolean> validateMaterials(@RequestBody List<Long> ids);
}

@FeignClient(value = "masterdata-server", contextId = "productApi")
public interface ProductApi {

    /**
     * 批量获取产品信息
     */
    @PostMapping("/rpc-api/masterdata/product/batch-get")
    CommonResult<List<ProductRespDTO>> batchGetProducts(@RequestBody List<Long> ids);

    /**
     * 更新产品当前配方信息（供recipe模块调用）
     */
    @PostMapping("/rpc-api/masterdata/product/update-recipe")
    CommonResult<Boolean> updateCurrentRecipe(@RequestParam("productId") Long productId,
                                               @RequestParam("recipeId") Long recipeId,
                                               @RequestParam("version") Integer version);

    /**
     * 更新产品当前工艺信息（供recipe模块调用）
     */
    @PostMapping("/rpc-api/masterdata/product/update-process")
    CommonResult<Boolean> updateCurrentProcess(@RequestParam("productId") Long productId,
                                               @RequestParam("processId") Long processId,
                                               @RequestParam("version") Integer version);
}
```

#### 6.6.2 recipe模块提供的Feign接口

在 `ims-module-recipe-api` 模块中定义：

```java
package cn.lucky.ims.module.recipe.api;

import cn.lucky.ims.framework.common.pojo.CommonResult;
import cn.lucky.ims.module.recipe.api.dto.*;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@FeignClient(value = "recipe-server", contextId = "recipeApi")
public interface RecipeApi {

    /**
     * 【聚合API】按产品ID批量查询配方+原料完整信息（含BOM快照）
     * 一次Feign调用搞定，内部聚合recipe表 + recipe_item表 + md_material表 + recipe_bom_snapshot表数据
     */
    @PostMapping("/rpc-api/recipe/recipe/batch-by-products")
    CommonResult<Map<Long, RecipeWithMaterialsRespDTO>> batchGetRecipesByProductIds(
            @RequestBody List<Long> productIds);

    /**
     * 【聚合API】按单个产品ID查询配方+原料完整信息
     */
    @GetMapping("/rpc-api/recipe/recipe/by-product/get")
    CommonResult<RecipeWithMaterialsRespDTO> getRecipeByProductId(
            @RequestParam("productId") Long productId);

    /**
     * 获取配方BOM快照（已展开的平级原料列表）
     */
    @GetMapping("/rpc-api/recipe/recipe/bom/get")
    CommonResult<RecipeBomRespDTO> getBomSnapshot(@RequestParam("recipeId") Long recipeId);

    /**
     * 计算原料需求
     */
    @PostMapping("/rpc-api/recipe/recipe/material-requirement/calculate")
    CommonResult<Map<Long, BigDecimal>> calculateMaterialRequirement(
            @RequestParam("productId") Long productId,
            @RequestParam("productQty") BigDecimal productQty);

    /**
     * 获取配方成本
     */
    @GetMapping("/rpc-api/recipe/recipe/cost/get")
    CommonResult<RecipeCostRespDTO> getRecipeCost(@RequestParam("recipeId") Long recipeId);

    /**
     * 通知原料价格变更（供purchase-service调用）
     * 内部逻辑：查找引用该原料的所有已生效配方，标记cost_stale=1
     */
    @PostMapping("/rpc-api/recipe/recipe/cost/notify-price-change")
    CommonResult<Integer> notifyPriceChange(@RequestBody PriceChangeNotifyReqDTO reqDTO);
}

@FeignClient(value = "recipe-server", contextId = "processApi")
public interface ProcessApi {

    /**
     * 获取产品当前生效工艺路线
     */
    @GetMapping("/rpc-api/recipe/process/route/get")
    CommonResult<ProcessRouteWithStepsDTO> getProcessRoute(@RequestParam("productId") Long productId);
}
```

**Feign DTO定义：**

```java
package cn.lucky.ims.module.recipe.api.dto;

import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

/**
 * 原料价格变更通知请求
 */
@Data
public class PriceChangeNotifyReqDTO {
    /** 原料ID */
    private Long materialId;
    /** 旧价格 */
    private BigDecimal oldPrice;
    /** 新价格 */
    private BigDecimal newPrice;
}

/**
 * 配方成本响应
 */
@Data
public class RecipeCostRespDTO {
    private Long recipeId;
    private String recipeCode;
    private String recipeName;
    /** 总成本 DECIMAL（14,6） */
    private BigDecimal totalCost;
    /** 单位成本 DECIMAL（14,6） */
    private BigDecimal unitCost;
    /** 成本是否过期 */
    private Integer costStale;
    /** 成本明细 */
    private List<CostItemDTO> items;
}

@Data
public class CostItemDTO {
    private Long materialId;
    private String materialCode;
    private String materialName;
    private BigDecimal quantity;       // DECIMAL（14,6）
    private BigDecimal unitPrice;      // DECIMAL（14,6）
    private BigDecimal totalCost;      // DECIMAL（14,6）
}

/**
 * BOM快照响应
 */
@Data
public class RecipeBomRespDTO {
    private Long id;
    private Long recipeId;
    private BigDecimal totalCost;      // DECIMAL（14,6）
    private Date snapshotTime;
    private List<BomItemRespDTO> bomItems;
}

@Data
public class BomItemRespDTO {
    private Integer level;
    private Long materialId;
    private String materialCode;
    private String materialName;
    private String unitCode;
    private BigDecimal quantity;       // DECIMAL（14,6）
    private BigDecimal unitPrice;      // DECIMAL（14,6）
    private BigDecimal totalCost;      // DECIMAL（14,6）
    private Long sourceRecipeId;
    private String sourceRecipeName;
}
```

### 6.7 报表方案

使用 yudao-cloud 自带的 `ims-module-report` 模块，无需自建报表服务。

#### 6.7.1 方案概述

| 特性 | 说明 |
|------|------|
| **报表引擎** | 积木报表（JimuReport），支持SQL数据集+拖拽设计 |
| **大屏** | GoView，支持拖拽设计数据大屏 |
| **数据源** | 直接SQL查询（共用数据库优势，无需跨服务聚合） |
| **使用者** | 非技术人员可通过拖拽设计报表，无需开发介入 |

#### 6.7.2 SQL数据集示例

由于所有模块共用一个数据库，报表可直接通过SQL查询关联数据：

**示例1：产品配方成本明细报表**

```sql
SELECT
    p.product_code AS '产品编码',
    p.product_name AS '产品名称',
    rg.group_name AS '配方组',
    r.recipe_code AS '配方编码',
    r.recipe_name AS '配方名称',
    r.recipe_type AS '配方类型',
    r.version AS '配方版本',
    CASE r.status WHEN 0 THEN '草稿' WHEN 1 THEN '已生效' WHEN 2 THEN '已失效' END AS '配方状态',
    ri.source_type AS '来源类型',
    CASE ri.source_type WHEN 1 THEN '原料/包材' WHEN 2 THEN '半成品配方' END AS '来源说明',
    ri.material_code AS '原料编码',
    ri.material_name AS '原料名称',
    ri.unit_code AS '单位',
    ri.quantity AS '用量',
    ri.percentage AS '占比（%）',
    ri.loss_rate AS '损耗率（%）',
    m.spec AS '原料规格',
    r.total_cost AS '配方总成本',
    CASE r.cost_stale WHEN 0 THEN '有效' WHEN 1 THEN '过期待重算' END AS '成本状态'
FROM md_product p
LEFT JOIN recipe_product_group prg ON p.id = prg.product_id AND prg.is_primary = 1
LEFT JOIN recipe_group rg ON prg.recipe_group_id = rg.id
LEFT JOIN recipe r ON p.current_recipe_id = r.id
LEFT JOIN recipe_item ri ON r.id = ri.recipe_id
LEFT JOIN md_material m ON ri.material_id = m.id
WHERE p.deleted = 0
  AND r.deleted = 0
  AND ri.deleted = 0
ORDER BY p.product_code, ri.sort
```

**示例2：BOM展开报表（基于recipe_bom_snapshot，多级配方嵌套）**

```sql
SELECT
    r.recipe_code AS '配方编码',
    r.recipe_name AS '配方名称',
    r.recipe_type AS '配方类型',
    p.product_code AS '产品编码',
    p.product_name AS '产品名称',
    bi.level AS 'BOM层级',
    bi.material_code AS '原料编码',
    bi.material_name AS '原料名称',
    bi.unit_code AS '单位',
    bi.quantity AS '展开用量',
    bi.unit_price AS '单价（元）',
    bi.total_cost AS '小计成本（元）',
    bi.source_recipe_id AS '来源配方ID',
    bi.source_recipe_name AS '来源配方名称',
    bs.snapshot_time AS '快照时间',
    bs.total_cost AS '配方总成本（元）'
FROM recipe_bom_snapshot bs
JOIN recipe r ON bs.recipe_id = r.id
LEFT JOIN md_product p ON r.product_id = p.id
JOIN recipe_bom_snapshot_item bi ON bs.id = bi.snapshot_id
WHERE bs.deleted = 0
  AND bi.deleted = 0
ORDER BY r.recipe_code, bi.level, bi.material_code
```

**示例3：成本分析报表（基于BOM快照，按原料分类汇总）**

```sql
SELECT
    mc.name AS '原料分类',
    bi.material_code AS '原料编码',
    bi.material_name AS '原料名称',
    COUNT(DISTINCT bs.recipe_id) AS '被配方引用次数',
    SUM（bi.quantity） AS '总用量',
    AVG（bi.unit_price） AS '平均单价（元）',
    SUM（bi.total_cost） AS '总成本（元）',
    CASE
        WHEN MAX（bs.cost_stale） = 1 THEN '含过期成本'
        ELSE '成本有效'
    END AS '成本状态'
FROM recipe_bom_snapshot bs
JOIN recipe_bom_snapshot_item bi ON bs.id = bi.snapshot_id
LEFT JOIN md_material m ON bi.material_id = m.id
LEFT JOIN md_material_category mc ON m.category_id = mc.id
WHERE bs.deleted = 0
  AND bi.deleted = 0
GROUP BY mc.name, bi.material_code, bi.material_name
ORDER BY SUM（bi.total_cost） DESC
```

**示例4：工艺路线汇总报表**

```sql
SELECT
    p.product_code AS '产品编码',
    p.product_name AS '产品名称',
    pr.route_code AS '工艺编码',
    pr.route_name AS '工艺名称',
    pr.version AS '工艺版本',
    CASE pr.status WHEN 0 THEN '草稿' WHEN 1 THEN '已生效' WHEN 2 THEN '已失效' END AS '工艺状态',
    CASE pr.is_template WHEN 1 THEN '是' ELSE '否' END AS '是否模板',
    pr.template_name AS '模板名称',
    ps.step_code AS '步骤编码',
    ps.step_name AS '步骤名称',
    ps.step_type AS '步骤类型',
    ps.sequence AS '工序顺序',
    ps.duration AS '工时（分钟）',
    pp.param_name AS '参数名称',
    pp.param_type AS '参数类型',
    pp.unit AS '单位',
    pp.value AS '参数值',
    pp.value_min AS '最小值',
    pp.value_max AS '最大值'
FROM md_product p
LEFT JOIN process_route pr ON p.current_process_id = pr.id
LEFT JOIN process_step ps ON pr.id = ps.route_id
LEFT JOIN process_param pp ON ps.id = pp.step_id
WHERE p.deleted = 0
  AND pr.deleted = 0
  AND ps.deleted = 0
ORDER BY p.product_code, ps.sequence
```

**示例5：原料使用统计报表**

```sql
SELECT
    m.material_code AS '原料编码',
    m.material_name AS '原料名称',
    mc.name AS '原料分类',
    m.spec AS '规格',
    COUNT(DISTINCT r.id) AS '被配方引用次数',
    COUNT(DISTINCT p.id) AS '关联产品数'
FROM md_material m
LEFT JOIN md_material_category mc ON m.category_id = mc.id
LEFT JOIN recipe_item ri ON m.id = ri.material_id AND ri.source_type = 1
LEFT JOIN recipe r ON ri.recipe_id = r.id AND r.status = 1 AND r.deleted = 0
LEFT JOIN md_product p ON r.product_id = p.id AND p.deleted = 0
WHERE m.deleted = 0
GROUP BY m.id, m.material_code, m.material_name, mc.name, m.spec
ORDER BY COUNT(DISTINCT r.id) DESC
```

#### 6.7.3 后续演进

当前阶段所有模块共用一个数据库，报表SQL可直接跨表查询。后续如需拆分数据库：

- `ims-module-report` 支持**多数据源**配置
- 可为报表模块配置多个数据源，分别指向不同业务库
- 报表SQL需调整为单库查询或使用视图/ETL方案
- 此演进路径平滑，不影响业务模块代码

### 6.8 开发规范

#### 6.8.1 命名规范

| 类型 | masterdata规范 | recipe规范 |
|------|---------------|-----------|
| 表名 | `md_` 前缀 | `recipe_` / `process_` 前缀 |
| DO类 | `XxxDO` 后缀 | `XxxDO` 后缀 |
| VO类 | `XxxVO` 后缀 | `XxxVO` 后缀 |
| DTO类 | `XxxDTO` 后缀 | `XxxDTO` 后缀 |
| Service | `XxxService` | `XxxService` |
| Controller | `XxxController` | `XxxController` |
| Mapper | `XxxMapper` | `XxxMapper` |

#### 6.8.2 注解使用

```java
// masterdata模块 DO类注解
@TableName("md_material")
@KeySequence("md_material_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MaterialDO extends BaseDO {
    // ...
}

// masterdata模块 Controller注解
@RestController
@RequestMapping("/admin-api/masterdata/material")
@Tag(name = "管理后台 - 原料管理")
public class MaterialController {
    // ...
}

// recipe模块 DO类注解
@TableName("recipe")
@KeySequence("recipe_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RecipeDO extends BaseDO {
    // ...
}

// recipe模块 Controller注解
@RestController
@RequestMapping("/admin-api/recipe/recipe")
@Tag(name = "管理后台 - 配方管理")
public class RecipeController {
    // ...
}

// Service注解（各模块通用）
@Service
@Validated
public class MaterialServiceImpl implements MaterialService {
    // ...
}
```

#### 6.8.3 权限配置

```java
// masterdata模块权限
@PostMapping("/create")
@Operation(summary = "创建原料")
@PreAuthorize("@ss.hasPermission('masterdata:material:create')")
public CommonResult<Long> createMaterial(@Valid @RequestBody MaterialSaveReqVO createReqVO) {
    return success(materialService.createMaterial(createReqVO));
}

// recipe模块权限
@PostMapping("/create")
@Operation(summary = "创建配方")
@PreAuthorize("@ss.hasPermission('recipe:recipe:create')")
public CommonResult<Long> createRecipe(@Valid @RequestBody RecipeSaveReqVO createReqVO) {
    return success(recipeService.createRecipe(createReqVO));
}

// recipe模块 - 启用配方权限
@PostMapping("/enable")
@Operation(summary = "启用配方")
@PreAuthorize("@ss.hasPermission('recipe:recipe:enable')")
public CommonResult<Boolean> enableRecipe(@RequestParam("id") Long id) {
    recipeService.enableRecipe(id);
    return success(true);
}

// recipe模块 - 失效配方权限
@PostMapping("/disable")
@Operation(summary = "失效配方")
@PreAuthorize("@ss.hasPermission('recipe:recipe:disable')")
public CommonResult<Boolean> disableRecipe(@RequestParam("id") Long id) {
    recipeService.disableRecipe(id);
    return success(true);
}
```

### 6.9 测试要点

| 测试项 | 测试内容 | 预期结果 |
|--------|---------|---------|
| 基础CRUD | 创建/更新/删除/查询 | 操作成功，数据正确 |
| 多租户隔离 | 不同租户数据隔离 | 租户A看不到租户B的数据 |
| 权限控制 | 无权限用户访问 | 返回403错误 |
| 配方版本 | 创建新版本、查看历史 | 版本记录完整，可回溯 |
| 工艺参数 | 参数范围校验 | 超出范围提示错误 |
| 外键关联 | 删除被引用的原料 | 提示存在关联，禁止删除 |
| 聚合API | 按产品ID批量查询配方+原料+BOM快照 | 一次调用返回完整数据，性能达标 |
| Feign调用 | 跨模块调用 | 调用成功，数据一致 |
| 启用/失效 | 配方启用自动生成BOM快照 | 启用后BOM快照自动生成，产品current_recipe_id更新 |
| 成本重算 | 原料价格变更通知+批量重算 | cost_stale正确标记，重算后成本更新 |
| 多级BOM | 半成品配方嵌套展开 | BOM快照正确展开为平级原料列表 |
| 工艺模板 | 从模板复制工艺路线 | 复制后步骤和参数完整，产品ID正确关联 |
| 实验配方 | 创建无产品关联的实验配方 | product_id为NULL，配方类型为实验配方 |
| 精度校验 | quantity/cost字段DECIMAL（14,6）精度 | 数据库存储和API传输精度一致 |

### 6.10 后续迭代计划

| 阶段 | 内容 | 涉及模块 | 预计工期 |
|------|------|---------|---------|
| **第一阶段** | masterdata模块：原料管理、产品管理基础CRUD | ims-module-masterdata | 2周 |
| **第二阶段** | recipe模块：配方管理（含版本、BOM快照、成本管理） | ims-module-recipe | 3周 |
| **第三阶段** | recipe模块：工艺管理（含参数、模板功能） | ims-module-recipe | 2周 |
| **第四阶段** | recipe模块：配方组管理（多级树结构、产品关联） | ims-module-recipe | 1周 |
| **第五阶段** | 跨模块集成：Feign调用、聚合API、RPC通知、下游服务对接 | 两个模块 | 2周 |
| **第六阶段** | 报表配置：积木报表SQL数据集、拖拽设计（含BOM展开报表、成本分析报表） | ims-module-report | 1周 |
| **后续扩展** | ims-module-product（产品SKU、价格、上下架） | ims-module-product | 待定 |
| **后续扩展** | ims-module-order、wms、pms、production 等 | 各业务模块 | 待定 |

---

> **附录：参考资源**
>
> - yudao-cloud 官方文档：https://doc.iocoder.cn/
> - yudao-cloud GitHub：https://github.com/YunaiV/ruoyi-vue-pro
