# 2026-05-26 会话记忆

## 本周聚焦

- 只关注 **tiny-erp**，不处理其他项目
- 只关注 **md（主数据）** 和 **im（库存）** 两个模块
- **不再关注** om（销售）、mc（采购）、fin（财务）、home、system 等其他模块

## 模块路径确认

包路径: `com.witsoft.erp`
项目路径: `/home/yangxuan/Projects/IdeaProjects/tiny-erp`

### md 模块（主数据）
`module/md/` — 已完整实现
- `category` — 物料分类
- `customer` — 客户
- `factory` — 工厂
- `material` — 物料
- `supplier` — 供应商
- `warehouse` — 仓库

### im 模块（库存）
`module/im/` — 已完整实现
- `inbound` — 入库单（含采购入库、生产入库）
- `outbound` — 出库单
- `stock` — 库存查询、库存流水、呆滞分析
- `stocktake` — 库存盘点（含快速盘点）

## 原记忆清理

旧记忆（memory/2026-05-23.md）中关于 `bd` 模块的命名实际上是 `md`（代码实际使用的是 `md`，不是 `bd`）。
ome/mc/fin/home/system 模块依然存在但不处理。

## 注释规范（已写入 MEMORY.md）

1. 所有公共接口和类必须包含 Javadoc
2. 注释语言：中文描述业务背景，技术术语保留英文（如 NPE）
3. 禁止生成 "Gets the value of X" 这类无意义的 getter/setter 注释
4. 复杂算法必须在代码块上方解释核心逻辑
5. 禁止使用 HTML 标签

## 开发注意事项（已写入 MEMORY.md）

1. **复用优先**：逻辑雷同时提取公共方法复用
2. **业务表仅存物料 id**：物料相关字段仅存 `material_id`，其他数据通过关联查询填充
3. **填充在 Service 层**：关联数据填充逻辑统一在 Service 层处理
4. **首个版本直接改 DDL**：脚本直接修改建表语句，不需要写 ALTER TABLE
5. **字典优先用 DictService**：字典取值优先使用 DictService 公共方法

## 库存业务表字段清理

**背景：** 业务表只存物料 ID，物料编码/名称/规格/单位通过关联查询填充。

**DDL 改动（50-inventory.sql）：**
- `im_stock` — 移除 material_code, material_name, spec, unit
- `im_stock_transaction` — 移除 material_code, material_name
- `im_inbound_item` — 移除 material_code, material_name, spec, unit
- `im_outbound_item` — 移除 material_code, material_name, spec, unit

**实体类：** Stock, StockTransaction, InboundItem, OutboundItem — 移除对应字段

**请求 DTO：** InboundItemSaveReqDTO, OutboundItemSaveReqDTO, StocktakeItemSaveReqDTO, QuickStocktakeItemDTO — 只保留 materialId

**接口改动：**
- StockTransactionService.recordTransaction() — 移除 materialCode/materialName 参数
- MaterialService — 新增 searchMaterialIdsByKeyword() 方法

**Service 改动：**
- StockTransactionServiceImpl — keyword 搜索改为两步（先查物料表匹配 ID）；DTO 填充从 materialInfoMap 读取
- StockServiceImpl — 同上
- InboundServiceImpl — saveItems 不 set 冗余字段；recordTransaction 去参；history 从 material 填充
- OutboundServiceImpl — 同上
- MaterialServiceImpl — 实现 searchMaterialIdsByKeyword()

**编译验证：** `mvn compile -q` ✅

## Git 提交约定

- **commit 跳过 application.yml** — `git add` 时用 `git add . -- ':!application.yml'` 或手动排除
- **commit 后不要 push** — 只做本地提交，不推送远程
# 2026-05-26 会话记忆

## 物料信息填充公共服务 (MaterialFillService)

### 核心设计决策

1. **实体设计原则**
   - `InboundItem` 和 `OutboundItem` 只存储 `materialId`
   - 不存储物料编码、名称、规格、单位等字段
   - 防止物料信息变更后历史数据不一致

2. **批量查询模式**
   ```java
   List<Long> materialIds = items.stream().map(Item::getMaterialId).collect(Collectors.toList());
   Map<Long, MaterialInfoDTO> materialInfoMap = materialFillService.getMaterialInfoMap(materialIds);
   ```
   - 避免 N+1 查询问题
   - 一次查询所有物料信息，性能更优

