Răsfoiți Sursa

完善设备管理流程:补充 t_device_relation 写入能力、修复设备删除逻辑、集成阿里云 IoT 设备注册

- 新增 DeviceRelation CRUD(add/removeById/removeByDevice/listByStationId),写 DB 时同步 Redis 和本地缓存
- 新增管理后台 DeviceRelationController,提供短编号绑定/解绑 REST API
- KymCache 新增 removeWashShortIdMapping/removeShortId2StationId 清理方法
- WashDeviceServiceImpl.add() 集成阿里云 IoT RegisterDevice,productKey 由配置统一管理
- WashDeviceServiceImpl.remove() 实现完整删除:清理 IoT 设备 → 清理 DeviceRelation → 删除本地记录
- AliyunLotClient 新增 registerDevice/deleteDevice 封装
- 前端移除 productKey 必填校验及表单项,productKey 完全由配置接管
- 前端修复设备删除 URL(/delete/ → /remove/)及删除确认弹窗显示 deviceName

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 zile în urmă
părinte
comite
8bfc71ba8a

+ 1 - 1
admin-web-new/src/api/station.ts

@@ -47,7 +47,7 @@ export const getDeviceDetail = (id: number) => {
 
 /** 删除设备 */
 export const removeDevice = (id: number) => {
-  return http.request<any>("get", `/washDevice/delete/${id}`);
+  return http.request<any>("get", `/washDevice/remove/${id}`);
 };
 
 /** 修改设备 */

+ 1 - 5
admin-web-new/src/views/admin/station/device-dialog.vue

@@ -26,8 +26,7 @@ const state = reactive({
     functions: ""
   },
   rules: {
-    deviceName: [{ required: true, message: "请输入设备名称", trigger: "blur" }],
-    productKey: [{ required: true, message: "请输入产品key", trigger: "blur" }]
+    deviceName: [{ required: true, message: "请输入设备名称", trigger: "blur" }]
   }
 });
 
@@ -123,9 +122,6 @@ defineExpose({ open });
       <el-form-item label="设备名称" prop="deviceName">
         <el-input v-model="state.ruleForm.deviceName" placeholder="请输入设备名称" />
       </el-form-item>
-      <el-form-item label="产品 Key" prop="productKey">
-        <el-input v-model="state.ruleForm.productKey" placeholder="请输入产品key" />
-      </el-form-item>
       <el-form-item label="状态" prop="status">
         <ext-d-select v-model="state.ruleForm.status" type="Device.state" placeholder="请选择状态" />
       </el-form-item>

+ 0 - 9
admin-web/src/views/admin/station/device/dialog.vue

@@ -36,14 +36,6 @@
               class="wd200">
           </el-input-number>
         </el-form-item>
-        <el-form-item label="产品key" prop="productKey" class="wd350">
-          <el-input
-              v-model="state.ruleForm.productKey"
-              placeholder="产品key"
-              clearable
-              class="wd200">
-          </el-input>
-        </el-form-item>
         <el-form-item label="所在网点" prop="stationId" class="wd350">
           <ext-select
               class="wd200"
@@ -152,7 +144,6 @@ const initState = () => ({
   },
   rules: {
     deviceName: [u.validator.required],
-    productKey: [u.validator.required],
   },
 })
 

+ 2 - 2
admin-web/src/views/admin/station/device/index.vue

