3 天以前 ac2545f3cd01f3061a107e4980fc6fa59eaeaa6b
feat(mes): 新增袋码破损替换功能

- 添加袋码替换相关的错误码定义
- 在数据库表mes_pro_bag_code中增加作废时间和作废原因字段
- 创建袋码替换记录表mes_pro_bag_code_replace用于追踪替换关系
- 添加袋码破损替换菜单权限和接口
- 实现replaceBagCodes方法处理袋码替换逻辑
- 更新袋码追溯信息展示替换关系
- 支持已绑定托盘的袋码替换时继承托盘关系
- 添加替换相关的数据访问对象和映射器方法
已修改11个文件
已添加13个文件
867 ■■■■■ 文件已修改
.playwright-mcp/page-2026-08-21T03-59-08-154Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-21T03-59-21-224Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-21T04-00-18-943Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-21T04-00-37-255Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-21T04-00-59-933Z.yml 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-21T04-01-18-956Z.yml 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_pro_bag_code_replacement_integration.md 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_bag_code_replacement.sql 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_batch_bag_code_init.sql 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeReplaceItemRespVO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeReplaceReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeReplaceRespVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRespVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeTraceRespVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeReplaceDO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeReplaceMapper.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/batch/MesProBatchMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-21T03-59-08-154Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-21T03-59-21-224Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-21T04-00-18-943Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-21T04-00-37-255Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-21T04-00-59-933Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
- generic [ref=f3e7]:
  - complementary [ref=f3e9]:
    - generic [ref=f3e10]:
      - generic [ref=f3e11] [cursor=pointer]
      - link "工业AI智造平台 å·¥ä¸šAI智造平台" [ref=f3e16] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工业AI智造平台" [ref=f3e19]
        - generic [ref=f3e20]: å·¥ä¸šAI智造平台
      - menu [ref=f3e23]:
        - menuitem "首页" [ref=f3e24] [cursor=pointer]
        - listitem [ref=f3e29]:
          - generic [ref=f3e30] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f3e38]:
          - generic [ref=f3e39] [cursor=pointer]: ä¸€è¢‹ä¸€ç ç®¡ç†
          - list [ref=f3e349]:
            - menuitem "批次生产管理" [ref=f3e350] [cursor=pointer]
            - menuitem "赋码管理" [ref=f3e355] [cursor=pointer]
            - menuitem "托盘码管理" [ref=f3e361] [cursor=pointer]
            - menuitem "产线维护" [ref=f3e368] [cursor=pointer]
        - listitem [ref=f3e45]:
          - generic [ref=f3e46] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
      - generic [ref=f3e53] [cursor=pointer]
  - generic [ref=f3e58]:
    - generic [ref=f3e59]:
      - banner [ref=f3e60]:
        - button [ref=f3e61] [cursor=pointer]
        - button [ref=f3e64] [cursor=pointer]
        - navigation "breadcrumb" [ref=f3e69]:
          - list [ref=f3e70]:
            - listitem [ref=f3e71]:
              - link "首页" [disabled] [ref=f3e72]
        - generic [ref=f3e77]:
          - generic [ref=f3e79] [cursor=pointer]:
            - generic [ref=f3e83]: æœç´¢
            - generic [ref=f3e84]: Ctrl K
          - button [ref=f3e86] [cursor=pointer]
          - button "dark" [ref=f3e91] [cursor=pointer]
          - button [ref=f3e100] [cursor=pointer]
          - button [ref=f3e106] [cursor=pointer]
          - button [ref=f3e109] [cursor=pointer]
          - button [ref=f3e115] [cursor=pointer]:
            - button [ref=f3e117]
          - button "工作台" [ref=f3e121] [cursor=pointer]
          - button [ref=f3e125] [cursor=pointer]:
            - img "超级管理员" [ref=f3e130]
      - generic [ref=f3e132]:
        - generic [ref=f3e133]: é¦–页
        - generic [ref=f3e152]:
          - button [ref=f3e153] [cursor=pointer]
          - generic [ref=f3e160] [cursor=pointer]
          - generic [ref=f3e164] [cursor=pointer]
    - main [ref=f3e171]:
      - generic [ref=f3e174]:
        - generic [ref=f3e175]:
          - generic [ref=f3e185] [cursor=pointer]:
            - generic [ref=f3e186]: å“ç§æ€»æ•°
            - generic [ref=f3e187]: "1"
          - generic [ref=f3e198] [cursor=pointer]:
            - generic [ref=f3e199]: MES å“ç§
            - generic [ref=f3e200]: "0"
          - generic [ref=f3e211] [cursor=pointer]:
            - generic [ref=f3e212]: å“ç§åˆ†ç±»
            - generic [ref=f3e213]: "1"
          - generic [ref=f3e222] [cursor=pointer]:
            - generic [ref=f3e223]: è®¡é‡å•位
            - generic [ref=f3e224]: "1"
        - generic [ref=f3e226]:
          - generic [ref=f3e234] [cursor=pointer]:
            - generic [ref=f3e235]: äº§çº¿
            - generic [ref=f3e236]: "1"
          - generic [ref=f3e248] [cursor=pointer]:
            - generic [ref=f3e249]: ç”Ÿäº§æ‰¹æ¬¡
            - generic [ref=f3e250]: "1"
          - generic [ref=f3e259] [cursor=pointer]:
            - generic [ref=f3e260]: è¢‹ç 
            - generic [ref=f3e261]: "100"
          - generic [ref=f3e272] [cursor=pointer]:
            - generic [ref=f3e273]: æ‰˜ç›˜ç 
            - generic [ref=f3e274]: "10"
        - generic [ref=f3e276]:
          - generic [ref=f3e280]:
            - generic [ref=f3e281]: è¢‹ç ç”Ÿæˆè¶‹åŠ¿
            - generic [ref=f3e283]:
              - generic [ref=f3e284] [cursor=pointer]:
                - generic [ref=f3e285]:
                  - radio "近 7 å¤©" [checked]
                - generic [ref=f3e286]: è¿‘ 7 å¤©
              - generic [ref=f3e287] [cursor=pointer]:
                - generic [ref=f3e288]:
                  - radio "近 30 å¤©"
                - generic [ref=f3e289]: è¿‘ 30 å¤©
          - generic [ref=f3e294]: æ‰¹æ¬¡çŠ¶æ€åˆ†å¸ƒ
          - generic [ref=f3e303]: è¢‹ç çŠ¶æ€åˆ†å¸ƒ
        - generic [ref=f3e312]:
          - generic [ref=f3e313]: å¿«æ·å…¥å£
          - generic [ref=f3e317]:
            - generic [ref=f3e318]: å“ç§ç®¡ç†
            - generic [ref=f3e326]: å“ç§åˆ†ç±»
            - generic [ref=f3e334]: æ‰¹æ¬¡ç”Ÿäº§
            - generic [ref=f3e343]: èµ‹ç ç®¡ç†
