2 天以前 f7273277023f3497ac071bf98f556a569cb4dd25
feat(mes): 重构袋码生成与物料分类功能

- 移除托盘容量超限错误码定义
- 添加品种主计量单位类型配置相关错误码
- 删除物料分类相关字段和查询逻辑
- 新增计量单位类型字段支持48千克/100千克/吨
- 实现按品种主单位类型生成袋码的业务逻辑
- 添加原始袋码生成功能支持直接指定数量
- 优化袋码绑定批次和托盘的业务流程
- 更新API接口适配新的数据结构变化
已添加7个文件
已修改40个文件
1393 ■■■■ 文件已修改
docs/mdm_item_remove_category_purchase_price_integration.md 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_unit_measure_type_integration.md 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_bag_code_package_refactor_integration.md 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_bag_code_package_refactor_requirements.md 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_pro_batch_processing_team_integration.md 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/pro_batch_processing_team.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/dto/MdmUnitMeasureRespDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRespVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchPageReqVO.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchRespVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchSaveReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletBatchCreateReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletPreGenerateReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeDO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/batch/MesProBatchDO.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/batch/MesProBatchMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/MesProQrLabelBuilder.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletServiceImpl.java 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/static/trace.html 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_item_remove_category_purchase_price_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
# å“ç§ç®¡ç†åŽ»æŽ‰ã€Œå“ç§åˆ†ç±»ã€ã€Œé‡‡è´­ä»·ã€ - å‰ç«¯è”调方案
## èƒŒæ™¯
基础数据-品种管理不再维护「品种分类」和「采购价」两个字段:
- æ•°æ®åº“列 `mdm_item.category_id`、`mdm_item.purchase_price` ä¿ç•™ï¼ˆåŽ†å²æ•°æ®ä¸åˆ ï¼‰ï¼Œä½†æŽ¥å£å±‚ä¸å†æŽ¥æ”¶å’Œè¿”å›žã€‚
- MDM→MES åŒæ­¥ä¸å†ä¼ å“ç§åˆ†ç±»ï¼ŒåŒæ­¥ç”Ÿæˆçš„ MES ç‰©æ–™ã€Œç‰©æ–™åˆ†ç±»ã€ä¸ºç©ºï¼Œå¯åœ¨ MES ç‰©æ–™äº§å“é¡µé¢æ‰‹å·¥è¡¥å¡«ã€‚
- MES→MDM åå‘同步(MES ç‰©æ–™å‹¾é€‰ã€ŒåŒæ­¥åˆ° MDM」回写分类)保持不变。
## API å˜åŒ–
| æ–¹æ³• | è·¯å¾„ | å˜åŒ– |
|------|------|------|
| POST | /mdm/item/create | è¯·æ±‚体不再接收 `categoryId`、`purchasePrice`(传入将被忽略) |
| PUT | /mdm/item/update | åŒä¸Š |
| GET | /mdm/item/get | å“åº”不再返回 `categoryId`、`categoryName`、`purchasePrice` |
| GET | /mdm/item/page | å“åº”不再返回上述三字段;不再支持 `categoryId` æŸ¥è¯¢å‚æ•° |
| GET | /mdm/item/simple-list | å“åº”不再返回上述三字段 |
| GET | /mdm/item/list-by-ids | å“åº”不再返回上述三字段 |
**响应示例(节选):**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "code": "PZ0001",
        "name": "水稻品种A",
        "specification": "500g/袋",
        "unitMeasureId": 2,
        "unitMeasureName": "袋",
        "salesPrice": 15.00,
        "syncedMes": true
      }
    ],
    "total": 1
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹ï¼ˆæœ¬æ‰¹å·²åŒæ­¥ä¿®æ”¹ï¼Œä¾›å‰ç«¯å›¢é˜Ÿå¤æ ¸ï¼‰
### 1. å“ç§ç®¡ç†é¡µé¢ï¼ˆsrc/views/basicData/mdm/)
| æ–‡ä»¶ | ä¿®æ”¹å†…容 |
|------|----------|
| data.ts | åˆ é™¤è¡¨å•「品种分类」TreeSelect、「采购价」InputNumber;删除列表「品种分类」「采购价」列;删除 `buildCategoryTree` åŠ MES ç‰©æ–™åˆ†ç±»æŽ¥å£ import |
| index.vue | åˆ é™¤åˆ†ç±»åˆ—表缓存、分类名称/颜色工具函数、`#categoryName` æ’槽及分类加载逻辑 |
| modules/form.vue | åˆ é™¤æ‰“开表单时加载分类树的逻辑 |
| components/data.ts | å“ç§é€‰æ‹©å¼¹çª—删除「品种分类」搜索项与列 |
| components/select-dialog.vue | åˆ é™¤åˆ†ç±»ç¼“存、名称/颜色函数、`#categoryId` æ’槽及分类加载逻辑 |
### 2. API ç±»åž‹ï¼ˆsrc/api/mdm/item/index.ts)
- `MdmItemApi.Item` åˆ é™¤ `categoryId`、`categoryName`、`purchasePrice`。
- `MdmItemApi.PageParams` åˆ é™¤ `categoryId`。
### 3. ä¾èµ–采购价自动算价的单据页(行为变化,重点复核)
品种不再有采购价,以下两处选品后「单价」不再自动带入,改为默认 0、由用户手工填写:
| æ–‡ä»¶ | ä¿®æ”¹å†…容 |
|------|----------|
| src/views/erp/purchase/order/modules/item-form.vue | é€‰æ‹©å“ç§åŽ `productPrice` ä¸å†å–采购价,置 0 æ‰‹å·¥å¡«å†™ |
| src/views/erp/purchase/request/modules/item-form.vue | åŒä¸Š |
其余单据(销售、库存等)使用的是 ERP äº§å“ï¼ˆ`erp_product`)自身价格或销售价,不受影响。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ–°å¢ž/编辑品种 | æ— åˆ†ç±»ã€æ— é‡‡è´­ä»·å­—段;保存接口忽略这两个入参 |
| ç¼–辑历史品种 | åŽ†å²å·²å¡«çš„åˆ†ç±»/采购价在更新时不会被清空(仅不再回显),也不会被覆盖 |
| å“ç§åŒæ­¥åˆ° MES | ç”Ÿæˆçš„ MES ç‰©æ–™äº§å“ã€Œç‰©æ–™åˆ†ç±»ã€ä¸ºç©ºï¼›MES ç‰©æ–™äº§å“é¡µé¢ä»å¯æ‰‹å·¥è¡¥å¡«åˆ†ç±» |
| MES ç‰©æ–™å‹¾é€‰åŒæ­¥åˆ° MDM | åˆ†ç±»å›žå†™ `mdm_item.category_id` ä¿æŒåŽŸé€»è¾‘ï¼ˆå“ç§ç®¡ç†é¡µé¢ä¸å±•ç¤ºè¯¥å€¼ï¼‰ |
| å“ç§åˆ†ç±»ç®¡ç†æ¨¡å— | ä¸æ”¹åŠ¨ï¼Œåˆ†ç±»æ•°æ®ä»ä¾› MES ç‰©æ–™åˆ†ç±»ç­‰åœºæ™¯ä½¿ç”¨ |
## æ³¨æ„äº‹é¡¹
- é”€å”®ä»·ã€æˆæœ¬ä»·æœªä¸‹çº¿ï¼Œè¡¨å•与列表保持原样。
- åˆ—表删除两列后列宽自行微调。
- å“ç§é€‰æ‹©å¼¹çª—(MdmItemSelect/MdmItemSelectDialog)被采购、库存等多处业务页复用,删除分类列后请回归验证选品带入(编码/名称/单位/条码)是否正常。
docs/mdm_unit_measure_type_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
# MDM è®¡é‡å•位新增「类型」字段 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åŸºç¡€æ•°æ® > MDM > è®¡é‡å•位(列表、新增/编辑弹窗)
## API
接口路径不变,仅请求/响应体新增 `type` å­—段。
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/unit/create | åˆ›å»ºè®¡é‡å•位(请求体新增 type) |
| PUT | /mdm/unit/update | æ›´æ–°è®¡é‡å•位(请求体新增 type) |
| GET | /mdm/unit/get | èŽ·å¾—è®¡é‡å•ä½ï¼ˆå“åº”æ–°å¢ž type) |
| GET | /mdm/unit/page | èŽ·å¾—è®¡é‡å•ä½åˆ†é¡µï¼ˆå“åº”æ–°å¢ž type) |
| GET | /mdm/unit/list | èŽ·å¾—è®¡é‡å•ä½åˆ—è¡¨ï¼ˆå“åº”æ–°å¢ž type) |
**请求参数(新增):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| type | String | æ˜¯ | ç±»åž‹ï¼Œå•选,可选值固定为:`48千克`、`100千克`、`吨`(三选一,直接存文本;后端 `@NotBlank` æ ¡éªŒï¼‰ |
**响应字段(新增):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| type | String | ç±»åž‹ï¼ŒåŽ†å²æ•°æ®ä¸º null |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "code": "BAG",
    "name": "袋",
    "type": "48千克",
    "status": 0
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹è¯´æ˜Žï¼ˆå·²éšæœ¬æ¬¡åŽç«¯æ”¹åŠ¨ä¸€å¹¶å®Œæˆï¼Œä¾›æ ¸å¯¹ï¼‰
| ä½ç½® | è¦æ±‚ |
|------|------|
| æ–°å¢ž/编辑弹窗表单 | å¢žåŠ ã€Œç±»åž‹ã€ä¸‹æ‹‰é€‰æ‹©ï¼Œé€‰é¡¹å›ºå®šä¸‰é¡¹ï¼š48千克 / 100千克 / å¨ï¼Œå•选、必填 |
| åˆ—表表格 | å¢žåŠ ã€Œç±»åž‹ã€åˆ—å±•ç¤ºï¼Œç©ºå€¼æ˜¾ç¤ºä¸º `-` |
| æŸ¥è¯¢è¡¨å• | æš‚不提供类型筛选(后端分页接口未加该过滤条件) |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å­˜é‡æ•°æ® | è€è®°å½• `type` ä¸ºç©ºï¼Œåˆ—表展示 `-`;编辑老记录时必须补选类型后才能保存(后端强校验) |
| é€‰é¡¹æ¥æº | é€‰é¡¹ä¸ºå‰ç«¯å†™æ­»ä¸‰é¡¹æ–‡æœ¬ï¼Œä¸å»ºæ•°æ®å­—典;提交的 value å³ä¸­æ–‡æ–‡æœ¬æœ¬èº« |
| åŽç«¯æ ¡éªŒ | `@NotBlank` å¿…填校验;取值范围不限制,前端下拉需保证只会提交三个固定值 |
## æ³¨æ„äº‹é¡¹
- `type` ä¸ºæ–‡æœ¬å­—段(VARCHAR(20)),保存时传中文原文,不要传编码或数字。
- è¯¥å­—段必填,为空时接口返回参数校验错误「类型不能为空」。
- è®¡é‡å•位被物料引用后仍可修改类型,无级联限制。
docs/mes_bag_code_package_refactor_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
# ä¸€è¢‹ä¸€ç ç”Ÿäº§é€»è¾‘改造 - å‰ç«¯è”调方案
> é…å¥—需求方案:`docs/mes_bag_code_package_refactor_requirements.md`。本方案覆盖接口变更与页面数据带入,前端主体改动已随本次改造同步完成,供联调回归使用。
## æ¶‰åŠé¡µé¢
- èµ‹ç ç®¡ç†ï¼ˆè¢‹ç ï¼‰ï¼šç”Ÿæˆæ•°æ®åŒ…弹窗、生成原始袋码弹窗、袋码列表、录入批次、绑定托盘、追溯弹窗
- ç”Ÿäº§æ‰¹æ¬¡ï¼šæ–°å¢ž/编辑/补录表单(新增「绑定托盘」多选)、列表、搜索区
- æ‰˜ç›˜ç®¡ç†ï¼šé¢„生成弹窗、列表、绑定批次弹窗
- å…¬å…± H5 æº¯æºé¡µã€æ‰«ç è¿½æº¯å¼¹çª—(回归)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç”Ÿæˆæ•°æ®åŒ…:选品种 â†’ å‰ç«¯æŒ‰å“ç§ä¸»è®¡é‡å•位类型联动(查 `/mdm/item/get` å– `unitMeasureId`,再查 `/mdm/unit/get?id=` å– `type`):
   - `48千克`/`100千克` â†’ è¾“å…¥**托盘数**,预览「每托 N è¢‹ Ã— æ‰˜ç›˜æ•° = è¢‹ç æ•°ã€
   - `吨` â†’ è¾“å…¥**袋码数**(无托盘)
   - ç±»åž‹æœªé…ç½® â†’ æç¤ºå‰å¾€ã€Œè®¡é‡å•位」维护,提交时后端也会兜底报错
2. ç”Ÿæˆç»“果:同一 uuid è¢‹ç ç»„ + è‡ªåŠ¨ç”Ÿæˆçš„ä¸´æ—¶æ‰˜ç›˜ï¼ˆPL-序号),袋码已带品种、已绑托盘。
3. å½•入批次:勾选袋码 â†’ é€‰ç”Ÿäº§æ‰¹æ¬¡ã€‚后端级联:临时托盘升正式码(批次号-P序号)并绑批次、回写袋码冗余托盘码。混品种会被拦截。
4. æ‰¹æ¬¡ä¸å†å½•å…¥/展示品种;品种信息全部由袋码数据包携带。
5. æ–°å¢ž/补录/编辑批次时均可选「绑定托盘」(多选、非必填,数据源 `GET /mes/pro/pallet/unbound-list`,展示临时托盘码+已装袋数):勾选后批次状态直接为**生产中**,后端同事务复用托盘绑定批次逻辑(托盘升正式码、托盘下袋码级联录入),任一校验失败整体回滚;编辑时仅需勾选本次新增的托盘(已绑定托盘不在下拉中、不可解绑)。
## API
| æ–¹æ³• | è·¯å¾„ | å˜æ›´ |
|------|------|------|
| POST | /mes/pro/bag-code/generate | è¯·æ±‚/响应结构变更(见下) |
| POST | /mes/pro/bag-code/raw-generate | **新增**:生成原始袋码,请求 `{ itemId, count, remark? }`;仅按数量生成、不换算计量单位类型、不生成托盘;响应结构同 generate(`type/perPallet` ä¸ºç©ºã€`palletIds` ç©ºæ•°ç»„) |
| PUT | /mes/pro/bag-code/bind-batch | è·¯å¾„不变;新增品种一致校验、托盘级联绑定;已导出印刷(PRINTED)袋码也允许录入批次 |
| PUT | /mes/pro/pallet/bind-batch | è·¯å¾„不变;托盘绑定批次时,托盘下未绑批次的袋码自动级联录入同一批次(品种一致校验、回写批次袋数) |
| DELETE | /mes/pro/bag-code/delete-by-uuid | è·¯å¾„不变;级联删除本组自动生成托盘 |
| POST | /mes/pro/bag-code/replace | ä¸å˜ï¼›æ–°è¢‹ç è‡ªåŠ¨ç»§æ‰¿åŽŸå“ç§ |
| GET | /mes/pro/bag-code/page、list-by-uuid、get-by-code | å“åº”新增品种字段;page æ–°å¢žå¯é€‰æŸ¥è¯¢å‚æ•° `itemId`(品种,等值匹配;导出 Excel åŒå£å¾„) |
| POST | /mes/pro/pallet/batch-create、pre-generate | è¯·æ±‚去掉 capacity |
| GET | /mes/pro/pallet/page | å“åº”去掉 capacity,保留 autoGenerated |
| POST/PUT | /mes/pro/batch/create、update、fill-info | è¯·æ±‚去掉 itemId;三者均新增可选 `palletIds`(绑定时批次进入生产中) |
| GET | /mes/pro/pallet/unbound-list | **新增**:未绑定批次的临时托盘列表(权限 mes:pro-pallet:query),供批次新增/补录绑定托盘下拉 |
| GET | /mes/pro/batch/page、get、simple-list | å“åº”去掉 itemId/itemCode/itemName/itemSpecification |
**生成数据包请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | æ˜¯ | å“ç§ç¼–号 |
| palletCount | Integer | æ¡ä»¶å¿…å¡« | æ‰˜ç›˜æ•°ï¼ˆç±»åž‹=N千克) |
| bagCount | Integer | æ¡ä»¶å¿…å¡« | è¢‹ç æ•°ï¼ˆç±»åž‹=吨) |
| remark | String | å¦ | å¤‡æ³¨ |
**生成数据包响应示例:**
```json
{
  "code": 0,
  "data": {
    "uuid": "生成组UUID",
    "type": "48千克",
    "perPallet": 48,
    "totalCount": 96,
    "palletIds": [101, 102]
  }
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å“ç§ç¼–码/名称(itemCode/itemName) | è¢‹ç åˆ—表、追溯、导出 Excel | ç”Ÿæˆæ—¶ç»‘定在袋码上 |
| autoGenerated | æ‰˜ç›˜åˆ—表「自动生成」Tag | æ•°æ®åŒ…自动创建的托盘标识 |
| å“ç§ï¼ˆæ‰¹æ¬¡é¡µï¼‰ | ä¸å†å±•示 | è¡¨å•/列表/搜索均已移除 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç±»åž‹=N千克 ç”Ÿæˆ | è¢‹ç æ•° = N Ã— æ‰˜ç›˜æ•°ï¼Œè‡ªåŠ¨å»º N ä¸ªæ‰˜ç›˜å¹¶å‡åˆ†ç»‘定 |
| ç±»åž‹=吨 ç”Ÿæˆ | æ— æ‰˜ç›˜ï¼Œè¢‹ç ä»…绑批次 |
| ç”ŸæˆåŽŸå§‹è¢‹ç  | ä»…需品种+数量;袋码带品种、不绑批次/托盘,后续录入批次、绑托盘、导出印刷、删除组与数据包袋码完全一致;不校验品种计量单位类型 |
| å½•入批次混品种 | æŠ¥é”™ã€Œå½•入批次的袋码必须属于同一品种」(与批次已有袋码一并校验) |
| æ‰˜ç›˜ç»‘定批次 | æ‰˜ç›˜ä¸‹æœªç»‘批次的袋码(已生成/已导出印刷)自动随托盘录入同一批次,袋码冗余托盘码同步更新为正式码 |
| æ‰¹æ¬¡æ–°å¢ž/补录/编辑绑定托盘 | palletIds éžç©ºæ—¶æ‰¹æ¬¡çŠ¶æ€ç›´æŽ¥ç½®ã€Œç”Ÿäº§ä¸­ã€ï¼ŒåŒäº‹åŠ¡å¤ç”¨æ‰˜ç›˜ç»‘å®šæ‰¹æ¬¡é€»è¾‘ï¼ˆæ­£å¼ç +袋码级联+品种一致校验),失败整体回滚;新增/补录未勾选则维持草稿;编辑取消勾选不回退状态、已绑定托盘不可解绑 |
| ç»‘托盘容量 | æ— ä¸Šé™ï¼Œè‡ªåŠ¨æ‰˜ç›˜å¯ç»§ç»­æ‰‹åŠ¨ç»‘å…¥ |
| åˆ é™¤æ•°æ®åŒ… | ç»„内托盘已绑批次或有正式码则不可删,否则级联删自动托盘 |
| è¿½æº¯å“ç§ | ä¼˜å…ˆè¢‹ç å“ç§ï¼›åŽ†å²è¢‹ç å›žé€€æ‰¹æ¬¡å“ç§ï¼ˆå…¬å…± H5 åŒæ­¥æ”¯æŒï¼‰ |
| æ‰¹æ¬¡å· | PC + yyyyMMdd + äº§çº¿ç¼–码 + æµæ°´å·ï¼ˆä¸å†å«å“ç§ç¼–码) |
## æ³¨æ„äº‹é¡¹
- `generate` æ—§å‚æ•° `count` å·²åºŸå¼ƒï¼Œåªä¼  `palletCount` æˆ– `bagCount`(按品种类型二选一)。
- æ‰¹æ¬¡ `itemId` ç›¸å…³å­—段已从接口移除,勿再引用(批次追溯视图 `get-by-code` çš„品种字段保留,可能为空=新批次)。
- æ‰˜ç›˜è¿½æº¯è§†å›¾ï¼ˆPalletTrace/qr-trace-modal)仍返回 `capacity` ä¾›åŽ†å²æ•°æ®å±•ç¤ºï¼Œæ–°æ‰˜ç›˜ä¸ºç©ºã€‚
- è€æ•°æ®ï¼ˆåŽ†å²ç©ºç™½è¢‹ç ï¼‰å“ç§ä¸ºç©ºå±žæ­£å¸¸ï¼Œè¿½æº¯èµ°æ‰¹æ¬¡å›žé€€ã€‚
- `palletIds` åœ¨ create/update/fill-info å‡ç”Ÿæ•ˆï¼›ä¸‹æ‹‰æ¯æ¬¡æ‰“开弹窗重新拉取 `unbound-list`,若提交时所选托盘已被他人绑定,后端整体报错回滚(本次批次操作不生效),提示用户刷新重试。
- è¢‹ç åˆ†é¡µï¼ˆ`/page`)后端已改为**创建时间倒序**(同秒内按编号倒序),前端无需再传排序参数;导出 Excel åŒå£å¾„。
- åŽŸå§‹è¢‹ç ä¸Žæ•°æ®åŒ…è¢‹ç åŒè¡¨åŒè§„åˆ™ï¼ˆuuid æˆç»„、可整组删除),列表页通过生成组 UUID åŒºåˆ†æ¥æºã€‚
- åˆ—表搜索区新增「品种」条件(复用 `MdmItemSelect`,可清空),传 `itemId` ç­‰å€¼è¿‡æ»¤ï¼›å¯¼å‡º Excel æºå¸¦åŒä¸€æŸ¥è¯¢æ¡ä»¶åŒå£å¾„生效。历史空白袋码品种为空,按品种筛选不会命中。
docs/mes_bag_code_package_refactor_requirements.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
# ä¸€è¢‹ä¸€ç ç”Ÿäº§é€»è¾‘改造 - éœ€æ±‚方案
> çŠ¶æ€ï¼šå·²ç¡®è®¤å¹¶å®Œæˆå®žæ–½ï¼ˆ2026-09-17)。决策点定稿见文末「6. ç¡®è®¤ç»“论」。
## 1. éœ€æ±‚背景
一袋一码的生产逻辑调整:
- æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†**不再绑定品种**,品种下沉到「赋码管理」的数据包上。
- ç”Ÿæˆæ•°æ®åŒ…时选择**品种**并输入**托盘数**,系统按品种主计量单位的「类型」自动计算袋码数量并生成袋码。
- æŒ‰æ‰˜ç›˜æ•°**自动生成托盘并自动绑定**袋码;托盘不再需要「可装袋数」。
- å“ç§ä¸ºã€Œå¨ã€æ—¶æ— æ‰˜ç›˜ï¼šæ”¹ä¸ºç›´æŽ¥è¾“å…¥**袋码数**,袋码只绑生产批次、不绑托盘码。
## 2. çŽ°çŠ¶è¦ç‚¹
| å¯¹è±¡ | çŽ°çŠ¶ |
|------|------|
| æ•°æ®åŒ…生成 | `POST /mes/pro/bag-code/generate` ä»…ä¼  `count`,生成空白袋码(无品种/批次/托盘) |
| æ‰¹æ¬¡ | `mes_pro_batch.itemId` ç»‘定品种;批次号 = PC + æ—¥æœŸ + äº§çº¿ç¼–码 + å“ç§ç¼–码 + æµæ°´å· |
| æ‰˜ç›˜ | æ‰‹åŠ¨åˆ›å»ºï¼ˆæ‰¹é‡ç”Ÿæˆ/预生成),创建时填 `capacity å¯è£…袋数`;袋码手动绑托盘并校验容量 |
| å“ç§â†’单位 | `mdm_item.unitMeasureId â†’ mdm_unit_measure.type`(类型选项:48千克/100千克/吨,本次已加) |
| è¿½æº¯ | æ‰«ç è¿½æº¯çš„品种信息取自批次 `batch.itemId` |
## 3. æ”¹é€ æ–¹æ¡ˆ
### 3.1 ç”Ÿæˆæ•°æ®åŒ…(赋码管理,核心改动)
**换算规则**(后端计算,不信任前端传值):
| å“ç§ä¸»å•位类型 | è¾“å…¥ | ç”Ÿæˆç»“æžœ |
|------|------|------|
| 48千克 | æ‰˜ç›˜æ•° P | è¢‹ç  48 Ã— P ä¸ªï¼›è‡ªåŠ¨ç”Ÿæˆ P ä¸ªæ‰˜ç›˜ï¼Œæ¯æ‰˜ç»‘定 48 ä¸ªè¢‹ç  |
| 100千克 | æ‰˜ç›˜æ•° P | è¢‹ç  100 Ã— P ä¸ªï¼›è‡ªåŠ¨ç”Ÿæˆ P ä¸ªæ‰˜ç›˜ï¼Œæ¯æ‰˜ç»‘定 100 ä¸ªè¢‹ç  |
| å¨ | è¢‹ç æ•° N | è¢‹ç  N ä¸ªï¼›ä¸ç”Ÿæˆæ‰˜ç›˜ï¼Œè¢‹ç ä¸ç»‘托盘 |
| ç±»åž‹ä¸ºç©º/无法解析 | - | æŠ¥é”™ï¼šå“ç§ä¸»è®¡é‡å•位未配置类型(48千克/100千克/吨) |
**接口变更** `POST /mes/pro/bag-code/generate`:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | æ˜¯ | å“ç§ï¼ˆmdm_item ç¼–号) |
| palletCount | Integer | æ¡ä»¶å¿…å¡« | æ‰˜ç›˜æ•°ï¼ˆç±»åž‹ä¸º N千克 æ—¶å¿…填) |
| bagCount | Integer | æ¡ä»¶å¿…å¡« | è¢‹ç æ•°ï¼ˆç±»åž‹ä¸º å¨ æ—¶å¿…填) |
| remark | String | å¦ | å¤‡æ³¨ |
响应新增:`uuid、type、perPallet(每托袋数)、totalCount(生成袋码数)、palletIds(自动生成的托盘)`。
**事务逻辑**:一次生成 = åŒä¸€ uuid çš„袋码组 + åŒæ‰¹è‡ªåŠ¨ç”Ÿæˆçš„æ‰˜ç›˜ï¼›æ‰˜ç›˜å…ˆä»¥**临时码(PL-序号)**落库并完成袋码绑定,正式托盘码在「录入批次」时生成。
**生成数据包弹窗(前端)**:选择品种(沿用品种选择组件)→ è¯»å–该品种主单位类型(前端查 `mdm_unit_measure`,类型为空给提示)→ æŒ‰ç±»åž‹åˆ‡æ¢è¾“入「托盘数」或「袋码数」→ å®žæ—¶é¢„览"将生成 è¢‹ç  X ä¸ª / æ‰˜ç›˜ Y ä¸ª"。
### 3.2 å½•入批次(保持后录入,扩展级联)
`PUT /mes/pro/bag-code/bind-batch` æµç¨‹ä¸å˜ï¼Œæ–°å¢žçº§è”:
1. è¢‹ç ç»‘定批次(现状)。
2. è¯¥ç»„袋码所绑定的临时托盘自动绑定同一批次,并分配正式托盘码 `批次号-P序号`,回写批次托盘数。
3. æ–°å¢žæ ¡éªŒï¼šåŒä¸€æ•°æ®åŒ…(同批绑定袋码)品种必须一致(生成时天然一致,防手动混选)。
### 3.3 è¢‹ç æ•°æ®æ¨¡åž‹
`mes_pro_bag_code` æ–°å¢žï¼š`item_id`、`item_code`、`item_name`(冗余)。
- åˆ—表/查询:显示品种列(编码/名称可检索本期不做,仅展示)。
- åˆ é™¤æ•°æ®åŒ…(delete-by-uuid):级联删除本组**自动生成**的托盘(前提仍是全部未绑批次;已录批次维持现状不可删)。
- è¢‹ç ä¸å¼ºåˆ¶æ‰˜ç›˜ï¼š`pallet_id` ä¿æŒå¯ç©ºï¼ˆå¨ç±»åž‹ï¼‰ã€‚
### 3.4 æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†
- **去掉品种字段**:创建/编辑表单、列表列、查询条件、Save/Resp/Page VO å‡ç§»é™¤ `itemId`;DO ä¸Žæ•°æ®åº“列 `mes_pro_batch.item_id` ä¿ç•™ï¼ˆåŽ†å²æ•°æ®ã€è¿½æº¯å›žé€€ç”¨ï¼‰ï¼Œæ³¨é‡Šæ ‡è®°"已废弃"。
- **批次号规则改为** `PC + yyyyMMdd + äº§çº¿ç¼–码 + æµæ°´å·`(`generateBatchCode` ä¸å†æ‹¼å“ç§ç¼–码;自动编码规则配置无需改)。
- ç¼–辑批次不再因品种变化重新生成批次号。
### 3.5 æ‰˜ç›˜ç®¡ç†
- æ‰¹é‡ç”Ÿæˆ/预生成接口去掉 `capacity` å…¥å‚;表单去掉「可装袋数」字段;列表去掉该列。DB åˆ—保留兼容历史。
- è¢‹ç æ‰‹åŠ¨ç»‘æ‰˜ç›˜åŽ»æŽ‰å®¹é‡æ ¡éªŒï¼ˆ`capacity` åˆ¤ç©ºé€»è¾‘天然跳过,删除相关校验代码)。
- æ‰˜ç›˜åˆ—表为自动生成托盘打标识(复用 `auto_generated` å­—段 = true)。
### 3.6 è¿½æº¯/扫码/看板(回归项)
- è¢‹ç è¿½æº¯ï¼ˆ`/get-by-code`、TraceRespVO):品种改为**优先取袋码 item_id,历史袋码回退批次 itemId**。
- æ‰¹æ¬¡è¿½æº¯è§†å›¾ï¼ˆMesProBatchTraceRespVO):保留品种返回但可能为空(历史批次有值)。
- æ‰«ç å…¥åº“、销售出库、袋码首页看板:不直接依赖批次品种的逻辑不动,涉及处回归验证。
## 4. æ•°æ®åº“变更(mom-thzy)
```sql
ALTER TABLE mes_pro_bag_code ADD COLUMN item_id BIGINT NULL COMMENT '品种(物料)编号' AFTER pallet_code;
ALTER TABLE mes_pro_bag_code ADD COLUMN item_code VARCHAR(64) NULL COMMENT '品种编码(冗余)' AFTER item_id;
ALTER TABLE mes_pro_bag_code ADD COLUMN item_name VARCHAR(255) NULL COMMENT '品种名称(冗余)' AFTER item_code;
-- mes_pro_batch.item_id ä¿ç•™ä¸åˆ ï¼ŒJava æ³¨é‡Šæ ‡è®°åºŸå¼ƒ
```
## 5. å‰åŽç«¯æ”¹åŠ¨æ¸…å•
| å±‚ | æ–‡ä»¶ | æ”¹åЍ |
|----|------|------|
| DB | mes_pro_bag_code | +item_id/item_code/item_name |
| åŽç«¯ | MesProBagCodeGenerateReqVO/RespVO、ServiceImpl | æ–°å‚数、类型换算、自动建托盘、事务 |
| åŽç«¯ | MesProBagCodeDO/RespVO/Mapper | +品种字段 |
| åŽç«¯ | bind-batch æµç¨‹ | çº§è”托盘绑批次、品种一致性校验 |
| åŽç«¯ | MesProBatchServiceImpl/VO | åŽ»å“ç§ã€æ‰¹æ¬¡å·è§„åˆ™ã€ç¼–è¾‘é€»è¾‘ |
| åŽç«¯ | MesProPalletServiceImpl/VO | åŽ» capacity å…¥å‚与校验 |
| åŽç«¯ | delete-by-uuid | çº§è”删自动托盘 |
| åŽç«¯ | è¿½æº¯ VO | å“ç§ä¼˜å…ˆè¢‹ç ã€å›žé€€æ‰¹æ¬¡ |
| å‰ç«¯ | pro/bagCode generate-form | å“ç§+托盘数/袋码数表单 |
| å‰ç«¯ | pro/bagCode åˆ—表 | +品种列 |
| å‰ç«¯ | pro/batch form/list/search | ç§»é™¤å“ç§ |
| å‰ç«¯ | pro/pallet form/list | ç§»é™¤å¯è£…袋数 |
## 6. ç¡®è®¤ç»“论(定稿)
1. **混品种**:要。一次 bind-batch å‹¾é€‰çš„袋码品种必须一致,否则报错「录入批次的袋码必须属于同一品种」(历史空白袋码不参与校验)。
2. **自动托盘可否再手动增绑**:无容量上限,装满后仍可继续手动绑入袋码。
3. **批次列表历史数据**:老批次 itemId ç›´æŽ¥ä¸å±•示即可,无数据迁移;追溯视图保留批次品种做历史回退。
4. **流程与批次号**:保持「先生成数据包、后录入批次」;批次号规则改为 `PC + yyyyMMdd + äº§çº¿ç¼–码 + æµæ°´å·`。
## 7. å®žæ–½è®°å½•(2026-09-17)
- DB(mom-thzy):`mes_pro_bag_code` æ–°å¢ž `item_id/item_code/item_name`;`mdm_unit_measure.type`(48千克/100千克/吨);`mes_pro_batch.item_id` ä¿ç•™æ ‡åºŸå¼ƒã€‚
- åŽç«¯ï¼š`generate` é‡å†™ï¼ˆç±»åž‹æ¢ç®—+自动临时托盘)、`bind-batch` çº§è”托盘正式码+品种一致校验、`delete-by-uuid` çº§è”删自动托盘、`replace` ç»§æ‰¿å“ç§ã€æ‰˜ç›˜åˆ›å»ºåŽ» `capacity` å…¥å‚、袋码绑托盘去容量校验、批次 VO/Service åŽ»å“ç§ã€æ‰¹æ¬¡å·å•äº§çº¿è¦ç´ ã€è¢‹ç /公共追溯与二维码标签品种「袋码优先、批次回退」。
- å‰ç«¯ï¼šè¢‹ç ç”Ÿæˆå¼¹çª—(品种+托盘数/袋码数动态+预览)、列表品种列;批次表单/列表/搜索去品种;托盘表单/列表去可装袋数、列表加「自动生成」标识、绑托盘下拉去容量提示。
- ç¼–译:`mvn compile -pl yudao-module-mes -am -q` é€šè¿‡ã€‚
## 8. è¡¥å……需求:批次操作绑定托盘(2026-09-17)
- æ‰¹æ¬¡æ–°å¢ž/补录/编辑均可选「绑定托盘」(多选、非必填):勾选后批次状态直接置**生产中**,同事务复用托盘绑定批次逻辑(正式托盘码、托盘下袋码级联录入、品种一致校验),任一步失败整体回滚。
- ç¼–辑时仅传本次新增勾选的托盘;已绑定托盘不在下拉中(unbound-list å·²è¿‡æ»¤ï¼‰ï¼Œä¸æ”¯æŒè§£ç»‘,未勾选不改状态、不截断回退。
- æ–°æŽ¥å£ `GET /mes/pro/pallet/unbound-list`:batch_id ä¸ºç©ºã€æ— æ­£å¼æ‰˜ç›˜ç ä¸”未作废的临时托盘。
- å‰ç«¯è”调细则见 `docs/mes_bag_code_package_refactor_integration.md`。
## 9. è¡¥å……需求:原始袋码与列表排序(2026-09-17)
- æ–°å¢ž `POST /mes/pro/bag-code/raw-generate`:仅选品种+袋码数量即生成对应数量袋码(带品种冗余、uuid æˆç»„),**不校验计量单位类型、不生成托盘**;后续录入批次/绑托盘/印刷/删除与数据包袋码一致。赋码管理页工具栏加「生成原始袋码」弹窗入口。
- è¢‹ç åˆ†é¡µæŸ¥è¯¢æ”¹ä¸ºæŒ‰**创建时间倒序**(同秒按编号倒序),原「袋序号升序」废弃。
- ç¼–译 `mvn compile -pl yudao-module-mes -am -q` é€šè¿‡ã€å‰ç«¯ç±»åž‹æ£€æŸ¥æ”¹åŠ¨æ–‡ä»¶é›¶é”™è¯¯ã€‚
## 10. è¡¥å……需求:袋码列表品种查询(2026-09-17)
- èµ‹ç ç®¡ç†åˆ—表搜索区新增「品种」条件(复用 `MdmItemSelect` é€‰æ‹©ç»„件),按 `mes_pro_bag_code.item_id` ç­‰å€¼è¿‡æ»¤ï¼›åˆ†é¡µä¸Žå¯¼å‡º Excel åŒå£å¾„生效。
- åŽ†å²ç©ºç™½è¢‹ç  `item_id` ä¸ºç©ºï¼Œé€‰æ‹©å“ç§åŽä¸ä¼šå‘½ä¸­ï¼ˆå±žæ­£å¸¸ï¼‰ã€‚§3.3 åŽŸã€Œç¼–ç /名称可检索本期不做,仅展示」就此升级为按品种编号检索。
docs/mes_pro_batch_processing_team_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
# æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼šéšè—äº§çº¿ç¼–码、字段改名、新增加工班组 - å‰ç«¯è”调方案
## èƒŒæ™¯
批次生产管理页面(/mes/pro/batch)调整:
- éšè—ã€Œäº§çº¿ç¼–码」(列表列、搜索项、Excel å¯¼å‡ºå‡ä¸å†å±•示;接口仍返回 `lineCode`)。
- ã€Œäº§çº¿åç§°ã€å±•示文案统一改为「加工单位」(数据仍为 `lineName`,来源产线主数据)。
- ã€Œè®¡åˆ’数量」展示文案改为「加工数量」(数据仍为 `planQuantity`;2026-09-17 ç”±ã€Œä»·æ ¼æ•°é‡ã€å†æ¬¡æ›´åï¼‰ã€‚
- æ–°å¢žã€ŒåŠ å·¥ç­ç»„ã€å­—æ®µï¼Œæ‰‹å·¥å¡«å†™æ–‡æœ¬ï¼Œæœ€é•¿ 64 å­—符,位置在「加工单位」之后。
## API å˜åŒ–
| æ–¹æ³• | è·¯å¾„ | å˜åŒ– |
|------|------|------|
| POST | /mes/pro/batch/create | è¯·æ±‚体新增 `processingTeam`(可选) |
| PUT | /mes/pro/batch/update | åŒä¸Š |
| PUT | /mes/pro/batch/fill-info | åŒä¸Šï¼ˆä¸´æ—¶æ‰¹æ¬¡è¡¥å½•) |
| GET | /mes/pro/batch/get | å“åº”新增 `processingTeam` |
| GET | /mes/pro/batch/page | å“åº”新增 `processingTeam`;新增查询参数 `processingTeam`(模糊匹配) |
**字段说明:**
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| processingTeam | String(64) | å¦ | åŠ å·¥ç­ç»„ï¼Œæ‰‹å·¥å¡«å†™ï¼Œä¸é€‰äº§çº¿ä¹Ÿå¯å¡« |
**响应示例(节选):**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "code": "PC20260917L01NY001",
        "lineName": "包装一线",
        "processingTeam": "一号加工班组",
        "planQuantity": 1000,
        "ownerUserName": "张三"
      }
    ],
    "total": 1
  }
}
```
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ–°å¢ž/补录/编辑批次 | ã€ŒåŠ å·¥ç­ç»„ã€ä¸ºå¯é€‰æ–‡æœ¬è¾“å…¥ï¼›ç¼–è¾‘æ—¶ä¸ä¼ ä¸ä¼šæ¸…ç©ºåŽ†å²å€¼ |
| åŠ å·¥å•ä½ | ä»ç”±é€‰æ‹©äº§çº¿ï¼ˆ`lineId`)回填 `lineName`,仅展示文案变化 |
| åŠ å·¥æ•°é‡ | ä»…展示文案变化,字段与校验逻辑不变 |
| äº§çº¿ç¼–码 | æŽ¥å£ä¿ç•™ `lineCode`(扫码、批次号生成等仍依赖),页面与导出隐藏 |
| å¯¼å‡º Excel | åˆ—同步变化:去掉「产线编码」,「产线名称→加工单位」,新增「加工班组」,「计划数量→加工数量」 |
## æ•°æ®åº“变更
`mes_pro_batch` æ–°å¢žåˆ— `processing_team VARCHAR(64) NULL COMMENT '加工班组'`(脚本:`docs/sql/pro_batch_processing_team.sql`)。
## æ³¨æ„äº‹é¡¹
- æ‰«ç è¿½æº¯å¼¹çª—(qr-trace-modal)、批次追溯页(qc/batchtrace)本次未要求调整,仍展示产线编码/名称与计划数量文案,如需统一另行处理。
- æœ¬æ‰¹åŽç«¯å·²åŒæ­¥ä¿®æ”¹å‰ç«¯ `src/views/mes/pro/batch/`(列、搜索表单、编辑表单)与 `src/api/mes/pro/batch/index.ts` ç±»åž‹ï¼Œä¾›å‰ç«¯å›¢é˜Ÿå¤æ ¸ã€‚
docs/sql/pro_batch_processing_team.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
-- æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼šæ–°å¢žã€ŒåŠ å·¥ç­ç»„ã€å­—æ®µ
-- æ‰§è¡Œåº“:mom-thzy(local)
ALTER TABLE mes_pro_batch ADD COLUMN processing_team VARCHAR(64) NULL COMMENT '加工班组(手工填写)' AFTER line_name;
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/dto/MdmUnitMeasureRespDTO.java
@@ -18,6 +18,9 @@
    @Schema(description = "单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个")
    private String name;
    @Schema(description = "类型(48千克/100千克/吨)", example = "48千克")
    private String type;
    @Schema(description = "是否主单位", example = "true")
    private Boolean primaryFlag;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -8,12 +8,10 @@
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemStatusUpdateReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSyncUpdateReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemBatchConfigService;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
@@ -47,8 +45,6 @@
    @Resource
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @Resource
@@ -129,9 +125,6 @@
    @PreAuthorize("@ss.hasPermission('mdm:item:query')")
    public CommonResult<PageResult<MdmItemRespVO>> getItemPage(@Valid MdmItemPageReqVO pageReqVO) {
        PageResult<MdmItemDO> pageResult = itemService.getItemPage(pageReqVO);
        // å…³è”查询分类名称
        Map<Long, MdmItemCategoryDO> categoryMap = itemCategoryService.getItemCategoryMap(
                convertSet(pageResult.getList(), MdmItemDO::getCategoryId));
        // å…³è”查询计量单位名称
        Map<Long, MdmUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(pageResult.getList(), MdmItemDO::getUnitMeasureId));