3. **Service 层填充**
   - 所有物料信息填充逻辑在 Service 层处理
   - Controller 只负责返回 DTO
   - 不放在 Mapper 或 Controller 层

### 修改的 Service 类（共 6 个）

| Service | 方法 | 用途 |
|---------|------|------|
| InboundServiceImpl | executeInbound(), cancel() | 入库/冲销时记录流水 |
| OutboundServiceImpl | executeOutbound(), cancel() | 出库/冲销时记录流水 |
| FinAccountsAutoServiceImpl | createApFromInbound(), cancelApFromInbound() | 采购应付联动 |
| FinAccountsAutoServiceImpl | createArFromOutbound(), cancelArFromOutbound() | 销售应收联动 |
| HomeServiceImpl | getLatestNews() | 首页动态显示 |
| DeliveryServiceImpl | confirmOutbound() | 发货确认 |

### 物料类型字典

- Dict Type: `material_type`
- 值：`P`-原材料，`SA`-半成品，`FG`-成品
- SQL 脚本：`scripts/postgresql/22-material-type-dict.sql`

### Git 状态

- 所有修改已 commit（13+ commits）
- **禁止 push** - 用户明确要求
- 分支：master

### 经验教训

1. 修改实体类后，所有引用该实类的地方都要检查
2. 编译错误可能分散在多个模块（im, fin, home, om）
3. 使用 `mvn clean compile` 确保完整编译，避免 IDE 缓存误导
# 2026-05-26 会话记忆

## 本周聚焦

- 只关注 **tiny-erp**，不处理其他项目
- 只关注 **md（主数据）** 和 **im（库存）** 两个模块
- **不再关注** om（销售）、mc（采购）、fin（财务）、home、system 等其他模块

## 模块路径确认

包路径: `com.witsoft.erp`
项目路径: `/home/yangxuan/Projects/IdeaProjects/tiny-erp`

### md 模块（主数据）
`module/md/` — 已完整实现
- `category` — 物料分类
- `customer` — 客户
- `factory` — 工厂
- `material` — 物料
- `supplier` — 供应商
- `warehouse` — 仓库

### im 模块（库存）
`module/im/` — 已完整实现
- `inbound` — 入库单（含采购入库、生产入库）
- `outbound` — 出库单
- `stock` — 库存查询、库存流水、呆滞分析
- `stocktake` — 库存盘点（含快速盘点）

## 原记忆清理

旧记忆（memory/2026-05-23.md）中关于 `bd` 模块的命名实际上是 `md`（代码实际使用的是 `md`，不是 `bd`）。
ome/mc/fin/home/system 模块依然存在但不处理。

## 注释规范（已写入 MEMORY.md）

1. 所有公共接口和类必须包含 Javadoc
2. 注释语言：中文描述业务背景，技术术语保留英文（如 NPE）
3. 禁止生成 "Gets the value of X" 这类无意义的 getter/setter 注释
4. 复杂算法必须在代码块上方解释核心逻辑
5. 禁止使用 HTML 标签

## 开发注意事项（已写入 MEMORY.md）

1. **复用优先**：逻辑雷同时提取公共方法复用
2. **业务表仅存物料 id**：物料相关字段仅存 `material_id`，其他数据通过关联查询填充
3. **填充在 Service 层**：关联数据填充逻辑统一在 Service 层处理
4. **首个版本直接改 DDL**：脚本直接修改建表语句，不需要写 ALTER TABLE
5. **字典优先用 DictService**：字典取值优先使用 DictService 公共方法

## Git 提交约定

- **commit 跳过 application.yml** — `git add . -- ':!application.yml'` 排除，防止泄露配置
- **commit 后不要 push** — 只做本地 commit，不推送远程仓库

## 库存业务表字段清理

**背景：** 业务表只存物料 ID，物料编码/名称/规格/单位通过关联查询填充。

**DDL 改动（50-inventory.sql）：**
- `im_stock` — 移除 material_code, material_name, spec, unit
- `im_stock_transaction` — 移除 material_code, material_name
- `im_inbound_item` — 移除 material_code, material_name, spec, unit
- `im_outbound_item` — 移除 material_code, material_name, spec, unit

