2026-06-01 284034e851859cf2e0008ef3c353666bc12943a1
feat(stock): 添加库存物联设备绑定和实时数采功能

- 新增库存绑定物联设备功能,warehouse字段改为存储设备ID
- 添加iotRealtime接口获取绑定设备实时数采数据
- 实现与精创物联平台对接获取温湿度CO2等数据
- 添加出库批次号自定义功能支持手动填写
- 优化质量检测服务批量处理逻辑
- 移除库存查询中不必要的warehouse字段返回
已添加3个文件
已修改5个文件
451 ■■■■■ 文件已修改
doc/20260601_出库领用批次_前端联调文档.md 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260601_库存物联设备绑定_前端联调文档.md 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/dto/BatchQuickInspectRequest.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockIotRealtimeDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260601_³ö¿âÁìÓÃÅú´Î_ǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
# å‡ºåº“领用 â€” å…è®¸ç”¨æˆ·å¡«å†™å‡ºåº“批次 â€” å‰ç«¯è”调文档
## å˜æ›´æ¦‚è¿°
出库领用时新增**出库批次**(`outboundBatches`)字段,允许用户在出库表单中手动填写。若留空则系统自动生成(格式:`CK20260601-001`)。
---
## æ¶‰åŠçš„æŽ¥å£
两个出库接口均已支持 `outboundBatches` å­—段:
| æŽ¥å£ | è¯´æ˜Ž |
|------|------|
| `POST /stockInventory/subtractStockInventory` | ç›´æŽ¥æ‰£å‡åº“存(含出库批次) |
| `POST /stockInventory/addStockOutRecordOnly` | ä»…创建出库记录不扣库存(审批流) |
---
## è¯·æ±‚参数新增字段
`StockInventoryDto` ä¸­å·²æœ‰çš„可选字段:
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| `outboundBatches` | `String` | **否** | å‡ºåº“批次号,用户可手动填写 |
### æäº¤ç¤ºä¾‹
```json
{
  "productModelId": 123,
  "batchNo": "BN20250501",
  "qualitity": 50,
  "outboundBatches": "2025-领料-第3批",
  "recordType": "1",
  "recordId": 0
}
```
### å­—段行为
| åœºæ™¯ | `outboundBatches` ä¼ å€¼ | ç»“æžœ |
|------|----------------------|------|
| ç”¨æˆ·æ‰‹åЍ填写 | `"2025-领料-第3批"` | ä½¿ç”¨ç”¨æˆ·å¡«å†™çš„值 |
| ç”¨æˆ·ç•™ç©º | `null` æˆ–不传 | ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆ `CK20260601-001` |
| ç”¨æˆ·ä¼ ç©ºä¸² | `""` | ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆ |
---
## å‰ç«¯äº¤äº’
### å‡ºåº“表单
在出库/领用弹窗中新增一个输入框:
```
┌─────────────────────────────────────┐
│  å‡ºåº“领用                            â”‚
├─────────────────────────────────────┤
│  äº§å“è§„格:  é©¬é“ƒè–¯-大号              â”‚
│  æ‰¹å·ï¼š      BN20250501              â”‚
│  å‡ºåº“数量:  [____50____]            â”‚
│  å‡ºåº“批次:  [2025-领料-第3批]  (选填) â”‚
│                                      â”‚
│  æç¤ºï¼šç•™ç©ºå°†è‡ªåŠ¨ç”Ÿæˆ CK20260601-001  â”‚
│                                      â”‚
│  [取消]              [确认出库]       â”‚
└─────────────────────────────────────┘
```
### UI å»ºè®®
- **输入框标签:** "出库批次"
- **Placeholder:** "留空自动生成"
- **提示文字(灰色小字):** "不填则自动生成,格式:CK年月日-序号"
- **长度限制:** å»ºè®® 100 å­—符以内
---
## å‡ºåº“记录列表展示
`GET /stockOutRecord/listPage` è¿”回的出库记录中已有 `outboundBatches` å­—段,前端可直接展示:
```json
{
  "id": 5001,
  "outboundBatches": "CK20260601-001",
  "batchNo": "BN20250501",
  "stockOutNum": 50,
  "productName": "马铃薯",
  "model": "大号",
  "recordType": "1",
  "approvalStatus": 0
}
```
列表中 `outboundBatches` åˆ—建议设置可点击排序和搜索。
---
## æ³¨æ„äº‹é¡¹
1. **仅新增字段,向后兼容:** ä¸ä¼  `outboundBatches` ä¿æŒåŽŸæœ‰è¡Œä¸ºï¼ˆè‡ªåŠ¨ç”Ÿæˆï¼‰ï¼Œå·²æœ‰è°ƒç”¨ä¸å—å½±å“
2. **不校验唯一性:** æ‰¹æ¬¡å·ä¸å¼ºåˆ¶å”¯ä¸€ï¼Œç”¨æˆ·å¯å¡«å†™è‡ªå®šä¹‰æ ¼å¼
3. **存量数据:** åŽ†å²å‡ºåº“è®°å½•çš„æ‰¹æ¬¡å·ä¸ºè‡ªåŠ¨ç”Ÿæˆçš„æ ¼å¼ï¼ˆ`CK` å‰ç¼€ï¼‰ï¼Œä¸Žç”¨æˆ·è‡ªå®šä¹‰æ‰¹æ¬¡å…±å­˜
4. **审批流同样支持:** `/addStockOutRecordOnly` åˆ›å»ºå¾…审批出库记录时也支持传入批次号
doc/20260601_¿â´æÎïÁªÉ豸°ó¶¨_ǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,213 @@
# åº“存管理 â€” ç‰©è”设备绑定 & å®žæ—¶æ•°é‡‡ â€” å‰ç«¯è”调文档
## å˜æ›´æ¦‚è¿°
库存表 `stock_inventory` çš„ `warehouse` å­—段改为存储**物联设备ID**(多选,逗号分隔)。涉及两个接口:
- **接口一(设备绑定):** å¤ç”¨çŽ°æœ‰åº“å­˜å¢žæ”¹æŽ¥å£ï¼Œ`warehouse` å­—段传逗号分隔的设备ID
- **接口二(实时数采):** æ–°å¢ž `GET /stockInventory/iotRealtime/{id}`,查询绑定设备的实时温湿度/CO2等数据
---
## æŽ¥å£ä¸€ï¼šè®¾å¤‡ç»‘定
### 1.1 æŽ¥å£ä¿¡æ¯
| é¡¹ç›® | å†…容 |
|------|------|
| æŽ¥å£åœ°å€ | `POST /stockInventory/addstockInventory` |
| è¯·æ±‚方式 | `POST` |
| æŽ¥å£è¯´æ˜Ž | æ–°å¢žåº“存(`warehouse` å­—段传物联设备ID,多选逗号分隔) |
> ç¼–辑库存同理,在原有编辑接口中将 `warehouse` æ”¹ä¸ºè®¾å¤‡ID即可。
### 1.2 èŽ·å–å¯é€‰è®¾å¤‡åˆ—è¡¨
**复用已有接口:** `GET /device/ledger/page`
| å‚æ•° | å€¼ | è¯´æ˜Ž |
|------|-----|------|
| `isIotDevice` | `1` | åªæŸ¥è¯¢ç‰©è”设备 |
| `size` | `999` | ä¸€æ¬¡æ‹‰å–全部 |
响应关键字段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| `id` | `Long` | è®¾å¤‡ID(提交时填入 warehouse) |
| `deviceName` | `String` | è®¾å¤‡åç§° |
| `deviceModel` | `String` | è®¾å¤‡åž‹å· |
| `externalCode` | `String` | ç‰©è”平台GUID |
### 1.3 è¯·æ±‚参数
`StockInventoryDto` ä¸­ `warehouse` å­—段传**逗号分隔的设备ID字符串**:
```json
{
  "productModelId": 123,
  "batchNo": "BN20250501",
  "warehouse": "1,3,5",
  "qualitity": 100
}
```
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| `warehouse` | `String` | å¦ | ç»‘定的物联设备ID,逗号分隔,如 `"1,3,5"` |
| å…¶ä»–字段 | â€” | â€” | ä¸ŽåŽŸæœ‰åº“å­˜æ–°å¢ž/编辑接口一致 |
---
## æŽ¥å£äºŒï¼šå®žæ—¶æ•°é‡‡æŸ¥çœ‹
### 2.1 æŽ¥å£ä¿¡æ¯
| é¡¹ç›® | å†…容 |
|------|------|
| æŽ¥å£åœ°å€ | `GET /stockInventory/iotRealtime/{id}` |
| è¯·æ±‚方式 | `GET` |
| æŽ¥å£è¯´æ˜Ž | èŽ·å–æŒ‡å®šåº“å­˜è®°å½•ç»‘å®šçš„ç‰©è”è®¾å¤‡å®žæ—¶æ•°é‡‡æ•°æ® |
### 2.2 è¯·æ±‚参数
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| `id` | `Long` | æ˜¯ | åº“存记录ID(路径参数) |
### 2.3 å“åº”示例
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "inventoryId": 1001,
    "iotDeviceIds": "1,3",
    "devices": [
      {
        "deviceId": "1",
        "deviceName": "温湿度传感器A",
        "deviceModel": "TH-100",
        "externalCode": "abc123-guid",
        "guid": "abc123-guid",
        "status": "在线",
        "temperature": "25.6℃",
        "humidity": "65.2%",
        "co2": "420ppm",
        "light": "800lux",
        "battery": "85%"
      },
      {
        "deviceId": "3",
        "deviceName": "温湿度传感器B",
        "deviceModel": "TH-200",
        "externalCode": "def456-guid",
        "guid": "def456-guid",
        "status": "offline",
        "statusMessage": "device not returned by remote API"
      }
    ]
  }
}
```
### 2.4 devices å­—段说明
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| `deviceId` | `String` | è®¾å¤‡ID |
| `deviceName` | `String` | è®¾å¤‡åç§° |
| `deviceModel` | `String` | è®¾å¤‡åž‹å· |
| `externalCode` | `String` | ç‰©è”平台GUID |
| `guid` | `String` | ç‰©è”平台GUID(同 externalCode) |
| `status` | `String` | `在线` / `offline` / `error` |
| `statusMessage` | `String` | çŠ¶æ€æè¿°ï¼ˆå¼‚å¸¸æ—¶æœ‰å€¼ï¼‰ |
| `temperature` | `String` | æ¸©åº¦å€¼ï¼ˆå«å•位,如 `25.6℃`) |
| `humidity` | `String` | æ¹¿åº¦å€¼ï¼ˆå«å•位,如 `65.2%`) |
| `co2` | `String` | CO2值(含单位,如 `420ppm`) |
| `light` | `String` | å…‰ç…§å€¼ï¼ˆå«å•位,如 `800lux`) |
| `battery` | `String` | ç”µé‡ï¼ˆå«å•位,如 `85%`) |
### 2.5 æ— ç»‘定设备时
```json
{
  "code": 200,
  "data": {
    "inventoryId": 1002,
    "iotDeviceIds": "",
    "devices": []
  }
}
```
---
## 3. å‰ç«¯äº¤äº’流程
```
库存列表页
  â”œâ”€ warehouse åˆ—:显示已绑定的设备名称(根据设备ID查 device_ledger æ˜ å°„)
  â””─ æ“ä½œåˆ—:[查看数采] æŒ‰é’®
       â”‚
       â””─ ç‚¹å‡» â†’ è°ƒç”¨æŽ¥å£äºŒ GET /stockInventory/iotRealtime/{id}
                â”‚
                â””─ å¼¹å‡ºå¼¹çª—/抽屉
                   â”œâ”€ è®¾å¤‡åç§° + åž‹å·
                   â”œâ”€ çŠ¶æ€æŒ‡ç¤ºç¯ï¼ˆåœ¨çº¿=绿 / ç¦»çº¿=灰 / å¼‚常=红)
                   â”œâ”€ æ¸©åº¦ã€æ¹¿åº¦ã€CO2、光照、电量 æ•°å€¼å¡ç‰‡
                   â””─ 30秒自动刷新(可选)