@@ -158,11 +151,6 @@
                        .collect(Collectors.toMap(MesWmWarehouseRespDTO::getId, w -> w));
        // èµ‹å€¼
        respList.forEach(resp -> {
            // åˆ†ç±»åç§°
            MdmItemCategoryDO category = categoryMap.get(resp.getCategoryId());
            if (category != null) {
                resp.setCategoryName(category.getName());
            }
            MdmUnitMeasureDO unit = unitMap.get(resp.getUnitMeasureId());
            if (unit != null) {
                resp.setUnitMeasureName(unit.getName());
@@ -215,13 +203,6 @@
     * å¡«å……物料响应 VO çš„关联名称(分类、主单位、辅单位、仓库名称)
     */
    private void enrichItemRespVO(MdmItemRespVO resp) {
        // åˆ†ç±»åç§°
        if (resp.getCategoryId() != null) {
            MdmItemCategoryDO category = itemCategoryService.getItemCategory(resp.getCategoryId());
            if (category != null) {
                resp.setCategoryName(category.getName());
            }
        }
        // ä¸»å•位名称
        if (resp.getUnitMeasureId() != null) {
            MdmUnitMeasureDO unit = unitMeasureService.getUnitMeasure(resp.getUnitMeasureId());
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemPageReqVO.java
@@ -23,9 +23,6 @@
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "物料分类编号", example = "1")
    private Long categoryId;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java
@@ -25,12 +25,6 @@
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "物料分类名称", example = "五金配件")
    private String categoryName;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
@@ -75,9 +69,6 @@
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java
@@ -24,9 +24,6 @@
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号(关联 MES ç‰©æ–™åˆ†ç±» mes_md_item_type)", example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", example = "1")
    private Long unitMeasureId;
@@ -53,9 +50,6 @@
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal;
@@ -18,6 +19,10 @@
    @Schema(description = "单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个")
    private String name;
    @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "48千克")
    @NotBlank(message = "类型不能为空")
    private String type;
    @Schema(description = "是否主单位", example = "true")
    private Boolean primaryFlag;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java
@@ -40,6 +40,13 @@
    private String name;
    /**
     * ç±»åž‹
     *
     * å¯é€‰å€¼ï¼š48千克、100千克、吨
     */
    private String type;
    /**
     * æ˜¯å¦ä¸»å•位
     */
    private Boolean primaryFlag;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -2,28 +2,20 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemBatchConfigMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemTypeApi;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -33,7 +25,6 @@
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
@@ -49,8 +40,6 @@
    private MdmItemMapper itemMapper;
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @Resource
    private MdmBrandService brandService;
@@ -60,9 +49,6 @@
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
    @Override
@@ -115,10 +101,9 @@
    }
    /**
     * åŒæ­¥ç‰©æ–™åˆ° MES(单位/分类按 code æ˜ å°„)
     * åŒæ­¥ç‰©æ–™åˆ° MES(单位按 code æ˜ å°„)
     *
     * MDM çš„ unitMeasureId/categoryId ä¸Ž MES çš„ unitMeasureId/itemTypeId åˆ†å±žä¸¤å¥—主数据,
     * è¿™é‡Œå…ˆåœ¨ MDM ä¾§è§£æžå‡º code,再交由 MesMdItemApi åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
     * å“ç§ç®¡ç†å·²åŽ»æŽ‰ã€Œå“ç§åˆ†ç±»ã€å­—æ®µï¼ŒåŒæ­¥ä¸å†ä¼ é€’ MES ç‰©æ–™åˆ†ç±»ï¼ˆitem_type_id ç•™ç©ºï¼Œå¯åœ¨ MES ä¾§è¡¥å¡«ï¼‰ã€‚
     * åŒæ­¥å¤±è´¥å°†æŠ›å‡ºå¼‚常,触发外层 @Transactional æ•´ä½“回滚(MDM æ“ä½œä¸€å¹¶å›žæ»šï¼‰ã€‚
     */
    private void syncItemToMes(MdmItemSaveReqVO reqVO) {
@@ -131,12 +116,6 @@
            }
            unitCode = unitMeasure.getCode();
        }
        // è§£æžåˆ†ç±» id -> code(使用 MDM æœ¬åœ°åˆ†ç±»æœåŠ¡ï¼‰
        String itemTypeCode = null;
        if (reqVO.getCategoryId() != null) {
            CommonResult<MesMdItemTypeRespDTO> itemType = mesMdItemTypeApi.getItemType(reqVO.getCategoryId());
            itemTypeCode = itemType.getData().getCode();
        }
        // ç»„装同步 DTO
        MesMdItemSyncReqDTO syncReqDTO = new MesMdItemSyncReqDTO();
        syncReqDTO.setCode(reqVO.getCode());