**实体类：** Stock, StockTransaction, InboundItem, OutboundItem — 移除对应字段

**请求 DTO：** InboundItemSaveReqDTO, OutboundItemSaveReqDTO, StocktakeItemSaveReqDTO, QuickStocktakeItemDTO — 只保留 materialId

**接口改动：**
- StockTransactionService.recordTransaction() — 移除 materialCode/materialName 参数
- MaterialService — 新增 searchMaterialIdsByKeyword() 方法

**Service 改动：**
- StockTransactionServiceImpl — keyword 搜索改为两步（先查物料表匹配 ID）；DTO 填充从 materialInfoMap 读取
- StockServiceImpl — 同上
- InboundServiceImpl — saveItems 不 set 冗余字段；recordTransaction 去参；history 从 material 填充
- OutboundServiceImpl — 同上
- MaterialServiceImpl — 实现 searchMaterialIdsByKeyword()

**编译验证：** `mvn compile -q` ✅

## 物料信息填充公共服务 (MaterialFillService)

### 核心设计决策

1. **实体设计原则**
   - `InboundItem` 和 `OutboundItem` 只存储 `materialId`
   - 不存储物料编码、名称、规格、单位等字段
   - 防止物料信息变更后历史数据不一致

2. **批量查询模式**
   ```java
   List<Long> materialIds = items.stream().map(Item::getMaterialId).collect(Collectors.toList());
   Map<Long, MaterialInfoDTO> materialInfoMap = materialFillService.getMaterialInfoMap(materialIds);
   ```
   - 避免 N+1 查询问题
   - 一次查询所有物料信息，性能更优

3. **Service 层填充**
   - 所有物料信息填充逻辑在 Service 层处理
   - Controller 只负责返回 DTO
   - 不放在 Mapper 或 Controller 层

### 修改的 Service 类（共 6 个）

| Service | 方法 | 用途 |
|---------|------|------|
| InboundServiceImpl | executeInbound(), cancel() | 入库/冲销时记录流水 |
| OutboundServiceImpl | executeOutbound(), cancel() | 出库/冲销时记录流水 |
| FinAccountsAutoServiceImpl | createApFromInbound(), cancelApFromInbound() | 采购应付联动 |
| FinAccountsAutoServiceImpl | createArFromOutbound(), cancelArFromOutbound() | 销售应收联动 |
| HomeServiceImpl | getLatestNews() | 首页动态显示 |
| DeliveryServiceImpl | confirmOutbound() | 发货确认 |

### 物料类型字典

- Dict Type: `material_type`
- 值：`P`-原材料，`SA`-半成品，`FG`-成品
- SQL 脚本：`scripts/postgresql/22-material-type-dict.sql`

### Git 状态

- 所有修改已 commit（13+ commits）
- **禁止 push** - 用户明确要求
- 分支：master

## 物料选择器（Material Selector）

新增物料选择器功能，用于入库/出库操作时弹窗选物料。

### 接口

| 接口 | 位置 | 数据源 | 仓库参数 |
|------|------|--------|---------|
| `POST /im/inbound/material-selector` | InboundController | md_material | 不需要 |
| `POST /im/outbound/material-selector` | OutboundController | im_stock JOIN md_material | 需要 warehouseId |

### 请求参数（MaterialSelectorReqDTO → md.material.dto）

- `categoryId` — 0=常用物料，>0=按分类筛选
- `keyword` — 物料编码/名称/规格关键字
- `warehouseId` — 出库场景需要
- 继承 `PageQuery`（pageNum, pageSize）

### 常用物料 TOP10 逻辑

- **口径：** 近 30 天出入库明细条目数最多的 10 个物料
- **入库：** `im_inbound_item` GROUP BY material_id ORDER BY COUNT(*) DESC LIMIT 10
- **出库：** `im_outbound_item` 同上
- **SQL 放 Mapper XML**（方便后续扩展）

### 分类筛选逻辑

- **入库：** MyBatis Plus LambdaQueryWrapper 查 `md_material` 表，按 `category_id` + `keyword` 分页
- **出库：** XML 联表 `im_stock JOIN md_material`，按 `category_id` + `keyword` + `warehouseId` 分页，只查库存 > 0 的数据