新增/编辑库存弹窗
  â””─ warehouse å­—段 â†’ ç‰©è”设备多选下拉框
       â”‚
       â””─ æ•°æ®æºï¼šæŽ¥å£ GET /device/ledger/page?isIotDevice=1
          â””─ æäº¤ï¼šè°ƒç”¨æŽ¥å£ä¸€ï¼Œwarehouse = "1,3,5"
```
### å¼¹çª— UI å»ºè®®
- **设备状态:** é¡¶éƒ¨æ˜¾ç¤ºçŠ¶æ€æ ‡ç­¾ï¼ˆåœ¨çº¿/离线/异常),配合绿/灰/红色圆点
- **数据卡片:** æ¯é¡¹æ•°æ®ç”¨å¡ç‰‡å±•示,图标 + æ•°å€¼ + å•位
- **自动刷新:** å¯åŠ ä¸€ä¸ªå¼€å…³ï¼Œå¼€å¯åŽæ¯30秒重新请求接口二
- **空状态:** æœªç»‘定设备时显示"暂无绑定的物联设备"引导去绑定
---
## 4. æ•°æ®æ¥æºè¯´æ˜Ž
实时数采数据来自 **精创物联平台 (Elitech IoT)**:
- API åœ°å€ï¼š`https://new.e-elitech.cn/api/data-api`
- æ•°æ®ç¼“存:Redis ç¼“å­˜ 30 ç§’
- æ•°æ®é¡¹ç¼–码对照:
| paramCode | å«ä¹‰ | å•位 |
|-----------|------|------|
| `0110` | æ¸©åº¦ | â„ƒ |
| `0120` | æ¹¿åº¦ | % |
| `0130` | CO2 | ppm |
| `0100` | å…‰ç…§ | lux |
| `0042` | ç”µé‡ | % |
---
## 5. æŽ¥å£æ±‡æ€»
| åºå· | æŽ¥å£ | æ–¹æ³• | è¯´æ˜Ž | çŠ¶æ€ |
|------|------|------|------|------|
| æŽ¥å£ä¸€ | `/stockInventory/addstockInventory` | POST | æ–°å¢žåº“存,warehouse ä¼ è®¾å¤‡ID | å·²æœ‰ |
| â€” | åº“存编辑接口 | PUT | ç¼–辑库存,warehouse ä¼ è®¾å¤‡ID | å·²æœ‰ |
| æŽ¥å£äºŒ | `/stockInventory/iotRealtime/{id}` | GET | æŸ¥è¯¢ç»‘定设备实时数采数据 | æ–°å¢ž |
| è¾…助 | `/device/ledger/page?isIotDevice=1` | GET | èŽ·å–å¯é€‰ç‰©è”è®¾å¤‡åˆ—è¡¨ | å·²æœ‰ |
---
## 6. æ³¨æ„äº‹é¡¹
1. **warehouse å­—段语义变更:** è¯¥å­—段原来存储仓库字典值,现改为存储物联设备ID(逗号分隔),请确保历史数据兼容
2. **设备必须绑定物联平台:** åªæœ‰ `isIotDevice=1` ä¸” `externalCode` éžç©ºçš„设备才能获取实时数据
3. **实时数据有缓存:** æŽ¥å£äºŒæœ‰ 30 ç§’ Redis ç¼“存,同一批设备 30 ç§’内重复请求不会重复调用外部 API
4. **部分设备离线:** å½“物联平台未返回某设备数据时,`status` ä¸º `offline`,前端应展示离线状态而非报错
5. **批量请求:** æŽ¥å£äºŒä¸€æ¬¡è¯·æ±‚可获取多个设备数据,前端无需逐个设备轮询
src/main/java/com/ruoyi/quality/dto/BatchQuickInspectRequest.java
@@ -4,7 +4,6 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@@ -18,15 +17,6 @@
    @Schema(description = "指标标准ID")
    private Long testStandardId;
    @Schema(description = "总数量")
    private BigDecimal quantity;
    @Schema(description = "合格数量")
    private BigDecimal qualifiedQuantity;
    @Schema(description = "不合格数量")
    private BigDecimal unqualifiedQuantity;
    @Schema(description = "检测单位")
    private String checkCompany;
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -200,33 +200,11 @@
        if (!validResults.contains(request.getCheckResult())) {
            return R.fail("检测结果必须为:合格、不合格、部分合格");
        }
        if (request.getQuantity() == null || request.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            return R.fail("总数量必须大于0");
        }
        if (request.getTestStandardId() == null) {
            return R.fail("指标标准ID不能为空");
        }
        // quantity = qualifiedQuantity + unqualifiedQuantity
        BigDecimal qty = request.getQuantity();
        BigDecimal qualified = request.getQualifiedQuantity() != null ? request.getQualifiedQuantity() : BigDecimal.ZERO;
        BigDecimal unqualified = request.getUnqualifiedQuantity() != null ? request.getUnqualifiedQuantity() : BigDecimal.ZERO;
        if (qty.compareTo(qualified.add(unqualified)) != 0) {
            return R.fail("总数量必须等于合格数量加不合格数量");
        }
        String checkResult = request.getCheckResult();
        if ("合格".equals(checkResult)) {
            if (qualified.compareTo(qty) != 0 || unqualified.compareTo(BigDecimal.ZERO) != 0) {
                return R.fail("检验结果为合格时,合格数量应等于总数量,不合格数量应为0");
            }
        } else if ("不合格".equals(checkResult)) {
            if (qualified.compareTo(BigDecimal.ZERO) != 0 || unqualified.compareTo(qty) != 0) {
                return R.fail("检验结果为不合格时,合格数量应为0,不合格数量应等于总数量");
            }
        } else {
            if (qualified.compareTo(BigDecimal.ZERO) <= 0 || unqualified.compareTo(BigDecimal.ZERO) <= 0) {
                return R.fail("检验结果为部分合格时,合格数量和不合格数量都必须大于0");
            }
        }
        // è§£æžæ£€æµ‹æ—¥æœŸ
        Date checkTimeDate = null;