@@ -144,7 +123,6 @@
        syncReqDTO.setName(reqVO.getName());
        syncReqDTO.setSpecification(reqVO.getSpecification());
        syncReqDTO.setUnitCode(unitCode);
        syncReqDTO.setItemTypeCode(itemTypeCode);
        syncReqDTO.setSafeStockFlag(Boolean.TRUE); // ä¿ç•™ MDM çš„ minStock/maxStock,避免被 MES æ¸…é›¶
        syncReqDTO.setMinStock(reqVO.getMinStock());
        syncReqDTO.setMaxStock(reqVO.getMaxStock());
@@ -274,17 +252,6 @@
        }
    }
    private void validateCategoryExists(Long categoryId) {
        if (categoryId == null) {
            return;
        }
        // ä½¿ç”¨ MDM æœ¬åœ°åˆ†ç±»æœåŠ¡æ ¡éªŒ
        MdmItemCategoryDO category = itemCategoryService.getItemCategory(categoryId);
        if (category == null) {
            throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS);
        }
    }
    private void validateUnitMeasureExists(Long unitMeasureId) {
        if (unitMeasureService.getUnitMeasure(unitMeasureId) == null) {
            throw exception(MDM_UNIT_MEASURE_NOT_EXISTS);
@@ -328,17 +295,8 @@
    @Override
    public PageResult<MdmItemDO> getItemPage(MdmItemPageReqVO pageReqVO) {
        // æŸ¥æ‰¾æ—¶ï¼Œå¦‚果查找某个分类编号,则包含它的子分类
        Set<Long> categoryIds = null;
        if (pageReqVO.getCategoryId() != null) {
            categoryIds = new HashSet<>();
            categoryIds.add(pageReqVO.getCategoryId());
            List<cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO> children =
                    itemCategoryService.getItemCategoryChildrenList(pageReqVO.getCategoryId());
            categoryIds.addAll(convertSet(children, cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO::getId));
        }
        // åˆ†é¡µæŸ¥è¯¢
        return itemMapper.selectPage(pageReqVO, categoryIds);
        return itemMapper.selectPage(pageReqVO, (Collection<Long>) null);
    }
    @Override
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java
@@ -21,9 +21,6 @@
    @Schema(description = "计量单位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PCS")
    private String unitCode;
    @Schema(description = "物料分类编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "TYPE001")
    private String itemTypeCode;
    @Schema(description = "是否启用安全库存", example = "false")
    private Boolean safeStockFlag;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java
@@ -5,12 +5,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.batchconfig.MesMdItemBatchConfigSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -27,7 +25,7 @@
 * MES ç‰©æ–™äº§å“ API å®žçŽ°ç±»
 *
 * ä¾› MDM ç­‰æ¨¡å—同步物料主数据到 MES ä½¿ç”¨ã€‚
 * å•位、分类按 code åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
 * å•位按 code åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
 *
 * @author è¶…级管理员
 */
@@ -40,8 +38,6 @@
    private MesMdItemService itemService;
    @Resource
    private MesMdItemMapper itemMapper;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
@@ -92,7 +88,9 @@
    }
    /**
     * å°†åŒæ­¥ DTO è½¬æ¢ä¸º MES ç‰©æ–™äº§å“ SaveReqVO,并按 code è§£æžå•位/分类 id
     * å°†åŒæ­¥ DTO è½¬æ¢ä¸º MES ç‰©æ–™äº§å“ SaveReqVO,并按 code è§£æžå•位 id
     *
     * MDM å“ç§ç®¡ç†å·²åŽ»æŽ‰ã€Œå“ç§åˆ†ç±»ã€ï¼ŒåŒæ­¥ä¸å†è®¾ç½®ç‰©æ–™åˆ†ç±»ï¼ˆitem_type_id ç•™ç©ºï¼Œå¯åœ¨ MES ä¾§æ‰‹å·¥è¡¥å¡«ï¼‰
     */
    private MesMdItemSaveReqVO buildSaveReqVO(MesMdItemSyncReqDTO syncReqDTO) {
        // è§£æžè®¡é‡å•位 code -> id
@@ -100,15 +98,9 @@
        if (unitMeasure == null) {
            throw exception(MD_UNIT_MEASURE_NOT_EXISTS);
        }
        // è§£æžç‰©æ–™åˆ†ç±» code -> id
        MesMdItemTypeDO itemType = itemTypeService.getItemTypeByCode(syncReqDTO.getItemTypeCode());
        if (itemType == null) {
            throw exception(MD_ITEM_TYPE_NOT_EXISTS);
        }
        // ç»„装(code/name/specification/safeStockFlag/minStock/maxStock/highValue/batchFlag/remark åŒåæ‹·è´ï¼‰
        MesMdItemSaveReqVO saveReqVO = BeanUtils.toBean(syncReqDTO, MesMdItemSaveReqVO.class);
        saveReqVO.setUnitMeasureId(unitMeasure.getId());
        saveReqVO.setItemTypeId(itemType.getId());
        return saveReqVO;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java
@@ -29,8 +29,7 @@
    @NotNull(message = "计量单位不能为空")
    private Long unitMeasureId;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "物料分类编号不能为空")
    @Schema(description = "物料分类编号", example = "1")
    private Long itemTypeId;
    @Schema(description = "是否启用安全库存", example = "false")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRawGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRespVO;
@@ -77,6 +78,14 @@
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:create')")
    public CommonResult<MesProBagCodeGenerateRespVO> generateBagCodes(@Valid @RequestBody MesProBagCodeGenerateReqVO generateReqVO) {
        return success(bagCodeService.generateBagCodes(generateReqVO));
    }
    @PostMapping("/raw-generate")
    @Operation(summary = "生成原始袋码(仅选品种与袋码数量,不关联计量单位类型、不生成托盘)")
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:create')")
    public CommonResult<MesProBagCodeGenerateRespVO> generateRawBagCodes(
            @Valid @RequestBody MesProBagCodeRawGenerateReqVO rawGenerateReqVO) {
        return success(bagCodeService.generateRawBagCodes(rawGenerateReqVO));
    }
    @PostMapping("/replace")
@@ -242,8 +251,7 @@
                    .setProduceDate(batch.getProduceDate())
                    .setLineCode(batch.getLineCode())
                    .setLineName(batch.getLineName())
                    .setOwnerUserId(batch.getOwnerUserId())
                    .setItemId(batch.getItemId());
                    .setOwnerUserId(batch.getOwnerUserId());
            // è´£ä»»äºº
            if (batch.getOwnerUserId() != null) {
                AdminUserRespDTO user = adminUserApi.getUser(batch.getOwnerUserId());
@@ -251,14 +259,17 @@
                    respVO.setOwnerUserName(user.getNickname());
                }
            }
            // å“ç§ä¿¡æ¯
            if (batch.getItemId() != null) {
                MesMdmItemDO item = mdmItemService.getItem(batch.getItemId());
                if (item != null) {
                    respVO.setItemCode(item.getCode())
                            .setItemName(item.getName())
                            .setItemSpecification(item.getSpecification());
                }
        }
        // å“ç§ä¿¡æ¯ï¼šä¼˜å…ˆå–袋码上的品种(数据包生成时绑定),历史袋码回退批次品种
        Long itemId = bagCode.getItemId() != null ? bagCode.getItemId()
                : (batch == null ? null : batch.getItemId());
        if (itemId != null) {
            MesMdmItemDO item = mdmItemService.getItem(itemId);
            if (item != null) {
                respVO.setItemId(itemId)
                        .setItemCode(item.getCode())
                        .setItemName(item.getName())
                        .setItemSpecification(item.getSpecification());
            }
        }
        return respVO;
@@ -268,11 +279,12 @@
     * æž„建袋码二维码标签列表(二维码编码纯码值,图片叠加关键信息文字)
     */
    private List<QrLabelInfo> buildBagCodeQrLabels(List<MesProBagCodeDO> list) {
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆæ‰¹æ¬¡ã€å“ç§ã€è´£ä»»äººï¼‰
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆæ‰¹æ¬¡ã€å“ç§ã€è´£ä»»äººï¼‰ï¼›å“ç§é›†åˆå«è¢‹ç è‡ªèº«å“ç§ä¸ŽåŽ†å²æ‰¹æ¬¡å“ç§
        Map<Long, MesProBatchDO> batchMap = convertMap(
                batchService.getBatchList(convertSet(list, MesProBagCodeDO::getBatchId)), MesProBatchDO::getId);
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(batchMap.values(), MesProBatchDO::getItemId));
        Set<Long> itemIds = convertSet(list, MesProBagCodeDO::getItemId);
        itemIds.addAll(convertSet(batchMap.values(), MesProBatchDO::getItemId));
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(itemIds);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(batchMap.values(), MesProBatchDO::getOwnerUserId));
        // 2. æž„建标签
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
@@ -9,10 +9,17 @@
@Data
public class MesProBagCodeGenerateReqVO {
    @Schema(description = "生成袋数(本次生成的袋码数量)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "生成袋数不能为空")
    @Positive(message = "生成袋数必须大于 0")
    private Integer count;
    @Schema(description = "品种(物料)编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "品种不能为空")
    private Long itemId;
    @Schema(description = "托盘数(品种主单位类型为 48千克/100千克 æ—¶å¿…填,袋码数 = æ¯æ‰˜è¢‹æ•° Ã— æ‰˜ç›˜æ•°ï¼‰", example = "5")
    @Positive(message = "托盘数必须大于 0")
    private Integer palletCount;
    @Schema(description = "袋码数(品种主单位类型为 å¨ æ—¶å¿…填,不生成托盘)", example = "100")
    @Positive(message = "袋码数必须大于 0")
    private Integer bagCount;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
@@ -3,10 +3,24 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - MES è¢‹ç ç”Ÿæˆå“åº”")
@Data
public class MesProBagCodeGenerateRespVO {
    @Schema(description = "生成组 UUID")
    private String uuid;
    @Schema(description = "品种主单位类型", example = "48千克")
    private String type;
    @Schema(description = "每托盘袋数(类型为 å¨ æ—¶ä¸º 0)", example = "48")
    private Integer perPallet;
    @Schema(description = "本次生成的袋码总数", example = "240")
    private Integer totalCount;
    @Schema(description = "自动生成的托盘编号列表(类型为 å¨ æ—¶ä¸ºç©ºï¼‰")
    private List<Long> palletIds;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodePageReqVO.java
@@ -32,6 +32,9 @@
    @Schema(description = "绑定托盘编号", example = "1024")
    private Long palletId;
    @Schema(description = "品种(物料)编号", example = "1024")
    private Long itemId;
    @Schema(description = "状态(字典 mes_pro_bag_code_status)", example = "10")
    private Integer status;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
@Schema(description = "管理后台 - MES åŽŸå§‹è¢‹ç ç”Ÿæˆ Request VO(仅品种+数量,不关联计量单位类型、不生成托盘)")
@Data
public class MesProBagCodeRawGenerateReqVO {
    @Schema(description = "品种(物料)编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "品种不能为空")
    private Long itemId;
    @Schema(description = "袋码数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "袋码数量不能为空")
    @Positive(message = "袋码数量必须大于 0")
    private Integer count;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRespVO.java
@@ -58,6 +58,20 @@
    @ColumnWidth(30)
    private String palletCode;
    @Schema(description = "品种编号", example = "1")
    @ExcelIgnore
    private Long itemId;
    @Schema(description = "品种编码")
    @ExcelProperty("品种编码")
    @ColumnWidth(20)
    private String itemCode;
    @Schema(description = "品种名称")
    @ExcelProperty("品种名称")
    @ColumnWidth(20)
    private String itemName;
    @Schema(description = "状态(字典 mes_pro_bag_code_status)", example = "10")
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_PRO_BAG_CODE_STATUS)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java
@@ -213,21 +213,13 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆè´£ä»»äººã€å“ç§ç‰©æ–™ï¼‰
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆè´£ä»»äººï¼‰ï¼›å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…ï¼Œæ‰¹æ¬¡åˆ—è¡¨ä¸å†å±•ç¤º
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesProBatchDO::getOwnerUserId));
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(list, MesProBatchDO::getItemId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesProBatchRespVO.class, vo -> {
            MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user ->
                    vo.setOwnerUserName(user.getNickname()));
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode());
                vo.setItemName(item.getName());
                vo.setItemSpecification(item.getSpecification());
            });
        });
        return BeanUtils.toBean(list, MesProBatchRespVO.class, vo ->
                MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user ->
                        vo.setOwnerUserName(user.getNickname())));
    }
    private List<MesProBatchWorkerRespVO> buildWorkerRespVOList(List<MesProBatchWorkerDO> workers) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchPageReqVO.java