### 响应

- 复用 `StockPageDTO`（im.stock.dto）
- 物料编码/名称/规格/单位/分类等从 `MaterialFillService` 填充

### 新增文件（7个）

| 文件 | 路径 |
|------|------|
| MaterialSelectorReqDTO | md.material.dto |
| MaterialSelectorService | md.material.service |
| MaterialSelectorServiceImpl | md.material.service.impl |
| InboundItemMapper.xml | resources/mapper/im/ |
| OutboundItemMapper.xml | resources/mapper/im/ |
| StockMapper.xml | resources/mapper/im/ |
| | |

### 修改文件（5个）

| 文件 | 改动 |
|------|------|
| InboundItemMapper.java | + selectRecentMaterialIds() |
| OutboundItemMapper.java | + selectRecentMaterialIds() |
| StockMapper.java | + selectStockPage() / countStockPage() |
| InboundController.java | + /material-selector 端点 |
| OutboundController.java | + /material-selector 端点 |

## 历史单据创建人名称

### 改动

- **InboundHistoryItemDTO** — 新增 `creatorName` 字段
- **OutboundHistoryItemDTO** — 新增 `creatorName` 字段
- **InboundServiceImpl.listHistory()** — 收集 creators → 批量查 sys_user(username→nickname) → 填充
- **OutboundServiceImpl.listHistory()** — 同上

### 逻辑

1. 从单据列表收集所有 `creator`（用户名）
2. `userService.lambdaQuery().in(User::getUsername, creators).list()`
3. 转 `Map<String, String>`（username → nickname）
4. 逐行 `dto.setCreatorName()`
5. 一次 SQL，不 N+1
# 2026-05-26 会话记忆

## 本周聚焦

- 只关注 **tiny-erp**，不处理其他项目
- 只关注 **md（主数据）** 和 **im（库存）** 两个模块
- **不再关注** om（销售）、mc（采购）、fin（财务）、home、system 等其他模块

## 模块路径确认

包路径: `com.witsoft.erp`
项目路径: `/home/yangxuan/Projects/IdeaProjects/tiny-erp`

### md 模块（主数据）
`module/md/` — 已完整实现
- `category` — 物料分类
- `customer` — 客户
- `factory` — 工厂
- `material` — 物料
- `supplier` — 供应商
- `warehouse` — 仓库

### im 模块（库存）
`module/im/` — 已完整实现
- `inbound` — 入库单（含采购入库、生产入库）
- `outbound` — 出库单
- `stock` — 库存查询、库存流水、呆滞分析
- `stocktake` — 库存盘点（含快速盘点）

## 原记忆清理

旧记忆（memory/2026-05-23.md）中关于 `bd` 模块的命名实际上是 `md`（代码实际使用的是 `md`，不是 `bd`）。
ome/mc/fin/home/system 模块依然存在但不处理。

## 注释规范（已写入 MEMORY.md）

1. 所有公共接口和类必须包含 Javadoc
2. 注释语言：中文描述业务背景，技术术语保留英文（如 NPE）
3. 禁止生成 "Gets the value of X" 这类无意义的 getter/setter 注释
4. 复杂算法必须在代码块上方解释核心逻辑
5. 禁止使用 HTML 标签

## 开发注意事项（已写入 MEMORY.md）

1. **复用优先**：逻辑雷同时提取公共方法复用
2. **业务表仅存物料 id**：物料相关字段仅存 `material_id`，其他数据通过关联查询填充
3. **填充在 Service 层**：关联数据填充逻辑统一在 Service 层处理
4. **首个版本直接改 DDL**：脚本直接修改建表语句，不需要写 ALTER TABLE
5. **字典优先用 DictService**：字典取值优先使用 DictService 公共方法

## Git 提交约定

- **commit 跳过 application.yml** — `git add . -- ':!application.yml'` 排除，防止泄露配置
- **commit 后不要 push** — 只做本地 commit，不推送远程仓库

## 库存业务表字段清理

**背景：** 业务表只存物料 ID，物料编码/名称/规格/单位通过关联查询填充。

**DDL 改动（50-inventory.sql）：**
- `im_stock` — 移除 material_code, material_name, spec, unit
- `im_stock_transaction` — 移除 material_code, material_name
- `im_inbound_item` — 移除 material_code, material_name, spec, unit
- `im_outbound_item` — 移除 material_code, material_name, spec, unit

