2026-06-04 feb86efc4e8a8d2da00e4832bcd81825726617bf
feat(iot): 物联设备接口新增存放位置字段

- 在JClyController中修改设备映射逻辑,将设备名称映射改为设备对象映射
- 为实时数据添加存放位置字段存储和传输功能
- 在StockInventoryController的库存设备接口中增加存放位置字段返回
- 添加详细的前端联调文档说明新增字段的使用方法
- 更新数据结构支持设备存放位置信息的获取和展示
- 完善设备台账与实时数据接口的数据关联逻辑
已添加1个文件
已修改2个文件
253 ■■■■■ 文件已修改
doc/20260604_物联设备接口新增存放位置字段前端联调文档.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/controller/JclyController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260604_ÎïÁªÉ豸½Ó¿ÚÐÂÔö´æ·ÅλÖÃ×Ö¶Îǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,233 @@
# ç‰©è”设备接口新增存放位置字段前端联调文档
## å˜æ›´æ¦‚è¿°
本次变更在两个物联设备相关接口的返回数据中新增了 `storageLocation`(存放位置)字段,供前端展示设备所在的物理位置信息。
---
## ä¸€ã€æ•°é‡‡æŽ¥å£ - getRealData
### æŽ¥å£ä¿¡æ¯
| é¡¹ç›® | å€¼ |
|------|-----|
| æŽ¥å£è·¯å¾„ | `/iot/getRealData` |
| è¯·æ±‚方式 | GET |
| æŽ¥å£æè¿° | å®žæ—¶èŽ·å–ç‰©è”è®¾å¤‡çš„æ¸©æ¹¿åº¦ã€äºŒæ°§åŒ–ç¢³ç­‰æ•°é‡‡æ•°æ® |
### è¯·æ±‚参数
无参数
### è¿”回数据结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "guid": "设备外部编码",
      "deviceName": "设备名称",
      "storageLocation": "存放位置【新增】",
      "status": "在线/离线/error",
      "statusMessage": "状态说明(仅在异常时返回)",
      "light": "光照值+单位(如:150Lux)",
      "temperature": "温度值+单位(如:25.5℃)",
      "humidity": "湿度值+单位(如:60%RH)",
      "co2": "二氧化碳值+单位(如:400ppm)",
      "battery": "电池电量+单位(如:85%)"
    }
  ]
}
```
### è¿”回字段说明
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | æ˜¯å¦æ–°å¢ž |
|--------|------|------|----------|
| guid | String | è®¾å¤‡å¤–部编码(externalCode) | å¦ |
| deviceName | String | è®¾å¤‡åç§° | å¦ |
| **storageLocation** | String | **设备存放位置** | **是** |
| status | String | è®¾å¤‡çŠ¶æ€ï¼šåœ¨çº¿ã€ç¦»çº¿ã€error | å¦ |
| statusMessage | String | çŠ¶æ€è¡¥å……è¯´æ˜Žï¼ˆä»…åœ¨å¼‚å¸¸æ—¶å­˜åœ¨ï¼‰ | å¦ |
| light | String | å…‰ç…§å€¼ï¼ˆå¸¦å•位) | å¦ |
| temperature | String | æ¸©åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| humidity | String | æ¹¿åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| co2 | String | äºŒæ°§åŒ–碳值(带单位) | å¦ |
| battery | String | ç”µæ± ç”µé‡ï¼ˆå¸¦å•位) | å¦ |
### å‰ç«¯è”调示例
```javascript
// è°ƒç”¨æŽ¥å£
axios.get('/iot/getRealData').then(res => {
  const devices = res.data;
  devices.forEach(device => {
    console.log(`设备:${device.deviceName}`);
    console.log(`存放位置:${device.storageLocation}`); // æ–°å¢žå­—段
    console.log(`温度:${device.temperature}`);
    console.log(`湿度:${device.humidity}`);
    console.log(`CO2:${device.co2}`);
    console.log(`状态:${device.status}`);
  });
});
```
---
## äºŒã€åº“存物联设备接口 - iotRealtime
### æŽ¥å£ä¿¡æ¯
| é¡¹ç›® | å€¼ |
|------|-----|
| æŽ¥å£è·¯å¾„ | `/stockInventory/iotRealtime` |
| è¯·æ±‚方式 | POST |
| Content-Type | application/json |
| æŽ¥å£æè¿° | èŽ·å–åº“å­˜ç»‘å®šçš„ç‰©è”è®¾å¤‡å®žæ—¶æ•°é‡‡æ•°æ® |
### è¯·æ±‚参数
```json
[设备ID1, è®¾å¤‡ID2, è®¾å¤‡ID3]
```
**参数说明:**
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| - | List<Long> | æ˜¯ | ç‰©è”设备ID数组 |
### è¿”回数据结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "devices": [
      {
        "deviceId": "设备ID",
        "deviceName": "设备名称",
        "deviceModel": "设备规格型号",
        "externalCode": "设备外部编码",
        "storageLocation": "存放位置【新增】",
        "guid": "设备外部编码",
        "status": "在线/离线/error",
        "statusMessage": "状态说明(仅在异常时返回)",
        "light": "光照值+单位",
        "temperature": "温度值+单位",
        "humidity": "湿度值+单位",
        "co2": "二氧化碳值+单位",
        "battery": "电池电量+单位"
      }
    ]
  }
}
```
### è¿”回字段说明(devices æ•°ç»„元素)
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | æ˜¯å¦æ–°å¢ž |
|--------|------|------|----------|
| deviceId | String | è®¾å¤‡ID | å¦ |
| deviceName | String | è®¾å¤‡åç§° | å¦ |
| deviceModel | String | è®¾å¤‡è§„格型号 | å¦ |
| externalCode | String | è®¾å¤‡å¤–部编码 | å¦ |
| **storageLocation** | String | **设备存放位置** | **是** |
| guid | String | è®¾å¤‡å¤–部编码(来自数采平台) | å¦ |
| status | String | è®¾å¤‡çŠ¶æ€ï¼šåœ¨çº¿ã€ç¦»çº¿ã€error | å¦ |
| statusMessage | String | çŠ¶æ€è¡¥å……è¯´æ˜Žï¼ˆä»…åœ¨å¼‚å¸¸æ—¶å­˜åœ¨ï¼‰ | å¦ |
| light | String | å…‰ç…§å€¼ï¼ˆå¸¦å•位) | å¦ |
| temperature | String | æ¸©åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| humidity | String | æ¹¿åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| co2 | String | äºŒæ°§åŒ–碳值(带单位) | å¦ |
| battery | String | ç”µæ± ç”µé‡ï¼ˆå¸¦å•位) | å¦ |
### å‰ç«¯è”调示例
```javascript
// è°ƒç”¨æŽ¥å£
const deviceIds = [1, 2, 3];
axios.post('/stockInventory/iotRealtime', deviceIds).then(res => {
  const result = res.data;
  result.devices.forEach(device => {
    console.log(`设备ID:${device.deviceId}`);
    console.log(`设备名称:${device.deviceName}`);
    console.log(`存放位置:${device.storageLocation}`); // æ–°å¢žå­—段
    console.log(`规格型号:${device.deviceModel}`);
    console.log(`温度:${device.temperature}`);
    console.log(`湿度:${device.humidity}`);
    console.log(`状态:${device.status}`);
  });
});
```
---
## ä¸‰ã€å‰ç«¯æ”¹é€ è¦ç‚¹
### 1. åˆ—表/卡片展示改造
若前端有设备列表或卡片展示,需新增存放位置展示区域:
```html
<!-- ç¤ºä¾‹ï¼šè®¾å¤‡ä¿¡æ¯å¡ç‰‡ -->
<div class="device-card">
  <div class="device-name">{{ device.deviceName }}</div>
  <!-- æ–°å¢žå­˜æ”¾ä½ç½®å±•示 -->
  <div class="storage-location">存放位置:{{ device.storageLocation || '暂无' }}</div>
  <div class="device-status">状态:{{ device.status }}</div>
  <div class="device-data">
    <span>温度:{{ device.temperature }}</span>
    <span>湿度:{{ device.humidity }}</span>
    <span>CO2:{{ device.co2 }}</span>
  </div>
</div>
```
### 2. è¡¨æ ¼åˆ—新增
若前端使用表格展示设备数据,需新增存放位置列:
| åˆ—名 | å­—段 | è¯´æ˜Ž |
|------|------|------|
| è®¾å¤‡åç§° | deviceName | - |
| **存放位置** | **storageLocation** | **新增列** |
| æ¸©åº¦ | temperature | - |
| æ¹¿åº¦ | humidity | - |
| CO2 | co2 | - |
| çŠ¶æ€ | status | - |
### 3. ç©ºå€¼å¤„理
`storageLocation` å­—段可能为空字符串,前端需做兜底处理:
```javascript
const location = device.storageLocation || '暂无存放位置信息';
```
---
## å››ã€æ•°æ®æ¥æºè¯´æ˜Ž
`storageLocation` å­—段来源于设备台账表(`device_ledger`)的 `storage_location` å­—段,该字段在设备台账管理模块维护。
---
## äº”、测试要点
1. **正常场景**:设备台账中已维护存放位置,接口返回应包含该字段值
2. **空值场景**:设备台账中存放位置为空,接口应返回空字符串(前端需兜底展示)
3. **离线设备**:设备离线时,`storageLocation` å­—段仍应正常返回
4. **批量查询**:多个设备ID查询时,每个设备都应返回 `storageLocation` å­—段
---
## å…­ã€ç‰ˆæœ¬è®°å½•
| ç‰ˆæœ¬ | æ—¥æœŸ | å˜æ›´å†…容 |
|------|------|----------|
| v1.0 | 2026-06-04 | æ–°å¢ž storageLocation(存放位置)字段 |
src/main/java/com/ruoyi/http/service/controller/JclyController.java
@@ -41,24 +41,29 @@
                .isNotNull(DeviceLedger::getExternalCode)
                .ne(DeviceLedger::getExternalCode, ""));
        Map<String, String> guidDeviceNameMap = iotDevices.stream()
        Map<String, DeviceLedger> guidDeviceMap = iotDevices.stream()
                .filter(item -> StringUtils.isNotEmpty(item.getExternalCode()))
                .collect(Collectors.toMap(
                        item -> item.getExternalCode().trim(),
                        item -> StringUtils.isNotEmpty(item.getDeviceName()) ? item.getDeviceName().trim() : "",
                        (oldValue, newValue) -> StringUtils.isNotEmpty(oldValue) ? oldValue : newValue,
                        item -> item,
                        (oldValue, newValue) -> StringUtils.isNotEmpty(oldValue.getDeviceName()) ? oldValue : newValue,
                        LinkedHashMap::new
                ));
        List<String> guidList = new ArrayList<>(guidDeviceNameMap.keySet());
        List<String> guidList = new ArrayList<>(guidDeviceMap.keySet());
        List<Map<String, String>> maps = realTimeEnergyConsumptionService
                .getRealData(guidList);
        for (Map<String, String> item : maps) {
            String guid = item.get("guid");
            if (StringUtils.isNotEmpty(guid)) {
                String deviceName = guidDeviceNameMap.get(guid.trim());
                if (StringUtils.isNotEmpty(deviceName)) {
                    item.put("deviceName", deviceName);
                DeviceLedger device = guidDeviceMap.get(guid.trim());
                if (device != null) {
                    if (StringUtils.isNotEmpty(device.getDeviceName())) {
                        item.put("deviceName", device.getDeviceName().trim());
                    }
                    if (StringUtils.isNotEmpty(device.getStorageLocation())) {
                        item.put("storageLocation", device.getStorageLocation().trim());
                    }
                }
            }
        }
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -192,6 +192,7 @@
            data.put("deviceName", device.getDeviceName() != null ? device.getDeviceName() : "");
            data.put("deviceModel", device.getDeviceModel() != null ? device.getDeviceModel() : "");
            data.put("externalCode", device.getExternalCode() != null ? device.getExternalCode() : "");
            data.put("storageLocation", device.getStorageLocation() != null ? device.getStorageLocation() : "");
            Map<String, String> rt = realTimeDataMap.getOrDefault(
                    device.getExternalCode() != null ? device.getExternalCode().trim() : "",
                    Collections.emptyMap());