@@ -23,18 +23,18 @@
    @Schema(description = "临时批次码", example = "PB-00000001")
    private String tempCode;
    @Schema(description = "品种(产品物料)编号", example = "1")
    private Long itemId;
    @Schema(description = "产线编号", example = "1")
    private Long lineId;
    @Schema(description = "产线编码", example = "L01")
    private String lineCode;
    @Schema(description = "产线名称", example = "包装一线")
    @Schema(description = "产线名称(页面展示为「加工单位」)", example = "包装一线")
    private String lineName;
    @Schema(description = "加工班组", example = "一号加工班组")
    private String processingTeam;
    @Schema(description = "责任人用户编号", example = "1")
    private Long ownerUserId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchRespVO.java
@@ -30,41 +30,29 @@
    @ExcelIgnore
    private String tempCode;
    @Schema(description = "品种(产品物料)编号", example = "1")
    @ExcelIgnore
    private Long itemId;
    @Schema(description = "品种编码")
    @ExcelProperty("品种编码")
    private String itemCode;
    @Schema(description = "品种名称")
    @ExcelProperty("品种名称")
    private String itemName;
    @Schema(description = "品种规格")
    @ExcelProperty("品种规格")
    private String itemSpecification;
    @Schema(description = "产线编号", example = "1")
    @ExcelIgnore
    private Long lineId;
    @Schema(description = "产线编码", example = "L01")
    @ExcelProperty("产线编码")
    @Schema(description = "产线编码(页面已隐藏,仅接口返回)", example = "L01")
    @ExcelIgnore
    private String lineCode;
    @Schema(description = "产线名称", example = "包装一线")
    @ExcelProperty("产线名称")
    @Schema(description = "产线名称(页面展示为「加工单位」)", example = "包装一线")
    @ExcelProperty("加工单位")
    private String lineName;
    @Schema(description = "加工班组", example = "一号加工班组")
    @ExcelProperty("加工班组")
    private String processingTeam;
    @Schema(description = "生产日期")
    @ExcelProperty("生产日期")
    @ColumnWidth(20)
    private LocalDateTime produceDate;
    @Schema(description = "计划数量", example = "1000")
    @ExcelProperty("计划数量")
    @Schema(description = "加工数量(原计划数量,页面展示为「加工数量」)", example = "1000")
    @ExcelProperty("加工数量")
    private BigDecimal planQuantity;
    @Schema(description = "责任人用户编号", example = "1")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchSaveReqVO.java