.playwright-mcp/page-2026-08-21T04-01-18-956Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
- generic [ref=f3e7]:
  - complementary [ref=f3e9]:
    - generic [ref=f3e10]:
      - generic [ref=f3e11] [cursor=pointer]
      - link "工业AI智造平台 å·¥ä¸šAI智造平台" [ref=f3e16] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工业AI智造平台" [ref=f3e19]
        - generic [ref=f3e20]: å·¥ä¸šAI智造平台
      - menu [ref=f3e23]:
        - menuitem "首页" [ref=f3e24] [cursor=pointer]
        - listitem [ref=f3e29]:
          - generic [ref=f3e30] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f3e38]:
          - generic [ref=f3e39] [cursor=pointer]: ä¸€è¢‹ä¸€ç ç®¡ç†
          - list [ref=f3e349]:
            - menuitem "批次生产管理" [ref=f3e350] [cursor=pointer]
            - menuitem "赋码管理" [active] [ref=f3e355] [cursor=pointer]
            - menuitem "托盘码管理" [ref=f3e361] [cursor=pointer]
            - menuitem "产线维护" [ref=f3e368] [cursor=pointer]
        - listitem [ref=f3e45]:
          - generic [ref=f3e46] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
      - generic [ref=f3e53] [cursor=pointer]
  - generic [ref=f3e58]:
    - generic [ref=f3e59]:
      - banner [ref=f3e60]:
        - button [ref=f3e61] [cursor=pointer]
        - button [ref=f3e64] [cursor=pointer]
        - navigation "breadcrumb" [ref=f3e69]:
          - list [ref=f3e70]:
            - listitem [ref=f3e375]:
              - link "一袋一码管理" [ref=f3e376] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f3e380]
            - listitem [ref=f3e383]:
              - link "赋码管理" [disabled] [ref=f3e384]
        - generic [ref=f3e77]:
          - generic [ref=f3e79] [cursor=pointer]:
            - generic [ref=f3e83]: æœç´¢
            - generic [ref=f3e84]: Ctrl K
          - button [ref=f3e86] [cursor=pointer]
          - button "dark" [ref=f3e91] [cursor=pointer]
          - button [ref=f3e100] [cursor=pointer]
          - button [ref=f3e106] [cursor=pointer]
          - button [ref=f3e109] [cursor=pointer]
          - button [ref=f3e115] [cursor=pointer]:
            - button [ref=f3e117]
          - button "工作台" [ref=f3e121] [cursor=pointer]
          - button [ref=f3e125] [cursor=pointer]:
            - img "超级管理员" [ref=f3e130]
      - generic [ref=f3e132]:
        - generic [ref=f3e137]:
          - generic [ref=f3e138] [cursor=pointer]: é¦–页
          - generic [ref=f3e392] [cursor=pointer]: èµ‹ç ç®¡ç†
        - generic [ref=f3e152]:
          - button [ref=f3e153] [cursor=pointer]
          - generic [ref=f3e160] [cursor=pointer]
          - generic [ref=f3e164] [cursor=pointer]
    - main [ref=f3e171]:
      - generic [ref=f3e174]:
        - generic [ref=f3e175]:
          - generic [ref=f3e185] [cursor=pointer]:
            - generic [ref=f3e186]: å“ç§æ€»æ•°
            - generic [ref=f3e187]: "1"
          - generic [ref=f3e198] [cursor=pointer]:
            - generic [ref=f3e199]: MES å“ç§
            - generic [ref=f3e200]: "0"
          - generic [ref=f3e211] [cursor=pointer]:
            - generic [ref=f3e212]: å“ç§åˆ†ç±»
            - generic [ref=f3e213]: "1"
          - generic [ref=f3e222] [cursor=pointer]:
            - generic [ref=f3e223]: è®¡é‡å•位
            - generic [ref=f3e224]: "1"
        - generic [ref=f3e226]:
          - generic [ref=f3e234] [cursor=pointer]:
            - generic [ref=f3e235]: äº§çº¿
            - generic [ref=f3e236]: "1"
          - generic [ref=f3e248] [cursor=pointer]:
            - generic [ref=f3e249]: ç”Ÿäº§æ‰¹æ¬¡
            - generic [ref=f3e250]: "1"
          - generic [ref=f3e259] [cursor=pointer]:
            - generic [ref=f3e260]: è¢‹ç 
            - generic [ref=f3e261]: "100"
          - generic [ref=f3e272] [cursor=pointer]:
            - generic [ref=f3e273]: æ‰˜ç›˜ç 
            - generic [ref=f3e274]: "10"
        - generic [ref=f3e276]:
          - generic [ref=f3e280]:
            - generic [ref=f3e281]: è¢‹ç ç”Ÿæˆè¶‹åŠ¿
            - generic [ref=f3e283]:
              - generic [ref=f3e284] [cursor=pointer]:
                - generic [ref=f3e285]:
                  - radio "近 7 å¤©" [checked]
                - generic [ref=f3e286]: è¿‘ 7 å¤©
              - generic [ref=f3e287] [cursor=pointer]:
                - generic [ref=f3e288]:
                  - radio "近 30 å¤©"
                - generic [ref=f3e289]: è¿‘ 30 å¤©
          - generic [ref=f3e294]: æ‰¹æ¬¡çŠ¶æ€åˆ†å¸ƒ
          - generic [ref=f3e303]: è¢‹ç çŠ¶æ€åˆ†å¸ƒ
        - generic [ref=f3e312]:
          - generic [ref=f3e313]: å¿«æ·å…¥å£
          - generic [ref=f3e317]:
            - generic [ref=f3e318]: å“ç§ç®¡ç†
            - generic [ref=f3e326]: å“ç§åˆ†ç±»
            - generic [ref=f3e334]: æ‰¹æ¬¡ç”Ÿäº§
            - generic [ref=f3e343]: èµ‹ç ç®¡ç†