**实体类：** Stock, StockTransaction, InboundItem, OutboundItem — 移除对应字段

**请求 DTO：** InboundItemSaveReqDTO, OutboundItemSaveReqDTO, StocktakeItemSaveReqDTO, QuickStocktakeItemDTO — 只保留 materialId

**接口改动：**
- StockTransactionService.recordTransaction() — 移除 materialCode/materialName 参数
- MaterialService — 新增 searchMaterialIdsByKeyword() 方法

**Service 改动：**
- StockTransactionServiceImpl — keyword 搜索改为两步（先查物料表匹配 ID）；DTO 填充从 materialInfoMap 读取
- StockServiceImpl — 同上
- InboundServiceImpl — saveItems 不 set 冗余字段；recordTransaction 去参；history 从 material 填充
- OutboundServiceImpl — 同上
- MaterialServiceImpl — 实现 searchMaterialIdsByKeyword()

**编译验证：** `mvn compile -q` ✅

## 物料信息填充公共服务 (MaterialFillService)

### 核心设计决策

1. **实体设计原则**
   - `InboundItem` 和 `OutboundItem` 只存储 `materialId`
   - 不存储物料编码、名称、规格、单位等字段
   - 防止物料信息变更后历史数据不一致

2. **批量查询模式**
   ```java
   List<Long> materialIds = items.stream().map(Item::getMaterialId).collect(Collectors.toList());
   Map<Long, MaterialInfoDTO> materialInfoMap = materialFillService.getMaterialInfoMap(materialIds);
   ```
   - 避免 N+1 查询问题
   - 一次查询所有物料信息，性能更优

3. **Service 层填充**
   - 所有物料信息填充逻辑在 Service 层处理
   - Controller 只负责返回 DTO
   - 不放在 Mapper 或 Controller 层

### 修改的 Service 类（共 6 个）

| Service | 方法 | 用途 |
|---------|------|------|
| InboundServiceImpl | executeInbound(), cancel() | 入库/冲销时记录流水 |
| OutboundServiceImpl | executeOutbound(), cancel() | 出库/冲销时记录流水 |
| FinAccountsAutoServiceImpl | createApFromInbound(), cancelApFromInbound() | 采购应付联动 |
| FinAccountsAutoServiceImpl | createArFromOutbound(), cancelArFromOutbound() | 销售应收联动 |
| HomeServiceImpl | getLatestNews() | 首页动态显示 |
| DeliveryServiceImpl | confirmOutbound() | 发货确认 |

### 物料类型字典

- Dict Type: `material_type`
- 值：`P`-原材料，`SA`-半成品，`FG`-成品
- SQL 脚本：`scripts/postgresql/22-material-type-dict.sql`

### Git 状态

- 所有修改已 commit（13+ commits）
- **禁止 push** - 用户明确要求
- 分支：master

## 物料选择器（Material Selector）

新增物料选择器功能，用于入库/出库操作时弹窗选物料。

### 接口

| 接口 | 位置 | 数据源 | 仓库参数 |
|------|------|--------|---------|
| `POST /im/inbound/material-selector` | InboundController | md_material | 不需要 |
| `POST /im/outbound/material-selector` | OutboundController | im_stock JOIN md_material | 需要 warehouseId |

### 请求参数（MaterialSelectorReqDTO → md.material.dto）

- `categoryId` — 0=常用物料，>0=按分类筛选
- `keyword` — 物料编码/名称/规格关键字
- `warehouseId` — 出库场景需要
- 继承 `PageQuery`（pageNum, pageSize）

### 常用物料 TOP10 逻辑

- **口径：** 近 30 天出入库明细条目数最多的 10 个物料
- **入库：** `im_inbound_item` GROUP BY material_id ORDER BY COUNT(*) DESC LIMIT 10
- **出库：** `im_outbound_item` 同上
- **SQL 放 Mapper XML**（方便后续扩展）

### 分类筛选逻辑

- **入库：** MyBatis Plus LambdaQueryWrapper 查 `md_material` 表，按 `category_id` + `keyword` 分页
- **出库：** XML 联表 `im_stock JOIN md_material`，按 `category_id` + `keyword` + `warehouseId` 分页，只查库存 > 0 的数据