@@ -4,6 +4,7 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -20,20 +21,20 @@
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "品种(产品物料)编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "品种不能为空")
    private Long itemId;
    @Schema(description = "产线编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "产线不能为空")
    @Schema(description = "产线编号(页面展示为「加工单位」)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "加工单位不能为空")
    private Long lineId;
    @Schema(description = "加工班组(手工填写)", example = "一号加工班组")
    @Size(max = 64, message = "加工班组长度不能超过 64")
    private String processingTeam;
    @Schema(description = "生产日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "生产日期不能为空")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime produceDate;
    @Schema(description = "计划数量", example = "1000")
    @Schema(description = "加工数量(原计划数量,页面展示为「加工数量」)", example = "1000")
    private BigDecimal planQuantity;
    @Schema(description = "责任人用户编号(责任落实到人)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@@ -48,6 +49,9 @@
    @Valid
    private List<Worker> workers;
    @Schema(description = "绑定托盘编号列表(可选;绑定时批次直接进入生产中,托盘下袋码级联录入本批次;新增/补录/编辑均可传入,编辑时仅需传本次新增勾选的托盘)")
    private List<Long> palletIds;
    @Schema(description = "生产人员配置")
    @Data
    public static class Worker {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java
@@ -159,6 +159,14 @@
        return success(BeanUtils.toBean(list, MesProPalletRespVO.class));
    }
    @GetMapping("/unbound-list")
    @Operation(summary = "获得未绑定批次的临时托盘列表(供批次新增/补录绑定托盘下拉使用)")
    @PreAuthorize("@ss.hasPermission('mes:pro-pallet:query')")
    public CommonResult<List<MesProPalletRespVO>> getUnboundPalletList() {
        List<MesProPalletDO> list = palletService.getUnboundPalletList();
        return success(BeanUtils.toBean(list, MesProPalletRespVO.class));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出托盘码 Excel")
    @PreAuthorize("@ss.hasPermission('mes:pro-pallet:export')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletBatchCreateReqVO.java
@@ -18,10 +18,6 @@
    @Positive(message = "生成数量必须大于 0")
    private Integer count;
    @Schema(description = "可装袋数(袋/托盘)", example = "25")
    @Positive(message = "可装袋数必须大于 0")
    private Integer capacity;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletPreGenerateReqVO.java
@@ -14,10 +14,6 @@
    @Positive(message = "生成数量必须大于 0")
    private Integer count;
    @Schema(description = "可装袋数(袋/托盘)", example = "25")
    @Positive(message = "可装袋数必须大于 0")
    private Integer capacity;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletRespVO.java
@@ -41,10 +41,6 @@
    @ExcelProperty("托盘序号")
    private Integer palletNo;
    @Schema(description = "可装袋数(袋/托盘)", example = "25")
    @ExcelProperty("可装袋数")
    private Integer capacity;
    @Schema(description = "已绑定袋码数量", example = "10")
    @ExcelProperty("已装袋数")
    private Integer bagCount;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java
@@ -119,6 +119,12 @@
            return "未知";
        }
        switch (bizType) {
            case MesBizTypeConstants.QC_SEED_QUALITY:
                return "种子质检";
            case MesBizTypeConstants.WM_ITEM_RECEIPT_CANCEL:
                return "采购入库取消";
            case MesBizTypeConstants.WM_STOCK_TAKING:
                return "盘点调整";
            case MesBizTypeConstants.WM_STOCK_ADJUST_IN:
                return "自定义入库";
            case MesBizTypeConstants.WM_STOCK_ADJUST_OUT:
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeDO.java
@@ -28,6 +28,18 @@
    private String batchCode;
    private Long palletId;
    private String palletCode;
    /**
     * å“ç§ï¼ˆç‰©æ–™ï¼‰ç¼–号(生成数据包时选定,关联 mdm_item)
     */
    private Long itemId;
    /**
     * å“ç§ç¼–码(冗余)
     */
    private String itemCode;
    /**
     * å“ç§åç§°ï¼ˆå†—余)
     */
    private String itemName;
    private Integer status;
    private Integer qcStatus;
    private LocalDateTime printTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/batch/MesProBatchDO.java
@@ -14,8 +14,9 @@
/**
 * MES ç”Ÿäº§æ‰¹æ¬¡ DO
 * <p>
 * æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼ˆä¸€è¢‹ä¸€ç æ¨¡å—):关联品种(MDM ç‰©æ–™ï¼‰ã€äº§çº¿ï¼ˆäº§çº¿ç»´æŠ¤ï¼‰ä¸Žç”Ÿäº§äººå‘˜ï¼ˆäººå‘˜é…ç½®è§ {@link MesProBatchWorkerDO}),责任落实到人。
 * æ‰¹æ¬¡å·è‡ªåŠ¨ç”Ÿæˆï¼Œç¼–ç è§„åˆ™åŒ…å«æ—¥æœŸã€äº§çº¿ç¼–ç ã€å“ç§ç¼–ç ã€æµæ°´å·ç­‰è¦ç´ ï¼Œå¹¶ä¸Žè¢‹ç ï¼ˆä¸€è¢‹ä¸€ç ï¼‰ã€æ‰˜ç›˜ç è‡ªåŠ¨å…³è”ã€‚
 * æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼ˆä¸€è¢‹ä¸€ç æ¨¡å—):关联产线(产线维护)与生产人员(人员配置见 {@link MesProBatchWorkerDO}),责任落实到人。
 * å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…(赋码管理),批次不再绑定品种。
 * æ‰¹æ¬¡å·è‡ªåŠ¨ç”Ÿæˆï¼Œç¼–ç è§„åˆ™åŒ…å«æ—¥æœŸã€äº§çº¿ç¼–ç ã€æµæ°´å·ç­‰è¦ç´ ï¼Œå¹¶ä¸Žè¢‹ç ï¼ˆä¸€è¢‹ä¸€ç ï¼‰ã€æ‰˜ç›˜ç è‡ªåŠ¨å…³è”ã€‚
 *
 * @author è¶…级管理员
 */
@@ -35,7 +36,7 @@
    @TableId
    private Long id;
    /**
     * æ‰¹æ¬¡å·ï¼ˆè‡ªåŠ¨ç”Ÿæˆï¼šå‰ç¼€ + æ—¥æœŸ + äº§çº¿ç¼–码 + å“ç§ç¼–码 + æµæ°´å·ï¼›è¡¥å½•业务信息后生成)
     * æ‰¹æ¬¡å·ï¼ˆè‡ªåŠ¨ç”Ÿæˆï¼šå‰ç¼€ + æ—¥æœŸ + äº§çº¿ç¼–码 + æµæ°´å·ï¼›è¡¥å½•业务信息后生成)
     */
    private String code;
    /**
@@ -43,9 +44,11 @@
     */
    private String tempCode;
    /**
     * å“ç§ï¼ˆäº§å“ç‰©æ–™ï¼‰ç¼–号
     * å“ç§ï¼ˆäº§å“ç‰©æ–™ï¼‰ç¼–号(已废弃)
     * <p>
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO#getId()}
     * <p>
     * å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…({@code mes_pro_bag_code.item_id}),新批次不再写入;仅保留供历史批次追溯回退
     */
    private Long itemId;
    /**
@@ -59,10 +62,14 @@
     */
    private String lineCode;
    /**
     * äº§çº¿åç§°ï¼ˆå†—余列,由 {@link #lineId} å›žå¡«ï¼‰
     * äº§çº¿åç§°ï¼ˆå†—余列,由 {@link #lineId} å›žå¡«ï¼Œé¡µé¢å±•示为「加工单位」)
     */
    private String lineName;
    /**
     * åŠ å·¥ç­ç»„ï¼ˆæ‰‹å·¥å¡«å†™ï¼‰
     */
    private String processingTeam;
    /**
     * ç”Ÿäº§æ—¥æœŸ
     */
    private LocalDateTime produceDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java
@@ -91,9 +91,11 @@
                .likeIfPresent(MesProBagCodeDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesProBagCodeDO::getUuid, reqVO.getUuid())
                .eqIfPresent(MesProBagCodeDO::getPalletId, reqVO.getPalletId())
                .eqIfPresent(MesProBagCodeDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesProBagCodeDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesProBagCodeDO::getCreateTime, reqVO.getCreateTime())
                .orderByAsc(MesProBagCodeDO::getBagNo));
                .orderByDesc(MesProBagCodeDO::getCreateTime)
                .orderByDesc(MesProBagCodeDO::getId));
    }
    /**
@@ -108,6 +110,18 @@
                .set(MesProBagCodeDO::getPalletId, palletId)
                .set(MesProBagCodeDO::getPalletCode, palletCode)
                .in(MesProBagCodeDO::getId, ids));
    }
    /**
     * æ‰˜ç›˜æ­£å¼ç ç”ŸæˆåŽï¼Œå›žå†™å…¶ä¸‹æ‰€æœ‰è¢‹ç çš„冗余托盘码
     *
     * @param palletId   æ‰˜ç›˜ç¼–号
     * @param palletCode æ­£å¼æ‰˜ç›˜ç 
     */
    default void updatePalletCodeByPalletId(Long palletId, String palletCode) {
        update(null, new LambdaUpdateWrapper<MesProBagCodeDO>()
                .set(MesProBagCodeDO::getPalletCode, palletCode)
                .eq(MesProBagCodeDO::getPalletId, palletId));
    }
    /**
@@ -132,10 +146,24 @@
    Integer selectMaxBagNo();
    @Update("<script>UPDATE mes_pro_bag_code SET batch_id = #{batchId}, batch_code = #{batchCode}, update_time = NOW() " +
            "WHERE deleted = 0 AND batch_id IS NULL AND status = #{status} AND id IN " +
            "WHERE deleted = 0 AND batch_id IS NULL AND status IN " +
            "<foreach collection='statuses' item='st' open='(' separator=',' close=')'>#{st}</foreach> AND id IN " +
            "<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
    int bindBatchByIds(@Param("ids") Collection<Long> ids, @Param("batchId") Long batchId,
                       @Param("batchCode") String batchCode, @Param("status") Integer status);
                       @Param("batchCode") String batchCode, @Param("statuses") Collection<Integer> statuses);
    /** æŸ¥è¯¢æ‰˜ç›˜ä¸‹å°šæœªç»‘定批次的袋码(托盘绑定批次时级联录入用) */
    default List<MesProBagCodeDO> selectUnboundListByPalletIds(Collection<Long> palletIds) {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
                .in(MesProBagCodeDO::getPalletId, palletIds)
                .isNull(MesProBagCodeDO::getBatchId)
                .orderByAsc(MesProBagCodeDO::getId));
    }
    /** æ‰¹æ¬¡å†…有效袋码的品种编号集合(忽略历史空品种袋码),用于录入批次时的品种一致性校验 */
    @Select("SELECT DISTINCT item_id FROM mes_pro_bag_code " +
            "WHERE deleted = 0 AND status <> 30 AND batch_id = #{batchId} AND item_id IS NOT NULL")
    List<Long> selectDistinctItemIdsByBatchId(@Param("batchId") Long batchId);
    default List<MesProBagCodeDO> selectListByIdsForUpdate(Collection<Long> ids) {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/batch/MesProBatchMapper.java
@@ -50,10 +50,10 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProBatchDO>()
                .likeIfPresent(MesProBatchDO::getCode, reqVO.getCode())
                .likeIfPresent(MesProBatchDO::getTempCode, reqVO.getTempCode())
                .eqIfPresent(MesProBatchDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesProBatchDO::getLineId, reqVO.getLineId())
                .likeIfPresent(MesProBatchDO::getLineCode, reqVO.getLineCode())
                .likeIfPresent(MesProBatchDO::getLineName, reqVO.getLineName())
                .likeIfPresent(MesProBatchDO::getProcessingTeam, reqVO.getProcessingTeam())
                .eqIfPresent(MesProBatchDO::getOwnerUserId, reqVO.getOwnerUserId())
                .eqIfPresent(MesProBatchDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesProBatchDO::getProduceDate, reqVO.getProduceDate())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -39,6 +40,15 @@
        return selectList(MesProPalletDO::getBatchId, batchId);
    }
    /** æŸ¥è¯¢æœªç»‘定生产批次的临时托盘(无正式托盘码且未作废),供批次新增/补录绑定托盘下拉使用 */
    default List<MesProPalletDO> selectUnboundList() {
        return selectList(new LambdaQueryWrapperX<MesProPalletDO>()
                .isNull(MesProPalletDO::getBatchId)
                .isNull(MesProPalletDO::getCode)
                .eq(MesProPalletDO::getStatus, MesProPalletStatusEnum.IN_USE.getStatus())
                .orderByAsc(MesProPalletDO::getId));
    }
    default PageResult<MesProPalletDO> selectPage(MesProPalletPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProPalletDO>()
                .likeIfPresent(MesProPalletDO::getCode, reqVO.getCode())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -851,12 +851,14 @@
    ErrorCode PRO_BAG_CODE_BIND_PALLET_MISMATCH = new ErrorCode(1_040_752_003, "袋码与托盘不属于同一生产批次,不允许绑定");
    ErrorCode PRO_BAG_CODE_BIND_PALLET_BOUND = new ErrorCode(1_040_752_004, "存在已绑定托盘的袋码,请先解绑");
    ErrorCode PRO_BAG_CODE_UUID_NOT_EXISTS = new ErrorCode(1_040_752_005, "袋码生成组不存在");
    ErrorCode PRO_BAG_CODE_BIND_PALLET_OVER_CAPACITY = new ErrorCode(1_040_752_006, "绑定后袋码数量超过托盘可装袋数");
    ErrorCode PRO_BAG_CODE_REPLACE_BATCH_STATUS_INVALID = new ErrorCode(1_040_752_007, "生产批次状态不允许破损袋码替换");
    ErrorCode PRO_BAG_CODE_REPLACE_BATCH_MISMATCH = new ErrorCode(1_040_752_008, "待替换袋码不属于指定生产批次");
    ErrorCode PRO_BAG_CODE_REPLACE_ALREADY_REPLACED = new ErrorCode(1_040_752_009, "袋码已完成替换,不允许重复替换");
    ErrorCode PRO_BAG_CODE_REPLACE_STATUS_INVALID = new ErrorCode(1_040_752_010, "袋码状态不允许破损替换");
    ErrorCode PRO_BAG_CODE_REPLACE_PALLET_INVALID = new ErrorCode(1_040_752_011, "袋码原托盘不存在或已作废,不允许继承");
    ErrorCode PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED = new ErrorCode(1_040_752_012, "品种主计量单位未配置类型,仅支持 48千克/100千克/吨");
    ErrorCode PRO_BAG_CODE_PALLET_COUNT_REQUIRED = new ErrorCode(1_040_752_013, "该品种按托盘生成袋码,托盘数必须大于 0");
    ErrorCode PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH = new ErrorCode(1_040_752_014, "录入批次的袋码必须属于同一品种");
    // ========== MES ç”Ÿäº§ç®¡ç†-托盘码(1-040-753-000) ==========
    ErrorCode PRO_PALLET_NOT_EXISTS = new ErrorCode(1_040_753_000, "托盘码不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/MesProQrLabelBuilder.java
@@ -118,6 +118,10 @@
        if (StrUtil.isNotEmpty(bagCode.getPalletCode())) {
            keyValues.put("绑定托盘", bagCode.getPalletCode());
        }
        // å“ç§ä¼˜å…ˆå–袋码自身(数据包生成时绑定,未录批次也可打印),历史袋码回退批次品种
        if (StrUtil.isNotEmpty(bagCode.getItemName())) {
            keyValues.put("品种", bagCode.getItemName());
        }
        if (batch != null) {
            fillBatchKeyValues(keyValues, batch, itemMap, userMap);
        }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -137,8 +137,10 @@
        validateItemCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒç‰©æ–™åç§°çš„唯一性
        validateItemNameUnique(reqVO.getId(), reqVO.getName());
        // æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨
        validateItemTypeExists(reqVO.getItemTypeId());
        // æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨ï¼ˆMDM å“ç§åŒæ­¥ä¸ä¼ åˆ†ç±»ï¼Œå…è®¸ä¸ºç©ºï¼‰
        if (reqVO.getItemTypeId() != null) {
            validateItemTypeExists(reqVO.getItemTypeId());
        }
        // æ ¡éªŒè®¡é‡å•位存在
        validateUnitMeasureExists(reqVO.getUnitMeasureId());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRawGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
@@ -31,6 +32,15 @@
     */
    MesProBagCodeGenerateRespVO generateBagCodes(@Valid MesProBagCodeGenerateReqVO generateReqVO);
    /**
     * ç”ŸæˆåŽŸå§‹è¢‹ç ï¼šä»…é€‰å“ç§+袋码数量,不校验计量单位类型、不生成托盘;
     * è¢‹ç å¸¦å“ç§å†—余,后续可正常录入批次/绑定托盘
     *
     * @param rawGenerateReqVO ç”Ÿæˆä¿¡æ¯
     * @return ç”Ÿæˆç»„ UUID ä¸Žæ•°é‡ï¼ˆtype/perPallet/palletIds ä¸ºç©ºï¼‰
     */
    MesProBagCodeGenerateRespVO generateRawBagCodes(@Valid MesProBagCodeRawGenerateReqVO rawGenerateReqVO);
    void bindBatch(@Valid MesProBagCodeBindBatchReqVO bindReqVO);
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
@@ -3,15 +3,20 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeBindBatchReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeBindReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRawGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceItemRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeReplaceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
@@ -23,6 +28,7 @@
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -34,8 +40,13 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -61,17 +72,92 @@
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmUnitMeasureApi unitMeasureApi;
    /**
     * è®¡é‡å•位类型「吨」(无托盘模式)
     */
    private static final String UNIT_TYPE_TON = "吨";
    /**
     * è®¡é‡å•位类型「N千克」:每托盘装 N ä¸ªè¢‹ç 
     */
    private static final Pattern KG_TYPE_PATTERN = Pattern.compile("^(\\d{1,5})千克$");
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProBagCodeGenerateRespVO generateBagCodes(MesProBagCodeGenerateReqVO generateReqVO) {
        if (generateReqVO.getCount() == null || generateReqVO.getCount() <= 0) {
            throw exception(PRO_BAG_CODE_GENERATE_COUNT_INVALID);
        // 1. æ ¡éªŒå“ç§å¹¶è¯»å–主计量单位类型
        MesMdmItemDO item = mdmItemService.validateItemExists(generateReqVO.getItemId());
        String type = getUnitType(item);
        // 2. æŒ‰ç±»åž‹æ¢ç®—袋码数量:N千克 = æ¯æ‰˜ N è¢‹ Ã— æ‰˜ç›˜æ•°ï¼›å¨ = ç›´æŽ¥è¾“入袋码数(无托盘)
        boolean ton = UNIT_TYPE_TON.equals(type);
        int perPallet = ton ? 0 : parseKgPerPallet(type);
        int totalCount;
        int palletCount = 0;
        if (ton) {
            if (generateReqVO.getBagCount() == null || generateReqVO.getBagCount() <= 0) {
                throw exception(PRO_BAG_CODE_GENERATE_COUNT_INVALID);
            }
            totalCount = generateReqVO.getBagCount();
        } else {
            if (generateReqVO.getPalletCount() == null || generateReqVO.getPalletCount() <= 0) {
                throw exception(PRO_BAG_CODE_PALLET_COUNT_REQUIRED);
            }
            palletCount = generateReqVO.getPalletCount();
            totalCount = perPallet * palletCount;
        }
        // ç”Ÿæˆé˜¶æ®µåªåˆ›å»ºå…¨å±€å”¯ä¸€è¢‹ç ï¼Œç”Ÿäº§æ‰¹æ¬¡åœ¨åŽç»­å½•å…¥
        int startNo = bagCodeMapper.selectMaxBagNo() + 1;
        // 3. ç”Ÿæˆé˜¶æ®µï¼šè¢‹ç ç›´æŽ¥ç»‘定品种;生产批次在后续录入
        String uuid = IdUtil.fastSimpleUUID();
        List<MesProPalletDO> pallets = new ArrayList<>(palletCount);
        if (!ton) {
            int startSeq = palletMapper.selectMaxTempCodeSeq() + 1;
            for (int i = 0; i < palletCount; i++) {
                pallets.add(new MesProPalletDO()
                        .setTempCode("PL-" + StrUtil.padPre(String.valueOf(startSeq + i), 8, '0'))
                        .setBagCount(perPallet)
                        .setAutoGenerated(true)
                        .setStatus(MesProPalletStatusEnum.IN_USE.getStatus()));
            }
            palletMapper.insertBatch(pallets);
        }
        int startNo = bagCodeMapper.selectMaxBagNo() + 1;
        List<MesProBagCodeDO> bagCodes = new ArrayList<>(totalCount);
        for (int i = 0; i < totalCount; i++) {
            int bagNo = startNo + i;
            MesProPalletDO pallet = ton ? null : pallets.get(i / perPallet);
            bagCodes.add(new MesProBagCodeDO()
                    .setUuid(uuid)
                    .setCode("BAG-" + StrUtil.padPre(String.valueOf(bagNo), 8, '0'))
                    .setBagNo(bagNo)
                    .setItemId(item.getId())
                    .setItemCode(item.getCode())
                    .setItemName(item.getName())
                    .setPalletId(pallet == null ? null : pallet.getId())
                    .setPalletCode(pallet == null ? null : pallet.getTempCode())
                    .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
                    .setRemark(generateReqVO.getRemark()));
        }
        bagCodeMapper.insertBatch(bagCodes);
        return new MesProBagCodeGenerateRespVO()
                .setUuid(uuid)
                .setType(type)
                .setPerPallet(perPallet)
                .setTotalCount(totalCount)
                .setPalletIds(pallets.stream().map(MesProPalletDO::getId).toList());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProBagCodeGenerateRespVO generateRawBagCodes(MesProBagCodeRawGenerateReqVO generateReqVO) {
        // åŽŸå§‹è¢‹ç ä¸ä¾èµ–è®¡é‡å•ä½ç±»åž‹ä¸Žæ‰˜ç›˜ï¼šä»…æŒ‰æ•°é‡ç”Ÿæˆï¼Œè¢‹ç å¸¦å“ç§å†—ä½™ï¼ŒåŽç»­å¯æ­£å¸¸å½•å…¥æ‰¹æ¬¡/绑定托盘
        MesMdmItemDO item = mdmItemService.validateItemExists(generateReqVO.getItemId());
        String uuid = IdUtil.fastSimpleUUID();
        int startNo = bagCodeMapper.selectMaxBagNo() + 1;
        List<MesProBagCodeDO> bagCodes = new ArrayList<>(generateReqVO.getCount());
        for (int i = 0; i < generateReqVO.getCount(); i++) {
            int bagNo = startNo + i;
@@ -79,11 +165,48 @@
                    .setUuid(uuid)
                    .setCode("BAG-" + StrUtil.padPre(String.valueOf(bagNo), 8, '0'))
                    .setBagNo(bagNo)
                    .setItemId(item.getId())
                    .setItemCode(item.getCode())
                    .setItemName(item.getName())
                    .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
                    .setRemark(generateReqVO.getRemark()));
        }
        bagCodeMapper.insertBatch(bagCodes);
        return new MesProBagCodeGenerateRespVO().setUuid(uuid);
        return new MesProBagCodeGenerateRespVO()
                .setUuid(uuid)
                .setTotalCount(bagCodes.size())
                .setPalletIds(List.of());
    }
    /**
     * è¯»å–品种主计量单位的类型(48千克/100千克/吨),未配置或无法识别时报错
     */
    private String getUnitType(MesMdmItemDO item) {
        String type = null;
        if (item.getUnitMeasureId() != null) {
            CommonResult<MdmUnitMeasureRespDTO> result = unitMeasureApi.getUnitMeasure(item.getUnitMeasureId());
            if (result != null && result.isSuccess() && result.getData() != null) {
                type = StrUtil.trimToNull(result.getData().getType());
            }
        }
        if (type == null) {
            throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
        }
        if (!UNIT_TYPE_TON.equals(type) && !KG_TYPE_PATTERN.matcher(type).matches()) {
            throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
        }
        return type;
    }
    /**
     * è§£æžã€ŒN千克」类型得出每托盘袋数
     */
    private int parseKgPerPallet(String type) {
        Matcher matcher = KG_TYPE_PATTERN.matcher(type);
        if (!matcher.matches()) {
            throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
        }
        return Integer.parseInt(matcher.group(1));
    }
    @Override
@@ -103,15 +226,60 @@
        List<Long> ids = bindReqVO.getIds().stream().distinct().toList();
        List<MesProBagCodeDO> bagCodes = bagCodeMapper.selectListByIdsForUpdate(ids);
        if (bagCodes.size() != ids.size() || bagCodes.stream().anyMatch(code ->
                code.getBatchId() != null || !MesProBagCodeStatusEnum.GENERATED.getStatus().equals(code.getStatus()))) {
                code.getBatchId() != null
                        || (!MesProBagCodeStatusEnum.GENERATED.getStatus().equals(code.getStatus())
                        && !MesProBagCodeStatusEnum.PRINTED.getStatus().equals(code.getStatus())))) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        // å½•入的袋码必须属于同一品种,且与批次已有袋码品种一致(历史空白袋码 item ä¸ºç©ºä¸å‚与校验)
        Set<Long> itemIds = new LinkedHashSet<>();
        bagCodes.stream().map(MesProBagCodeDO::getItemId).filter(Objects::nonNull).forEach(itemIds::add);
        itemIds.addAll(bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId()));
        if (itemIds.size() > 1) {
            throw exception(PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH);
        }
        if (bagCodeMapper.bindBatchByIds(ids, batch.getId(), batch.getCode(),
                MesProBagCodeStatusEnum.GENERATED.getStatus()) != ids.size()) {
                List.of(MesProBagCodeStatusEnum.GENERATED.getStatus(),
                        MesProBagCodeStatusEnum.PRINTED.getStatus())) != ids.size()) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
                .setBagQuantity(bagCodeMapper.selectEffectiveCountByBatchId(batch.getId())));
        // çº§è”:本组袋码绑定的未入批次托盘,分配正式托盘码并绑定批次
        bindPalletsToBatch(bagCodes, batch);
    }
    /**
     * è¢‹ç å½•入批次时,将其绑定的临时托盘(未绑定批次)级联绑定到同一批次:
     * åˆ†é…æ­£å¼æ‰˜ç›˜ç ï¼ˆæ‰¹æ¬¡å·-P序号)、回写袋码上的冗余托盘码、累计批次托盘数
     */
    private void bindPalletsToBatch(List<MesProBagCodeDO> bagCodes, MesProBatchDO batch) {
        List<Long> palletIds = bagCodes.stream().map(MesProBagCodeDO::getPalletId)
                .filter(Objects::nonNull).distinct().sorted().toList();
        if (palletIds.isEmpty()) {
            return;
        }
        int nextPalletNo = palletMapper.selectMaxPalletNo(batch.getId()) + 1;
        int boundCount = 0;
        for (Long palletId : palletIds) {
            MesProPalletDO pallet = palletMapper.selectByIdForUpdate(palletId);
            if (pallet == null || pallet.getBatchId() != null || StrUtil.isNotEmpty(pallet.getCode())) {
                continue;
            }
            String palletCode = batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(nextPalletNo++), 2, '0');
            palletMapper.updateById(new MesProPalletDO().setId(palletId)
                    .setCode(palletCode)
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(nextPalletNo - 1));
            // è¢‹ç ä¸Šçš„冗余托盘码由临时码更新为正式码
            bagCodeMapper.updatePalletCodeByPalletId(palletId, palletCode);
            boundCount++;
        }
        if (boundCount > 0) {
            batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
                    .setPalletQuantity((batch.getPalletQuantity() == null ? 0 : batch.getPalletQuantity()) + boundCount));
        }
    }
    @Override