docs/mes_pro_bag_code_replacement_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
# ä¸€è¢‹ä¸€ç ç ´æŸè¢‹ç æ›¿æ¢ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- èµ‹ç ç®¡ç†ï¼šè¢‹ç åˆ—表、袋码详情、扫码追溯
- æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼šæ‰¹æ¬¡è¯¦æƒ…及有效袋码数量展示
- æ‰˜ç›˜ç ç®¡ç†ï¼šæ‰˜ç›˜è¯¦æƒ…及绑定袋码列表
本次只新增后端接口和字段约定,不修改前端项目代码。
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç”Ÿäº§äººå‘˜åœ¨è¢‹ç åˆ—表中选择同一批次下包装破损的袋码,记录其数据库编号。
2. å‰ç«¯è°ƒç”¨ç ´æŸæ›¿æ¢æŽ¥å£ï¼Œä¼ å…¥æ‰¹æ¬¡ç¼–号、旧袋码编号列表和破损原因。
3. åŽç«¯åœ¨ä¸€ä¸ªäº‹åŠ¡å†…å°†æ—§è¢‹ç æ”¹ä¸ºâ€œå·²ä½œåºŸâ€ï¼Œç”Ÿæˆç­‰é‡æ–°è¢‹ç ï¼Œå¹¶è¿”å›žæ—§ç ä¸Žæ–°ç ä¸€å¯¹ä¸€æ˜ å°„ã€‚
4. å¦‚果旧袋码已绑定启用托盘,新袋码自动继承原托盘;旧码从托盘当前有效袋码中移除,托盘已绑定数量保持不变。
5. å‰ç«¯æ ¹æ®å“åº”中的 `replaceUuid` æˆ–新袋码列表刷新袋码列表,并调用现有袋码导出接口完成新标签打印。
6. æ–°è¢‹ç å¯¼å‡ºåŽçŠ¶æ€ç”±â€œå·²ç”Ÿæˆâ€å˜ä¸ºâ€œå·²å¯¼å‡ºå°åˆ·â€ï¼›æ—§è¢‹ç ä¸å‚ä¸Žæ‰“å°ã€‚
7. æ‰«ææ—§ç ä»å¯æŸ¥è¯¢åŽŸæ‰¹æ¬¡å’Œä½œåºŸä¿¡æ¯ï¼›æ‰«ææ–°ç å¯æŸ¥è¯¢å½“å‰æ‰¹æ¬¡ã€æ‰˜ç›˜å’Œæ›¿æ¢æ¥æºã€‚
## API
### 1. ç ´æŸè¢‹ç æ›¿æ¢
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|---|---|---|
| POST | `/mes/pro/bag-code/replace` | ä½œåºŸç ´æŸæ—§è¢‹ç å¹¶åœ¨åŽŸæ‰¹æ¬¡ç”Ÿæˆç­‰é‡æ–°è¢‹ç  |
权限:`mes:pro-bag-code:replace`
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|---|---|---|---|
| `batchId` | Long | æ˜¯ | ç”Ÿäº§æ‰¹æ¬¡ç¼–号 |
| `oldIds` | Array<Long> | æ˜¯ | å¾…作废袋码编号,去重后至少 1 ä¸ªï¼Œå•次最多 200 ä¸ª |
| `reason` | String | æ˜¯ | æ›¿æ¢åŽŸå› ï¼Œä¾‹å¦‚â€œåŒ…è£…ç ´æŸâ€ï¼Œæœ€é•¿ 255 ä¸ªå­—符 |
**请求示例:**
```json
{
  "batchId": 1001,
  "oldIds": [5001, 5002],
  "reason": "包装破损"
}
```
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|---|---|---|
| `replaceUuid` | String | æœ¬æ¬¡æ›¿æ¢ç»„ UUID,可用于识别本次新码集合 |
| `batchId` | Long | ç”Ÿäº§æ‰¹æ¬¡ç¼–号 |
| `batchCode` | String | ç”Ÿäº§æ‰¹æ¬¡å· |
| `count` | Integer | æœ¬æ¬¡æ›¿æ¢æ•°é‡ |
| `items` | Array | æ–°æ—§è¢‹ç æ˜ å°„ |
| `items.oldBagCodeId` | Long | æ—§è¢‹ç ç¼–号 |
| `items.oldCode` | String | æ—§è¢‹ç  |
| `items.oldBagNo` | Integer | æ—§è¢‹åºå· |
| `items.oldStatus` | Integer | ä½œåºŸå‰çŠ¶æ€ |
| `items.oldPalletId` | Long | æ—§è¢‹ç åŽŸæ‰˜ç›˜ç¼–å·ï¼Œå¯ä¸ºç©º |
| `items.oldPalletCode` | String | æ—§è¢‹ç åŽŸæ‰˜ç›˜ç ï¼Œå¯ä¸ºç©º |
| `items.newBagCodeId` | Long | æ–°è¢‹ç ç¼–号 |
| `items.newCode` | String | æ–°è¢‹ç  |
| `items.newBagNo` | Integer | æ–°è¢‹åºå· |
| `items.newPalletId` | Long | æ–°è¢‹ç å½“前托盘编号,可为空 |
| `items.newPalletCode` | String | æ–°è¢‹ç å½“前托盘码,可为空 |
| `items.reason` | String | æ›¿æ¢åŽŸå›  |
| `items.replaceTime` | String | æ›¿æ¢æ—¶é—´ |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "replaceUuid": "9f6e4c4b2b2c4d5b8a8a8c0a0b0c0d0e",
    "batchId": 1001,
    "batchCode": "PC20260821L01ITEM001",
    "count": 2,
    "items": [
      {
        "oldBagCodeId": 5001,
        "oldCode": "PC20260821L01ITEM001-0001",
        "oldBagNo": 1,
        "oldStatus": 20,
        "oldPalletId": 3001,
        "oldPalletCode": "PC20260821L01ITEM001-P01",
        "newBagCodeId": 6001,
        "newCode": "PC20260821L01ITEM001-1201",
        "newBagNo": 1201,
        "newPalletId": 3001,
        "newPalletCode": "PC20260821L01ITEM001-P01",
        "reason": "包装破损",
        "replaceTime": "2026-08-21 15:30:00"
      }
    ]
  }
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|---|---|---|
| `status` | è¢‹ç åˆ—表、袋码详情、扫码追溯 | 10 å·²ç”Ÿæˆã€20 å·²å¯¼å‡ºå°åˆ·ã€30 å·²ä½œåºŸ |
| `cancelTime` | è¢‹ç è¯¦æƒ…、扫码追溯 | ä»…作废袋码返回 |
| `cancelReason` | è¢‹ç è¯¦æƒ…、扫码追溯 | å±•示破损或其他替换原因 |
| `replacementUuid` | è¢‹ç è¯¦æƒ…、扫码追溯 | æ ‡è¯†æ›¿æ¢æ“ä½œ |
| `replacedByCode` | ä½œåºŸæ—§ç è¿½æº¯ | å±•示替换后的新袋码 |
| `replacementOfCode` | æ–°ç è¿½æº¯ | å±•示被替换的旧袋码 |
| `bagQuantity` | æ‰¹æ¬¡åˆ—表、批次详情 | æŒ‰å½“前有效袋码数量统计,不包含已作废袋码 |
| `palletCode` | è¢‹ç åˆ—表、托盘详情、扫码追溯 | å·²ç»‘定启用托盘的替换新码继承原托盘 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|---|---|
| æ™®é€šç”Ÿæˆ | ä»ä»…允许生产中批次,使用 `/mes/pro/bag-code/generate` |
| ç ´æŸæ›¿æ¢ | å…è®¸ç”Ÿäº§ä¸­ã€å·²å®Œæˆæ‰¹æ¬¡ï¼›è‰ç¨¿å’Œå·²ä½œåºŸæ‰¹æ¬¡ä¸å…è®¸ |
| æ—§ç çŠ¶æ€ | ä»…已生成、已导出印刷可以替换;已作废不能重复替换 |
| æ‰¹æ¬¡å½’属 | `oldIds` å¿…须全部属于请求中的 `batchId` |
| æ–°ç åºå· | ä»Žæ‰¹æ¬¡åŽ†å²æœ€å¤§è¢‹åºå·ç»§ç»­é€’å¢žï¼Œå·²ä½œåºŸå’Œé€»è¾‘åˆ é™¤åºå·ä¹Ÿä¸å¤ç”¨ |
| æ•°é‡ | ä½œåºŸ N ä¸ªå¹¶ç”Ÿæˆ N ä¸ªï¼Œæ‰¹æ¬¡æœ‰æ•ˆè¢‹ç æ•°é‡ä¿æŒä¸å˜ |
| æœªè£…托盘 | æ—§ç ä½œåºŸï¼Œæ–°ç ä¿æŒæœªç»‘定 |
| å·²è£…托盘 | åŽŸæ‰˜ç›˜å¿…é¡»å­˜åœ¨ä¸”å¯ç”¨ï¼›æ—§ç è§£ç»‘å¹¶ä½œåºŸï¼Œæ–°ç è‡ªåŠ¨ç»§æ‰¿åŽŸæ‰˜ç›˜ |
| æ‰˜ç›˜æ•°é‡ | æ›¿æ¢å‰åŽæ‰˜ç›˜æœ‰æ•ˆç»‘定数量保持不变 |
| æ‰“印 | æ–°ç åˆå§‹ä¸ºå·²ç”Ÿæˆï¼›å¯¼å‡ºåŽå˜ä¸ºå·²å¯¼å‡ºå°åˆ·ï¼›æ—§ç ä¸å¾—导出打印 |
| è¿½æº¯ | æ—§ç è®°å½•保留并显示已作废,不返回袋码不存在;新旧码可互相追溯 |
| åŽŸå­æ€§ | ä½œåºŸã€ç”Ÿæˆã€æ‰˜ç›˜ç»§æ‰¿ã€æ›¿æ¢è®°å½•写入任一步失败,全部事务回滚 |
## æŽ¥å£è°ƒç”¨æ—¶åº
1. èŽ·å–è¢‹ç åˆ†é¡µï¼ŒæŒ‰æ‰¹æ¬¡å’ŒçŠ¶æ€ç­›é€‰æœ‰æ•ˆè¢‹ç ã€‚
2. ç”¨æˆ·å‹¾é€‰ç ´æŸè¢‹ç å¹¶å¡«å†™åŽŸå› ã€‚
3. è°ƒç”¨ `/mes/pro/bag-code/replace`。
4. æˆåŠŸåŽåˆ·æ–°è¢‹ç åˆ—è¡¨å’Œæ‰¹æ¬¡æ•°é‡ã€‚
5. ä¾æ®å“åº”的新袋码集合,按 `replaceUuid` æŸ¥è¯¢æˆ–导出新码进行打印。
6. æ‰“印成功后刷新状态;如需扫码确认,分别扫描旧码和新码核对替换关系。
## æ³¨æ„äº‹é¡¹
- `oldIds` ä¸è¦æ··å…¥ä¸åŒæ‰¹æ¬¡çš„袋码;服务端会再次校验。
- ä¸è¦è°ƒç”¨æ™®é€š `/generate` å®žçŽ°ç ´æŸè¡¥ç ï¼Œæ™®é€šç”Ÿæˆä¼šå¢žåŠ è¢‹ç æ•°é‡ä¸”ä¸ä¿å­˜æ›¿æ¢å…³ç³»ã€‚
- æ–°è¢‹ç ä¸å¤ç”¨æ—§è¢‹ç çš„ code æˆ–袋序号,1200 è¢‹æ›¿æ¢ 2 è¢‹æ—¶æ–°ç é€šå¸¸ä»Ž 1201、1202 ç»§ç»­ç”Ÿæˆã€‚
- å·²ä½œåºŸæ—§ç ä»å¯è¿½æº¯ï¼Œä½†ä¸å¯ç»‘定托盘、不可再次替换、不可打印。
- åŽŸæ‰˜ç›˜å·²ä½œåºŸæˆ–ä¸å­˜åœ¨æ—¶ï¼Œå·²ç»‘å®šè¢‹ç çš„æ›¿æ¢ä¼šå¤±è´¥ï¼Œéœ€è¦å…ˆå¤„ç†æ‰˜ç›˜çŠ¶æ€ã€‚
- æ‰¹æ¬¡è¿½æº¯ä¸­çš„袋码对象包含状态字段;展示作废标识时不要只显示 `code` å­—符串。
- æ›¿æ¢æŽ¥å£è¿”回的新码初始未打印,导出成功后才变为“已导出印刷”。
sql/mysql/mes_pro_bag_code_replacement.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
-- ä¸€è¢‹ä¸€ç ï¼šç ´æŸè¢‹ç æ›¿æ¢è¿ç§»
-- è¯´æ˜Žï¼šæ—§è¢‹ç ä¿ç•™å¹¶æ ‡è®°å·²ä½œåºŸï¼Œæ–°è¢‹ç ä¸Žæ—§è¢‹ç é€šè¿‡æ›¿æ¢è®°å½•关联。
SET @cancel_time_exists = (
    SELECT COUNT(*) FROM information_schema.columns
    WHERE table_schema = DATABASE() AND table_name = 'mes_pro_bag_code' AND column_name = 'cancel_time'
);
SET @sql = IF(@cancel_time_exists = 0,
    'ALTER TABLE mes_pro_bag_code ADD COLUMN cancel_time DATETIME DEFAULT NULL COMMENT ''作废时间'' AFTER print_time',
    'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @cancel_reason_exists = (
    SELECT COUNT(*) FROM information_schema.columns
    WHERE table_schema = DATABASE() AND table_name = 'mes_pro_bag_code' AND column_name = 'cancel_reason'
);
SET @sql = IF(@cancel_reason_exists = 0,
    'ALTER TABLE mes_pro_bag_code ADD COLUMN cancel_reason VARCHAR(255) DEFAULT NULL COMMENT ''作废原因'' AFTER cancel_time',
    'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS `mes_pro_bag_code_replace` (
    `id`              BIGINT       NOT NULL AUTO_INCREMENT COMMENT '编号',
    `replace_uuid`    VARCHAR(32)  NOT NULL COMMENT '替换组 UUID',
    `batch_id`        BIGINT       NOT NULL COMMENT '生产批次编号',
    `batch_code`      VARCHAR(64)  DEFAULT NULL COMMENT '生产批次号',
    `old_bag_code_id` BIGINT       NOT NULL COMMENT '旧袋码编号',
    `old_code`        VARCHAR(64)  NOT NULL COMMENT '旧袋码',
    `old_bag_no`      INT          NOT NULL COMMENT '旧袋序号',
    `old_status`      TINYINT      NOT NULL COMMENT '旧袋码作废前状态',
    `old_pallet_id`   BIGINT       DEFAULT NULL COMMENT '旧袋码原托盘编号',
    `old_pallet_code` VARCHAR(64)  DEFAULT NULL COMMENT '旧袋码原托盘码',
    `new_bag_code_id` BIGINT       NOT NULL COMMENT '新袋码编号',
    `new_code`        VARCHAR(64)  NOT NULL COMMENT '新袋码',
    `new_bag_no`      INT          NOT NULL COMMENT '新袋序号',
    `reason`          VARCHAR(255) NOT NULL COMMENT '替换原因',
    `replace_time`    DATETIME     NOT NULL COMMENT '替换时间',
    `creator`         VARCHAR(64)  DEFAULT '' COMMENT '创建者',
    `create_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`         VARCHAR(64)  DEFAULT '' COMMENT '更新者',
    `update_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`         BIT(1)       NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_old_bag_code_id` (`old_bag_code_id`),
    UNIQUE KEY `uk_new_bag_code_id` (`new_bag_code_id`),
    KEY `idx_replace_uuid` (`replace_uuid`),
    KEY `idx_batch_id` (`batch_id`),
    KEY `idx_old_code` (`old_code`),
    KEY `idx_new_code` (`new_code`)
) ENGINE = InnoDB COMMENT = 'MES è¢‹ç æ›¿æ¢è®°å½•';
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 602629, '袋码破损替换', 'mes:pro-bag-code:replace', 3, 6, 602618, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 602629 OR (`permission` = 'mes:pro-bag-code:replace' AND `deleted` = b'0'));
sql/mysql/mes_pro_batch_bag_code_init.sql
@@ -94,6 +94,8 @@
    `pallet_code` VARCHAR(64)  DEFAULT NULL COMMENT '托盘码(冗余,便于展示)',
    `status`      TINYINT      NOT NULL DEFAULT 10 COMMENT '状态(字典 mes_pro_bag_code_status:10 å·²ç”Ÿæˆ 20 å·²å¯¼å‡ºå°åˆ· 30 å·²ä½œåºŸï¼‰',
    `print_time`  DATETIME     DEFAULT NULL COMMENT '导出(交付印刷)时间',
    `cancel_time` DATETIME     DEFAULT NULL COMMENT '作废时间',
    `cancel_reason` VARCHAR(255) DEFAULT NULL COMMENT '作废原因',
    `remark`      VARCHAR(255) DEFAULT NULL COMMENT '备注',
    `creator`     VARCHAR(64)  DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -105,10 +107,43 @@
    KEY `idx_batch_id` (`batch_id`),
    KEY `idx_uuid` (`uuid`),
    KEY `idx_pallet_id` (`pallet_id`)
) ENGINE = InnoDB COMMENT = 'MES è¢‹ç èµ‹ç ï¼ˆä¸€è¢‹ä¸€ç ï¼‰';
 ) ENGINE = InnoDB COMMENT = 'MES è¢‹ç èµ‹ç ï¼ˆä¸€è¢‹ä¸€ç ï¼‰';
-- ----------------------------
-- 5. æ‰¹æ¬¡å·è‡ªåŠ¨ç¼–ç è§„åˆ™ï¼šPRO_BATCH_CODE
-- 5. è¢‹ç æ›¿æ¢è®°å½•表
-- ----------------------------
CREATE TABLE IF NOT EXISTS `mes_pro_bag_code_replace` (
    `id`              BIGINT       NOT NULL AUTO_INCREMENT COMMENT '编号',
    `replace_uuid`    VARCHAR(32)  NOT NULL COMMENT '替换组 UUID',
    `batch_id`        BIGINT       NOT NULL COMMENT '生产批次编号',
    `batch_code`      VARCHAR(64)  DEFAULT NULL COMMENT '生产批次号',
    `old_bag_code_id` BIGINT       NOT NULL COMMENT '旧袋码编号',
    `old_code`        VARCHAR(64)  NOT NULL COMMENT '旧袋码',
    `old_bag_no`      INT          NOT NULL COMMENT '旧袋序号',
    `old_status`      TINYINT      NOT NULL COMMENT '旧袋码作废前状态',
    `old_pallet_id`   BIGINT       DEFAULT NULL COMMENT '旧袋码原托盘编号',
    `old_pallet_code` VARCHAR(64)  DEFAULT NULL COMMENT '旧袋码原托盘码',
    `new_bag_code_id` BIGINT       NOT NULL COMMENT '新袋码编号',
    `new_code`        VARCHAR(64)  NOT NULL COMMENT '新袋码',
    `new_bag_no`      INT          NOT NULL COMMENT '新袋序号',
    `reason`          VARCHAR(255) NOT NULL COMMENT '替换原因',
    `replace_time`    DATETIME     NOT NULL COMMENT '替换时间',
    `creator`         VARCHAR(64)  DEFAULT '' COMMENT '创建者',
    `create_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`         VARCHAR(64)  DEFAULT '' COMMENT '更新者',
    `update_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`         BIT(1)       NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_old_bag_code_id` (`old_bag_code_id`),
    UNIQUE KEY `uk_new_bag_code_id` (`new_bag_code_id`),
    KEY `idx_replace_uuid` (`replace_uuid`),
    KEY `idx_batch_id` (`batch_id`),
    KEY `idx_old_code` (`old_code`),
    KEY `idx_new_code` (`new_code`)
) ENGINE = InnoDB COMMENT = 'MES è¢‹ç æ›¿æ¢è®°å½•';
-- ----------------------------
-- 6. æ‰¹æ¬¡å·è‡ªåŠ¨ç¼–ç è§„åˆ™ï¼šPRO_BATCH_CODE
--    ç»“构:固定字符(PC) + æ—¥æœŸ(yyyyMMdd) + è¾“入字符(产线编码+品种编码) + æµæ°´å·(3位,按日循环)
--    ç¤ºä¾‹ï¼šPC20260818L01NY001
-- ----------------------------
@@ -189,7 +224,8 @@
(602620, '生成二维码数据包', 'mes:pro-bag-code:generate', 3, 2, 602618, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602621, '袋码删除', 'mes:pro-bag-code:delete', 3, 3, 602618, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602622, '袋码导出', 'mes:pro-bag-code:export', 3, 4, 602618, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602623, '袋码绑定托盘', 'mes:pro-bag-code:bind', 3, 5, 602618, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
(602623, '袋码绑定托盘', 'mes:pro-bag-code:bind', 3, 5, 602618, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(602629, '袋码破损替换', 'mes:pro-bag-code:replace', 3, 6, 602618, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- æ‰˜ç›˜ç ç®¡ç†
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java
@@ -12,6 +12,8 @@
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.MesProBagCodePageReqVO;
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;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeTraceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
@@ -73,6 +75,14 @@
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:create')")
    public CommonResult<String> generateBagCodes(@Valid @RequestBody MesProBagCodeGenerateReqVO generateReqVO) {
        return success(bagCodeService.generateBagCodes(generateReqVO));
    }
    @PostMapping("/replace")
    @Operation(summary = "作废破损袋码并生成等量替换袋码")
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:replace')")
    public CommonResult<MesProBagCodeReplaceRespVO> replaceBagCodes(
            @Valid @RequestBody MesProBagCodeReplaceReqVO replaceReqVO) {
        return success(bagCodeService.replaceBagCodes(replaceReqVO));
    }
    @DeleteMapping("/delete-by-uuid")
@@ -203,6 +213,18 @@
    private MesProBagCodeTraceRespVO buildBagCodeTraceRespVO(MesProBagCodeDO bagCode) {
        MesProBagCodeTraceRespVO respVO = BeanUtils.toBean(bagCode, MesProBagCodeTraceRespVO.class);
        var replacement = bagCode.getStatus() != null
                && bagCode.getStatus().equals(cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum.CANCELED.getStatus())
                ? bagCodeService.getReplacementByOldBagCodeId(bagCode.getId())
                : bagCodeService.getReplacementByNewBagCodeId(bagCode.getId());
        if (replacement != null) {
            respVO.setReplacementUuid(replacement.getReplaceUuid());
            if (bagCode.getId().equals(replacement.getOldBagCodeId())) {
                respVO.setReplacedByCode(replacement.getNewCode());
            } else {
                respVO.setReplacementOfCode(replacement.getOldCode());
            }
        }
        // æ‰¹æ¬¡ä¿¡æ¯
        MesProBatchDO batch = batchService.getBatch(bagCode.getBatchId());
        if (batch != null) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeReplaceItemRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES è¢‹ç æ›¿æ¢æ˜Žç»† Response VO")
@Data
public class MesProBagCodeReplaceItemRespVO {
    @Schema(description = "旧袋码编号")
    private Long oldBagCodeId;
    @Schema(description = "旧袋码")
    private String oldCode;
    @Schema(description = "旧袋序号")
    private Integer oldBagNo;
    @Schema(description = "旧袋码作废前状态")
    private Integer oldStatus;
    @Schema(description = "旧袋码原托盘编号")
    private Long oldPalletId;
    @Schema(description = "旧袋码原托盘码")
    private String oldPalletCode;
    @Schema(description = "新袋码编号")
    private Long newBagCodeId;
    @Schema(description = "新袋码")
    private String newCode;
    @Schema(description = "新袋序号")
    private Integer newBagNo;
    @Schema(description = "新袋码当前托盘编号")
    private Long newPalletId;
    @Schema(description = "新袋码当前托盘码")
    private String newPalletCode;
    @Schema(description = "替换原因")
    private String reason;
    @Schema(description = "替换时间")
    private LocalDateTime replaceTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeReplaceReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - MES ç ´æŸè¢‹ç æ›¿æ¢ Request VO")
@Data
public class MesProBagCodeReplaceReqVO {
    @Schema(description = "生产批次编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "生产批次编号不能为空")
    private Long batchId;
    @Schema(description = "待作废的袋码编号列表", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty(message = "待作废袋码不能为空")
    @Size(max = 200, message = "单次最多替换 200 ä¸ªè¢‹ç ")
    private List<Long> oldIds;
    @Schema(description = "替换原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "包装破损")
    @NotBlank(message = "替换原因不能为空")
    @Size(max = 255, message = "替换原因长度不能超过 255 ä¸ªå­—符")
    private String reason;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeReplaceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - MES ç ´æŸè¢‹ç æ›¿æ¢ Response VO")
@Data
public class MesProBagCodeReplaceRespVO {
    @Schema(description = "本次替换组 UUID")
    private String replaceUuid;
    @Schema(description = "生产批次编号")
    private Long batchId;
    @Schema(description = "生产批次号")
    private String batchCode;
    @Schema(description = "替换数量")
    private Integer count;
    @Schema(description = "替换明细")
    private List<MesProBagCodeReplaceItemRespVO> items;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRespVO.java
@@ -68,6 +68,14 @@
    @ColumnWidth(20)
    private LocalDateTime printTime;
    @Schema(description = "作废时间")
    @ExcelIgnore
    private LocalDateTime cancelTime;
    @Schema(description = "作废原因")
    @ExcelIgnore
    private String cancelReason;
    @Schema(description = "备注")
    @ExcelIgnore
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeTraceRespVO.java
@@ -26,6 +26,21 @@
    @Schema(description = "导出(交付印刷)时间")
    private LocalDateTime printTime;
    @Schema(description = "作废时间")
    private LocalDateTime cancelTime;
    @Schema(description = "作废原因")
    private String cancelReason;
    @Schema(description = "替换组 UUID")
    private String replacementUuid;
    @Schema(description = "替换后的新袋码")
    private String replacedByCode;
    @Schema(description = "被替换的旧袋码")
    private String replacementOfCode;
    // ========== æ‰¹æ¬¡ä¿¡æ¯ ==========
    @Schema(description = "生产批次编号", example = "1024")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeDO.java
@@ -79,6 +79,14 @@
     */
    private LocalDateTime printTime;
    /**
     * ä½œåºŸæ—¶é—´
     */
    private LocalDateTime cancelTime;
    /**
     * ä½œåºŸåŽŸå› 
     */
    private String cancelReason;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeReplaceDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@TableName("mes_pro_bag_code_replace")
@KeySequence("mes_pro_bag_code_replace_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesProBagCodeReplaceDO extends BaseDO {
    @TableId
    private Long id;
    private String replaceUuid;
    private Long batchId;
    private String batchCode;
    private Long oldBagCodeId;
    private String oldCode;
    private Integer oldBagNo;
    private Integer oldStatus;
    private Long oldPalletId;
    private String oldPalletCode;
    private Long newBagCodeId;
    private String newCode;
    private Integer newBagNo;
    private String reason;
    private LocalDateTime replaceTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java
@@ -98,6 +98,24 @@
    @Select("SELECT IFNULL(MAX(bag_no), 0) FROM mes_pro_bag_code WHERE batch_id = #{batchId}")
    Integer selectMaxBagNo(@Param("batchId") Long batchId);
    default List<MesProBagCodeDO> selectListByIdsForUpdate(Collection<Long> ids) {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
                .in(MesProBagCodeDO::getId, ids)
                .orderByAsc(MesProBagCodeDO::getId)
                .last("FOR UPDATE"));
    }
    @Update("<script>UPDATE mes_pro_bag_code SET status = #{toStatus}, cancel_time = #{cancelTime}, " +
            "cancel_reason = #{cancelReason}, pallet_id = NULL, pallet_code = NULL, update_time = NOW() " +
            "WHERE deleted = 0 AND id = #{id} AND status = #{fromStatus}</script>")
    int cancelById(@Param("id") Long id, @Param("fromStatus") Integer fromStatus,
                   @Param("toStatus") Integer toStatus, @Param("cancelTime") LocalDateTime cancelTime,
                   @Param("cancelReason") String cancelReason);
    @Select("SELECT COUNT(*) FROM mes_pro_bag_code WHERE batch_id = #{batchId} " +
            "AND deleted = 0 AND status <> 30")
    int selectEffectiveCountByBatchId(@Param("batchId") Long batchId);
    /**
     * æ‰¹é‡æ ‡è®°ä¸ºå·²å¯¼å‡ºå°åˆ·ï¼ˆä»…更新处于目标原状态的记录)
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeReplaceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeReplaceDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface MesProBagCodeReplaceMapper extends BaseMapperX<MesProBagCodeReplaceDO> {
    default List<MesProBagCodeReplaceDO> selectListByOldBagCodeIds(Collection<Long> oldBagCodeIds) {
        return selectList(new LambdaQueryWrapper<MesProBagCodeReplaceDO>()
                .in(MesProBagCodeReplaceDO::getOldBagCodeId, oldBagCodeIds));
    }
    default MesProBagCodeReplaceDO selectByOldBagCodeId(Long oldBagCodeId) {
        return selectOne(MesProBagCodeReplaceDO::getOldBagCodeId, oldBagCodeId);
    }
    default MesProBagCodeReplaceDO selectByNewBagCodeId(Long newBagCodeId) {
        return selectOne(MesProBagCodeReplaceDO::getNewBagCodeId, newBagCodeId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/batch/MesProBatchMapper.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
/**
 * MES ç”Ÿäº§æ‰¹æ¬¡ Mapper
@@ -23,6 +24,9 @@
        return selectCount(MesProBatchDO::getLineId, lineId);
    }
    @Select("SELECT * FROM mes_pro_batch WHERE id = #{id} AND deleted = 0 FOR UPDATE")
    MesProBatchDO selectByIdForUpdate(@org.apache.ibatis.annotations.Param("id") Long id);
    default PageResult<MesProBatchDO> selectPage(MesProBatchPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProBatchDO>()
                .likeIfPresent(MesProBatchDO::getCode, reqVO.getCode())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java
@@ -23,6 +23,9 @@
        return selectOne(MesProPalletDO::getCode, code);
    }
    @Select("SELECT * FROM mes_pro_pallet WHERE id = #{id} AND deleted = 0 FOR UPDATE")
    MesProPalletDO selectByIdForUpdate(@Param("id") Long id);
    default List<MesProPalletDO> selectListByBatchId(Long batchId) {
        return selectList(MesProPalletDO::getBatchId, batchId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -818,6 +818,11 @@
    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, "袋码原托盘不存在或已作废,不允许继承");
    // ========== 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/service/pro/bagcode/MesProBagCodeService.java
@@ -4,6 +4,8 @@
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.MesProBagCodePageReqVO;
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;
import jakarta.validation.Valid;
@@ -26,6 +28,11 @@
     * @return ç”Ÿæˆç»„ UUID(标记同一次生成的数据包)
     */
    String generateBagCodes(@Valid MesProBagCodeGenerateReqVO generateReqVO);
    /**
     * ä½œåºŸç ´æŸè¢‹ç å¹¶åœ¨åŽŸæ‰¹æ¬¡ç”Ÿæˆç­‰é‡æ›¿æ¢è¢‹ç 
     */
    MesProBagCodeReplaceRespVO replaceBagCodes(@Valid MesProBagCodeReplaceReqVO replaceReqVO);
    /**
     * åˆ é™¤ç”Ÿæˆç»„(仅"已生成"状态且未绑定托盘的袋码允许删除)
@@ -80,6 +87,13 @@
    MesProBagCodeDO getBagCodeByCode(String code);
    /**
     * æŸ¥è¯¢è¢‹ç æ›¿æ¢è®°å½•
     */
    cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeReplaceDO getReplacementByOldBagCodeId(Long oldBagCodeId);
    cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeReplaceDO getReplacementByNewBagCodeId(Long newBagCodeId);
    /**
     * èŽ·å¾—è¢‹ç åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
@@ -7,13 +7,20 @@
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.MesProBagCodePageReqVO;
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.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;
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.bagcode.MesProBagCodeReplaceMapper;
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 cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -24,7 +31,9 @@
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -43,6 +52,10 @@
    private MesProBagCodeMapper bagCodeMapper;
    @Resource
    private MesProBatchMapper batchMapper;
    @Resource
    private MesProBagCodeReplaceMapper replaceMapper;
    @Resource
    private MesProPalletMapper palletMapper;
    @Resource
    private MesProPalletService palletService;
@@ -51,7 +64,7 @@
    @Transactional(rollbackFor = Exception.class)
    public String generateBagCodes(MesProBagCodeGenerateReqVO generateReqVO) {
        // 1. æ ¡éªŒæ‰¹æ¬¡å­˜åœ¨ä¸”处于生产中状态
        MesProBatchDO batch = batchMapper.selectById(generateReqVO.getBatchId());
        MesProBatchDO batch = batchMapper.selectByIdForUpdate(generateReqVO.getBatchId());
        if (batch == null) {
            throw exception(PRO_BATCH_NOT_EXISTS);
        }
@@ -86,6 +99,118 @@
                .setBagQuantity((batch.getBagQuantity() == null ? 0 : batch.getBagQuantity())
                        + generateReqVO.getCount()));
        return uuid;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProBagCodeReplaceRespVO replaceBagCodes(MesProBagCodeReplaceReqVO replaceReqVO) {
        MesProBatchDO batch = batchMapper.selectByIdForUpdate(replaceReqVO.getBatchId());
        if (batch == null) {
            throw exception(PRO_BATCH_NOT_EXISTS);
        }
        if (!MesProBatchStatusEnum.PRODUCING.getStatus().equals(batch.getStatus())
                && !MesProBatchStatusEnum.FINISHED.getStatus().equals(batch.getStatus())) {
            throw exception(PRO_BAG_CODE_REPLACE_BATCH_STATUS_INVALID);
        }
        List<Long> oldIds = replaceReqVO.getOldIds().stream()
                .distinct().sorted().toList();
        List<MesProBagCodeDO> oldBagCodes = bagCodeMapper.selectListByIdsForUpdate(oldIds);
        if (oldBagCodes.size() != oldIds.size()) {
            throw exception(PRO_BAG_CODE_NOT_EXISTS);
        }
        for (MesProBagCodeDO oldBagCode : oldBagCodes) {
            if (!batch.getId().equals(oldBagCode.getBatchId())) {
                throw exception(PRO_BAG_CODE_REPLACE_BATCH_MISMATCH);
            }
            if (!MesProBagCodeStatusEnum.GENERATED.getStatus().equals(oldBagCode.getStatus())
                    && !MesProBagCodeStatusEnum.PRINTED.getStatus().equals(oldBagCode.getStatus())) {
                throw exception(PRO_BAG_CODE_REPLACE_STATUS_INVALID);
            }
            if (replaceMapper.selectByOldBagCodeId(oldBagCode.getId()) != null) {
                throw exception(PRO_BAG_CODE_REPLACE_ALREADY_REPLACED);
            }
        }
        Map<Long, MesProPalletDO> palletMap = new LinkedHashMap<>();
        oldBagCodes.stream().map(MesProBagCodeDO::getPalletId).filter(id -> id != null)
                .distinct().sorted().forEach(palletId -> {
                    MesProPalletDO pallet = palletMapper.selectByIdForUpdate(palletId);
                    if (pallet == null || !MesProPalletStatusEnum.IN_USE.getStatus().equals(pallet.getStatus())) {
                        throw exception(PRO_BAG_CODE_REPLACE_PALLET_INVALID);
                    }
                    palletMap.put(palletId, pallet);
                });
        String replaceUuid = IdUtil.fastSimpleUUID();
        LocalDateTime replaceTime = LocalDateTime.now();
        int startNo = bagCodeMapper.selectMaxBagNo(batch.getId()) + 1;
        List<MesProBagCodeDO> newBagCodes = new ArrayList<>(oldBagCodes.size());
        List<MesProBagCodeReplaceDO> replacements = new ArrayList<>(oldBagCodes.size());
        List<MesProBagCodeReplaceItemRespVO> items = new ArrayList<>(oldBagCodes.size());
        for (int i = 0; i < oldBagCodes.size(); i++) {
            MesProBagCodeDO oldBagCode = oldBagCodes.get(i);
            if (bagCodeMapper.cancelById(oldBagCode.getId(), oldBagCode.getStatus(),
                    MesProBagCodeStatusEnum.CANCELED.getStatus(), replaceTime, replaceReqVO.getReason()) != 1) {
                throw exception(PRO_BAG_CODE_REPLACE_STATUS_INVALID);
            }
            int newBagNo = startNo + i;
            MesProPalletDO pallet = oldBagCode.getPalletId() == null ? null : palletMap.get(oldBagCode.getPalletId());
            MesProBagCodeDO newBagCode = new MesProBagCodeDO()
                    .setUuid(replaceUuid)
                    .setCode(batch.getCode() + "-" + StrUtil.padPre(String.valueOf(newBagNo), 4, '0'))
                    .setBagNo(newBagNo)
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletId(pallet == null ? null : pallet.getId())
                    .setPalletCode(pallet == null ? null : pallet.getCode())
                    .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
                    .setRemark(replaceReqVO.getReason());
            newBagCodes.add(newBagCode);
            replacements.add(new MesProBagCodeReplaceDO()
                    .setReplaceUuid(replaceUuid)
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setOldBagCodeId(oldBagCode.getId())
                    .setOldCode(oldBagCode.getCode())
                    .setOldBagNo(oldBagCode.getBagNo())
                    .setOldStatus(oldBagCode.getStatus())
                    .setOldPalletId(oldBagCode.getPalletId())
                    .setOldPalletCode(oldBagCode.getPalletCode())
                    .setNewCode(newBagCode.getCode())
                    .setNewBagNo(newBagNo)
                    .setReason(replaceReqVO.getReason())
                    .setReplaceTime(replaceTime));
            items.add(new MesProBagCodeReplaceItemRespVO()
                    .setOldBagCodeId(oldBagCode.getId())
                    .setOldCode(oldBagCode.getCode())
                    .setOldBagNo(oldBagCode.getBagNo())
                    .setOldStatus(oldBagCode.getStatus())
                    .setOldPalletId(oldBagCode.getPalletId())
                    .setOldPalletCode(oldBagCode.getPalletCode())
                    .setNewCode(newBagCode.getCode())
                    .setNewBagNo(newBagNo)
                    .setNewPalletId(newBagCode.getPalletId())
                    .setNewPalletCode(newBagCode.getPalletCode())
                    .setReason(replaceReqVO.getReason())
                    .setReplaceTime(replaceTime));
        }
        bagCodeMapper.insertBatch(newBagCodes);
        for (int i = 0; i < replacements.size(); i++) {
            replacements.get(i).setNewBagCodeId(newBagCodes.get(i).getId());
            items.get(i).setNewBagCodeId(newBagCodes.get(i).getId());
        }
        replaceMapper.insertBatch(replacements);
        batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
                .setBagQuantity(bagCodeMapper.selectEffectiveCountByBatchId(batch.getId())));
        return new MesProBagCodeReplaceRespVO()
                .setReplaceUuid(replaceUuid)
                .setBatchId(batch.getId())
                .setBatchCode(batch.getCode())
                .setCount(newBagCodes.size())
                .setItems(items);
    }
    @Override
@@ -180,6 +305,16 @@
    }
    @Override
    public MesProBagCodeReplaceDO getReplacementByOldBagCodeId(Long oldBagCodeId) {
        return replaceMapper.selectByOldBagCodeId(oldBagCodeId);
    }
    @Override
    public MesProBagCodeReplaceDO getReplacementByNewBagCodeId(Long newBagCodeId) {
        return replaceMapper.selectByNewBagCodeId(newBagCodeId);
    }
    @Override
    public PageResult<MesProBagCodeDO> getBagCodePage(MesProBagCodePageReqVO pageReqVO) {
        return bagCodeMapper.selectPage(pageReqVO);
    }