### 响应

- 复用 `StockPageDTO`（im.stock.dto）
- 物料编码/名称/规格/单位/分类等从 `MaterialFillService` 填充

### 新增文件（7个）

| 文件 | 路径 |
|------|------|
| MaterialSelectorReqDTO | md.material.dto |
| MaterialSelectorService | md.material.service |
| MaterialSelectorServiceImpl | md.material.service.impl |
| InboundItemMapper.xml | resources/mapper/im/ |
| OutboundItemMapper.xml | resources/mapper/im/ |
| StockMapper.xml | resources/mapper/im/ |
| | |

### 修改文件（5个）

| 文件 | 改动 |
|------|------|
| InboundItemMapper.java | + selectRecentMaterialIds() |
| OutboundItemMapper.java | + selectRecentMaterialIds() |
| StockMapper.java | + selectStockPage() / countStockPage() |
| InboundController.java | + /material-selector 端点 |
| OutboundController.java | + /material-selector 端点 |

## 历史单据创建人名称

### 改动

- **InboundHistoryItemDTO** — 新增 `creatorName` 字段
- **OutboundHistoryItemDTO** — 新增 `creatorName` 字段
- **InboundServiceImpl.listHistory()** — 收集 creators → 批量查 sys_user(username→nickname) → 填充
- **OutboundServiceImpl.listHistory()** — 同上

### 逻辑

1. 从单据列表收集所有 `creator`（用户名）
2. `userService.lambdaQuery().in(User::getUsername, creators).list()`
3. 转 `Map<String, String>`（username → nickname）
4. 逐行 `dto.setCreatorName()`
5. 一次 SQL，不 N+1

## 实体名称字段清理（SupplierName/CustomerName/WarehouseName）

### 背景
单据实体（Inbound、Outbound、PurchaseOrder、SalesOrder、Delivery）及其 Item 实体中，之前还存储了供应商名称、客户名称、仓库名称等冗余字段。本次清理全部移除，只存 ID，显示名称通过 master-data 查询填充。

### 涉及实体
- **Inbound** — 移除 `supplierName`, `warehouseName`
- **Outbound** — 移除 `customerName`, `warehouseName`
- **PurchaseOrder** — 移除 `supplierName`
- **PurchaseOrderItem** — 移除了 `materialCode`, `materialName`, `spec`, `unit`（之前已完成）
- **SalesOrder** — 移除 `customerName`
- **SalesOrderItem** — 移除了物料冗余字段（之前已完成）
- **Delivery** — 移除 `customerName`
- **DeliveryItem** — 移除了物料冗余字段（之前已完成）

### DTO 改动
- **InboundSaveReqDTO / PurchaseInboundSaveReqDTO / ProductionInboundSaveReqDTO** — 只保留 supplierId/warehouseId，移除 supplierName/warehouseName
- **OutboundSaveReqDTO / SalesOutboundSaveReqDTO / MaterialOutboundSaveReqDTO** — 只保留 customerId/warehouseId，移除 customerName/warehouseName
- **InboundHistoryItemDTO / OutboundHistoryItemDTO** — **保留** display name 字段（响应 DTO 需要显示），通过 master-data lookup 填充

### 不做改动
- **Ar.java / Ap.java** — 保留 customerName / supplierName 作为财务历史快照
- **InboundHistoryItemDTO / OutboundHistoryItemDTO** — 保留 name 字段供前端展示

### 主要 Service 改动

| 文件 | 改动内容 |
|------|---------|
| **InboundServiceImpl.java** | 注入 WarehouseMapper；移除 entity setSupplierName/setWarehouseName；history DTO 名通过 supplierMapper + warehouseMapper 批量 lookup；autoInboundFromOrder 用 supplierId 取代 supplierName；updatePurchaseOrderAfterInbound 用 supplierId 查询 |
| **OutboundServiceImpl.java** | 注入 WarehouseMapper；移除 entity setCustomerName；history DTO 名通过 customerMapper + warehouseMapper 批量 lookup |
| **FinAccountsAutoServiceImpl.java** | `createApFromInbound`/`cancelApFromInbound` 使用 supplierMapper 查询 supplierName 填充 Ap；`createArFromOutbound`/`cancelArFromOutbound` 使用 customerMapper 查询 customerName |
| **PurchaseOrderServiceImpl.java** | `setSupplierName(reqDTO.getSupplierName())` → `setSupplierId(reqDTO.getSupplierId())` |
| **SalesOrderServiceImpl.java** | `setCustomerName(reqDTO.getCustomerName())` → `setCustomerId(reqDTO.getCustomerId())` |
| **DeliveryServiceImpl.java** | `setCustomerName(order.getCustomerName())` → `setCustomerId(order.getCustomerId())`；item 用 `materialFillService` 填充物料信息 |