@@ -239,8 +217,7 @@
        for (Long id : request.getIds()) {
            try {
                // ä½¿ç”¨ç‹¬ç«‹äº‹åŠ¡å¤„ç†æ¯ä¸ªæ£€éªŒå•ï¼Œé¿å…å•ä¸ªå¤±è´¥å½±å“æ•´ä½“äº‹åŠ¡
                processSingleInspect(id, request, checkResult, qty, qualified, unqualified, checkTimeDate);
                processSingleInspect(id, request, checkResult, checkTimeDate);
                success++;
            } catch (Exception e) {
                errors.add("检验单 " + id + " å¤„理失败:" + e.getMessage());
@@ -256,12 +233,11 @@
    /**
     * åœ¨ç‹¬ç«‹äº‹åŠ¡ä¸­å¤„ç†å•ä¸ªæ£€éªŒå•
     * æ•°é‡ã€åˆæ ¼æ•°é‡é»˜è®¤ä½¿ç”¨æ£€éªŒå•自身的数量,不合格数量为0
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void processSingleInspect(Long id, BatchQuickInspectRequest request,
                                     String checkResult, BigDecimal qty,
                                     BigDecimal qualified, BigDecimal unqualified,
                                     Date checkTimeDate) {
                                     String checkResult, Date checkTimeDate) {
        QualityInspect qualityInspect = qualityInspectMapper.selectById(id);
        if (qualityInspect == null) {
            throw new RuntimeException("检验单不存在");
@@ -270,6 +246,11 @@
            throw new RuntimeException("检验单已提交");
        }
        // æ•°é‡é»˜è®¤å–检验单自身的数量,不合格数量为0
        BigDecimal qty = qualityInspect.getQuantity() != null ? qualityInspect.getQuantity() : BigDecimal.ZERO;
        BigDecimal qualified = qty;
        BigDecimal unqualified = BigDecimal.ZERO;
        // 2. æ›´æ–°æ£€éªŒå•字段
        qualityInspect.setCheckResult(checkResult);
        qualityInspect.setTestStandardId(request.getTestStandardId());
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -1,15 +1,22 @@
package com.ruoyi.stock.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.service.IDeviceLedgerService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.http.service.impl.RealTimeEnergyConsumptionServiceImpl;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockIotRealtimeDto;
import com.ruoyi.stock.execl.StockInventoryExportData;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.service.StockInventoryService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -18,8 +25,8 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <p>
@@ -36,6 +43,8 @@
public class StockInventoryController {
    private StockInventoryService stockInventoryService;
    private IDeviceLedgerService deviceLedgerService;
    private RealTimeEnergyConsumptionServiceImpl realTimeEnergyConsumptionService;
    @GetMapping("/pagestockInventory")
    @Operation(summary = "分页查询库存")
@@ -147,4 +156,50 @@
    public R getByModelId(Long productModelId) {
        return R.ok(stockInventoryService.getByModelId(productModelId));
    }
    @PostMapping("/iotRealtime")
    @Operation(summary = "获取库存绑定的物联设备实时数采数据")
    @Log(title = "库存物联设备实时数采", businessType = BusinessType.OTHER)
    public R iotRealtime(@RequestBody List<Long> ids) {
        StockIotRealtimeDto result = new StockIotRealtimeDto();
        if (ids.isEmpty()) {
            result.setDevices(Collections.emptyList());
            return R.ok(result);
        }
        List<DeviceLedger> devices = deviceLedgerService.listByIds(ids);
        List<String> guidList = devices.stream()
                .map(DeviceLedger::getExternalCode)
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toList());
        Map<String, Map<String, String>> realTimeDataMap = new HashMap<>();
        if (!guidList.isEmpty()) {
            List<Map<String, String>> realTimeList = realTimeEnergyConsumptionService.getRealData(guidList);
            for (Map<String, String> item : realTimeList) {
                String guid = item.get("guid");
                if (StringUtils.isNotEmpty(guid)) {
                    realTimeDataMap.put(guid.trim(), item);
                }
            }
        }
        List<Map<String, String>> deviceDataList = devices.stream().map(device -> {
            Map<String, String> data = new LinkedHashMap<>();
            data.put("deviceId", String.valueOf(device.getId()));
            data.put("deviceName", device.getDeviceName() != null ? device.getDeviceName() : "");
            data.put("deviceModel", device.getDeviceModel() != null ? device.getDeviceModel() : "");
            data.put("externalCode", device.getExternalCode() != null ? device.getExternalCode() : "");
            Map<String, String> rt = realTimeDataMap.getOrDefault(
                    device.getExternalCode() != null ? device.getExternalCode().trim() : "",
                    Collections.emptyMap());
            data.putAll(rt);
            return data;
        }).collect(Collectors.toList());
        result.setDevices(deviceDataList);
        return R.ok(result);
    }
}
src/main/java/com/ruoyi/stock/dto/StockIotRealtimeDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.stock.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
@Schema(description = "库存绑定的物联设备实时数采数据")
public class StockIotRealtimeDto {
    @Schema(description = "库存记录ID")
    private Long inventoryId;
    @Schema(description = "绑定的物联设备ID列表(逗号分隔)")
    private String iotDeviceIds;
    @Schema(description = "设备实时数采数据列表")
    private List<Map<String, String>> devices;
}
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -134,6 +134,7 @@
        stockOutRecordDto.setStockOutNum(stockInventoryDto.getQualitity());
        stockOutRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        stockOutRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockOutRecordDto.setOutboundBatches(stockInventoryDto.getOutboundBatches());
        stockOutRecordDto.setType("0");
        stockOutRecordService.add(stockOutRecordDto);
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -228,8 +228,7 @@
        model,
        unit,
        product_name,
        product_id,
        warehouse
        product_id
        order by create_time desc
    </select>