@@ -289,8 +289,8 @@ const handleRowClick = (type: string, row: any) => {
 };
 // 删除点击
 const handleRowDelete = (row: any) => {
-  Msg.confirm(`此操作将永久删除:『${row.name}』,是否继续?`).then(() => {
-    $get(`/washDevice/delete/${row.id}`).then(() => {
+  Msg.confirm(`此操作将永久删除:『${row.deviceName}』,是否继续?`).then(() => {
+    $get(`/washDevice/remove/${row.id}`).then(() => {
       Msg.message("删除成功", 'success')
     }).catch(() => {
       Msg.message("删除失败", 'error')

+ 52 - 0
car-wash-admin/src/main/java/com/kym/admin/controller/DeviceRelationController.java

@@ -0,0 +1,52 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.kym.common.R;
+import com.kym.common.annotation.SysLog;
+import com.kym.common.controller.IController;
+import com.kym.entity.DeviceRelation;
+import com.kym.service.DeviceRelationService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * <p>
+ * 设备短编号关系管理
+ * </p>
+ *
+ * @author skyline
+ * @since 2025-05-31
+ */
+@RestController
+@RequestMapping("/device-relation")
+public class DeviceRelationController extends IController {
+
+    private final DeviceRelationService deviceRelationService;
+
+    public DeviceRelationController(DeviceRelationService deviceRelationService) {
+        this.deviceRelationService = deviceRelationService;
+    }
+
+    @SaCheckPermission("deviceRelation.add")
+    @SysLog("绑定设备短编号")
+    @PostMapping("add")
+    public R<?> add(@RequestBody DeviceRelation deviceRelation) {
+        return resp((t) -> deviceRelationService.add(deviceRelation));
+    }
+
+    @SaCheckPermission("deviceRelation.remove")
+    @SysLog("删除设备短编号关系")
+    @GetMapping("remove/{id}")
+    public R<?> removeById(@PathVariable Long id) {
+        return resp((t) -> deviceRelationService.removeById(id));
+    }
+
+    @SaCheckPermission("deviceRelation.list")
+    @SysLog("查询设备短编号关系")
+    @PostMapping("list")
+    public R<?> listByStationId(@RequestBody Map<String, String> params) {
+        return resp(() -> deviceRelationService.listByStationId(params.get("stationId")));
+    }
+
+}

+ 1 - 0
car-wash-admin/src/main/resources/application.yml

@@ -107,6 +107,7 @@ password:
 
 aliyun:
   lot:
+    productKey: k2af0H4KLSU
     amqp:
       accessKey: LTAI5tPSodsNfiWEQxqDe4XT
       accessSecret: xVswlKsGS6zHRixVwRjUmgGDuOxI2r

+ 9 - 0
car-wash-service/src/main/java/com/kym/service/DeviceRelationService.java

@@ -3,6 +3,8 @@ package com.kym.service;
 import com.kym.entity.DeviceRelation;
 import com.kym.service.mybatisplus.MyBaseService;
 
+import java.util.List;
+
 /**
  * <p>
  *  服务类
@@ -13,4 +15,11 @@ import com.kym.service.mybatisplus.MyBaseService;
  */
 public interface DeviceRelationService extends MyBaseService<DeviceRelation> {
 
+    void add(DeviceRelation deviceRelation);
+
+    void removeByDevice(String productKey, String deviceName);
+
+    List<DeviceRelation> listByStationId(String stationId);
+
+    void removeById(Long id);
 }

+ 50 - 0
car-wash-service/src/main/java/com/kym/service/aliyun/lot/AliyunLotClient.java

@@ -24,6 +24,7 @@ public class AliyunLotClient {
 
     private static final Base64.Encoder encoder = Base64.getEncoder();
     private static String iotInstanceId;
+    private static String productKey;
     @Value("${aliyun.lot.amqp.accessKey}")
     private String accessKey;
     @Value("${aliyun.lot.amqp.accessSecret}")
@@ -34,6 +35,15 @@ public class AliyunLotClient {
         AliyunLotClient.iotInstanceId = iotInstanceId;
     }
 
+    @Value("${aliyun.lot.productKey}")
+    public void setProductKey(String productKey) {
+        AliyunLotClient.productKey = productKey;
+    }
+
+    public static String getProductKey() {
+        return productKey;
+    }
+
     /**
      * 调用Iot20180120客户端发送请求
      * 2.CreateSubscribeRelation 创建MNS或AMQP服务端订阅
@@ -136,6 +146,46 @@ public class AliyunLotClient {
         return response;
     }
 
+    /**
+     * 注册设备到阿里云物联网平台
+     *
+     * @param productKey 产品Key
+     * @param deviceName 设备名称
+     * @return RegisterDeviceResponseBody.Data 包含 deviceName, deviceSecret, iotId 等
+     */
+    public static RegisterDeviceResponseBody.RegisterDeviceResponseBodyData registerDevice(String productKey, String deviceName) throws Exception {
+        RegisterDeviceRequest request = new RegisterDeviceRequest()
+                .setIotInstanceId(iotInstanceId)
+                .setProductKey(productKey)
+                .setDeviceName(deviceName);
+        log.info("-------------------RegisterDevice 注册设备:productKey={}, deviceName={}--------------------", productKey, deviceName);
+        RegisterDeviceResponse response = rRpcClient.registerDevice(request);
+        log.info(com.aliyun.teautil.Common.toJSONString(TeaModel.buildMap(response.body)));
+        if (!response.body.success) {
+            throw new RuntimeException("阿里云物联网平台注册设备失败: " + response.body.errorMessage);
+        }
+        return response.body.data;
+    }
+
+    /**
+     * 从阿里云物联网平台删除设备
+     *
+     * @param productKey 产品Key
+     * @param deviceName 设备名称
+     */
+    public static void deleteDevice(String productKey, String deviceName) throws Exception {
+        DeleteDeviceRequest request = new DeleteDeviceRequest()
+                .setIotInstanceId(iotInstanceId)
+                .setProductKey(productKey)
+                .setDeviceName(deviceName);
+        log.info("-------------------DeleteDevice 删除设备:productKey={}, deviceName={}--------------------", productKey, deviceName);
+        DeleteDeviceResponse response = rRpcClient.deleteDevice(request);
+        log.info(com.aliyun.teautil.Common.toJSONString(TeaModel.buildMap(response.body)));
+        if (!response.body.success) {
+            throw new RuntimeException("阿里云物联网平台删除设备失败: " + response.body.errorMessage);
+        }
+    }
+
     /**
      * 调用Iot20180120客户端发送请求
      * 1.QuerySubscribeRelation 查询MNS或AMQP服务端订阅

+ 19 - 0
car-wash-service/src/main/java/com/kym/service/cache/KymCache.java

@@ -164,6 +164,25 @@ public enum KymCache {
         map.forEach((k, v) -> KymCacheInjector.redisTemplate.opsForValue().set(RedisKeys.SHORT_ID_TO_STATION_ID + k, v));
     }
 
+    /**
+     * 删除短编号与设备信息的映射缓存
+     *
+     * @param shortId
+     */
+    public void removeWashShortIdMapping(String shortId) {
+        KymCacheInjector.redisTemplate.delete(RedisKeys.SHORT_ID_TO_PRODUCT_KEY_AND_DEVICE_NAME + shortId);
+        SHORT_ID_TO_PRODUCT_KEY_AND_DEVICE_NAME_MAPPING.remove(shortId);
+    }
+
+    /**
+     * 删除短编号与站点id的映射缓存
+     *
+     * @param shortId
+     */
+    public void removeShortId2StationId(String shortId) {
+        KymCacheInjector.redisTemplate.delete(RedisKeys.SHORT_ID_TO_STATION_ID + shortId);
+    }
+
     /**
      * 通过短编号获取站点id
      *

+ 62 - 0
car-wash-service/src/main/java/com/kym/service/impl/DeviceRelationServiceImpl.java

@@ -1,5 +1,7 @@
 package com.kym.service.impl;
 
+import com.kym.common.exception.BusinessException;
+import com.kym.common.utils.CommUtil;
 import com.kym.entity.DeviceRelation;
 import com.kym.mapper.DeviceRelationMapper;
 import com.kym.service.DeviceRelationService;
@@ -7,7 +9,9 @@ import com.kym.service.cache.KymCache;
 import com.kym.service.mybatisplus.MyBaseServiceImpl;
 import jakarta.annotation.PostConstruct;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -31,4 +35,62 @@ public class DeviceRelationServiceImpl extends MyBaseServiceImpl<DeviceRelationM
         });
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void add(DeviceRelation deviceRelation) {
+        CommUtil.asserts(CommUtil.isNotEmptyAndNull(deviceRelation.getShortId()), "短编号不能为空");
+        CommUtil.asserts(CommUtil.isNotEmptyAndNull(deviceRelation.getProductKey()), "产品key不能为空");
+        CommUtil.asserts(CommUtil.isNotEmptyAndNull(deviceRelation.getDeviceName()), "设备名称不能为空");
+        CommUtil.asserts(CommUtil.isNotEmptyAndNull(deviceRelation.getStationId()), "站点id不能为空");
+
+        Long count = lambdaQuery()
+                .eq(DeviceRelation::getShortId, deviceRelation.getShortId())
+                .count();
+        CommUtil.asserts(count == 0, "该短编号已存在");
+
+        count = lambdaQuery()
+                .eq(DeviceRelation::getProductKey, deviceRelation.getProductKey())
+                .eq(DeviceRelation::getDeviceName, deviceRelation.getDeviceName())
+                .count();
+        CommUtil.asserts(count == 0, "该设备已绑定短编号");
+
+        save(deviceRelation);
+
+        // 同步到缓存
+        KymCache.INSTANCE.putWashShortId2ProductKeyAndDeviceName(Map.of(deviceRelation.getShortId(), deviceRelation.getProductKey() + "," + deviceRelation.getDeviceName()));
+        KymCache.INSTANCE.putShortId2StationId(Map.of(deviceRelation.getShortId(), deviceRelation.getStationId()));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void removeByDevice(String productKey, String deviceName) {
+        var relation = lambdaQuery()
+                .eq(DeviceRelation::getProductKey, productKey)
+                .eq(DeviceRelation::getDeviceName, deviceName)
+                .one();
+        if (relation != null) {
+            removeById(relation.getId());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void removeById(Long id) {
+        var relation = getById(id);
+        if (relation == null) {
+            throw new BusinessException("设备关系不存在");
+        }
+        super.removeById(id);
+        KymCache.INSTANCE.removeWashShortIdMapping(relation.getShortId());
+        KymCache.INSTANCE.removeShortId2StationId(relation.getShortId());
+    }
+
+    @Override
+    public List<DeviceRelation> listByStationId(String stationId) {
+        return lambdaQuery()
+                .eq(DeviceRelation::getStationId, stationId)
+                .orderByAsc(DeviceRelation::getShortId)
+                .list();
+    }
+
 }

+ 30 - 3
car-wash-service/src/main/java/com/kym/service/impl/WashDeviceServiceImpl.java

@@ -12,10 +12,13 @@ import com.kym.entity.queryParams.DeviceQueryParams;
 import com.kym.entity.vo.WashDeviceVo;
 import com.kym.mapper.WashDeviceMapper;
 import com.kym.service.DeviceConfigService;
+import com.kym.service.DeviceRelationService;
 import com.kym.service.WashDeviceService;
 import com.kym.service.WashOrderService;
+import com.kym.service.aliyun.lot.AliyunLotClient;
 import com.kym.service.cache.KymCache;
 import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 
@@ -32,17 +35,20 @@ import java.util.stream.Collectors;
  * @author skyline
  * @since 2024-10-09
  */
+@Slf4j
 @Service
 public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, WashDevice> implements WashDeviceService {
 
     private WashOrderService washOrderService;
 
-
     private DeviceConfigService deviceConfigService;
 
-    public WashDeviceServiceImpl(WashOrderService washOrderService, DeviceConfigService deviceConfigService) {
+    private DeviceRelationService deviceRelationService;
+
+    public WashDeviceServiceImpl(WashOrderService washOrderService, DeviceConfigService deviceConfigService, DeviceRelationService deviceRelationService) {
         this.washOrderService = washOrderService;
         this.deviceConfigService = deviceConfigService;
+        this.deviceRelationService = deviceRelationService;
     }
 
 
@@ -132,7 +138,17 @@ public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, W
 
     @Override
     public void remove(long id) {
+        var device = getById(id);
+        CommUtil.asserts(device != null, "设备不存在");
 
+        try {
+            AliyunLotClient.deleteDevice(device.getProductKey(), device.getDeviceName());
+        } catch (Exception e) {
+            log.warn("阿里云IoT删除设备失败(已忽略,继续清理本地数据): productKey={}, deviceName={}", device.getProductKey(), device.getDeviceName(), e);
+        }
+
+        deviceRelationService.removeByDevice(device.getProductKey(), device.getDeviceName());
+        super.removeById(id);
     }
 
     @Override
@@ -160,6 +176,10 @@ public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, W
 
     @Override
     public void add(WashDevice device) {
+        // productKey 统一使用配置文件中的值,不接受前端传入
+        device.setProductKey(AliyunLotClient.getProductKey());
+        CommUtil.asserts(CommUtil.isNotEmptyAndNull(device.getDeviceName()), "设备名称不能为空");
+
         Long count = lambdaQuery()
                 .eq(WashDevice::getStationId, device.getStationId())
                 .eq(WashDevice::getDeviceName, device.getDeviceName())
@@ -172,8 +192,15 @@ public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, W
                     .count();
             CommUtil.asserts(seqCount == 0, "该站点下工位序号已存在");
         }
-        save(device);
 
+        try {
+            AliyunLotClient.registerDevice(device.getProductKey(), device.getDeviceName());
+        } catch (Exception e) {
+            log.error("阿里云IoT注册设备失败: productKey={}, deviceName={}", device.getProductKey(), device.getDeviceName(), e);
+            throw new BusinessException("阿里云物联网平台注册设备失败: " + e.getMessage());
+        }
+
+        save(device);
     }
 
     @Override