### 控制器查询条件
- PurchaseOrderController / SalesOrderController / DeliveryController — 查询条件从名称改为 ID

### 编译验证
- `mvn clean compile` ✅ — 零错误

## 集成测试（InboundOutboundIntegrationTest）

### 文件路径
`src/test/java/com/witsoft/erp/im/InboundOutboundIntegrationTest.java`

### 设计
- 使用 `TestRestTemplate`，`webEnvironment = RANDOM_PORT`
- `@TestMethodOrder(MethodOrderer.OrderAnnotation.class)` 按顺序执行
- 复用已有的 `Result<T>` 泛型返回包装类

### 测试覆盖（11 个用例）

| 序号 | 端点 | 说明 |
|------|------|------|
| 1 | POST /erp/system/user/login | 登录获取 Bearer token |
| 2 | POST /erp/im/inbound/production | 生产入库（2 行物料） |
| 3 | POST /erp/im/inbound/purchase | 采购入库（含单价） |
| 4 | POST /erp/im/inbound/history-list | 查询入库历史 |
| 5 | POST /erp/im/outbound/material | 领料出库 |
| 6 | POST /erp/im/outbound/sales | 销售出库（含单价） |
| 7 | POST /erp/im/outbound/history-list | 查询出库历史 |
| 8 | POST /erp/im/inbound/auto-from-order | 自动入库（基于采购订单） |
| 9 | POST /erp/im/stock/transaction/page | 查询库存流水 |
| 10 | DELETE /erp/im/inbound/{id} | 撤销入库单 |
| 11 | DELETE /erp/im/outbound/{id} | 撤销出库单 |

### 注意事项
- `application.yml` 中 `server.servlet.context-path: /erp`，所有路径须带 `/erp` 前缀
- 测试需要准备基础数据：物料 ID=3/4、供应商 ID=1、客户 ID=1、仓库 ID=1、采购订单 ID=1
- 当前 `authToken` 未通过 TestRestTemplate 的 basic auth，走的是 token-auth 模式
- 登录密码写的是 `***`，需要按实际数据库密码修改 `TEST_PASSWORD` 常量
## Controller 集成测试（TestRestTemplate）

### 现有测试文件

| 测试类 | 路径 | 用例数 | 覆盖 Controller |
|--------|------|--------|----------------|
| InboundOutboundIntegrationTest | `im/InboundOutboundIntegrationTest.java` | 11 | Inbound + Outbound（合并） |
| StockControllerTest | `im/stock/StockControllerTest.java` | 17 | StockController |
| StocktakeControllerTest | `im/stocktake/StocktakeControllerTest.java` | 17 | StocktakeController |
| InboundControllerTest | `im/inbound/InboundControllerTest.java` | 21 | InboundController |
| OutboundControllerTest | `im/outbound/OutboundControllerTest.java` | 21 | OutboundController |

### 测试设计模式（所有测试统一）

1. **TestRestTemplate + RANDOM_PORT** — 自动分配端口，避免冲突
2. **@TestMethodOrder(OrderAnnotation.class)** — 按 Order 顺序执行
3. **CONTEXT_PATH = "/erp"** — 手动拼接，因为自动 context-path 处理不可靠
4. **assumeToken() 守卫** — 登录失败后通过 `Assumptions.assumeTrue(authToken != null)` 跳过后续测试，避免 403
5. **密码可配置** — `System.getProperty("test.password", "admin123")`，通过 `-Dtest.password=xxx` 覆盖
6. **url() 辅助方法** — `"http://localhost:" + port + CONTEXT_PATH + path`

### InboundControllerTest（21 用例）