@@ -175,6 +343,9 @@
                    .setBagNo(newBagNo)
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setItemId(oldBagCode.getItemId())
                    .setItemCode(oldBagCode.getItemCode())
                    .setItemName(oldBagCode.getItemName())
                    .setPalletId(pallet == null ? null : pallet.getId())
                    .setPalletCode(pallet == null ? null : pallet.getCode())
                    .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
@@ -234,17 +405,26 @@
        if (CollUtil.isEmpty(bagCodes)) {
            throw exception(PRO_BAG_CODE_UUID_NOT_EXISTS);
        }
        // 2. æ ¡éªŒå‡ä¸ºå·²ç”ŸæˆçŠ¶æ€ä¸”æœªç»‘å®šæ‰˜ç›˜
        // 2. æ ¡éªŒå‡ä¸ºå·²ç”ŸæˆçŠ¶æ€
        if (bagCodes.stream().anyMatch(bagCode ->
                !MesProBagCodeStatusEnum.GENERATED.getStatus().equals(bagCode.getStatus()))) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        if (bagCodes.stream().anyMatch(bagCode -> bagCode.getPalletId() != null)) {
            throw exception(PRO_BAG_CODE_BIND_PALLET_BOUND);
        // 3. ç»‘定的托盘仅限"自动生成且未绑定批次"(生成数据包时自动绑定),级联删除;已入批次的托盘不允许随包删除
        Set<Long> palletIds = new LinkedHashSet<>();
        bagCodes.stream().map(MesProBagCodeDO::getPalletId).filter(Objects::nonNull).forEach(palletIds::add);
        for (Long palletId : palletIds) {
            MesProPalletDO pallet = palletMapper.selectById(palletId);
            if (pallet != null && (pallet.getBatchId() != null || StrUtil.isNotEmpty(pallet.getCode()))) {
                throw exception(PRO_BAG_CODE_BIND_PALLET_BOUND);
            }
        }
        // 3. åˆ é™¤ç”Ÿæˆç»„
        // 4. åˆ é™¤ç”Ÿæˆç»„与其自动生成的临时托盘
        bagCodeMapper.deleteByIds(bagCodes.stream().map(MesProBagCodeDO::getId).toList());
        // 4. å›žå‡æ‰¹æ¬¡è¢‹ç æ•°é‡
        if (!palletIds.isEmpty()) {
            palletMapper.deleteByIds(palletIds);
        }
        // 5. å›žå‡æ‰¹æ¬¡è¢‹ç æ•°é‡
        MesProBatchDO batch = batchMapper.selectById(bagCodes.get(0).getBatchId());
        if (batch != null && batch.getBagQuantity() != null) {
            batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
@@ -262,13 +442,8 @@
        }
        // 2. é”å®šå¹¶æ ¡éªŒè¢‹ç 
        List<MesProBagCodeDO> bagCodes = validateBagCodesForBind(bindReqVO.getIds(), pallet, true);
        // 3. æ ¡éªŒä¸è¶…过托盘可装袋数
        // 3. ç»‘定袋码并累加托盘已装袋数(托盘无容量上限)
        int currentCount = pallet.getBagCount() == null ? 0 : pallet.getBagCount();
        if (pallet.getCapacity() != null && pallet.getCapacity() > 0
                && currentCount + bagCodes.size() > pallet.getCapacity()) {
            throw exception(PRO_BAG_CODE_BIND_PALLET_OVER_CAPACITY);
        }
        // 4. ç»‘定袋码并累加托盘已装袋数
        bagCodeMapper.bindPalletByIds(bindReqVO.getIds(), pallet.getId(), pallet.getCode());
        palletService.updatePalletBagCount(pallet.getId(), currentCount + bagCodes.size());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
@@ -7,7 +7,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.line.MesProLineDO;
@@ -18,8 +18,8 @@
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.line.MesProLineService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
@@ -59,11 +59,12 @@
    private MesProPalletMapper palletMapper;
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesProLineService lineService;
    @Resource
@@ -72,27 +73,30 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createBatch(MesProBatchSaveReqVO createReqVO) {
        // 1. æ ¡éªŒå…³è”数据(品种、产线、生产人员)
        MesMdmItemDO item = mdmItemService.validateItemExists(createReqVO.getItemId());
        // 1. æ ¡éªŒå…³è”数据(产线、生产人员)
        MesProLineDO line = lineService.validateLineExistsAndEnable(createReqVO.getLineId());
        validateRelations(createReqVO);
        // 2. ç”Ÿæˆæ‰¹æ¬¡å·ï¼ˆç¼–码规则:前缀 + æ—¥æœŸ + äº§çº¿ç¼–码 + å“ç§ç¼–码 + æµæ°´å·ï¼‰
        String code = generateBatchCode(line.getCode(), item.getCode());
        // 2. ç”Ÿæˆæ‰¹æ¬¡å·ï¼ˆç¼–码规则:前缀 + æ—¥æœŸ + äº§çº¿ç¼–码 + æµæ°´å·ï¼‰
        String code = generateBatchCode(line.getCode());
        // 3. æ’入批次(产线编码/名称冗余回填)
        // 3. æ’入批次(产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(createReqVO.getPalletIds());
        MesProBatchDO batch = BeanUtils.toBean(createReqVO, MesProBatchDO.class)
                .setCode(code)
                .setLineCode(line.getCode())
                .setLineName(line.getName())
                .setBagQuantity(0)
                .setPalletQuantity(0)
                .setStatus(MesProBatchStatusEnum.DRAFT.getStatus());
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.insert(batch);
        // 4. æ’入生产人员配置
        insertBatchWorkers(batch.getId(), createReqVO.getWorkers());
        // 5. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦
        // 5. ç»‘定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(batch.getId(), createReqVO.getPalletIds());
        // 6. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦
        integrityService.recordBatchIntegrity(batch.getId());
        return batch.getId();
    }
@@ -130,21 +134,24 @@
            throw exception(PRO_BATCH_ALREADY_FILLED);
        }
        // 2. æ ¡éªŒå…³è”数据并生成正式批次号
        MesMdmItemDO item = mdmItemService.validateItemExists(fillReqVO.getItemId());
        MesProLineDO line = lineService.validateLineExistsAndEnable(fillReqVO.getLineId());
        validateRelations(fillReqVO);
        String code = generateBatchCode(line.getCode(), item.getCode());
        // 3. è½åº“业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填)
        String code = generateBatchCode(line.getCode());
        // 3. è½åº“业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(fillReqVO.getPalletIds());
        MesProBatchDO updateObj = BeanUtils.toBean(fillReqVO, MesProBatchDO.class)
                .setCode(code)
                .setLineCode(line.getCode())
                .setLineName(line.getName())
                .setStatus(MesProBatchStatusEnum.DRAFT.getStatus());
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.updateById(updateObj);
        // 4. é‡å»ºç”Ÿäº§äººå‘˜é…ç½®
        batchWorkerMapper.deleteByBatchId(fillReqVO.getId());
        insertBatchWorkers(fillReqVO.getId(), fillReqVO.getWorkers());
        // 5. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦ï¼ˆè¡¥å½•正式信息后覆盖)
        // 5. ç»‘定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(fillReqVO.getId(), fillReqVO.getPalletIds());
        // 6. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦ï¼ˆè¡¥å½•正式信息后覆盖)
        integrityService.recordBatchIntegrity(fillReqVO.getId());
    }
