14 小时以前 6d910029b6c81db0a2b66caa66952781397b153a
feat(mes): 实现H5溯源页面与二维码内容集成

- 配置不同环境的H5溯源页面基础地址(application-{env}.yaml)
- 重构MesProQrLabelBuilder实现二维码内容动态拼接URL功能
- 新增MES_TRACE_CODE_NOT_FOUND等溯源相关错误码定义
- 实现扫码入库事件与批次关联查询功能(MesProScanEventService)
- 添加种子质检合格后库存状态批量转换逻辑(MesQcSeedQualityServiceImpl)
- 建立生产批次与仓库批次桥接机制(MesWmBatchService)
- 实现按品种/批次/仓库维度的库存汇总统计功能
- 新增库存状态批量更新接口支持质检转合格场景
已添加21个文件
已修改29个文件
2566 ■■■■■ 文件已修改
docs/warehouse_stock_management_integration.md 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp_img_view.png 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmStockSummaryRespVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/MesWmStockTakingTaskController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/MesWmStockTakingTaskScanReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/MesWmStockWarningController.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/vo/MesWmStockWarningHandleReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/vo/MesWmStockWarningPageReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/vo/MesWmStockWarningRespVO.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/MesProTracePublicController.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicRespVO.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warning/MesWmStockWarningDO.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/scan/MesProScanEventMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stocktaking/task/MesWmStockTakingTaskLineMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/warning/MesWmStockWarningMapper.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockWarningTypeEnum.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesProperties.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesQrCodeConfig.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/MesProQrLabelBuilder.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/wm/MesWmStockWarningJob.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventServiceImpl.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java 348 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskServiceImpl.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warning/MesWmStockWarningService.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warning/MesWmStockWarningServiceImpl.java 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-dev.yaml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-jhhg.yaml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-local.yaml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-test.yaml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-thzy.yaml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-yzfx.yaml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application.yaml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/static/trace.html 433 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/warehouse_stock_management_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
# ä»“储优化 - ä¸‰åº“存状态流转 + é¢„è­¦/汇总/盘点 å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åº“存台账(`/mes/wm/material-stock/page`)
- ç”Ÿäº§çº¿æ‰«ç å…¥åº“(`/mes/pro/scan-in`)
- ç§å­è´¨æ£€ï¼ˆç§å­è´¨æ£€æäº¤ç•Œé¢ï¼‰
- é”€å”®å‡ºåº“(销售扫码拣货)
- åº“存预警(新增页面)
- åº“存多维汇总(新增页面/面板)
- PDA æ‰«ç ç›˜ç‚¹ï¼ˆç›˜ç‚¹ä»»åŠ¡æ‰«ç ç•Œé¢ï¼‰
## ä¸šåŠ¡è§„åˆ™ï¼šä¸‰åº“å­˜çŠ¶æ€æµè½¬
| æ“ä½œ | åº“存状态变化 | è¯´æ˜Ž |
|------|-------------|------|
| ç”Ÿäº§çº¿æ‰«ç å…¥åº“ | â†’ æš‚存库(TEMP=10) | æ‰«ç è¢‹ç å…¥åº“成功后,库存进入暂存库(待质检),不再直接进入合格库 |
| ç§å­è´¨æ£€æäº¤ã€Œåˆæ ¼ã€ | æš‚存库 â†’ åˆæ ¼åº“(QUALIFIED=20) | è´¨æ£€ç»“æžœ=合格时,按批次将暂存库库存整批转为合格库(正式库存) |
| é”€å”®æ‰«ç å‡ºåº“ | åˆæ ¼åº“ â†’ å·²å‡ºåº“(OUT=30) | æ‰£å‡åˆæ ¼åº“库存,数量≤0 æ—¶çŠ¶æ€è‡ªåŠ¨å˜ä¸ºå·²å‡ºåº“ |
| ä¸åˆæ ¼ï¼ˆè´¨æ£€ç»“æžœ=不合格) | ç•™åœ¨æš‚存库 | æœ¬æœŸä¸åšé€€è´§/报废流,由后续人工处理 |
> **核心变化**:生产线扫码入库从「直接进合格库」改为「先进暂存库待质检」,质检合格后才进入合格库。前端需在库存台账页按状态维度区分展示三个库。
### ç”Ÿäº§çº¿æ‰«ç å…¥åº“(关键修复,前端无参变化)
- æŽ¥å£ï¼š`POST /mes/pro/scan-in/inbound`,请求参数**保持不变**。
- åŽç«¯å†…部变化:打通生产批次体系与仓库批次体系,入库时自动桥接仓库批次并写入 `batchId`,库存进入**暂存库**。
- å‰ç«¯å½±å“ï¼šå…¥åº“成功后,库存不体现在「合格库」筛选下,而是体现在「暂存库」筛选下;需等待种子质检提交合格后才会出现在合格库。
## API
### ä¸€ã€åº“存台账(已有接口,建议增强查询条件展示)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/material-stock/page | åº“存台账分页,新增按 `stockState` è¿‡æ»¤ |
**请求参数(新增/强调):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| stockState | Integer | å¦ | åº“存状态:10-暂存库,20-合格库,30-已出库 |
| itemId | Long | å¦ | å“ç§ï¼ˆç‰©æ–™ï¼‰ID |
| batchCode | String | å¦ | æ‰¹æ¬¡å· |
| warehouseId | Long | å¦ | ä»“库 ID |
**响应字段(已有,无需新增):** å« itemCode、itemName、batchCode、warehouseName、quantity、stockState ç­‰ã€‚
### äºŒã€åº“存多维汇总(新增)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/material-stock/summary-by-item | æŒ‰å“ç§æ±‡æ€»æ•°é‡ |
| GET | /mes/wm/material-stock/summary-by-batch | æŒ‰æ‰¹æ¬¡æ±‡æ€»æ•°é‡ |
| GET | /mes/wm/material-stock/summary-by-warehouse | æŒ‰ä»“库汇总数量 |
**请求参数(均可选,复用库存查询条件):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| stockState | Integer | å¦ | åº“存状态过滤,不传则按状态分开展示 |
| itemId | Long | å¦ | å“ç§è¿‡æ»¤ |
| batchCode | String | å¦ | æ‰¹æ¬¡å·è¿‡æ»¤ |
| warehouseId | Long | å¦ | ä»“库过滤 |
| areaId / locationId | Long | å¦ | åº“位/库区过滤 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| itemId / itemCode / itemName | Long/String/String | å“ç§ç»´åº¦ï¼ˆæŒ‰å“ç§æ±‡æ€»æ—¶è¿”回) |
| batchId / batchCode | Long/String | æ‰¹æ¬¡ç»´åº¦ï¼ˆæŒ‰æ‰¹æ¬¡æ±‡æ€»æ—¶è¿”回) |
| warehouseId / warehouseName | Long/String | ä»“库维度(按仓库汇总时返回) |
| stockState / stockStateName | Integer/String | åº“存状态及名称 |
| totalQuantity | BigDecimal | æ±‡æ€»æ•°é‡ |
**响应示例:**
```json
{
  "code": 0,
  "data": [
    { "itemId": 1, "itemCode": "ITEM001", "itemName": "早稻种子", "stockState": 10, "stockStateName": "暂存库", "totalQuantity": 500.00 },
    { "itemId": 1, "itemCode": "ITEM001", "itemName": "早稻种子", "stockState": 20, "stockStateName": "合格库", "totalQuantity": 400.00 }
  ]
}
```
### ä¸‰ã€åº“存预警(新增)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/stock-warning/page | é¢„警分页(落库记录,含已处理历史) |
| GET | /mes/wm/stock-warning/current | å®žæ—¶è®¡ç®—当前预警(不落库,秒级) |
| POST | /mes/wm/stock-warning/refresh | é‡æ–°è®¡ç®—并落库当前预警 |
| PUT | /mes/wm/stock-warning/handle | å¤„理预警(标记已处理) |
**GET /page è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemCode | String | å¦ | å“ç§ç¼–码模糊查询 |
| batchCode | String | å¦ | æ‰¹æ¬¡å·æ¨¡ç³ŠæŸ¥è¯¢ |
| warningType | Integer | å¦ | é¢„警类型:10-低库存,20-临期,30-库龄 |
| status | Integer | å¦ | å¤„理状态:10-未处理,20-已处理 |
| pageNo / pageSize | Integer | æ˜¯ | åˆ†é¡µ |
**GET /page å“åº”字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | é¢„警记录编号 |
| itemCode / itemName | String | å“ç§ç¼–码/名称 |
| batchCode | String | æ‰¹æ¬¡å· |
| warehouseName | String | ä»“库名称 |
| warningType / warningTypeName | Integer/String | é¢„警类型及名称 |
| thresholdValue / currentValue | BigDecimal | é˜ˆå€¼ï¼ˆä½Žåº“å­˜=最低库存、临期=剩余天数阈值、库龄=超龄天数)/ å½“前值 |
| expireDate | String(datetime) | è¿‡æœŸæ—¥æœŸï¼ˆä¸´æœŸé¢„警才有) |
| receiptTime | String(datetime) | å…¥åº“时间(库龄预警才有) |
| status / statusName | Integer/String | å¤„理状态及名称 |
| handleTime / handleRemark | String/String | å¤„理时间/备注 |
**PUT /handle è¯·æ±‚体:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é¢„警记录编号 |
| handleRemark | String | å¦ | å¤„理备注 |
**业务规则:**
- é¢„警针对**合格库**(正式库存)计算:低库存=在库数量 â‰¤ å“ç§æœ€ä½Žåº“存;临期=批次剩余天数 â‰¤ å“ç§æœ‰æ•ˆæœŸå¤©æ•°ï¼›åº“龄=入库天数 â‰¥ 365 å¤©ã€‚
- é¢„警双通道:`/current` å®žæ—¶è®¡ç®—(适合看板刷新);`/page` ä¸ºå®šæ—¶è½åº“记录(适合列表,含处理历史)。
- å®šæ—¶ä»»åŠ¡ `MesWmStockWarningJob`(Quartz,JobHandler)每日重算:删除旧未处理预警、写入当前预警,已处理记录保留为历史。
### å››ã€PDA æ‰«ç ç›˜ç‚¹ï¼ˆæ–°å¢žï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mes/wm/stock-taking-task/scan | PDA æ‰«ç ç›˜ç‚¹ï¼ˆè§£æžè¿½æº¯ç å®šä½æ‰¹æ¬¡ï¼Œå†™å…¥ç›˜ç‚¹ç»“果) |
| POST | /mes/wm/stock-taking-task/process-difference | å¤„理盘点差异(盘盈入库/盘亏出库,调整库存) |
| POST | /mes/wm/stock-taking-task/finish | å®Œæˆç›˜ç‚¹ä»»åŠ¡ï¼ˆå·²æœ‰ï¼‰ |
**POST /scan è¯·æ±‚体:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| taskId | Long | æ˜¯ | ç›˜ç‚¹ä»»åŠ¡ç¼–å· |
| scanCode | String | æ˜¯ | è¿½æº¯ç /袋码/托盘码/批次号 |
| takingQuantity | BigDecimal | æ˜¯ | ç›˜ç‚¹æ•°é‡ |
| remark | String | å¦ | å¤‡æ³¨ |
**响应:** è¿”回盘点结果编号(Long)。
**业务规则:**
- ä»…**盘点中**状态的任务允许扫码(提交盘点任务后)。
- æ‰«ç è§£æžé¡ºåºï¼šæ‰¹æ¬¡å· â†’ è¢‹ç  â†’ æ‰˜ç›˜ç ï¼Œæœ€ç»ˆå®šä½åˆ°ä»“库批次。
- åŒä¸€ä»»åŠ¡è¡Œ**只允许盘点一次**,重复扫码报错「该盘点清单行已有盘点记录,不能重复盘点」;如需修改先删除对应盘点结果再重新扫码。
- æ‰«ç æ‰¹æ¬¡ä¸åœ¨ä»»åŠ¡èŒƒå›´å†…æ—¶æŠ¥ã€Œæ‰«ç æ‰¹æ¬¡ä¸åœ¨ç›˜ç‚¹ä»»åŠ¡èŒƒå›´å†…ã€ã€‚
- ç›˜ç‚¹æµç¨‹ï¼šåˆ›å»ºä»»åŠ¡ â†’ æäº¤ï¼ˆè¿›å…¥ç›˜ç‚¹ä¸­ï¼‰â†’ PDA æ‰«ç é€ä¸ªæ‰¹æ¬¡ç›˜ç‚¹ â†’ å®Œæˆï¼ˆä»»åŠ¡â†’å·²å®Œæˆï¼Œè§£å†»åº“å­˜ï¼‰â†’ å¤„理差异(写盘盈/盘亏库存流水)。
## æ•°æ®å¸¦å…¥è¯´æ˜Ž
| ä¸Šä¸€æ­¥ | ä¸‹ä¸€æ­¥ | å¸¦å…¥å­—段 |
|--------|--------|----------|
| ç”Ÿäº§çº¿æ‰«ç å…¥åº“ | åº“存台账(暂存库) | è¢‹ç â†’生产批次→仓库批次(batchId/batchCode/itemId/warehouseId è‡ªåŠ¨æ¡¥æŽ¥ï¼‰ |
| ç§å­è´¨æ£€æäº¤åˆæ ¼ | åº“存台账(合格库) | æŒ‰æ‰¹æ¬¡å·ï¼ˆbatchCode)将暂存库整批转合格库,无需前端传库存 ID |
| é”€å”®æ‰«ç å‡ºåº“ | åº“存台账(已出库) | é”€å”®æ‰«ç è§£æžæ‰¹æ¬¡ â†’ å®šä½åˆæ ¼åº“库存 â†’ æ‰£å‡ |
| PDA æ‰«ç ç›˜ç‚¹ | ç›˜ç‚¹ç»“æžœ/任务行 | è¿½æº¯ç  â†’ æ‰¹æ¬¡ â†’ ä»»åŠ¡è¡Œï¼ˆtaskId+batchId å®šä½ï¼‰ |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. åº“存台账页
- é¡¶éƒ¨ç­›é€‰å¢žåŠ ã€Œåº“å­˜çŠ¶æ€ã€ä¸‹æ‹‰ï¼ˆæš‚å­˜åº“/合格库/已出库),对应 `stockState` å‚数。
- åˆ—表增加「库存状态」列展示,状态名称可用接口返回的 `stockStateName`(台账)或前端字典。
### 2. ç”Ÿäº§çº¿æ‰«ç å…¥åº“页
- æç¤ºæ–‡æ¡ˆï¼šå…¥åº“成功进入「暂存库(待质检)」;原「直接为合格库存」的展示逻辑需调整。
### 3. ç§å­è´¨æ£€æäº¤
- æäº¤ã€Œåˆæ ¼ã€åŽï¼Œå¯åˆ·æ–°åº“存台账「合格库」看到该批次库存;若提交「不合格」,库存仍停留在「暂存库」。
### 4. é”€å”®æ‰«ç æ‹£è´§
- å‡ºåº“成功后可刷新库存台账,「合格库」扣减、数量≤0 çš„æ‰¹æ¬¡å‡ºçŽ°åœ¨ã€Œå·²å‡ºåº“ã€ã€‚
### 5. åº“存预警页(新增)
- åˆ—表展示 `/page` æ•°æ®ï¼›æä¾›ã€Œå®žæ—¶è®¡ç®—」按钮调 `/current`;「处理」按钮弹窗填备注调 `/handle`;「重新计算」按钮调 `/refresh`。
- é¢„警类型/处理状态列展示 `warningTypeName` / `statusName`。
### 6. åº“存汇总面板(新增)
- ä¸‰ä¸ªæ±‡æ€»æŽ¥å£åˆ†åˆ«å‡ºå“ç§/批次/仓库维度的数量面板,支持按 `stockState` ç­›é€‰ï¼Œå±•示 `totalQuantity`。
### 7. PDA ç›˜ç‚¹æ‰«ç 
- ç›˜ç‚¹ä»»åŠ¡è¯¦æƒ…è¿›å…¥ã€Œç›˜ç‚¹ä¸­ã€åŽï¼Œæä¾›æ‰«ç è¾“å…¥æ¡†ï¼ˆ`taskId` å›ºå®šä¸ºå½“前任务,`scanCode` æ‰«ç ã€`takingQuantity` æ‰‹è¾“)。
- æ‰«ç æˆåŠŸæç¤ºè¿”å›žçš„ç›˜ç‚¹ç»“æžœç¼–å·ï¼›é‡å¤ç›˜ç‚¹éœ€å…ˆåˆ ç»“æžœã€‚
## æ³¨æ„äº‹é¡¹
- æ‰€æœ‰æŽ¥å£å‡ä¸ºç®¡ç†åŽå°æŽ¥å£ï¼Œéœ€å¸¦ç™»å½• token è®¿é—®ï¼Œæƒé™ç‚¹ï¼š`mes:wm-stock-warning:query/update`、`mes:wm-stock-taking-task:update`、`mes:wm-material-stock:query`。
- é¢„警仅对**合格库**计算;暂存库与已出库不计入预警。
- ç”Ÿäº§çº¿æ‰«ç å…¥åº“修复后,历史数据中已存在但未桥接的批次,会在下次扫码/质检/销售时按生产批次号自动桥接创建仓库批次,老数据无需迁移。
- é¢„警定时任务需在管理后台「系统管理-定时任务」注册 JobHandler:`MesWmStockWarningJob`(每日执行一次)。
- æ±‡æ€»æŽ¥å£ä¸ºå†…存聚合,数据量大时建议前端按需过滤(传 stockState/warehouseId ç­‰ï¼‰ã€‚
tmp_img_view.png
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
@@ -13,6 +13,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockMoveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
@@ -123,6 +125,30 @@
        return success(true);
    }
    @GetMapping("/summary-by-item")
    @Operation(summary = "按品种汇总库存数量")
    @PreAuthorize("@ss.hasPermission('mes:wm-material-stock:query')")
    public CommonResult<List<MesWmStockSummaryRespVO>> getStockSummaryByItem(
            MesWmMaterialStockListReqVO reqVO) {
        return success(materialStockService.getStockSummaryByItem(reqVO));
    }
    @GetMapping("/summary-by-batch")
    @Operation(summary = "按批次汇总库存数量")
    @PreAuthorize("@ss.hasPermission('mes:wm-material-stock:query')")
    public CommonResult<List<MesWmStockSummaryRespVO>> getStockSummaryByBatch(
            MesWmMaterialStockListReqVO reqVO) {
        return success(materialStockService.getStockSummaryByBatch(reqVO));
    }
    @GetMapping("/summary-by-warehouse")
    @Operation(summary = "按仓库汇总库存数量")
    @PreAuthorize("@ss.hasPermission('mes:wm-material-stock:query')")
    public CommonResult<List<MesWmStockSummaryRespVO>> getStockSummaryByWarehouse(
            MesWmMaterialStockListReqVO reqVO) {
        return success(materialStockService.getStockSummaryByWarehouse(reqVO));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出库存台账 Excel")
    @PreAuthorize("@ss.hasPermission('mes:wm-material-stock:export')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmStockSummaryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - åº“存汇总 Response VO")
@Data
public class MesWmStockSummaryRespVO {
    @Schema(description = "品种ID(按品种汇总时返回)")
    private Long itemId;
    @Schema(description = "品种编码")
    private String itemCode;
    @Schema(description = "品种名称")
    private String itemName;
    @Schema(description = "批次ID(按批次汇总时返回)")
    private Long batchId;
    @Schema(description = "批次号")
    private String batchCode;
    @Schema(description = "仓库ID(按仓库汇总时返回)")
    private Long warehouseId;
    @Schema(description = "仓库名称")
    private String warehouseName;
    @Schema(description = "库存状态:10-暂存库,20-合格库,30-已出库")
    private Integer stockState;
    @Schema(description = "库存状态名称")
    private String stockStateName;
    @Schema(description = "汇总数量")
    private BigDecimal totalQuantity;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/MesWmStockTakingTaskController.java
@@ -130,6 +130,22 @@
        return success(true);
    }
    @PostMapping("/scan")
    @Operation(summary = "PDA æ‰«ç ç›˜ç‚¹")
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-taking-task:update')")
    public CommonResult<Long> scanStockTakingTask(@Valid @RequestBody MesWmStockTakingTaskScanReqVO scanReqVO) {
        return success(stockTakingTaskService.scanStockTakingTask(scanReqVO));
    }
    @PostMapping("/process-difference")
    @Operation(summary = "处理盘点差异,调整库存")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-taking-task:update')")
    public CommonResult<Boolean> processStockTakingDifference(@RequestParam("id") Long id) {
        stockTakingTaskService.processStockTakingDifference(id);
        return success(true);
    }
    // ==================== æ‹¼æŽ¥ VO ====================
    private List<MesWmStockTakingTaskRespVO> buildTaskRespVOList(List<MesWmStockTakingTaskDO> list) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/MesWmStockTakingTaskScanReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MES ç›˜ç‚¹ä»»åŠ¡ PDA æ‰«ç  Request VO")
@Data
public class MesWmStockTakingTaskScanReqVO {
    @Schema(description = "盘点任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "盘点任务编号不能为空")
    private Long taskId;
    @Schema(description = "追溯码/袋码/托盘码/批次号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty(message = "扫码内容不能为空")
    private String scanCode;
    @Schema(description = "盘点数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "盘点数量不能为空")
    private BigDecimal takingQuantity;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/MesWmStockWarningController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.warning;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningHandleReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningRespVO;
import cn.iocoder.yudao.module.mes.service.wm.warning.MesWmStockWarningService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES åº“存预警")
@RestController
@RequestMapping("/mes/wm/stock-warning")
@Validated
public class MesWmStockWarningController {
    @Resource
    private MesWmStockWarningService stockWarningService;
    @GetMapping("/page")
    @Operation(summary = "获得库存预警分页(落库记录)")
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-warning:query')")
    public CommonResult<PageResult<MesWmStockWarningRespVO>> getWarningPage(
            @Valid MesWmStockWarningPageReqVO pageReqVO) {
        return success(stockWarningService.getWarningPage(pageReqVO));
    }
    @GetMapping("/current")
    @Operation(summary = "实时计算当前库存预警(不落库)")
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-warning:query')")
    public CommonResult<List<MesWmStockWarningRespVO>> getCurrentWarnings() {
        return success(stockWarningService.getCurrentWarnings());
    }
    @PostMapping("/refresh")
    @Operation(summary = "重新计算并落库当前库存预警")
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-warning:update')")
    public CommonResult<Integer> refreshWarnings() {
        return success(stockWarningService.persistCurrentWarnings());
    }
    @PutMapping("/handle")
    @Operation(summary = "处理库存预警(标记已处理)")
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-warning:update')")
    public CommonResult<Boolean> handleWarning(@Valid @RequestBody MesWmStockWarningHandleReqVO reqVO) {
        stockWarningService.handle(reqVO);
        return success(true);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/vo/MesWmStockWarningHandleReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - åº“存预警处理 Request VO")
@Data
public class MesWmStockWarningHandleReqVO {
    @Schema(description = "预警记录编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "预警记录编号不能为空")
    private Long id;
    @Schema(description = "处理备注")
    private String handleRemark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/vo/MesWmStockWarningPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - åº“存预警分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesWmStockWarningPageReqVO extends PageParam {
    @Schema(description = "品种编码")
    private String itemCode;
    @Schema(description = "批次号")
    private String batchCode;
    @Schema(description = "预警类型:10-低库存,20-临期,30-库龄")
    private Integer warningType;
    @Schema(description = "处理状态:10-未处理,20-已处理")
    private Integer status;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warning/vo/MesWmStockWarningRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - åº“存预警 Response VO")
@Data
public class MesWmStockWarningRespVO {
    @Schema(description = "编号")
    private Long id;
    @Schema(description = "品种ID")
    private Long itemId;
    @Schema(description = "品种编码")
    private String itemCode;
    @Schema(description = "品种名称")
    private String itemName;
    @Schema(description = "批次ID")
    private Long batchId;
    @Schema(description = "批次号")
    private String batchCode;
    @Schema(description = "仓库ID")
    private Long warehouseId;
    @Schema(description = "仓库名称")
    private String warehouseName;
    @Schema(description = "预警类型:10-低库存,20-临期,30-库龄")
    private Integer warningType;
    @Schema(description = "预警类型名称")
    private String warningTypeName;
    @Schema(description = "阈值(低库存=最低库存,临期=剩余天数,库龄=超龄天数)")
    private BigDecimal thresholdValue;
    @Schema(description = "当前值(低库存=在库数量,临期=剩余天数,库龄=库龄天数)")
    private BigDecimal currentValue;
    @Schema(description = "过期日期(临期预警才有)")
    private LocalDateTime expireDate;
    @Schema(description = "入库时间(库龄预警才有)")
    private LocalDateTime receiptTime;
    @Schema(description = "处理状态:10-未处理,20-已处理")
    private Integer status;
    @Schema(description = "处理状态名称")
    private String statusName;
    @Schema(description = "处理人ID")
    private Long handleUserId;
    @Schema(description = "处理时间")
    private LocalDateTime handleTime;
    @Schema(description = "处理备注")
    private String handleRemark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/MesProTracePublicController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mes.controller.pub.trace;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
import cn.iocoder.yudao.module.mes.service.trace.MesProTracePublicService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * H5 æ‰«ç æº¯æº - å…¬å¼€æŸ¥è¯¢
 * <p>
 * å…ç™»å½•(@PermitAll)面向公众提供全程溯源查询,作为追溯体系的核心产品载体。
 * åŽç»­æ–°å¢žæ•°æ®æºåœ¨ {@link MesProTracePublicRespVO} ä¸­è¿½åŠ ä¿¡æ¯åŒºå—å³å¯ã€‚
 */
@Tag(name = "H5 æ‰«ç æº¯æº - å…¬å¼€æŸ¥è¯¢")
@RestController
@RequestMapping("/public/mes/trace")
@Validated
public class MesProTracePublicController {
    @Resource
    private MesProTracePublicService service;
    @GetMapping("/query")
    @PermitAll
    @Operation(summary = "根据追溯码查询全程溯源信息")
    public CommonResult<MesProTracePublicRespVO> query(@RequestParam("code") @NotEmpty(message = "追溯码不能为空") String code) {
        return success(service.queryTrace(code));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,212 @@
package cn.iocoder.yudao.module.mes.controller.pub.trace.vo;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
 * H5 æ‰«ç æº¯æº - å…¨ç¨‹æº¯æºä¿¡æ¯ Response VO
 * <p>
 * ä»¥ã€Œä¿¡æ¯åŒºå—」模块化组织,核心信息(品种/批次/人员/质检/入库)始终返回;
 * é‡‡è´­æ¥æºã€é”€å”®åŽ»å‘ã€åŠ å·¥æµè½¬ä¸ºå¯é€‰æ‰©å±•åŒºå—ï¼ˆæœ‰æ•°æ®æ‰éžç©ºï¼‰ã€‚åŽç»­æ–°å¢žæ•°æ®æºæŒ‰åŒä¸€æ¨¡å¼è¿½åŠ åŒºå—å­—æ®µã€‚
 */
@Schema(description = "H5 æ‰«ç æº¯æº - å…¨ç¨‹æº¯æºä¿¡æ¯ Response VO")
@Data
public class MesProTracePublicRespVO {
    @Schema(description = "追溯入口类型:BAG-袋码 / PALLET-托盘码 / BATCH-批次码", example = "BAG")
    private String sourceType;
    @Schema(description = "请求的追溯码(原始扫码值)", example = "PC20260828twoITEM_001")
    private String requestCode;
    // ====== å“ç§ä¿¡æ¯åŒºå— ======
    @Schema(description = "品种编码")
    private String itemCode;
    @Schema(description = "品种名称")
    private String itemName;
    @Schema(description = "品种规格")
    private String itemSpecification;
    // ====== ç”Ÿäº§æ‰¹æ¬¡ä¿¡æ¯åŒºå— ======
    @Schema(description = "批次号")
    private String batchCode;
    @Schema(description = "临时批次码")
    private String tempCode;
    @Schema(description = "批次状态", example = "20")
    private Integer batchStatus;
    @Schema(description = "批次状态名称", example = "生产中")
    private String batchStatusName;
    @Schema(description = "生产日期")
    private LocalDateTime produceDate;
    @Schema(description = "产线编码")
    private String lineCode;
    @Schema(description = "产线名称")
    private String lineName;
    @Schema(description = "责任人用户编号")
    private Long ownerUserId;
    @Schema(description = "责任人姓名")
    private String ownerUserName;
    @Schema(description = "计划数量")
    private BigDecimal planQuantity;
    @Schema(description = "已生成袋码数量")
    private Integer bagQuantity;
    @Schema(description = "已生成托盘数量")
    private Integer palletQuantity;
    @Schema(description = "入库时间")
    private LocalDateTime inboundTime;
    @Schema(description = "备注")
    private String remark;
    // ====== ç”Ÿäº§äººå‘˜åŒºå— ======
    @Schema(description = "生产人员列表")
    private List<WorkerVO> workers;
    // ====== æ‰˜ç›˜åŒºå— ======
    @Schema(description = "托盘总数")
    private Integer palletCount;
    @Schema(description = "托盘列表")
    private List<PalletVO> pallets;
    // ====== è¢‹ç åŒºå— ======
    @Schema(description = "袋码总数")
    private Long bagTotal;
    @Schema(description = "袋码列表(最多 100 æ¡ï¼‰")
    private List<BagVO> bags;
    // ====== å…¥åº“事件区块 ======
    @Schema(description = "最新入库事件")
    private InboundVO inbound;
    // ====== è´¨æ£€ä¿¡æ¯åŒºå— ======
    @Schema(description = "种子质检信息")
    private QualityVO quality;
    // ====== å¯é€‰æ‰©å±•区块(有数据才返回,空表返回空列表) ======
    @Schema(description = "采购来源(可选)")
    private List<MesWmBatchPurchaseSourceRespVO> purchaseSources;
    @Schema(description = "销售去向(可选)")
    private List<MesWmBatchSalesTargetRespVO> salesTargets;
    @Schema(description = "加工流转(可选)")
    private List<MesWmBatchProcessTraceRespVO> processTraces;
    @Schema(description = "生产人员")
    @Data
    public static class WorkerVO {
        @Schema(description = "岗位类型", example = "20")
        private Integer workerType;
        @Schema(description = "岗位类型名称", example = "操作工")
        private String workerTypeName;
        @Schema(description = "姓名")
        private String userName;
        @Schema(description = "备注")
        private String remark;
    }
    @Schema(description = "托盘信息")
    @Data
    public static class PalletVO {
        @Schema(description = "托盘码")
        private String code;
        @Schema(description = "托盘序号")
        private Integer palletNo;
        @Schema(description = "已装袋数")
        private Integer bagCount;
        @Schema(description = "可装袋数")
        private Integer capacity;
    }
    @Schema(description = "袋码信息")
    @Data
    public static class BagVO {
        @Schema(description = "袋码")
        private String code;
        @Schema(description = "袋序号")
        private Integer bagNo;
        @Schema(description = "绑定托盘码")
        private String palletCode;
        @Schema(description = "状态", example = "20")
        private Integer status;
        @Schema(description = "状态名称", example = "已导出印刷")
        private String statusName;
        @Schema(description = "入库时间")
        private LocalDateTime inboundTime;
    }
    @Schema(description = "入库事件")
    @Data
    public static class InboundVO {
        @Schema(description = "事件号")
        private String eventId;
        @Schema(description = "设备编码")
        private String deviceCode;
        @Schema(description = "产线编码")
        private String lineCode;
        @Schema(description = "入库数量")
        private BigDecimal quantity;
        @Schema(description = "状态", example = "1")
        private String status;
        @Schema(description = "事件时间")
        private LocalDateTime eventTime;
    }
    @Schema(description = "种子质检信息")
    @Data
    public static class QualityVO {
        @Schema(description = "质检结果", example = "1")
        private Integer result;
        @Schema(description = "质检结果名称", example = "合格")
        private String resultName;
        @Schema(description = "不合格原因")
        private String reason;
        @Schema(description = "检验员姓名")
        private String inspectorName;
        @Schema(description = "检验时间")
        private LocalDateTime inspectedAt;
        @Schema(description = "质检指标列表")
        private List<IndicatorVO> indicators;
    }
    @Schema(description = "质检指标")
    @Data
    public static class IndicatorVO {
        @Schema(description = "指标编码", example = "purity")
        private String indicatorCode;
        @Schema(description = "指标名称", example = "纯度")
        private String indicatorName;
        @Schema(description = "检测值")
        private BigDecimal value;
        @Schema(description = "下限")
        private BigDecimal minValue;
        @Schema(description = "上限")
        private BigDecimal maxValue;
        @Schema(description = "是否合格")
        private Boolean pass;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warning/MesWmStockWarningDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.warning;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES åº“存预警 DO
 * <p>
 * é¢„警类型见 {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmStockWarningTypeEnum}:
 * 10-低库存,20-临期,30-库龄。由实时计算查询 + å®šæ—¶ä»»åŠ¡è½åº“åŒé€šé“äº§ç”Ÿã€‚
 *
 * @author è¶…级管理员
 */
@TableName("mes_wm_stock_warning")
@KeySequence("mes_wm_stock_warning_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesWmStockWarningDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å“ç§ID
     *
     * å…³è” {@link MesMdmItemDO#getId()}
     */
    private Long itemId;
    /**
     * å“ç§ç¼–码
     */
    private String itemCode;
    /**
     * æ‰¹æ¬¡ID
     *
     * å…³è” {@link MesWmBatchDO#getId()}
     */
    private Long batchId;
    /**
     * æ‰¹æ¬¡å·
     */
    private String batchCode;
    /**
     * ä»“库ID
     *
     * å…³è” {@link MesWmWarehouseDO#getId()}
     */
    private Long warehouseId;
    /**
     * ä»“库名称
     */
    private String warehouseName;
    /**
     * é¢„警类型:10-低库存,20-临期,30-库龄
     */
    private Integer warningType;
    /**
     * é˜ˆå€¼ï¼ˆä½Žåº“å­˜=最低库存,临期=剩余天数,库龄=超龄天数)
     */
    private BigDecimal thresholdValue;
    /**
     * å½“前值(低库存=在库数量,临期=剩余天数,库龄=库龄天数)
     */
    private BigDecimal currentValue;
    /**
     * è¿‡æœŸæ—¥æœŸï¼ˆä¸´æœŸé¢„警才有)
     */
    private LocalDateTime expireDate;
    /**
     * å…¥åº“时间(库龄预警才有)
     */
    private LocalDateTime receiptTime;
    /**
     * å¤„理状态:10-未处理,20-已处理
     */
    private Integer status;
    /**
     * å¤„理人ID
     */
    private Long handleUserId;
    /**
     * å¤„理时间
     */
    private LocalDateTime handleTime;
    /**
     * å¤„理备注
     */
    private String handleRemark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/scan/MesProScanEventMapper.java
@@ -7,6 +7,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MesProScanEventMapper extends BaseMapperX<MesProScanEventDO> {
    default MesProScanEventDO selectByEventIdForUpdate(String eventId) {
@@ -29,4 +31,10 @@
                .eqIfPresent(MesProScanEventDO::getDeviceCode, deviceCode)
                .orderByDesc(MesProScanEventDO::getEventTime).last("LIMIT 1"));
    }
    default List<MesProScanEventDO> selectListByBatchId(Long batchId) {
        return selectList(new LambdaQueryWrapperX<MesProScanEventDO>()
                .eq(MesProScanEventDO::getBatchId, batchId)
                .orderByDesc(MesProScanEventDO::getEventTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java
@@ -1,5 +1,14 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface MesQcSeedQualityIndicatorMapper extends BaseMapperX<MesQcSeedQualityIndicatorDO> {}
import java.util.List;
@Mapper public interface MesQcSeedQualityIndicatorMapper extends BaseMapperX<MesQcSeedQualityIndicatorDO> {
    default List<MesQcSeedQualityIndicatorDO> selectListByQualityId(Long qualityId) {
        return selectList(new LambdaQueryWrapperX<MesQcSeedQualityIndicatorDO>()
                .eq(MesQcSeedQualityIndicatorDO::getQualityId, qualityId)
                .orderByAsc(MesQcSeedQualityIndicatorDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
@@ -5,9 +5,17 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MesQcSeedQualityMapper extends BaseMapperX<MesQcSeedQualityDO> {
    default MesQcSeedQualityDO selectByCode(String code) {
        return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getCode, code));
    }
    default List<MesQcSeedQualityDO> selectListByBatchCode(String batchCode) {
        return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>()
                .eq(MesQcSeedQualityDO::getBatchCode, batchCode)
                .orderByDesc(MesQcSeedQualityDO::getInspectedAt));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
@@ -206,4 +206,19 @@
                .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO));
    }
    /**
     * æŒ‰æ‰¹æ¬¡å·æ›´æ–°åº“存状态(用于种子质检合格后,将暂存库整批转为合格库)
     *
     * @param batchId   ä»“库批次ID
     * @param toState   ç›®æ ‡åº“存状态
     * @param fromState åŽŸåº“å­˜çŠ¶æ€ï¼ˆé™å®šåªæ›´æ–°è¯¥çŠ¶æ€çš„è®°å½•ï¼‰
     * @return å½±å“è¡Œæ•°
     */
    default int updateStockStateByBatchId(Long batchId, Integer toState, Integer fromState) {
        return update(new MesWmMaterialStockDO().setStockState(toState),
                new LambdaUpdateWrapper<MesWmMaterialStockDO>()
                        .eq(MesWmMaterialStockDO::getBatchId, batchId)
                        .eq(MesWmMaterialStockDO::getStockState, fromState));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stocktaking/task/MesWmStockTakingTaskLineMapper.java
@@ -40,4 +40,11 @@
                .eqIfPresent(MesWmStockTakingTaskLineDO::getAreaId, areaId));
    }
    default MesWmStockTakingTaskLineDO selectByTaskIdAndBatchId(Long taskId, Long batchId) {
        return selectOne(new LambdaQueryWrapperX<MesWmStockTakingTaskLineDO>()
                .eq(MesWmStockTakingTaskLineDO::getTaskId, taskId)
                .eq(MesWmStockTakingTaskLineDO::getBatchId, batchId)
                .last("LIMIT 1"));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/warning/MesWmStockWarningMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.warning;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warning.MesWmStockWarningDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES åº“存预警 Mapper
 */
@Mapper
public interface MesWmStockWarningMapper extends BaseMapperX<MesWmStockWarningDO> {
    default PageResult<MesWmStockWarningDO> selectPage(MesWmStockWarningPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmStockWarningDO>()
                .likeIfPresent(MesWmStockWarningDO::getItemCode, reqVO.getItemCode())
                .likeIfPresent(MesWmStockWarningDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesWmStockWarningDO::getWarningType, reqVO.getWarningType())
                .eqIfPresent(MesWmStockWarningDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesWmStockWarningDO::getCreateTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -682,6 +682,8 @@
    ErrorCode WM_STOCK_TAKING_TASK_RESULT_NOT_EXISTS = new ErrorCode(1_040_714_119, "盘点结果不存在");
    ErrorCode WM_STOCK_TAKING_TASK_LINE_ALREADY_TAKEN = new ErrorCode(1_040_714_120, "该盘点清单行已有盘点记录,不能重复盘点");
    ErrorCode WM_STOCK_TAKING_TASK_NOT_FINISHED = new ErrorCode(1_040_714_121, "只有已完成状态的盘点任务才能处理差异");
    ErrorCode WM_STOCK_TAKING_TASK_SCAN_BATCH_NOT_FOUND = new ErrorCode(1_040_714_122, "盘点扫码未定位到对应批次,请核对追溯码");
    ErrorCode WM_STOCK_TAKING_TASK_SCAN_BATCH_NOT_IN_TASK = new ErrorCode(1_040_714_123, "扫码批次不在盘点任务范围内");
    // ========== MES ä»“库管理-销售出库单(1-040-714-000) ==========
    ErrorCode WM_PRODUCT_SALES_NOT_EXISTS = new ErrorCode(1_040_714_000, "销售出库单不存在");
@@ -787,6 +789,9 @@
    ErrorCode WM_PACKAGE_HAS_CHILDREN = new ErrorCode(1_040_740_008, "存在子箱,不允许删除");
    ErrorCode WM_PACKAGE_LINE_NOT_EXISTS = new ErrorCode(1_040_740_100, "装箱明细不存在");
    // ========== MES ä»“库管理-库存预警(1-040-756-000) ==========
    ErrorCode WM_STOCK_WARNING_NOT_EXISTS = new ErrorCode(1_040_756_000, "库存预警记录不存在");
    // ========== MES äº§å“è®¾è®¡-设计项目(1-040-800-000) ==========
    ErrorCode PD_PROJECT_NOT_EXISTS = new ErrorCode(1_040_800_000, "设计项目不存在");
    ErrorCode PD_PROJECT_NOT_INITIATE = new ErrorCode(1_040_800_001, "设计项目不是待发布状态,不允许修改或删除");
@@ -869,4 +874,7 @@
    ErrorCode WMS_APPROVAL_STATUS_INVALID = new ErrorCode(1_040_900_004, "单据状态不正确,无法提交审批");
    ErrorCode WMS_APPROVAL_ALREADY_SUBMITTED = new ErrorCode(1_040_900_005, "单据已提交审批,请勿重复提交");
    // ========== MES å…¬å¼€æ‰«ç æº¯æºï¼ˆ1-040-760-000) ==========
    ErrorCode MES_TRACE_CODE_NOT_FOUND = new ErrorCode(1_040_760_000, "未查询到该追溯码,请核对二维码或联系官方");
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockWarningTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES åº“存预警类型枚举
 */
@Getter
@AllArgsConstructor
public enum MesWmStockWarningTypeEnum {
    LOW_STOCK(10, "低库存"),
    EXPIRING(20, "临期"),
    AGING(30, "库龄");
    private final Integer type;
    private final String name;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesProperties.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.framework.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * MES è‡ªå®šä¹‰é…ç½®é¡¹
 */
@Data
@Component
@ConfigurationProperties(prefix = "mes")
public class MesProperties {
    /**
     * H5 æº¯æºé¡µé¢åœ°å€ï¼ˆtrace.html)基础 URL,用于生成携带追溯码参数的二维码内容。
     * ä¸ºç©ºæ—¶äºŒç»´ç ä»ç¼–码纯码值(兼容旧码)。
     */
    private String traceBaseUrl = "";
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesQrCodeConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mes.framework.config;
import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
/**
 * MES äºŒç»´ç ç›¸å…³é…ç½®ï¼šå¯åŠ¨æ—¶å°† traceBaseUrl æ³¨å…¥äºŒç»´ç æ ‡ç­¾æž„建器,避免侵入 Controller è°ƒç”¨ç‚¹
 */
@Configuration(proxyBeanMethods = false)
public class MesQrCodeConfig {
    @Resource
    private MesProperties mesProperties;
    @PostConstruct
    public void init() {
        MesProQrLabelBuilder.setTraceBaseUrl(mesProperties.getTraceBaseUrl());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/MesProQrLabelBuilder.java
@@ -8,16 +8,39 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import java.nio.charset.StandardCharsets;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Map;
/**
 * MES äºŒç»´ç æ ‡ç­¾æž„建器:统一"二维码编码纯码值 + å›¾ç‰‡å åŠ å¯è¯»æ–‡æœ¬"的文本组装规则,
 * ä¿è¯æ‰¹æ¬¡ã€æ‰˜ç›˜ã€è¢‹ç ä¸‰ç§æ ‡ç­¾çš„关键信息保留一致
 * MES äºŒç»´ç æ ‡ç­¾æž„建器:统一"二维码编码内容 + å›¾ç‰‡å åŠ å¯è¯»æ–‡æœ¬"的文本组装规则,
 * ä¿è¯æ‰¹æ¬¡ã€æ‰˜ç›˜ã€è¢‹ç ä¸‰ç§æ ‡ç­¾çš„关键信息保留一致。
 * <p>
 * é…ç½®äº† {@code mes.trace-base-url} æ—¶ï¼ŒäºŒç»´ç å†…容为 H5 æº¯æºåœ°å€ï¼ˆå¸¦ code å‚数),否则回退为纯码值(兼容旧码)
 */
public final class MesProQrLabelBuilder {
    private MesProQrLabelBuilder() {
    }
    /**
     * H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼Œç”± {@code MesQrCodeConfig} å¯åŠ¨æ—¶æ³¨å…¥ï¼›ä¸ºç©ºæ—¶äºŒç»´ç ç¼–ç çº¯ç å€¼
     */
    private static volatile String traceBaseUrl = "";
    public static void setTraceBaseUrl(String traceBaseUrl) {
        MesProQrLabelBuilder.traceBaseUrl = StrUtil.blankToDefault(traceBaseUrl, "");
    }
    /**
     * ç»„装二维码内容:配置了溯源地址则拼 URL,否则返回纯码值
     */
    private static String buildQrCodeContent(String code) {
        if (StrUtil.isEmpty(traceBaseUrl)) {
            return code;
        }
        return traceBaseUrl + "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8);
    }
    /**
@@ -51,7 +74,7 @@
        if (user != null) {
            keyValues.put("责任人", user.getNickname());
        }
        return QrLabelInfo.builder().fileName(code).code(code).keyValues(keyValues).build();
        return QrLabelInfo.builder().fileName(code).code(buildQrCodeContent(code)).keyValues(keyValues).build();
    }
    /**
@@ -78,7 +101,7 @@
        if (batch != null) {
            fillBatchKeyValues(keyValues, batch, itemMap, userMap);
        }
        return QrLabelInfo.builder().fileName(code).code(code).keyValues(keyValues).build();
        return QrLabelInfo.builder().fileName(code).code(buildQrCodeContent(code)).keyValues(keyValues).build();
    }
    /**
@@ -98,7 +121,7 @@
        if (batch != null) {
            fillBatchKeyValues(keyValues, batch, itemMap, userMap);
        }
        return QrLabelInfo.builder().fileName(bagCode.getCode()).code(bagCode.getCode()).keyValues(keyValues).build();
        return QrLabelInfo.builder().fileName(bagCode.getCode()).code(buildQrCodeContent(bagCode.getCode())).keyValues(keyValues).build();
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/wm/MesWmStockWarningJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.job.wm;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.module.mes.service.wm.warning.MesWmStockWarningService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * MES åº“存预警定时落库 Job
 * <p>
 * é‡æ–°è®¡ç®—当前库存预警(低库存/临期/库龄),删除旧的未处理预警后写入,保留已处理历史。
 */
@Component
@Slf4j
public class MesWmStockWarningJob implements JobHandler {
    @Resource
    private MesWmStockWarningService stockWarningService;
    @Override
    public String execute(String param) {
        int count = stockWarningService.persistCurrentWarnings();
        log.info("[execute][库存预警定时落库 {} æ¡]", count);
        return String.format("库存预警定时落库 %s æ¡", count);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventService.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
public interface MesProScanEventService {
@@ -14,4 +15,12 @@
    PageResult<MesProScanEventDO> getPage(MesProScanEventPageReqVO reqVO);
    MesProScanEventDO getLatest(String deviceCode);
    Map<String, Object> getDashboard();
    /**
     * èŽ·å¾—æ‰¹æ¬¡ä¸‹çš„æ‰«ç å…¥åº“äº‹ä»¶åˆ—è¡¨ï¼ˆæŒ‰äº‹ä»¶æ—¶é—´å€’åºï¼‰ï¼Œç”¨äºŽå…¬å¼€æº¯æºå±•ç¤ºæœ€æ–°å…¥åº“äº‹ä»¶
     *
     * @param batchId æ‰¹æ¬¡ç¼–号
     * @return äº‹ä»¶åˆ—表
     */
    List<MesProScanEventDO> getByBatchId(Long batchId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventServiceImpl.java
@@ -6,13 +6,16 @@
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.scan.MesProScanEventDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
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.scan.MesProScanEventMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesProScanEventStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
@@ -23,6 +26,7 @@
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@@ -33,6 +37,7 @@
    @Resource private MesProBatchMapper batchMapper;
    @Resource private MesWmTransactionService transactionService;
    @Resource private MesWmWarehouseService warehouseService;
    @Resource private MesWmBatchService mesWmBatchService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -60,12 +65,19 @@
        if (warehouse == null) return failed(event, "MES_SCAN_WIP_WAREHOUSE_NOT_EXISTS", "WIP虚拟仓不存在");
        event.setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId());
        try {
            // ç”Ÿäº§æ‰¹æ¬¡æ¡¥æŽ¥ä»“库批次:库存/流水统一挂在 mes_wm_batch ä¸Šï¼Œæ‰«ç å…¥æš‚存库待质检
            MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(batch);
            if (wmBatch == null) {
                return failed(event, "MES_SCAN_WM_BATCH_NOT_EXISTS", "仓库批次桥接失败");
            }
            Long txId = transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType()).setBizType(MesBizTypeConstants.WM_PRODUCT_PRODUCE)
                    .setBizId(batch.getId()).setBizCode(batch.getCode()).setBizLineId(bag.getId())
                    .setItemId(batch.getItemId()).setQuantity(req.getQuantity()).setBatchCode(batch.getCode())
                    .setItemId(batch.getItemId()).setQuantity(req.getQuantity())
                    .setBatchId(wmBatch.getId()).setBatchCode(batch.getCode())
                    .setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId())
                    .setReceiptTime(LocalDateTime.now()).setCheckFlag(true));
                    .setReceiptTime(LocalDateTime.now()).setCheckFlag(true)
                    .setStockState(MesWmStockStateEnum.TEMP.getState()));
            event.setTransactionId(txId).setStatus(MesProScanEventStatusEnum.SUCCESS).setEventTime(LocalDateTime.now());
            eventMapper.updateById(event);
            bagCodeMapper.markInbound(bag.getId(), event.getId(), req.getDeviceCode(), req.getLineCode());
@@ -86,6 +98,7 @@
    @Override public MesProScanEventDO getByEventId(String eventId) { return eventMapper.selectOne(MesProScanEventDO::getEventId, eventId); }
    @Override public PageResult<MesProScanEventDO> getPage(MesProScanEventPageReqVO reqVO) { return eventMapper.selectPage(reqVO); }
    @Override public MesProScanEventDO getLatest(String deviceCode) { return eventMapper.selectLatest(deviceCode); }
    @Override public List<MesProScanEventDO> getByBatchId(Long batchId) { return eventMapper.selectListByBatchId(batchId); }
    @Override public Map<String, Object> getDashboard() {
        Map<String, Object> result = new HashMap<>();
        result.put("latest", eventMapper.selectLatest(null));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
@@ -1,3 +1,29 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
public interface MesQcSeedQualityService { Long submit(MesQcSeedQualitySubmitReqVO reqVO); }
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO;
import java.util.List;
public interface MesQcSeedQualityService {
    Long submit(MesQcSeedQualitySubmitReqVO reqVO);
    /**
     * æ ¹æ®æ‰¹æ¬¡å·æŸ¥è¯¢è´¨æ£€è®°å½•(按检验时间倒序,取最新),用于公开溯源。
     * ç§å­è´¨æ£€è¡¨æœªåˆå§‹åŒ–时返回空列表(容错,不影响追溯)。
     *
     * @param batchCode æ‰¹æ¬¡å·
     * @return è´¨æ£€è®°å½•列表
     */
    List<MesQcSeedQualityDO> getQualityByBatchCode(String batchCode);
    /**
     * æ ¹æ®è´¨æ£€è®°å½•编号查询质检指标列表
     *
     * @param qualityId è´¨æ£€è®°å½•编号
     * @return è´¨æ£€æŒ‡æ ‡åˆ—表
     */
    List<MesQcSeedQualityIndicatorDO> getIndicatorsByQualityId(Long qualityId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -2,20 +2,31 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.*;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.*;
@Service @Validated
@Service @Validated @Slf4j
public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
    private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
    @Resource private MesQcSeedQualityMapper qualityMapper;
    @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
    @Resource private MesProBatchService proBatchService;
    @Resource private MesWmBatchService mesWmBatchService;
    @Resource private MesWmMaterialStockService materialStockService;
    @Override @Transactional(rollbackFor = Exception.class)
    public Long submit(MesQcSeedQualitySubmitReqVO reqVO) {
@@ -36,7 +47,41 @@
        if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("四项指标不完整");
        quality.setResult(passed ? 1 : 2);
        qualityMapper.updateById(quality);
        // è´¨æ£€åˆæ ¼ â†’ åº“存整批由暂存库(待质检)转为合格库(正式库存)
        if (passed) {
            MesProBatchDO proBatch = proBatchService.getBatchByCode(reqVO.getBatchCode());
            if (proBatch != null) {
                MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(proBatch);
                if (wmBatch != null) {
                    int updated = materialStockService.updateStockStateByBatchId(wmBatch.getId(),
                            MesWmStockStateEnum.QUALIFIED.getState(), MesWmStockStateEnum.TEMP.getState());
                    log.info("[submit][质检合格,批次 {} åº“存由暂存库转合格库,影响 {} æ¡]", reqVO.getBatchCode(), updated);
                }
            }
        }
        // åº“位由后台配置服务接管,当前版本保留审计闭环,禁止前端传入库位。
        return quality.getId();
    }
    @Override
    public List<MesQcSeedQualityDO> getQualityByBatchCode(String batchCode) {
        try {
            return qualityMapper.selectListByBatchCode(batchCode);
        } catch (BadSqlGrammarException ex) {
            // ç§å­è´¨æ£€è¡¨æœªåˆå§‹åŒ–时容错,不阻塞公开溯源
            log.warn("[getQualityByBatchCode][种子质检表未初始化,批次 {} æŸ¥è¯¢å¤±è´¥: {}]", batchCode, ex.getMessage());
            return List.of();
        }
    }
    @Override
    public List<MesQcSeedQualityIndicatorDO> getIndicatorsByQualityId(Long qualityId) {
        try {
            return indicatorMapper.selectListByQualityId(qualityId);
        } catch (BadSqlGrammarException ex) {
            // ç§å­è´¨æ£€è¡¨æœªåˆå§‹åŒ–时容错
            log.warn("[getIndicatorsByQualityId][种子质检指标表未初始化,质检 {} æŸ¥è¯¢å¤±è´¥: {}]", qualityId, ex.getMessage());
            return List.of();
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.service.trace;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
/**
 * å…¬å¼€æ‰«ç æº¯æº Service æŽ¥å£
 * <p>
 * é¢å‘公众(免登录),根据袋码 / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç æŸ¥è¯¢å…¨ç¨‹æº¯æºä¿¡æ¯ã€‚作为追溯体系的核心聚合入口,
 * åŽç»­æ–°å¢žæ•°æ®æºï¼ˆè´¨æ£€æ‰©å±•、防伪验证、储运信息等)在聚合实现中追加信息区块即可。
 */
public interface MesProTracePublicService {
    /**
     * æ ¹æ®è¿½æº¯ç æŸ¥è¯¢å…¨ç¨‹æº¯æºä¿¡æ¯
     *
     * @param code è¿½æº¯ç ï¼ˆè¢‹ç  / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç ï¼Œå…¼å®¹æ•´ H5 URL)
     * @return å…¨ç¨‹æº¯æºä¿¡æ¯
     */
    MesProTracePublicRespVO queryTrace(String code);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,348 @@
package cn.iocoder.yudao.module.mes.service.trace;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
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.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO;
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.MesProBatchWorkerTypeEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import cn.iocoder.yudao.module.mes.service.pro.scan.MesProScanEventService;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.MES_TRACE_CODE_NOT_FOUND;
/**
 * å…¬å¼€æ‰«ç æº¯æº Service å®žçް
 * <p>
 * è¿½æº¯ç è§£æžé¡ºåºï¼šè¢‹ç  â†’ æ‰˜ç›˜ç  â†’ æ‰¹æ¬¡ç ï¼ˆå«ä¸´æ—¶æ‰¹æ¬¡ç ï¼‰ã€‚以生产批次为主数据源聚合全程信息。
 */
@Service
@Validated
@Slf4j
public class MesProTracePublicServiceImpl implements MesProTracePublicService {
    /**
     * H5 è¢‹ç åˆ—表展示上限(全量统计 + é™é‡æ˜Žç»†ï¼‰
     */
    private static final int BAG_LIST_LIMIT = 100;
    @Resource
    private MesProBagCodeService bagCodeService;
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesProBatchService batchService;
    @Resource
    private MesProScanEventService scanEventService;
    @Resource
    private MesQcSeedQualityService qcSeedQualityService;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmBatchService mesWmBatchService;
    @Override
    public MesProTracePublicRespVO queryTrace(String code) {
        String rawCode = StrUtil.trim(StrUtil.nullToEmpty(code));
        String traceCode = normalize(rawCode);
        if (StrUtil.isEmpty(traceCode)) {
            throw exception(MES_TRACE_CODE_NOT_FOUND);
        }
        // 1. è§£æžè¿½æº¯å…¥å£ï¼Œå®šä½æ‰¹æ¬¡
        TraceEntry entry = resolveEntry(traceCode);
        if (entry.batchId == null) {
            throw exception(MES_TRACE_CODE_NOT_FOUND);
        }
        // 2. èšåˆå…¨ç¨‹ä¿¡æ¯
        return buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId);
    }
    // ==================== è¿½æº¯ç è§£æž ====================
    /**
     * å…¼å®¹æ•´ URL æ‰«ç ï¼ˆäºŒç»´ç å†…容为 H5 åœ°å€æ—¶ï¼‰ï¼Œä»…提取 code å‚数;否则原样返回
     */
    private String normalize(String code) {
        if (StrUtil.startWithIgnoreCase(code, "http")) {
            String query = StrUtil.subAfter(code, "?", false);
            if (StrUtil.isNotEmpty(query)) {
                // åŽ»æŽ‰ # é”šç‚¹
                query = StrUtil.subBefore(query, "#", false);
                for (String pair : StrUtil.split(query, "&")) {
                    String[] kv = pair.split("=", 2);
                    if (kv.length == 2 && "code".equals(kv[0])) {
                        return StrUtil.trim(URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
                    }
                }
            }
            return "";
        }
        return code;
    }
    private TraceEntry resolveEntry(String traceCode) {
        // â‘  è¢‹ç 
        MesProBagCodeDO bag = bagCodeService.getBagCodeByCode(traceCode);
        if (bag != null) {
            return new TraceEntry("BAG", bag.getBatchId());
        }
        // â‘¡ æ‰˜ç›˜ç 
        MesProPalletDO pallet = palletService.getPalletByCode(traceCode);
        if (pallet != null) {
            return new TraceEntry("PALLET", pallet.getBatchId());
        }
        // â‘¢ æ‰¹æ¬¡ç ï¼ˆå«ä¸´æ—¶æ‰¹æ¬¡ç ï¼ŒgetBatchByCode å†…部兼容)
        MesProBatchDO batch = batchService.getBatchByCode(traceCode);
        if (batch != null) {
            return new TraceEntry("BATCH", batch.getId());
        }
        return new TraceEntry("", null);
    }
    // ==================== èšåˆæž„建 ====================
    private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, String sourceType, Long batchId) {
        MesProBatchDO batch = batchService.getBatch(batchId);
        if (batch == null) {
            throw exception(MES_TRACE_CODE_NOT_FOUND);
        }
        MesProTracePublicRespVO resp = new MesProTracePublicRespVO();
        resp.setSourceType(sourceType);
        resp.setRequestCode(requestCode);
        // å“ç§åŒºå—
        fillItem(resp, batch);
        // æ‰¹æ¬¡åŒºå—
        resp.setBatchCode(batch.getCode()).setTempCode(batch.getTempCode())
                .setBatchStatus(batch.getStatus()).setBatchStatusName(batchStatusName(batch.getStatus()))
                .setProduceDate(batch.getProduceDate()).setLineCode(batch.getLineCode()).setLineName(batch.getLineName())
                .setOwnerUserId(batch.getOwnerUserId()).setOwnerUserName(userName(batch.getOwnerUserId()))
                .setPlanQuantity(batch.getPlanQuantity()).setBagQuantity(batch.getBagQuantity())
                .setPalletQuantity(batch.getPalletQuantity()).setInboundTime(batch.getInboundTime())
                .setRemark(batch.getRemark());
        // äººå‘˜åŒºå—
        resp.setWorkers(buildWorkers(batchId));
        // æ‰˜ç›˜åŒºå—
        fillPallets(resp, batchId);
        // è¢‹ç åŒºå—
        fillBags(resp, batchId);
        // å…¥åº“事件区块
        resp.setInbound(buildInbound(batchId));
        // è´¨æ£€åŒºå—
        resp.setQuality(buildQuality(batch));
        // å¯é€‰æ‰©å±•区块(有数据才返回)
        fillOptionalModules(resp, batch);
        return resp;
    }
    private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch) {
        if (batch.getItemId() == null) {
            return;
        }
        MesMdmItemDO item = mdmItemService.getItem(batch.getItemId());
        if (item == null) {
            return;
        }
        resp.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification());
    }
    private List<MesProTracePublicRespVO.WorkerVO> buildWorkers(Long batchId) {
        List<MesProBatchWorkerDO> workers = batchService.getBatchWorkerListByBatchId(batchId);
        if (CollUtil.isEmpty(workers)) {
            return List.of();
        }
        Set<Long> userIds = convertSet(workers, MesProBatchWorkerDO::getUserId);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        return convertList(workers, worker -> {
            MesProTracePublicRespVO.WorkerVO vo = new MesProTracePublicRespVO.WorkerVO();
            vo.setWorkerType(worker.getWorkerType());
            vo.setWorkerTypeName(workerTypeName(worker.getWorkerType()));
            vo.setRemark(worker.getRemark());
            AdminUserRespDTO user = userMap.get(worker.getUserId());
            vo.setUserName(user == null ? null : user.getNickname());
            return vo;
        });
    }
    private void fillPallets(MesProTracePublicRespVO resp, Long batchId) {
        List<MesProPalletDO> pallets = palletService.getPalletListByBatchId(batchId);
        resp.setPalletCount(CollUtil.isEmpty(pallets) ? 0 : pallets.size());
        resp.setPallets(convertList(pallets, pallet -> {
            MesProTracePublicRespVO.PalletVO vo = new MesProTracePublicRespVO.PalletVO();
            vo.setCode(pallet.getCode()).setPalletNo(pallet.getPalletNo())
                    .setBagCount(pallet.getBagCount()).setCapacity(pallet.getCapacity());
            return vo;
        }));
    }
    private void fillBags(MesProTracePublicRespVO resp, Long batchId) {
        List<MesProBagCodeDO> bags = bagCodeService.getBagCodeListByBatchId(batchId);
        List<MesProBagCodeDO> safeBags = bags == null ? List.of() : bags;
        resp.setBagTotal((long) safeBags.size());
        resp.setBags(convertList(safeBags.stream().limit(BAG_LIST_LIMIT).toList(), bag -> {
            MesProTracePublicRespVO.BagVO vo = new MesProTracePublicRespVO.BagVO();
            vo.setCode(bag.getCode()).setBagNo(bag.getBagNo()).setPalletCode(bag.getPalletCode())
                    .setStatus(bag.getStatus()).setStatusName(bagCodeStatusName(bag.getStatus()))
                    .setInboundTime(bag.getInboundTime());
            return vo;
        }));
    }
    private MesProTracePublicRespVO.InboundVO buildInbound(Long batchId) {
        List<MesProScanEventDO> events = scanEventService.getByBatchId(batchId);
        if (CollUtil.isEmpty(events)) {
            return null;
        }
        MesProScanEventDO event = events.get(0);
        MesProTracePublicRespVO.InboundVO vo = new MesProTracePublicRespVO.InboundVO();
        vo.setEventId(event.getEventId()).setDeviceCode(event.getDeviceCode()).setLineCode(event.getLineCode())
                .setQuantity(event.getQuantity()).setStatus(event.getStatus()).setEventTime(event.getEventTime());
        return vo;
    }
    private MesProTracePublicRespVO.QualityVO buildQuality(MesProBatchDO batch) {
        List<MesQcSeedQualityDO> list = qcSeedQualityService.getQualityByBatchCode(batch.getCode());
        if (CollUtil.isEmpty(list)) {
            return null;
        }
        MesQcSeedQualityDO quality = list.get(0);
        MesProTracePublicRespVO.QualityVO vo = new MesProTracePublicRespVO.QualityVO();
        vo.setResult(quality.getResult()).setResultName(qualityResultName(quality.getResult()))
                .setReason(quality.getReason()).setInspectorName(userName(quality.getInspectorId()))
                .setInspectedAt(quality.getInspectedAt());
        List<MesQcSeedQualityIndicatorDO> indicators = qcSeedQualityService.getIndicatorsByQualityId(quality.getId());
        vo.setIndicators(convertList(indicators, ind -> {
            MesProTracePublicRespVO.IndicatorVO i = new MesProTracePublicRespVO.IndicatorVO();
            i.setIndicatorCode(ind.getIndicatorCode()).setIndicatorName(indicatorName(ind.getIndicatorCode()))
                    .setValue(ind.getValue()).setMinValue(ind.getMinValue()).setMaxValue(ind.getMaxValue());
            i.setPass(ind.getValue() != null && ind.getMinValue() != null && ind.getMaxValue() != null
                    && ind.getValue().compareTo(ind.getMinValue()) >= 0 && ind.getValue().compareTo(ind.getMaxValue()) <= 0);
            return i;
        }));
        return vo;
    }
    private void fillOptionalModules(MesProTracePublicRespVO resp, MesProBatchDO batch) {
        try {
            List<MesWmBatchPurchaseSourceRespVO> purchaseSources = mesWmBatchService.getPurchaseSourceList(batch.getCode());
            List<MesWmBatchSalesTargetRespVO> salesTargets = mesWmBatchService.getSalesTargetList(batch.getCode());
            List<MesWmBatchProcessTraceRespVO> processTraces = mesWmBatchService.getProcessTraceList(batch.getCode());
            resp.setPurchaseSources(CollUtil.isNotEmpty(purchaseSources) ? purchaseSources : List.of());
            resp.setSalesTargets(CollUtil.isNotEmpty(salesTargets) ? salesTargets : List.of());
            resp.setProcessTraces(CollUtil.isNotEmpty(processTraces) ? processTraces : List.of());
        } catch (Exception ex) {
            // ä»“库批次体系(采购/销售/流转)未启用时容错,不影响核心追溯
            log.warn("[fillOptionalModules][批次 {} æ‰©å±•模块查询失败: {}]", batch.getCode(), ex.getMessage());
            resp.setPurchaseSources(List.of());
            resp.setSalesTargets(List.of());
            resp.setProcessTraces(List.of());
        }
    }
    // ==================== åç§°æ˜ å°„ ====================
    private String userName(Long userId) {
        if (userId == null) {
            return null;
        }
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        return user == null ? null : user.getNickname();
    }
    private String batchStatusName(Integer status) {
        if (status == null) {
            return "";
        }
        for (MesProBatchStatusEnum e : MesProBatchStatusEnum.values()) {
            if (e.getStatus().equals(status)) {
                return e.getName();
            }
        }
        return String.valueOf(status);
    }
    private String bagCodeStatusName(Integer status) {
        if (status == null) {
            return "";
        }
        for (MesProBagCodeStatusEnum e : MesProBagCodeStatusEnum.values()) {
            if (e.getStatus().equals(status)) {
                return e.getName();
            }
        }
        return String.valueOf(status);
    }
    private String workerTypeName(Integer workerType) {
        if (workerType == null) {
            return "";
        }
        for (MesProBatchWorkerTypeEnum e : MesProBatchWorkerTypeEnum.values()) {
            if (e.getType().equals(workerType)) {
                return e.getName();
            }
        }
        return String.valueOf(workerType);
    }
    private String qualityResultName(Integer result) {
        if (result == null) {
            return "";
        }
        return switch (result) {
            case 0 -> "处理中";
            case 1 -> "合格";
            case 2 -> "不合格";
            default -> String.valueOf(result);
        };
    }
    private String indicatorName(String code) {
        if (code == null) {
            return "";
        }
        return switch (code) {
            case "purity" -> "纯度";
            case "cleanliness" -> "净度";
            case "germination_rate" -> "发芽率";
            case "moisture" -> "水分";
            default -> code;
        };
    }
    private record TraceEntry(String sourceType, Long batchId) {
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import java.util.List;
@@ -131,4 +132,15 @@
     */
    MesWmBatchDO getBatchBySn(String sn);
    /**
     * æ ¹æ®ç”Ÿäº§æ‰¹æ¬¡èŽ·å–æˆ–åˆ›å»ºä»“åº“æ‰¹æ¬¡ï¼ˆæ‰“é€šç”Ÿäº§æ‰¹æ¬¡ä½“ç³»ä¸Žä»“åº“æ‰¹æ¬¡ä½“ç³»ï¼‰
     * <p>
     * ç”Ÿäº§çº¿æ‰«ç å…¥åº“、销售出库、质检转合格等流程,库存与流水统一挂在仓库批次(mes_wm_batch)上,
     * é€šè¿‡ç”Ÿäº§æ‰¹æ¬¡å·ï¼ˆmes_pro_batch.code)桥接,已存在则直接复用,否则新建。
     *
     * @param proBatch ç”Ÿäº§æ‰¹æ¬¡
     * @return ä»“库批次(含 id)
     */
    MesWmBatchDO getOrCreateByProBatch(MesProBatchDO proBatch);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
@@ -10,12 +10,14 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.sn.MesWmSnDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.sn.MesWmSnMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
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.wm.barcode.MesWmBarcodeService;
@@ -415,4 +417,28 @@
        return batchMapper.selectByCode(snDO.getBatchCode());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesWmBatchDO getOrCreateByProBatch(MesProBatchDO proBatch) {
        if (proBatch == null || StrUtil.isBlank(proBatch.getCode())) {
            return null;
        }
        // 1. å¤ç”¨å·²æœ‰ä»“库批次(同一生产批次号)
        MesWmBatchDO existing = batchMapper.selectByCode(proBatch.getCode());
        if (existing != null) {
            return existing;
        }
        // 2. æ–°å»ºä»“库批次:以生产批次号作为仓库批次号,质量状态置为待检验
        MesWmBatchDO batch = MesWmBatchDO.builder()
                .code(proBatch.getCode())
                .itemId(proBatch.getItemId())
                .produceDate(proBatch.getProduceDate())
                .receiptDate(java.time.LocalDateTime.now())
                .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
                .remark("生产批次自动桥接")
                .build();
        batchMapper.insert(batch);
        return batch;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockMoveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import jakarta.validation.Valid;
@@ -142,6 +143,40 @@
    void updateMaterialStockState(Long id, Integer stockState);
    /**
     * æŒ‰å“ç§ç»´åº¦æ±‡æ€»åº“存数量(内存聚合,支持 stockState ç­‰è¿‡æ»¤æ¡ä»¶ï¼‰
     *
     * @param reqVO è¿‡æ»¤æ¡ä»¶
     * @return å“ç§æ±‡æ€»åˆ—表
     */
    List<MesWmStockSummaryRespVO> getStockSummaryByItem(MesWmMaterialStockListReqVO reqVO);
    /**
     * æŒ‰æ‰¹æ¬¡ç»´åº¦æ±‡æ€»åº“存数量(内存聚合,支持 stockState ç­‰è¿‡æ»¤æ¡ä»¶ï¼‰
     *
     * @param reqVO è¿‡æ»¤æ¡ä»¶
     * @return æ‰¹æ¬¡æ±‡æ€»åˆ—表
     */
    List<MesWmStockSummaryRespVO> getStockSummaryByBatch(MesWmMaterialStockListReqVO reqVO);
    /**
     * æŒ‰ä»“库维度汇总库存数量(内存聚合,支持 stockState ç­‰è¿‡æ»¤æ¡ä»¶ï¼‰
     *
     * @param reqVO è¿‡æ»¤æ¡ä»¶
     * @return ä»“库汇总列表
     */
    List<MesWmStockSummaryRespVO> getStockSummaryByWarehouse(MesWmMaterialStockListReqVO reqVO);
    /**
     * æŒ‰ä»“库批次更新库存状态(用于种子质检合格后,将暂存库整批转为合格库)
     *
     * @param batchId   ä»“库批次ID
     * @param toState   ç›®æ ‡åº“存状态
     * @param fromState åŽŸåº“å­˜çŠ¶æ€ï¼ˆé™å®šåªæ›´æ–°è¯¥çŠ¶æ€çš„è®°å½•ï¼‰
     * @return å½±å“è¡Œæ•°
     */
    int updateStockStateByBatchId(Long batchId, Integer toState, Integer fromState);
    /**
     * åº“位混放规则校验
     *
     * æ ¹æ®åº“位配置的物料和批次混放规则,检查是否允许在该库位存放指定物料/批次
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockMoveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
@@ -39,6 +40,8 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -170,6 +173,86 @@
    }
    @Override
    public List<MesWmStockSummaryRespVO> getStockSummaryByItem(MesWmMaterialStockListReqVO reqVO) {
        List<MesWmMaterialStockDO> stocks = getMaterialStockList(reqVO);
        List<MesWmStockSummaryRespVO> list = buildStockSummary(stocks,
                stock -> String.valueOf(stock.getItemId()),
                (vo, stock) -> vo.setItemId(stock.getItemId()));
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(stocks, MesWmMaterialStockDO::getItemId));
        list.forEach(vo -> {
            MesMdmItemDO item = itemMap.get(vo.getItemId());
            if (item != null) {
                vo.setItemCode(item.getCode()).setItemName(item.getName());
            }
        });
        return list;
    }
    @Override
    public List<MesWmStockSummaryRespVO> getStockSummaryByBatch(MesWmMaterialStockListReqVO reqVO) {
        List<MesWmMaterialStockDO> stocks = getMaterialStockList(reqVO);
        return buildStockSummary(stocks,
                stock -> stock.getBatchId() + "|" + (stock.getBatchCode() != null ? stock.getBatchCode() : ""),
                (vo, stock) -> vo.setBatchId(stock.getBatchId()).setBatchCode(stock.getBatchCode()));
    }
    @Override
    public List<MesWmStockSummaryRespVO> getStockSummaryByWarehouse(MesWmMaterialStockListReqVO reqVO) {
        List<MesWmMaterialStockDO> stocks = getMaterialStockList(reqVO);
        List<MesWmStockSummaryRespVO> list = buildStockSummary(stocks,
                stock -> String.valueOf(stock.getWarehouseId()),
                (vo, stock) -> vo.setWarehouseId(stock.getWarehouseId()));
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(stocks, MesWmMaterialStockDO::getWarehouseId));
        list.forEach(vo -> {
            MesWmWarehouseDO warehouse = warehouseMap.get(vo.getWarehouseId());
            if (warehouse != null) {
                vo.setWarehouseName(warehouse.getName());
            }
        });
        return list;
    }
    /**
     * å†…存聚合库存数量:按维度键 + åº“存状态分组,累加在库数量
     */
    private List<MesWmStockSummaryRespVO> buildStockSummary(List<MesWmMaterialStockDO> stocks,
                                                            Function<MesWmMaterialStockDO, String> dimKeyFn,
                                                            BiConsumer<MesWmStockSummaryRespVO, MesWmMaterialStockDO> dimSetter) {
        if (CollUtil.isEmpty(stocks)) {
            return Collections.emptyList();
        }
        Map<String, MesWmStockSummaryRespVO> map = new LinkedHashMap<>();
        for (MesWmMaterialStockDO stock : stocks) {
            String key = dimKeyFn.apply(stock) + "|" + stock.getStockState();
            MesWmStockSummaryRespVO vo = map.computeIfAbsent(key, k -> {
                MesWmStockSummaryRespVO newVo = new MesWmStockSummaryRespVO();
                dimSetter.accept(newVo, stock);
                newVo.setStockState(stock.getStockState());
                newVo.setStockStateName(stockStateName(stock.getStockState()));
                newVo.setTotalQuantity(BigDecimal.ZERO);
                return newVo;
            });
            BigDecimal qty = stock.getQuantity() != null ? stock.getQuantity() : BigDecimal.ZERO;
            vo.setTotalQuantity(vo.getTotalQuantity().add(qty));
        }
        return new ArrayList<>(map.values());
    }
    private String stockStateName(Integer state) {
        if (state == null) {
            return "";
        }
        for (MesWmStockStateEnum e : MesWmStockStateEnum.values()) {
            if (e.getState().equals(state)) {
                return e.getName();
            }
        }
        return String.valueOf(state);
    }
    @Override
    public MesWmMaterialStockDO getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
                                                         Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime) {
        // 1. æŸ¥æ‰¾å·²æœ‰åº“存记录(vendorId å‚与唯一键)
@@ -222,6 +305,11 @@
    }
    @Override
    public int updateStockStateByBatchId(Long batchId, Integer toState, Integer fromState) {
        return materialStockMapper.updateStockStateByBatchId(batchId, toState, fromState);
    }
    @Override
    public void checkAreaMixingRule(Long areaId, Long itemId, Long batchId) {
        // 1.1 æ ¡éªŒåº“位必须存在
        MesWmWarehouseAreaDO area = areaService.validateWarehouseAreaExists(areaId);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesDetailMapper;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
@@ -34,6 +35,7 @@
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeService;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.stockreserve.MesWmStockReserveService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
@@ -75,7 +77,9 @@
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesProBatchService wmBatchService;
    private MesProBatchService proBatchService;
    @Resource
    private MesWmBatchService mesWmBatchService;
    @Resource
    private MesWmProductSalesLineService productSalesLineService;
@@ -241,9 +245,14 @@
                    .setCodeId(codeId).setScanContent(reqVO.getScanContent()).setAlreadyScanned(true)
                    .setDetailId(existed.getId()).setOutboundQuantity(existed.getQuantity()).setMessage("该码已扫描");
        }
        Long batchId = pallet != null ? pallet.getBatchId() : bag.getBatchId();
        MesProBatchDO batch = wmBatchService.getBatch(batchId);
        Long proBatchId = pallet != null ? pallet.getBatchId() : bag.getBatchId();
        MesProBatchDO batch = proBatchService.getBatch(proBatchId);
        if (batch == null) {
            throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS);
        }
        // ç”Ÿäº§æ‰¹æ¬¡æ¡¥æŽ¥ä»“库批次:扫码入库写入的是仓库批次(mes_wm_batch),此处必须用仓库批次 id æŸ¥åº“å­˜
        MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(batch);
        if (wmBatch == null) {
            throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS);
        }
        List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(reqVO.getSalesId());
@@ -255,7 +264,7 @@
        if (quantity == null) {
            quantity = line.getQuantity();
        }
        List<MesWmMaterialStockDO> stocks = materialStockService.getMaterialStockListByItemAndBatch(batch.getItemId(), batchId);
        List<MesWmMaterialStockDO> stocks = materialStockService.getMaterialStockListByItemAndBatch(batch.getItemId(), wmBatch.getId());
        BigDecimal outboundQuantity = quantity;
        MesWmMaterialStockDO stock = stocks.stream().filter(item -> item.getQuantity().compareTo(outboundQuantity) >= 0).findFirst().orElse(null);
        if (stock == null) {
@@ -268,13 +277,13 @@
        }
        MesWmProductSalesDetailDO detail = new MesWmProductSalesDetailDO().setLineId(line.getId())
                .setSalesId(reqVO.getSalesId()).setItemId(batch.getItemId()).setQuantity(quantity)
                .setMaterialStockId(stock.getId()).setBatchId(batchId).setBatchCode(batch.getCode())
                .setMaterialStockId(stock.getId()).setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode())
                .setWarehouseId(stock.getWarehouseId()).setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId())
                .setBagCodeId(pallet == null ? codeId : null).setPalletCodeId(pallet != null ? codeId : null);
        productSalesDetailMapper.insert(detail);
        return new MesWmProductSalesScanRespVO().setScanType(pallet != null ? "PALLET" : "BAG")
                .setCodeId(codeId).setScanContent(reqVO.getScanContent()).setItemId(batch.getItemId())
                .setBatchId(batchId).setBatchCode(batch.getCode()).setMaterialStockId(stock.getId())
                .setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()).setMaterialStockId(stock.getId())
                .setAvailableQuantity(stock.getQuantity()).setScanQuantity(quantity).setOutboundQuantity(quantity)
                .setAlreadyScanned(false).setDetailId(detail.getId()).setWarehouseId(stock.getWarehouseId())
                .setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId()).setMessage("扫码成功");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskService.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.MesWmStockTakingTaskPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.MesWmStockTakingTaskSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.MesWmStockTakingTaskScanReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskDO;
import jakarta.validation.Valid;
@@ -97,6 +98,14 @@
    PageResult<MesWmStockTakingTaskDO> getStockTakingTaskPage(MesWmStockTakingTaskPageReqVO pageReqVO);
    /**
     * PDA æ‰«ç ç›˜ç‚¹ï¼šè§£æžè¿½æº¯ç ï¼ˆè¢‹ç /托盘码/批次号)定位批次,更新盘点结果与任务行
     *
     * @param scanReqVO æ‰«ç ç›˜ç‚¹è¯·æ±‚
     * @return ç›˜ç‚¹ç»“果编号
     */
    Long scanStockTakingTask(MesWmStockTakingTaskScanReqVO scanReqVO);
    /**
     * å¤„理盘点差异,调整库存
     *
     * è¯¥æ–¹æ³•会比较盘点结果与库存台账的差异:
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskServiceImpl.java
@@ -2,17 +2,29 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.MesWmStockTakingTaskPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.MesWmStockTakingTaskSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.MesWmStockTakingTaskScanReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.result.MesWmStockTakingTaskResultSaveReqVO;
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.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskResultDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.stocktaking.task.MesWmStockTakingTaskLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.stocktaking.task.MesWmStockTakingTaskMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockTakingTaskStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.stocktaking.plan.MesWmStockTakingPlanService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
@@ -56,6 +68,17 @@
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private MesWmStockTakingTaskLineMapper stockTakingTaskLineMapper;
    @Resource
    private MesWmBatchService mesWmBatchService;
    @Resource
    private MesProBatchService proBatchService;
    @Resource
    private MesProBagCodeMapper bagCodeMapper;
    @Resource
    private MesProPalletMapper palletMapper;
    @Resource
    private AdminUserApi adminUserApi;
@@ -231,6 +254,69 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long scanStockTakingTask(MesWmStockTakingTaskScanReqVO scanReqVO) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨ä¸”å¤„äºŽç›˜ç‚¹ä¸­çŠ¶æ€
        validateStockTakingTaskExistsAndApproving(scanReqVO.getTaskId());
        // 2. è§£æžè¿½æº¯ç å®šä½ä»“库批次(支持批次号/袋码/托盘码,走生产批次桥接)
        MesWmBatchDO wmBatch = resolveWmBatchByScanCode(scanReqVO.getScanCode());
        if (wmBatch == null) {
            throw exception(WM_STOCK_TAKING_TASK_SCAN_BATCH_NOT_FOUND);
        }
        // 3. åœ¨ä»»åŠ¡èŒƒå›´å†…å®šä½ä»»åŠ¡è¡Œ
        MesWmStockTakingTaskLineDO line = stockTakingTaskLineMapper
                .selectByTaskIdAndBatchId(scanReqVO.getTaskId(), wmBatch.getId());
        if (line == null) {
            throw exception(WM_STOCK_TAKING_TASK_SCAN_BATCH_NOT_IN_TASK);
        }
        // 4. å†™å…¥ç›˜ç‚¹ç»“果(复用 createStockTakingTaskResult:含重复盘点校验,联动更新任务行盘点数量与盈亏状态)
        MesWmStockTakingTaskResultSaveReqVO resultReqVO = new MesWmStockTakingTaskResultSaveReqVO()
                .setTaskId(line.getTaskId()).setLineId(line.getId())
                .setMaterialStockId(line.getMaterialStockId())
                .setItemId(line.getItemId()).setBatchId(line.getBatchId()).setBatchCode(line.getBatchCode())
                .setWarehouseId(line.getWarehouseId()).setLocationId(line.getLocationId()).setAreaId(line.getAreaId())
                .setTakingQuantity(scanReqVO.getTakingQuantity())
                .setRemark(scanReqVO.getRemark());
        return stockTakingTaskResultService.createStockTakingTaskResult(resultReqVO);
    }
    /**
     * è§£æžè¿½æº¯ç å®šä½ä»“库批次:依次匹配批次号、袋码、托盘码,生产批次经桥接返回仓库批次
     */
    private MesWmBatchDO resolveWmBatchByScanCode(String scanCode) {
        if (StrUtil.isBlank(scanCode)) {
            return null;
        }
        // 1. ç›´æŽ¥å‘½ä¸­ä»“库批次号
        MesWmBatchDO wmBatch = mesWmBatchService.getBatchByCode(scanCode);
        if (wmBatch != null) {
            return wmBatch;
        }
        // 2. å‘½ä¸­è¢‹ç 
        MesProBagCodeDO bag = bagCodeMapper.selectByCode(scanCode);
        if (bag != null && bag.getBatchId() != null) {
            return bridgeProBatch(bag.getBatchId());
        }
        // 3. å‘½ä¸­æ‰˜ç›˜ç 
        MesProPalletDO pallet = palletMapper.selectByCode(scanCode);
        if (pallet != null && pallet.getBatchId() != null) {
            return bridgeProBatch(pallet.getBatchId());
        }
        return null;
    }
    private MesWmBatchDO bridgeProBatch(Long proBatchId) {
        MesProBatchDO proBatch = proBatchService.getBatch(proBatchId);
        if (proBatch == null) {
            return null;
        }
        return mesWmBatchService.getOrCreateByProBatch(proBatch);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void processStockTakingDifference(Long id) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨ä¸”å·²å®Œæˆ
        MesWmStockTakingTaskDO task = validateStockTakingTaskExists(id);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -127,8 +127,13 @@
        materialStockService.updateMaterialStockQuantity(materialStock.getId(), reqDTO.getQuantity(), checkFlag);
        MesWmMaterialStockDO updatedStock = materialStockService.getMaterialStock(materialStock.getId());
        BigDecimal newQuantity = updatedStock.getQuantity();
        materialStockService.updateMaterialStockState(updatedStock.getId(), newQuantity.signum() <= 0
                ? MesWmStockStateEnum.OUT.getState() : MesWmStockStateEnum.QUALIFIED.getState());
        // åº“存状态:显式指定优先(如扫码入暂存库),否则按数量自动判定(≤0 å·²å‡ºåº“,>0 åˆæ ¼åº“)
        Integer stockState = reqDTO.getStockState();
        if (stockState == null) {
            stockState = newQuantity.signum() <= 0
                    ? MesWmStockStateEnum.OUT.getState() : MesWmStockStateEnum.QUALIFIED.getState();
        }
        materialStockService.updateMaterialStockState(updatedStock.getId(), stockState);
        // 3. æ’入事务流水
        MesWmTransactionDO transaction = MesWmTransactionDO.builder()
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java
@@ -118,4 +118,14 @@
     */
    private Boolean checkFlag;
    // ===== åº“存状态控制(非存储字段) =====
    /**
     * æŒ‡å®šåº“存状态(入库时显式指定;为空时按数量自动判定:≤0 å·²å‡ºåº“,>0 åˆæ ¼åº“)
     * <p>
     * ç”Ÿäº§çº¿æ‰«ç å…¥åº“ä¼  {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum#TEMP},
     * å…¥åº“先入暂存库待质检。
     */
    private Integer stockState;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warning/MesWmStockWarningService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mes.service.wm.warning;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningHandleReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningRespVO;
import java.util.List;
/**
 * MES åº“存预警 Service æŽ¥å£
 * <p>
 * é¢„警双通道:实时计算查询 + å®šæ—¶ä»»åŠ¡è½åº“ã€‚
 */
public interface MesWmStockWarningService {
    /**
     * èŽ·å¾—åº“å­˜é¢„è­¦åˆ†é¡µï¼ˆè½åº“è®°å½•ï¼‰
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return é¢„警分页
     */
    PageResult<MesWmStockWarningRespVO> getWarningPage(MesWmStockWarningPageReqVO pageReqVO);
    /**
     * å®žæ—¶è®¡ç®—当前库存预警(低库存 / ä¸´æœŸ / åº“龄),不落库
     *
     * @return å½“前预警列表
     */
    List<MesWmStockWarningRespVO> getCurrentWarnings();
    /**
     * è½åº“当前预警(定时任务调用):删除旧的未处理预警,写入当前预警,保留已处理历史
     *
     * @return æœ¬æ¬¡è½åº“条数
     */
    int persistCurrentWarnings();
    /**
     * å¤„理预警(标记已处理)
     *
     * @param reqVO å¤„理请求
     */
    void handle(MesWmStockWarningHandleReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warning/MesWmStockWarningServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,237 @@
package cn.iocoder.yudao.module.mes.service.wm.warning;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningHandleReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warning.MesWmStockWarningDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.warning.MesWmStockWarningMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockWarningTypeEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_STOCK_WARNING_NOT_EXISTS;
/**
 * MES åº“存预警 Service å®žçŽ°ç±»
 * <p>
 * ä½Žåº“存、临期、库龄三类预警。只针对合格库(正式库存),暂存库(待质检)与已出库不计入预警。
 */
@Service
@Validated
@Slf4j
public class MesWmStockWarningServiceImpl implements MesWmStockWarningService {
    /**
     * æœªå¤„理
     */
    private static final Integer STATUS_UNHANDLED = 10;
    /**
     * å·²å¤„理
     */
    private static final Integer STATUS_HANDLED = 20;
    /**
     * åº“龄预警阈值(天)
     */
    private static final int AGING_DAYS = 365;
    @Resource
    private MesWmStockWarningMapper warningMapper;
    @Resource
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmBatchService batchService;
    @Override
    public PageResult<MesWmStockWarningRespVO> getWarningPage(MesWmStockWarningPageReqVO pageReqVO) {
        PageResult<MesWmStockWarningDO> pageResult = warningMapper.selectPage(pageReqVO);
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(pageResult.getList(), MesWmStockWarningDO::getItemId));
        return new PageResult<>(convertList(pageResult.getList(),
                warning -> toRespVO(warning, itemMap.get(warning.getItemId()))), pageResult.getTotal());
    }
    @Override
    public List<MesWmStockWarningRespVO> getCurrentWarnings() {
        List<MesWmStockWarningDO> warnings = computeCurrentWarnings();
        if (CollUtil.isEmpty(warnings)) {
            return List.of();
        }
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(warnings, MesWmStockWarningDO::getItemId));
        return convertList(warnings, warning -> toRespVO(warning, itemMap.get(warning.getItemId())));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int persistCurrentWarnings() {
        // 1. å®žæ—¶è®¡ç®—当前预警
        List<MesWmStockWarningDO> current = computeCurrentWarnings();
        // 2. åˆ é™¤æ—§çš„æœªå¤„理预警(已处理记录作为历史保留)
        warningMapper.delete(new LambdaQueryWrapperX<MesWmStockWarningDO>()
                .eq(MesWmStockWarningDO::getStatus, STATUS_UNHANDLED));
        // 3. å†™å…¥å½“前预警
        if (CollUtil.isNotEmpty(current)) {
            current.forEach(warningMapper::insert);
        }
        log.info("[persistCurrentWarnings][库存预警落库 {} æ¡]", current.size());
        return current.size();
    }
    @Override
    public void handle(MesWmStockWarningHandleReqVO reqVO) {
        MesWmStockWarningDO warning = warningMapper.selectById(reqVO.getId());
        if (warning == null) {
            throw exception(WM_STOCK_WARNING_NOT_EXISTS);
        }
        warningMapper.updateById(new MesWmStockWarningDO()
                .setId(warning.getId())
                .setStatus(STATUS_HANDLED)
                .setHandleUserId(SecurityFrameworkUtils.getLoginUserId())
                .setHandleTime(LocalDateTime.now())
                .setHandleRemark(reqVO.getHandleRemark()));
    }
    // ==================== ç§æœ‰æ–¹æ³• ====================
    /**
     * å®žæ—¶è®¡ç®—当前预警(低库存 / ä¸´æœŸ / åº“龄),针对合格库存
     */
    private List<MesWmStockWarningDO> computeCurrentWarnings() {
        List<MesWmStockWarningDO> result = new ArrayList<>();
        // ä»…扫描合格库(正式库存)
        List<MesWmMaterialStockDO> stocks = materialStockService.getMaterialStockList(
                new MesWmMaterialStockListReqVO().setStockState(MesWmStockStateEnum.QUALIFIED.getState()));
        if (CollUtil.isEmpty(stocks)) {
            return result;
        }
        LocalDateTime now = LocalDateTime.now();
        Set<Long> itemIds = convertSet(stocks, MesWmMaterialStockDO::getItemId);
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(itemIds);
        Set<Long> warehouseIds = convertSet(stocks, MesWmMaterialStockDO::getWarehouseId);
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(warehouseIds);
        for (MesWmMaterialStockDO stock : stocks) {
            MesMdmItemDO item = itemMap.get(stock.getItemId());
            MesWmWarehouseDO warehouse = warehouseMap.get(stock.getWarehouseId());
            if (item == null) {
                continue;
            }
            MesWmBatchDO batch = stock.getBatchId() != null ? batchService.getBatch(stock.getBatchId()) : null;
            BigDecimal quantity = stock.getQuantity() != null ? stock.getQuantity() : BigDecimal.ZERO;
            // 1. ä½Žåº“存:在库数量 <= æœ€ä½Žåº“å­˜
            if (item.getMinStock() != null && quantity.compareTo(item.getMinStock()) <= 0) {
                result.add(buildWarning(stock, item, warehouse, MesWmStockWarningTypeEnum.LOW_STOCK,
                        item.getMinStock(), quantity, batch != null ? batch.getExpireDate() : null));
            }
            // 2. ä¸´æœŸï¼šæ‰¹æ¬¡æœ‰æœ‰æ•ˆæœŸï¼Œä¸”剩余天数 <= ç‰©æ–™é…ç½®çš„æœ‰æ•ˆæœŸå¤©æ•°
            if (item.getExpiryDay() != null && batch != null && batch.getExpireDate() != null) {
                long remainDays = ChronoUnit.DAYS.between(now.toLocalDate(), batch.getExpireDate().toLocalDate());
                if (remainDays <= item.getExpiryDay()) {
                    result.add(buildWarning(stock, item, warehouse, MesWmStockWarningTypeEnum.EXPIRING,
                            BigDecimal.valueOf(item.getExpiryDay()), BigDecimal.valueOf(Math.max(remainDays, 0)),
                            batch.getExpireDate()));
                }
            }
            // 3. åº“龄:入库时间超过阈值
            if (stock.getReceiptTime() != null) {
                long ageDays = ChronoUnit.DAYS.between(stock.getReceiptTime().toLocalDate(), now.toLocalDate());
                if (ageDays >= AGING_DAYS) {
                    result.add(buildWarning(stock, item, warehouse, MesWmStockWarningTypeEnum.AGING,
                            BigDecimal.valueOf(AGING_DAYS), BigDecimal.valueOf(ageDays), null));
                }
            }
        }
        return result;
    }
    private MesWmStockWarningDO buildWarning(MesWmMaterialStockDO stock, MesMdmItemDO item, MesWmWarehouseDO warehouse,
                                             MesWmStockWarningTypeEnum type, BigDecimal threshold, BigDecimal current,
                                             LocalDateTime expireDate) {
        return MesWmStockWarningDO.builder()
                .itemId(item.getId()).itemCode(item.getCode())
                .batchId(stock.getBatchId()).batchCode(stock.getBatchCode())
                .warehouseId(stock.getWarehouseId())
                .warehouseName(warehouse != null ? warehouse.getName() : null)
                .warningType(type.getType()).thresholdValue(threshold).currentValue(current)
                .expireDate(expireDate).receiptTime(stock.getReceiptTime())
                .status(STATUS_UNHANDLED)
                .build();
    }
    private MesWmStockWarningRespVO toRespVO(MesWmStockWarningDO warning, MesMdmItemDO item) {
        MesWmStockWarningRespVO vo = new MesWmStockWarningRespVO();
        vo.setId(warning.getId());
        vo.setItemId(warning.getItemId());
        vo.setItemCode(warning.getItemCode());
        vo.setItemName(item != null ? item.getName() : null);
        vo.setBatchId(warning.getBatchId());
        vo.setBatchCode(warning.getBatchCode());
        vo.setWarehouseId(warning.getWarehouseId());
        vo.setWarehouseName(warning.getWarehouseName());
        vo.setWarningType(warning.getWarningType());
        vo.setWarningTypeName(warningTypeName(warning.getWarningType()));
        vo.setThresholdValue(warning.getThresholdValue());
        vo.setCurrentValue(warning.getCurrentValue());
        vo.setExpireDate(warning.getExpireDate());
        vo.setReceiptTime(warning.getReceiptTime());
        vo.setStatus(warning.getStatus());
        vo.setStatusName(statusName(warning.getStatus()));
        vo.setHandleUserId(warning.getHandleUserId());
        vo.setHandleTime(warning.getHandleTime());
        vo.setHandleRemark(warning.getHandleRemark());
        return vo;
    }
    private String warningTypeName(Integer type) {
        if (type == null) {
            return "";
        }
        for (MesWmStockWarningTypeEnum e : MesWmStockWarningTypeEnum.values()) {
            if (e.getType().equals(type)) {
                return e.getName();
            }
        }
        return String.valueOf(type);
    }
    private String statusName(Integer status) {
        if (status == null) {
            return "";
        }
        return status.equals(STATUS_HANDLED) ? "已处理" : "未处理";
    }
}
yudao-server/src/main/resources/application-dev.yaml
@@ -1,6 +1,10 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
mes:
  trace-base-url: https://dev.your-domain.com/trace.html
--- #################### æ•°æ®åº“相关配置 ####################
spring:
yudao-server/src/main/resources/application-jhhg.yaml
@@ -1,6 +1,10 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
mes:
  trace-base-url: http://42.63.70.90:9003/trace.html
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
yudao-server/src/main/resources/application-local.yaml
@@ -1,6 +1,10 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
mes:
  trace-base-url: http://192.168.0.10:48080/trace.html
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
yudao-server/src/main/resources/application-test.yaml
@@ -1,6 +1,10 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
mes:
  trace-base-url: http://36.213.180.217:9500/trace.html
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
yudao-server/src/main/resources/application-thzy.yaml
@@ -1,6 +1,10 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
mes:
  trace-base-url: http://47.97.181.254:9001/trace.html
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
@@ -187,7 +191,7 @@
  storage: # æ–‡ä»¶ä¸Šä¼ é…ç½®
    path: /home/mom/javaWork/file # æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://47.97.181.254:9002 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    domain: http://47.97.181.254:9001 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    expired: 120 # token过期时间(分钟)
    use-limit: 10 # token使用次数上限
    jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWT密钥
yudao-server/src/main/resources/application-yzfx.yaml
@@ -1,6 +1,10 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
mes:
  trace-base-url: http://42.63.70.231:9001/trace.html
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
yudao-server/src/main/resources/application.yaml
@@ -1,3 +1,8 @@
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀,形如 http://域名:端口/trace.html),
# éœ€ä¸ºå…¬ä¼—扫码可访问的地址;各 profile å¯è¦†ç›–。留空则二维码编码纯码值(兼容旧码)。
mes:
  trace-base-url: http://localhost:48080/trace.html
spring:
  application:
    name: yudao-server
yudao-server/src/main/resources/static/trace.html
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,433 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no">
<title>产品溯源</title>
<style>
  :root {
    --primary: #2e7d32;
    --primary-light: #43a047;
    --primary-bg: #e8f5e9;
    --text: #1f2933;
    --text-sub: #6b7280;
    --border: #e5e7eb;
    --bg: #f5f7f4;
    --card-shadow: 0 1px 4px rgba(0,0,0,.06);
    --green: #2e7d32;
    --red: #d32f2f;
    --blue: #1565c0;
    --gray: #6b7280;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
  html, body { height: 100%; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
  }
  .wrap { max-width: 480px; margin: 0 auto; padding: 0 12px 32px; }
  /* é¡¶éƒ¨å“ç‰ŒåŒº */
  .brand {
    max-width: 480px; margin: 0 auto;
    padding: 20px 16px 14px;
    text-align: center;
  }
  .brand .logo {
    display: inline-flex; align-items: center; gap: 8px;
    font-size: 20px; font-weight: 700; color: var(--primary);
  }
  .brand .logo .mark {
    width: 26px; height: 26px; border-radius: 50%;
    background: linear-gradient(135deg, var(--primary-light), var(--primary));
    position: relative;
  }
  .brand .logo .mark::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 10px; height: 10px; border: 2px solid #fff; border-radius: 50%;
    transform: translate(-50%, -50%);
  }
  .brand .sub { margin-top: 4px; font-size: 12px; color: var(--text-sub); }
  /* æŸ¥è¯¢æ  */
  .query-bar { display: flex; gap: 8px; margin-bottom: 14px; }
  .query-bar input {
    flex: 1; height: 42px; padding: 0 12px;
    border: 1px solid var(--border); border-radius: 8px;
    font-size: 14px; background: #fff; outline: none;
  }
  .query-bar input:focus { border-color: var(--primary); }
  .query-bar button {
    height: 42px; padding: 0 18px;
    border: none; border-radius: 8px;
    background: var(--primary); color: #fff; font-size: 14px;
    cursor: pointer;
  }
  .query-bar button:active { background: var(--primary-light); }
  /* çŠ¶æ€ */
  .state { padding-top: 8px; }
  .loading { text-align: center; padding: 48px 0; color: var(--text-sub); }
  .spinner {
    width: 32px; height: 32px; margin: 0 auto 12px;
    border: 3px solid var(--primary-bg); border-top-color: var(--primary);
    border-radius: 50%; animation: spin .8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  .empty-tip {
    padding: 12px 16px; border-radius: 10px; background: #fff;
    box-shadow: var(--card-shadow); font-size: 14px; color: var(--text-sub);
    text-align: center;
  }
  .empty-tip.error { color: var(--red); background: #fdecec; }
  .empty-tip.primary { color: var(--primary); background: var(--primary-bg); }
  /* å¡ç‰‡ */
  .card {
    background: #fff; border-radius: 12px; box-shadow: var(--card-shadow);
    margin-bottom: 12px; overflow: hidden;
  }
  .card .card-hd {
    display: flex; align-items: center; gap: 8px;
    padding: 12px 16px; border-bottom: 1px solid var(--border);
    font-size: 15px; font-weight: 600;
  }
  .card .card-hd .bar { width: 4px; height: 16px; border-radius: 2px; background: var(--primary); }
  .card .card-hd .count { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--text-sub); }
  .card .card-bd { padding: 12px 16px; }
  /* ä¿¡æ¯è¡Œ */
  .kv { display: flex; padding: 6px 0; font-size: 14px; }
  .kv .k { flex: 0 0 88px; color: var(--text-sub); }
  .kv .v { flex: 1; word-break: break-all; }
  .kv .v.dim { color: var(--text-sub); }
  /* å¾½ç«  */
  .badge {
    display: inline-block; padding: 2px 8px; border-radius: 4px;
    font-size: 12px; line-height: 18px;
  }
  .badge.green { color: var(--green); background: #e8f5e9; }
  .badge.red { color: var(--red); background: #fdecec; }
  .badge.blue { color: var(--blue); background: #e3f2fd; }
  .badge.gray { color: var(--gray); background: #f1f2f4; }
  .badge-big { display: inline-block; padding: 4px 12px; border-radius: 16px; font-size: 14px; }
  .badge-big.green { color: #fff; background: var(--green); }
  .badge-big.red { color: #fff; background: var(--red); }
  .badge-big.gray { color: #fff; background: var(--gray); }
  /* åˆ—表 */
  .mini-table { width: 100%; border-collapse: collapse; font-size: 13px; }
  .mini-table th, .mini-table td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--border); }
  .mini-table th { color: var(--text-sub); font-weight: 400; background: #fafbf9; white-space: nowrap; }
  .mini-table tr:last-child td { border-bottom: none; }
  .mini-table td { word-break: break-all; }
  .pill-list { display: flex; flex-wrap: wrap; gap: 8px; }
  .pill {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 12px; border-radius: 16px; font-size: 13px;
    background: #f3f4f6;
  }
  .pill .dot { width: 6px; height: 6px; border-radius: 50%; }
  .scroll-list { max-height: 300px; overflow-y: auto; }
  .scroll-list::-webkit-scrollbar { width: 4px; }
  .scroll-list::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 2px; }
  footer {
    max-width: 480px; margin: 0 auto;
    padding: 20px 16px 28px; text-align: center;
    font-size: 12px; color: #9ca3af;
  }
</style>
</head>
<body>
<div class="brand">
  <div class="logo"><span class="mark"></span>种子产品溯源</div>
  <div class="sub">来源可查 Â· åŽ»å‘å¯è¿½ Â· è´£ä»»å¯ç©¶</div>
</div>
<div class="wrap">
  <!-- æ‰‹åŠ¨æŸ¥è¯¢æ ï¼šæ—  code å‚数时提示输入;有参数时自动查询 -->
  <div class="query-bar">
    <input id="codeInput" type="text" placeholder="请输入追溯码(袋码 / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç ï¼‰" autocomplete="off">
    <button id="searchBtn" type="button">查询</button>
  </div>
  <section id="state" class="state"></section>
</div>
<footer>本系统数据仅用于产品溯源,请通过正规渠道购买。</footer>
<script>
(function () {
  'use strict';
  var STATE = document.getElementById('state');
  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: '其他' };
  // ========== å·¥å…·å‡½æ•° ==========
  function esc(v) {
    if (v === null || v === undefined) return '';
    return String(v).replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }
  function text(v) {
    return (v === null || v === undefined || v === '') ? '-' : esc(v);
  }
  function fmtDate(s) {
    if (!s) return '-';
    return esc(String(s).replace('T', ' ').replace(/\.\d+$/, '').slice(0, 16));
  }
  function fmtNum(v) {
    return (v === null || v === undefined) ? '-' : esc(v);
  }
  function statusBadge(map, key) {
    var item = map[key];
    if (!item) return '<span class="badge gray">' + (key === null || key === undefined ? '' : esc(key)) + '</span>';
    return '<span class="badge ' + item[1] + '">' + esc(item[0]) + '</span>';
  }
  function card(title, countHtml, bodyHtml) {
    return '<div class="card">' +
      '<div class="card-hd"><span class="bar"></span>' + esc(title) +
      (countHtml ? '<span class="count">' + countHtml + '</span>' : '') +
      '</div>' + '<div class="card-bd">' + bodyHtml + '</div>' + '</div>';
  }
  function kv(k, v, dim) {
    return '<div class="kv"><span class="k">' + esc(k) + '</span><span class="v' + (dim ? ' dim' : '') + '">' + v + '</span></div>';
  }
  function isEmptyList(list) {
    return !list || list.length === 0;
  }
  // ========== æŸ¥è¯¢ ==========
  function query(code) {
    var value = (code || '').trim();
    if (!value) {
      renderState('<div class="empty-tip primary">请输入追溯码后再查询</div>');
      return;
    }
    renderState('<div class="loading"><div class="spinner"></div>正在查询追溯信息...</div>');
    fetch('/public/mes/trace/query?code=' + encodeURIComponent(value))
      .then(function (resp) { return resp.json(); })
      .then(function (res) {
        if (res.code === 0) {
          renderResult(res.data);
        } else {
          var msg = res.msg || '查询失败,请稍后重试';
          renderState('<div class="empty-tip error">' + esc(msg) + '</div>');
        }
      })
      .catch(function () {
        renderState('<div class="empty-tip error">网络异常,请检查网络后重试</div>');
      });
  }
  function renderState(html) {
    STATE.innerHTML = html;
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }
  // ========== ç»“果渲染(信息区块模块化,后续新数据源在此追加区块) ==========
  function renderResult(d) {
    if (!d) {
      renderState('<div class="empty-tip error">未查询到该追溯码,请核对二维码或联系官方</div>');
      return;
    }
    var html = '';
    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' });
  }
  // åŒºå—1:品种信息
  function buildItemCard(d) {
    if (!d.itemCode && !d.itemName && !d.itemSpecification) return '';
    return card('品种信息', null,
      kv('品种名称', text(d.itemName)) +
      kv('规格', text(d.itemSpecification)) +
      kv('品种编码', text(d.itemCode))
    );
  }
  // åŒºå—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)) : '')
    );
  }
  // åŒºå—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>' : '')
    );
  }
  // åŒºå—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);
  }
  // åŒºå—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))
    );
  }
  // åŒºå—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);
  }
  // åŒºå—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>'
    );
  }
  // åŒºå—8:可选扩展(采购来源 / é”€å”®å޻向 / åŠ å·¥æµè½¬ï¼‰ï¼Œæœ‰æ•°æ®æ‰å±•ç¤º
  function buildOptionalCard(d) {
    var html = '';
    if (!isEmptyList(d.purchaseSources)) {
      var rows = '<table class="mini-table"><thead><tr><th>采购入库单</th><th>供应商</th><th>数量</th><th>日期</th></tr></thead><tbody>';
      d.purchaseSources.forEach(function (s) {
        rows += '<tr><td>' + text(s.receiptCode) + '</td><td>' + text(s.vendorName) + '</td><td>' + fmtNum(s.receivedQuantity) + '</td><td>' + fmtDate(s.receiptDate) + '</td></tr>';
      });
      rows += '</tbody></table>';
      html += card('采购来源', null, rows);
    }
    if (!isEmptyList(d.salesTargets)) {
      var rows2 = '<table class="mini-table"><thead><tr><th>销售出库单</th><th>客户</th><th>数量</th><th>日期</th></tr></thead><tbody>';
      d.salesTargets.forEach(function (s) {
        rows2 += '<tr><td>' + text(s.outCode || s.salesOrderCode || '') + '</td><td>' + text(s.customerName) + '</td><td>' + fmtNum(s.quantity) + '</td><td>' + fmtDate(s.outTime) + '</td></tr>';
      });
      rows2 += '</tbody></table>';
      html += card('销售去向', null, rows2);
    }
    if (!isEmptyList(d.processTraces)) {
      var rows3 = '<table class="mini-table"><thead><tr><th>节点</th><th>说明</th><th>时间</th></tr></thead><tbody>';
      d.processTraces.forEach(function (t) {
        rows3 += '<tr><td>' + text(t.nodeName) + '</td><td>' + text(t.description) + '</td><td>' + fmtDate(t.processTime) + '</td></tr>';
      });
      rows3 += '</tbody></table>';
      html += card('加工流转', null, rows3);
    }
    return html;
  }
  // ========== åˆå§‹åŒ– ==========
  SEARCH_BTN.addEventListener('click', function () { query(CODE_INPUT.value); });
  CODE_INPUT.addEventListener('keydown', function (e) { if (e.key === 'Enter') query(CODE_INPUT.value); });
  var initialCode = new URLSearchParams(location.search).get('code') || '';
  CODE_INPUT.value = initialCode;
  if (initialCode) {
    query(initialCode);
  } else {
    renderState('<div class="empty-tip">扫码或输入追溯码,即可查询产品全程溯源信息</div>');
    CODE_INPUT.focus();
  }
})();
</script>
</body>
</html>