| 序号 | 端点 | 验证 |
|------|------|------|
| 1 | POST /system/user/login | 登录获取 Bearer token |
| 2-3 | POST /im/inbound/material-selector | 物料选择器（分类筛选） |
| 4-6 | POST /im/inbound | 新增 + 明细为空/类型为空校验边界 |
| 7-8 | GET /im/inbound/{id} | 详情 / 不存在返回 null |
| 9 | GET /im/inbound/{id}/items | 明细列表 |
| 10 | PUT /im/inbound | 修改 |
| 11-12 | PUT /im/inbound/{id}/cancel | 撤销 / 重复撤销 400 |
| 13-14 | DELETE /im/inbound/{id} | 删除 |
| 15-16 | POST /im/inbound/page | 分页（全查/仓库筛选） |
| 17 | POST /im/inbound/history-list | 历史明细列表 |
| 18 | POST /im/inbound/production | 生产入库 |
| 19-20 | POST /im/inbound/purchase | 采购入库 / 供应商为空 400 |
| 21 | POST /im/inbound/auto-from-order | 自动入库-订单不存在 400 |

### OutboundControllerTest（21 用例）

| 序号 | 端点 | 验证 |
|------|------|------|
| 1 | POST /system/user/login | 登录 |
| 2-3 | POST /im/outbound/material-selector | 物料选择器 |
| 4-6 | POST /im/outbound | 新增 + 明细为空/客户为空校验 |
| 7-8 | GET /im/outbound/{id} | 详情 / 不存在 |
| 9 | GET /im/outbound/{id}/items | 明细 |
| 10 | PUT /im/outbound | 修改 |
| 11-12 | PUT /im/outbound/{id}/cancel | 撤销 / 重复撤销 400 |
| 13-14 | DELETE /im/outbound/{id} | 删除 |
| 15-16 | POST /im/outbound/page | 分页（全查/客户筛选） |
| 17 | POST /im/outbound/history-list | 历史明细 |
| 18-19 | POST /im/outbound/material | 领料出库 / 明细为空 400 |
| 20-21 | POST /im/outbound/sales | 销售出库 / 客户为空 400 |

### StockControllerTest（17 用例）

覆盖 StockController 全部 4 个端点：

| 序号 | 端点 | 验证 |
|------|------|------|
| 1 | POST /system/user/login | 登录 |
| 2-6 | POST /im/stock/page | 空参数、仓库筛选、关键字、无匹配关键字、自定义分页 |
| 7-11 | POST /im/stock/list | 空、仓库、关键字、预警筛选、无匹配关键字 |
| 12-13 | GET /im/stock/{materialId} | 存在/不存在 |
| 14-16 | PUT /im/stock/safe-qty | 正常设置、物料不存在 400、参数校验 |
| 17 | POST /im/stock/page | DTO 字段完整性验证 |

### StocktakeControllerTest（17 用例）

覆盖 StocktakeController 全部 10 个端点：

| 序号 | 端点 | 验证 |
|------|------|------|
| 1 | POST /system/user/login | 登录 |
| 2-3 | POST /im/stocktake | 新增（草稿）/ 明细为空 400 |
| 4-5 | GET /im/stocktake/{id} | 详情 / 不存在 |
| 6 | GET /im/stocktake/{id}/items | 明细（含物料信息） |
| 7 | PUT /im/stocktake | 修改 |
| 8-9 | PUT /im/stocktake/{id}/audit | 审核 / 重复审核 400 |
| 10-11 | PUT /im/stocktake/{id}/cancel | 作废 |
| 12 | DELETE /im/stocktake/{id} | 删除 |
| 13 | POST /im/stocktake/quick-save | 快速盘点（创建即审核） |
| 14-15 | POST /im/stocktake/page | 分页（筛选/空 body） |
| 16-17 | POST /im/stocktake/history-list | 历史明细（空条件） |

### 已知问题
- **InboundController.getPage() 有两个 @RequestBody** — 测试用 Map 代理
- **OutboundController.getPage() 同理**
- **StocktakeController.getPage() 同理**
- 密码默认 `admin123`，通过 `-Dtest.password=xxx` 配置
- IntelliJ 解析 `junit-platform-launcher:1.8.2` 失败，已在 pom.xml 中添加依赖