@@ -157,29 +164,33 @@
        // 2. æ ¡éªŒå…³è”数据(生产人员)
        validateRelations(updateReqVO);
        // 3. æœªç”Ÿæˆæ­£å¼æ‰¹æ¬¡å·æˆ–品种/产线变化时重新生成批次号(批次号包含产线编码 + å“ç§ç¼–码要素)
        // 3. æœªç”Ÿæˆæ­£å¼æ‰¹æ¬¡å·æˆ–产线变化时重新生成批次号(批次号包含产线编码要素)
        boolean codeEmpty = StrUtil.isEmpty(oldBatch.getCode());
        boolean itemChanged = !Objects.equals(oldBatch.getItemId(), updateReqVO.getItemId());
        boolean lineChanged = !Objects.equals(oldBatch.getLineId(), updateReqVO.getLineId());
        String code = oldBatch.getCode();
        MesProBatchDO updateObj = BeanUtils.toBean(updateReqVO, MesProBatchDO.class);
        if (codeEmpty || itemChanged || lineChanged) {
            MesMdmItemDO item = mdmItemService.validateItemExists(updateReqVO.getItemId());
        if (codeEmpty || lineChanged) {
            // å˜æ›´äº§çº¿æ—¶ï¼Œç›®æ ‡äº§çº¿å¿…须启用;未变更则仅校验存在(避免停用产线的历史批次无法编辑其他字段)
            MesProLineDO line = (lineChanged || codeEmpty)
                    ? lineService.validateLineExistsAndEnable(updateReqVO.getLineId())
                    : lineService.validateLineExists(updateReqVO.getLineId());
            code = generateBatchCode(line.getCode(), item.getCode());
            code = generateBatchCode(line.getCode());
            updateObj.setLineCode(line.getCode()).setLineName(line.getName());
        }
        // 4. æ›´æ–°æ‰¹æ¬¡
        // 4. æ›´æ–°æ‰¹æ¬¡ï¼ˆå‹¾é€‰ç»‘定托盘时批次直接进入生产中;取消勾选不回退,已绑定托盘不可解绑)
        boolean bindPallets = CollUtil.isNotEmpty(updateReqVO.getPalletIds());
        updateObj.setCode(code);
        if (bindPallets) {
            updateObj.setStatus(MesProBatchStatusEnum.PRODUCING.getStatus());
        }
        batchMapper.updateById(updateObj);
        // 5. é‡å»ºç”Ÿäº§äººå‘˜é…ç½®
        batchWorkerMapper.deleteByBatchId(updateReqVO.getId());
        insertBatchWorkers(updateReqVO.getId(), updateReqVO.getWorkers());
        // 6. æ‘˜è¦è¦†ç›–范围含品种/产线/生产日期/计划数量,编辑后重新生成避免误报篡改
        // 6. ç»‘定新增勾选的托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(updateReqVO.getId(), updateReqVO.getPalletIds());
        // 7. æ‘˜è¦è¦†ç›–范围含品种/产线/生产日期/计划数量,编辑后重新生成避免误报篡改
        integrityService.recordBatchIntegrity(updateReqVO.getId());
    }
@@ -283,7 +294,7 @@
    /**
     * æ ¡éªŒå…³è”数据(生产人员:责任人 + ç”Ÿäº§äººå‘˜é…ç½®ï¼‰
     * <p>
     * å“ç§ã€äº§çº¿çš„æ ¡éªŒåœ¨åˆ›å»º/更新流程中内联执行(需取编码参与批次号生成)
     * äº§çº¿çš„æ ¡éªŒåœ¨åˆ›å»º/更新流程中内联执行(需取编码参与批次号生成)
     */
    private void validateRelations(MesProBatchSaveReqVO reqVO) {
        Set<Long> userIds = new HashSet<>();
@@ -293,16 +304,29 @@
    }
    /**
     * æ‰¹æ¬¡ç»‘定托盘(可选):同事务内复用托盘绑定批次逻辑,
     * ç”Ÿæˆæ­£å¼æ‰˜ç›˜ç å¹¶çº§è”录入托盘下未绑批次的袋码,失败整体回滚
     */
    private void bindPalletsToBatch(Long batchId, List<Long> palletIds) {
        if (CollUtil.isEmpty(palletIds)) {
            return;
        }
        MesProPalletBindBatchReqVO bindReqVO = new MesProPalletBindBatchReqVO();
        bindReqVO.setBatchId(batchId);
        bindReqVO.setIds(palletIds);
        palletService.bindBatch(bindReqVO);
    }
    /**
     * ç”Ÿæˆæ‰¹æ¬¡å·
     * <p>
     * ç¼–码规则:固定前缀(PC) + æ—¥æœŸ(yyyyMMdd) + è¾“入字符(产线编码 + å“ç§ç¼–码) + æµæ°´å·(按日循环)
     * ç¼–码规则:固定前缀(PC) + æ—¥æœŸ(yyyyMMdd) + è¾“入字符(产线编码) + æµæ°´å·(按日循环)
     *
     * @param lineCode äº§çº¿ç¼–码
     * @param itemCode å“ç§ï¼ˆç‰©æ–™ï¼‰ç¼–码
     * @return æ‰¹æ¬¡å·
     */
    private String generateBatchCode(String lineCode, String itemCode) {
        String inputChar = StrUtil.emptyToDefault(lineCode, "") + StrUtil.emptyToDefault(itemCode, "");
    private String generateBatchCode(String lineCode) {
        String inputChar = StrUtil.emptyToDefault(lineCode, "");
        return autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_BATCH_CODE.getCode(), inputChar);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletService.java
@@ -90,6 +90,13 @@
    List<MesProPalletDO> getPalletListByBatchId(Long batchId);
    /**
     * èŽ·å¾—æœªç»‘å®šç”Ÿäº§æ‰¹æ¬¡çš„ä¸´æ—¶æ‰˜ç›˜åˆ—è¡¨ï¼ˆä¾›æ‰¹æ¬¡æ–°å¢ž/补录绑定托盘下拉使用)
     *
     * @return æ‰˜ç›˜ç åˆ—表
     */
    List<MesProPalletDO> getUnboundPalletList();
    /**
     * èŽ·å¾—æ‰˜ç›˜ç åˆ—è¡¨ï¼ˆæŒ‰ç¼–å·åˆ—è¡¨ï¼‰
     *
     * @param ids ç¼–号列表
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletServiceImpl.java
@@ -7,11 +7,13 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletPreGenerateReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import jakarta.annotation.Resource;
@@ -22,7 +24,9 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -71,7 +75,6 @@
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(palletNo)
                    .setCapacity(createReqVO.getCapacity())
                    .setBagCount(0)
                    .setAutoGenerated(false)
                    .setStatus(MesProPalletStatusEnum.IN_USE.getStatus())
@@ -95,7 +98,6 @@
        for (int i = 0; i < reqVO.getCount(); i++) {
            list.add(new MesProPalletDO()
                    .setTempCode("PL-" + StrUtil.padPre(String.valueOf(startSeq + i), 8, '0'))
                    .setCapacity(reqVO.getCapacity())
                    .setBagCount(0)
                    .setAutoGenerated(false)
                    .setStatus(MesProPalletStatusEnum.IN_USE.getStatus())
@@ -136,7 +138,27 @@
            }
            pallets.add(pallet);
        }
        // 3. åˆ†é…æ­£å¼æ‰˜ç›˜ç ï¼šæ‰¹æ¬¡å· + -P + 2 ä½æ‰˜ç›˜åºå·ï¼ˆå«é€»è¾‘删除记录参与序号,保证不回退、码值唯一)
        // 3. çº§è”准备:托盘下未绑批次的袋码随托盘一起录入批次,品种须与批次已有袋码一致
        List<MesProBagCodeDO> bags = bagCodeMapper.selectUnboundListByPalletIds(ids);
        List<Long> bindBagIds = new ArrayList<>();
        Set<Long> bagItemIds = new LinkedHashSet<>();
        for (MesProBagCodeDO bag : bags) {
            if (!MesProBagCodeStatusEnum.GENERATED.getStatus().equals(bag.getStatus())
                    && !MesProBagCodeStatusEnum.PRINTED.getStatus().equals(bag.getStatus())) {
                continue;
            }
            bindBagIds.add(bag.getId());
            if (bag.getItemId() != null) {
                bagItemIds.add(bag.getItemId());
            }
        }
        if (!bindBagIds.isEmpty()) {
            bagItemIds.addAll(bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId()));
            if (bagItemIds.size() > 1) {
                throw exception(PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH);
            }
        }
        // 4. åˆ†é…æ­£å¼æ‰˜ç›˜ç ï¼šæ‰¹æ¬¡å· + -P + 2 ä½æ‰˜ç›˜åºå·ï¼ˆå«é€»è¾‘删除记录参与序号,保证不回退、码值唯一)
        int startNo = palletMapper.selectMaxPalletNo(batch.getId()) + 1;
        for (int i = 0; i < pallets.size(); i++) {
            int palletNo = startNo + i;
@@ -146,10 +168,20 @@
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(palletNo));
            // æ‰˜ç›˜ä¸‹è¢‹ç çš„冗余托盘码由临时码更新为正式码
            bagCodeMapper.updatePalletCodeByPalletId(pallets.get(i).getId(),
                    batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(palletNo), 2, '0'));
        }
        // 4. å›žå†™æ‰¹æ¬¡æ‰˜ç›˜ç æ•°é‡
        // 5. æ‰˜ç›˜ä¸‹è¢‹ç æ‰¹é‡ç»‘定本批次
        if (!bindBagIds.isEmpty() && bagCodeMapper.bindBatchByIds(bindBagIds, batch.getId(), batch.getCode(),
                List.of(MesProBagCodeStatusEnum.GENERATED.getStatus(),
                        MesProBagCodeStatusEnum.PRINTED.getStatus())) != bindBagIds.size()) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        // 6. å›žå†™æ‰¹æ¬¡æ‰˜ç›˜ç æ•°é‡ä¸Žæœ‰æ•ˆè¢‹ç æ•°é‡
        batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
                .setPalletQuantity((batch.getPalletQuantity() == null ? 0 : batch.getPalletQuantity()) + pallets.size()));
                .setPalletQuantity((batch.getPalletQuantity() == null ? 0 : batch.getPalletQuantity()) + pallets.size())
                .setBagQuantity(bagCodeMapper.selectEffectiveCountByBatchId(batch.getId())));
        return pallets.size();
    }
@@ -206,6 +238,11 @@
    }
    @Override
    public List<MesProPalletDO> getUnboundPalletList() {
        return palletMapper.selectUnboundList();
    }
    @Override
    public List<MesProPalletDO> getPalletListByIds(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return List.of();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -107,7 +107,7 @@
                throw exception(MES_TRACE_CODE_NOT_FOUND);
            }
            // 2. èšåˆå…¨ç¨‹ä¿¡æ¯
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId);
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId, entry.bagItemId);
            recordConsumerScan(rawCode, traceCode, entry.sourceType, entry.batchId, true);
            return result;
        } catch (RuntimeException ex) {
@@ -167,24 +167,24 @@
        // â‘  è¢‹ç 
        MesProBagCodeDO bag = bagCodeService.getBagCodeByCode(traceCode);
        if (bag != null) {
            return new TraceEntry("BAG", bag.getBatchId());
            return new TraceEntry("BAG", bag.getBatchId(), bag.getItemId());
        }
        // â‘¡ æ‰˜ç›˜ç 
        MesProPalletDO pallet = palletService.getPalletByCode(traceCode);
        if (pallet != null) {
            return new TraceEntry("PALLET", pallet.getBatchId());
            return new TraceEntry("PALLET", pallet.getBatchId(), null);
        }
        // â‘¢ æ‰¹æ¬¡ç ï¼ˆå«ä¸´æ—¶æ‰¹æ¬¡ç ï¼ŒgetBatchByCode å†…部兼容)
        MesProBatchDO batch = batchService.getBatchByCode(traceCode);
        if (batch != null) {
            return new TraceEntry("BATCH", batch.getId());
            return new TraceEntry("BATCH", batch.getId(), null);
        }
        return new TraceEntry("", null);
        return new TraceEntry("", null, null);
    }
    // ==================== èšåˆæž„建 ====================
    private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, String sourceType, Long batchId) {
    private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, String sourceType, Long batchId, Long bagItemId) {
        MesProBatchDO batch = batchService.getBatch(batchId);
        if (batch == null) {
            throw exception(MES_TRACE_CODE_NOT_FOUND);
@@ -193,7 +193,7 @@
        resp.setSourceType(sourceType);
        resp.setRequestCode(requestCode);
        // å“ç§åŒºå—
        fillItem(resp, batch);
        fillItem(resp, batch, bagItemId);
        // æ‰¹æ¬¡åŒºå—
        resp.setBatchCode(batch.getCode()).setTempCode(batch.getTempCode())
                .setBatchStatus(batch.getStatus()).setBatchStatusName(batchStatusName(batch.getStatus()))
@@ -220,11 +220,20 @@
        return resp;
    }
    private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch) {
        if (batch.getItemId() == null) {
    /**
     * å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…:优先取扫码袋码品种,历史批次回退批次品种,批次/托盘入口取批内任一袋码品种
     */
    private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch, Long bagItemId) {
        Long itemId = bagItemId != null ? bagItemId : batch.getItemId();
        if (itemId == null) {
            List<MesProBagCodeDO> bags = bagCodeService.getBagCodeListByBatchId(batch.getId());
            itemId = bags == null ? null : bags.stream().map(MesProBagCodeDO::getItemId)
                    .filter(id -> id != null).findFirst().orElse(null);
        }
        if (itemId == null) {
            return;
        }
        MesMdmItemDO item = mdmItemService.getItem(batch.getItemId());
        MesMdmItemDO item = mdmItemService.getItem(itemId);
        if (item == null) {
            return;
        }
@@ -458,7 +467,7 @@
        };
    }
    private record TraceEntry(String sourceType, Long batchId) {
    private record TraceEntry(String sourceType, Long batchId, Long bagItemId) {
    }
}
yudao-server/src/main/resources/static/trace.html
@@ -229,10 +229,6 @@
  var CODE_INPUT = document.getElementById('codeInput');
  var SEARCH_BTN = document.getElementById('searchBtn');
  var BATCH_STATUS = { 10: ['草稿', 'gray'], 20: ['生产中', 'blue'], 30: ['已完成', 'green'], 40: ['已作废', 'red'] };
  var BAG_STATUS = { 10: ['已生成', 'gray'], 20: ['已导出印刷', 'blue'], 30: ['已作废', 'red'] };
  var QUALITY_RESULT = { 0: ['处理中', 'gray'], 1: ['合格', 'green'], 2: ['不合格', 'red'] };
  var WORKER_TYPE = { 10: '组长', 20: '操作工', 30: '包装工', 40: '质检员', 50: '其他' };
  var FLOW_DIRECTION = { 1: ['入库', 'green'], 2: ['出库', 'blue'] };
  var FLOW_BIZ_TYPE = {
    110: '采购入库', 111: '采购入库取消', 112: '调拨出库', 113: '调拨入库',
@@ -327,12 +323,6 @@
    var html = '';
    html += buildVerifyCard(d);
    html += buildItemCard(d);
    html += buildBatchCard(d);
    html += buildQualityCard(d);
    html += buildWorkerCard(d);
    html += buildInboundCard(d);
    html += buildPalletCard(d);
    html += buildBagCard(d);
    html += buildOptionalCard(d);
    STATE.innerHTML = html || '<div class="empty-tip">暂无追溯数据</div>';
    window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -394,117 +384,7 @@
    );
  }
  // åŒºå—2:生产批次
  function buildBatchCard(d) {
    if (!d.batchCode && !d.tempCode) return '';
    var status = statusBadge(BATCH_STATUS, d.batchStatus);
    return card('生产批次', status,
      kv('批次号', text(d.batchCode)) +
      kv('状态', status) +
      kv('生产日期', fmtDate(d.produceDate)) +
      kv('产线', text(d.lineName ? (d.lineName + (d.lineCode ? ' / ' + d.lineCode : '')) : d.lineCode)) +
      kv('责任人', text(d.ownerUserName)) +
      kv('计划数量', fmtNum(d.planQuantity) === '-' ? '-' : fmtNum(d.planQuantity)) +
      kv('已生成袋码', fmtNum(d.bagQuantity)) +
      kv('已生成托盘', fmtNum(d.palletQuantity)) +
      kv('入库时间', fmtDate(d.inboundTime)) +
      (d.remark ? kv('备注', text(d.remark)) : ''),
      { collapsible: true, open: true }
    );
  }
  // åŒºå—3:种子质检
  function buildQualityCard(d) {
    var q = d.quality;
    if (!q) return '';
    var result = QUALITY_RESULT[q.result];
    var badge = result
      ? '<span class="badge-big ' + result[1] + '">' + esc(result[0]) + '</span>'
      : '<span class="badge-big gray">未知</span>';
    var rows = '';
    if (q.indicators && q.indicators.length) {
      rows = '<table class="mini-table"><thead><tr><th>指标</th><th>检测值</th><th>标准范围</th><th>结果</th></tr></thead><tbody>';
      q.indicators.forEach(function (i) {
        var range = (i.minValue !== null && i.minValue !== undefined && i.maxValue !== null && i.maxValue !== undefined)
          ? esc(i.minValue) + ' ~ ' + esc(i.maxValue) : '-';
        var pass = (i.pass === true)
          ? '<span class="badge green">合格</span>'
          : (i.pass === false ? '<span class="badge red">不合格</span>' : '<span class="badge gray">--</span>');
        rows += '<tr><td>' + text(i.indicatorName) + '</td><td>' + text(i.value) + '</td><td>' + range + '</td><td>' + pass + '</td></tr>';
      });
      rows += '</tbody></table>';
    }
    return card('种子质检', badge,
      kv('检验结论', badge) +
      kv('检验员', text(q.inspectorName)) +
      kv('检验时间', fmtDate(q.inspectedAt)) +
      (q.reason ? kv('不合格原因', text(q.reason)) : '') +
      (rows ? '<div style="margin-top:8px">' + rows + '</div>' : ''),
      { collapsible: true, open: false }
    );
  }
  // åŒºå—4:生产人员
  function buildWorkerCard(d) {
    if (isEmptyList(d.workers)) return '';
    var html = '<div class="pill-list">';
    d.workers.forEach(function (w) {
      var name = w.userName || '-';
      var type = w.workerTypeName || (WORKER_TYPE[w.workerType] || '');
      var color = { 10: 'var(--blue)', 20: 'var(--green)', 30: 'var(--primary-light)', 40: 'var(--red)', 50: 'var(--gray)' }[w.workerType] || 'var(--gray)';
      html += '<span class="pill"><span class="dot" style="background:' + color + '"></span>' +
        esc(type) + ':' + esc(name) + '</span>';
    });
    html += '</div>';
    return card('生产人员', null, html, { collapsible: true, open: false });
  }
  // åŒºå—5:扫码入库事件
  function buildInboundCard(d) {
    var e = d.inbound;
    if (!e) return '';
    return card('扫码入库', null,
      kv('事件号', text(e.eventId)) +
      kv('入库数量', fmtNum(e.quantity)) +
      kv('设备编码', text(e.deviceCode)) +
      kv('产线', text(e.lineCode)) +
      kv('入库时间', fmtDate(e.eventTime)),
      { collapsible: true, open: false }
    );
  }
  // åŒºå—6:托盘
  function buildPalletCard(d) {
    if (isEmptyList(d.pallets)) return '';
    var countHtml = '共 ' + esc(d.palletCount || d.pallets.length) + ' ä¸ª';
    var rows = '<table class="mini-table"><thead><tr><th>托盘码</th><th>序号</th><th>已装/可装</th></tr></thead><tbody>';
    d.pallets.forEach(function (p) {
      rows += '<tr><td>' + text(p.code) + '</td><td>' + fmtNum(p.palletNo) + '</td><td>' +
        fmtNum(p.bagCount) + ' / ' + fmtNum(p.capacity) + '</td></tr>';
    });
    rows += '</tbody></table>';
    return card('托盘信息', countHtml, rows, { collapsible: true, open: false });
  }
  // åŒºå—7:袋码(一袋一码)
  function buildBagCard(d) {
    if (d.bagTotal === null || d.bagTotal === undefined || d.bagTotal === 0) return '';
    var countHtml = '共 ' + esc(d.bagTotal) + ' ä¸ª' + (d.bags && d.bags.length < d.bagTotal ? '(展示前 ' + esc(d.bags.length) + ' ä¸ªï¼‰' : '');
    var rows = '<table class="mini-table"><thead><tr><th>袋码</th><th>袋号</th><th>托盘</th><th>状态</th></tr></thead><tbody>';
    (d.bags || []).forEach(function (b) {
      rows += '<tr><td>' + text(b.code) + '</td><td>' + fmtNum(b.bagNo) + '</td><td>' + text(b.palletCode) + '</td><td>' +
        statusBadge(BAG_STATUS, b.status) + '</td></tr>';
    });
    rows += '</tbody></table>';
    return card('袋码信息', countHtml,
      '<div class="scroll-list">' + rows + '</div>',
      { collapsible: true, open: false }
    );
  }
  // åŒºå—8:可选扩展(采购来源 / ç§å­æµå‘信息(销售出库) / åŠ å·¥æµè½¬ï¼‰ï¼Œæœ‰æ•°æ®æ‰å±•ç¤º
  // åŒºå—2:可选扩展(采购来源 / ç§å­æµå‘信息(仅销售出库) / åŠ å·¥æµè½¬ï¼‰ï¼Œæœ‰æ•°æ®æ‰å±•ç¤º
  function buildOptionalCard(d) {
    var html = '';
    if (!isEmptyList(d.purchaseSources)) {
@@ -515,9 +395,11 @@
      rows += '</tbody></table>';
      html += card('采购来源', null, rows, { collapsible: true, open: false });
    }
    if (!isEmptyList(d.seedFlows)) {
    // åªå±•示销售出库(bizType=119)的流水,生产入库/产品产出等不展示
    var salesFlows = (d.seedFlows || []).filter(function (f) { return f.bizType === 119; });
    if (!isEmptyList(salesFlows)) {
      var rows2 = '<table class="mini-table"><thead><tr><th>方向</th><th>业务</th><th>来源单号</th><th>客户</th><th>数量</th><th>时间</th><th>仓库/库区</th></tr></thead><tbody>';
      d.seedFlows.forEach(function (f) {
      salesFlows.forEach(function (f) {
        var qty = (f.quantity === null || f.quantity === undefined) ? '-' : fmtNum(Math.abs(Number(f.quantity)));
        var wh = [f.warehouseName, f.locationName].filter(Boolean).join(' / ') || '-';
        rows2 += '<tr><td>' + statusBadge(FLOW_DIRECTION, f.type) + '</td><td>' + text(FLOW_BIZ_TYPE[f.bizType] || f.bizType) +