diff --git a/intc-admin/src/main/resources/alarm-config-example.yml b/intc-admin/src/main/resources/alarm-config-example.yml deleted file mode 100644 index 534b3c8..0000000 --- a/intc-admin/src/main/resources/alarm-config-example.yml +++ /dev/null @@ -1,32 +0,0 @@ -# ==================== 告警配置示例 ==================== -# 将以下配置添加到 application.yml 或 application-dev.yml 中 - -# 告警阈值配置 -alarm: - threshold: - # 溶解氧阈值(mg/L) - dissolved-oxygen: - min: 4.0 # 最低阈值 - max: 15.0 # 最高阈值 - - # 温度阈值(℃) - temperature: - min: 10.0 # 最低阈值 - max: 35.0 # 最高阈值 - - # pH值阈值 - ph: - min: 6.5 # 最低阈值 - max: 8.5 # 最高阈值 - - # 盐度阈值 - salinity: - max: 35.0 # 最高阈值 - - # 电池电量阈值(%) - battery: - min: 20.0 # 最低阈值 - - # 告警通知配置 - notification: - interval: 30 # 告警通知间隔时间(分钟),同一设备在此时间内不会重复发送通知 diff --git a/intc-admin/src/main/resources/application-dev.yml b/intc-admin/src/main/resources/application-dev.yml index b4a82d4..007344d 100644 --- a/intc-admin/src/main/resources/application-dev.yml +++ b/intc-admin/src/main/resources/application-dev.yml @@ -224,7 +224,7 @@ aliyun: # 1-水质检测仪 ProductKey(请填写实际的 ProductKey) water-quality-monitor: a15hA3oBPmB # TODO: 请替换为实际的水质检测仪 ProductKey # 2-控制一体机 ProductKey(请填写实际的 ProductKey) - control-integrated: a15hA3oBPmB # TODO: 请替换为实际的控制一体机 ProductKey + control-integrated: a1Xj9dagTIx # TODO: 请替换为实际的控制一体机 ProductKey # AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret) amqp: # 是否启用 diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceErrorCode.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceErrorCode.java new file mode 100644 index 0000000..ea9a5aa --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceErrorCode.java @@ -0,0 +1,128 @@ +package com.intc.fishery.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 设备故障码常量定义 + * + * @author intc + */ +public class DefineDeviceErrorCode { + /** + * 电源关闭 + */ + public static final int PowerOff = 1; + + // 三相电压告警 (2-7) + /** + * 三相A相过压 + */ + public static final int Three_PhaseA_OverVoltage = 2; + /** + * 三相B相过压 + */ + public static final int Three_PhaseB_OverVoltage = 3; + /** + * 三相C相过压 + */ + public static final int Three_PhaseC_OverVoltage = 4; + /** + * 三相A相欠压 + */ + public static final int Three_PhaseA_UnderVoltage = 5; + /** + * 三相B相欠压 + */ + public static final int Three_PhaseB_UnderVoltage = 6; + /** + * 三相C相欠压 + */ + public static final int Three_PhaseC_UnderVoltage = 7; + + // 三相电流告警 (8-23) + /** + * 三相开关1路A相过流 + */ + public static final int Three_Switch1OverElectricA = 8; + public static final int Three_Switch1OverElectricB = 9; + public static final int Three_Switch1OverElectricC = 10; + public static final int Three_Switch2OverElectricA = 11; + public static final int Three_Switch2OverElectricB = 12; + public static final int Three_Switch2OverElectricC = 13; + public static final int Three_Switch3OverElectricA = 14; + public static final int Three_Switch3OverElectricB = 15; + public static final int Three_Switch3OverElectricC = 16; + public static final int Three_Switch4OverElectricA = 17; + public static final int Three_Switch4OverElectricB = 18; + public static final int Three_Switch4OverElectricC = 19; + /** + * 三相开关1路缺相 + */ + public static final int Three_Switch1ElectricEmpty = 20; + public static final int Three_Switch2ElectricEmpty = 21; + public static final int Three_Switch3ElectricEmpty = 22; + public static final int Three_Switch4ElectricEmpty = 23; + + // 单相电流告警 (24-31) + /** + * 单相开关1路过流 + */ + public static final int One_Switch1OverElectric = 24; + public static final int One_Switch2OverElectric = 25; + public static final int One_Switch3OverElectric = 26; + public static final int One_Switch4OverElectric = 27; + /** + * 单相开关1路缺相 + */ + public static final int One_Switch1ElectricEmpty = 28; + public static final int One_Switch2ElectricEmpty = 29; + public static final int One_Switch3ElectricEmpty = 30; + public static final int One_Switch4ElectricEmpty = 31; + + private static final Map ERROR_CODE_MESSAGES = new HashMap<>(); + + static { + ERROR_CODE_MESSAGES.put(PowerOff, "电源关闭"); + ERROR_CODE_MESSAGES.put(Three_PhaseA_OverVoltage, "A相过压"); + ERROR_CODE_MESSAGES.put(Three_PhaseB_OverVoltage, "B相过压"); + ERROR_CODE_MESSAGES.put(Three_PhaseC_OverVoltage, "C相过压"); + ERROR_CODE_MESSAGES.put(Three_PhaseA_UnderVoltage, "A相欠压"); + ERROR_CODE_MESSAGES.put(Three_PhaseB_UnderVoltage, "B相欠压"); + ERROR_CODE_MESSAGES.put(Three_PhaseC_UnderVoltage, "C相欠压"); + ERROR_CODE_MESSAGES.put(Three_Switch1OverElectricA, "1路A相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch1OverElectricB, "1路B相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch1OverElectricC, "1路C相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch2OverElectricA, "2路A相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch2OverElectricB, "2路B相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch2OverElectricC, "2路C相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch3OverElectricA, "3路A相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch3OverElectricB, "3路B相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch3OverElectricC, "3路C相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch4OverElectricA, "4路A相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch4OverElectricB, "4路B相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch4OverElectricC, "4路C相过流"); + ERROR_CODE_MESSAGES.put(Three_Switch1ElectricEmpty, "1路缺相"); + ERROR_CODE_MESSAGES.put(Three_Switch2ElectricEmpty, "2路缺相"); + ERROR_CODE_MESSAGES.put(Three_Switch3ElectricEmpty, "3路缺相"); + ERROR_CODE_MESSAGES.put(Three_Switch4ElectricEmpty, "4路缺相"); + ERROR_CODE_MESSAGES.put(One_Switch1OverElectric, "1路过流"); + ERROR_CODE_MESSAGES.put(One_Switch2OverElectric, "2路过流"); + ERROR_CODE_MESSAGES.put(One_Switch3OverElectric, "3路过流"); + ERROR_CODE_MESSAGES.put(One_Switch4OverElectric, "4路过流"); + ERROR_CODE_MESSAGES.put(One_Switch1ElectricEmpty, "1路缺相"); + ERROR_CODE_MESSAGES.put(One_Switch2ElectricEmpty, "2路缺相"); + ERROR_CODE_MESSAGES.put(One_Switch3ElectricEmpty, "3路缺相"); + ERROR_CODE_MESSAGES.put(One_Switch4ElectricEmpty, "4路缺相"); + } + + private DefineDeviceErrorCode() { + } + + /** + * 获取故障码对应的错误信息 + */ + public static String getErrorMessage(int errorCode) { + return ERROR_CODE_MESSAGES.getOrDefault(errorCode, ""); + } +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceWarnCode.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceWarnCode.java new file mode 100644 index 0000000..22b1166 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceWarnCode.java @@ -0,0 +1,96 @@ +package com.intc.fishery.constant; + +/** + * 设备告警码常量定义 + * + * @author intc + */ +public class DefineDeviceWarnCode { + /** + * 无告警 + */ + public static final int None = 0; + + /** + * 用户自定义告警最大码 + */ + public static final int UserMaxCode = 65536; + + /** + * 低溶氧告警 + */ + public static final int LowOxygen = 1; + + /** + * 高温告警 + */ + public static final int HighTemperature = 2; + + /** + * 低温告警 + */ + public static final int LowTemperature = 4; + + /** + * PH值告警 + */ + public static final int PHAlarm = 8; + + /** + * 盐度告警 + */ + public static final int SalinityAlarm = 16; + + /** + * 设备离线 + */ + public static final int DeviceOffline = 32; + + /** + * 设备失联 + */ + public static final int DeviceDead = 64; + + private DefineDeviceWarnCode() { + } + + /** + * 转换告警码为告警描述 + */ + public static String toWarnDescription(int warnCode) { + if (warnCode == None) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + if ((warnCode & LowOxygen) != 0) { + sb.append("低溶氧告警,"); + } + if ((warnCode & HighTemperature) != 0) { + sb.append("高温告警,"); + } + if ((warnCode & LowTemperature) != 0) { + sb.append("低温告警,"); + } + if ((warnCode & PHAlarm) != 0) { + sb.append("PH值告警,"); + } + if ((warnCode & SalinityAlarm) != 0) { + sb.append("盐度告警,"); + } + if ((warnCode & DeviceOffline) != 0) { + sb.append("设备离线,"); + } + if ((warnCode & DeviceDead) != 0) { + sb.append("设备失联,"); + } + + // 移除最后的逗号 + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + + return sb.toString(); + } +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java index 280b329..ad8c2c1 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java @@ -21,6 +21,19 @@ import com.intc.fishery.domain.vo.DeviceVo; import com.intc.fishery.domain.bo.DeviceBo; import com.intc.fishery.service.IDeviceService; import com.intc.common.mybatis.core.page.TableDataInfo; +import com.intc.fishery.domain.vo.PublicDeviceSimpleVo; +import com.intc.fishery.domain.vo.PublicPondIdNameVo; +import com.intc.fishery.domain.vo.PublicDeviceSwitchSimpleVo; +import com.intc.fishery.domain.Device; +import com.intc.fishery.domain.DeviceSwitch; +import com.intc.fishery.domain.Pond; +import com.intc.fishery.mapper.DeviceMapper; +import com.intc.fishery.mapper.DeviceSwitchMapper; +import com.intc.fishery.mapper.PondMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import java.util.stream.Collectors; +import java.util.Map; +import java.util.function.Function; /** * 设备管理 @@ -35,6 +48,9 @@ import com.intc.common.mybatis.core.page.TableDataInfo; public class DeviceController extends BaseController { private final IDeviceService deviceService; + private final DeviceMapper deviceMapper; + private final DeviceSwitchMapper deviceSwitchMapper; + private final PondMapper pondMapper; /** * 查询设备管理列表 @@ -102,4 +118,146 @@ public class DeviceController extends BaseController { @PathVariable Long[] ids) { return toAjax(deviceService.deleteWithValidByIds(List.of(ids), true)); } + + /** + * 查询用户所有设备简化列表 + * type=1: 返回完整信息(包含塘口信息和开关列表) + * type!=1: 只返回基本信息(id, deviceName, deviceType, deadTime) + * + * @param rootUserId 用户ID + * @param type 查询类型(1-完整信息,其他-基本信息) + */ + @GetMapping("/list_all_device") + public R> getDeviceSimpleList( + @RequestParam("rootUserId") Long rootUserId, + @RequestParam("type") Integer type) { + + // 查询用户的所有设备 + List devices = deviceMapper.selectList( + new LambdaQueryWrapper() + .eq(Device::getUserId, rootUserId) + .orderByDesc(Device::getCreateTime) + ); + + if (devices == null || devices.isEmpty()) { + return R.ok(List.of()); + } + + // type=1: 返回完整信息 + if (type == 1) { + // 批量查询所有需要的塘口信息 + List pondIds = devices.stream() + .map(Device::getPondId) + .filter(id -> id != null && id > 0) + .distinct() + .collect(Collectors.toList()); + + Map pondMap = Map.of(); + if (!pondIds.isEmpty()) { + List ponds = pondMapper.selectBatchIds(pondIds); + pondMap = ponds.stream() + .collect(Collectors.toMap(Pond::getId, Function.identity())); + } + + // 批量查询所有开关 + List deviceIds = devices.stream() + .map(Device::getId) + .collect(Collectors.toList()); + + List allSwitches = deviceSwitchMapper.selectList( + new LambdaQueryWrapper() + .in(DeviceSwitch::getDeviceId, deviceIds) + .orderByAsc(DeviceSwitch::getIndex) + ); + + // 按设备ID分组开关 + Map> switchMap = allSwitches.stream() + .collect(Collectors.groupingBy(DeviceSwitch::getDeviceId)); + + // 开关关联的塘口ID + List switchPondIds = allSwitches.stream() + .map(DeviceSwitch::getPondId) + .filter(id -> id != null && id > 0) + .distinct() + .collect(Collectors.toList()); + + // 合并塘口查询 + if (!switchPondIds.isEmpty()) { + List switchPonds = pondMapper.selectBatchIds(switchPondIds); + Map finalPondMap = pondMap; + pondMap = switchPonds.stream() + .collect(Collectors.toMap(Pond::getId, Function.identity(), (v1, v2) -> v1)); + pondMap.putAll(finalPondMap); + } + + Map finalPondMapForLambda = pondMap; + + // 构建返回结果 + List result = devices.stream() + .map(device -> { + PublicDeviceSimpleVo vo = new PublicDeviceSimpleVo(); + vo.setId(device.getId()); + vo.setDeviceName(device.getDeviceName()); + vo.setDeviceType(device.getDeviceType()); + vo.setDeadTime(device.getDeadTime()); + vo.setIsOxygenUsed(device.getIsOxygenUsed()); + + // 设置塘口信息 + if (device.getPondId() != null && device.getPondId() > 0) { + Pond pond = finalPondMapForLambda.get(device.getPondId()); + if (pond != null) { + PublicPondIdNameVo pondInfo = new PublicPondIdNameVo(); + pondInfo.setId(pond.getId()); + pondInfo.setPondName(pond.getPondName()); + vo.setPondInfo(pondInfo); + } + } + + // 设置开关列表 + List switches = switchMap.getOrDefault(device.getId(), List.of()); + List switchVos = switches.stream() + .map(s -> { + PublicDeviceSwitchSimpleVo switchVo = new PublicDeviceSwitchSimpleVo(); + switchVo.setId(s.getId()); + switchVo.setIndex(s.getIndex()); + switchVo.setSwitchName(s.getSwitchName()); + + // 设置开关的塘口信息 + if (s.getPondId() != null && s.getPondId() > 0) { + Pond pond = finalPondMapForLambda.get(s.getPondId()); + if (pond != null) { + PublicPondIdNameVo pondInfo = new PublicPondIdNameVo(); + pondInfo.setId(pond.getId()); + pondInfo.setPondName(pond.getPondName()); + switchVo.setPondInfo(pondInfo); + } + } + + return switchVo; + }) + .collect(Collectors.toList()); + vo.setListSwitch(switchVos); + + return vo; + }) + .collect(Collectors.toList()); + + return R.ok(result); + } + // type!=1: 只返回基本信息 + else { + List result = devices.stream() + .map(device -> { + PublicDeviceSimpleVo vo = new PublicDeviceSimpleVo(); + vo.setId(device.getId()); + vo.setDeviceName(device.getDeviceName()); + vo.setDeviceType(device.getDeviceType()); + vo.setDeadTime(device.getDeadTime()); + return vo; + }) + .collect(Collectors.toList()); + + return R.ok(result); + } + } } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java index 1e4f86f..2420b63 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java @@ -27,6 +27,36 @@ import com.intc.fishery.domain.vo.AquUserVo; import com.intc.fishery.domain.bo.AquUserBo; import com.intc.system.service.ISysUserService; import com.intc.system.domain.vo.SysUserVo; +import com.intc.fishery.domain.vo.PondDeviceListVo; +import com.intc.fishery.domain.vo.DeviceVo; +import com.intc.fishery.domain.vo.DeviceWithSwitchVo; +import com.intc.fishery.domain.vo.DeviceSwitchVo; +import com.intc.fishery.mapper.DeviceMapper; +import com.intc.fishery.mapper.DeviceSwitchMapper; +import com.intc.fishery.domain.Device; +import com.intc.fishery.domain.DeviceSwitch; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import java.util.stream.Collectors; +import com.intc.common.core.utils.MapstructUtils; +import cn.hutool.core.bean.BeanUtil; +import com.intc.fishery.domain.bo.PondSelectDeviceOrSwitchBo; +import com.intc.fishery.domain.Pond; +import com.intc.fishery.mapper.PondMapper; +import com.intc.fishery.mapper.LinkedCtrlMapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.springframework.transaction.annotation.Transactional; +import java.util.HashSet; +import java.util.Set; +import java.util.ArrayList; +import com.intc.fishery.domain.LinkedCtrl; +import com.intc.fishery.domain.vo.PublicPondMode1Vo; +import com.intc.fishery.domain.vo.PondMode1WarnCodeInfo; +import com.intc.fishery.mapper.DeviceErrorCodeMapper; +import com.intc.fishery.domain.DeviceErrorCode; +import com.intc.fishery.constant.DefineDeviceWarnCode; +import com.intc.fishery.constant.DefineDeviceErrorCode; +import com.intc.fishery.mapper.TimingCtrlMapper; +import com.intc.fishery.domain.TimingCtrl; /** * 塘口管理 @@ -43,6 +73,12 @@ public class PondController extends BaseController { private final IPondService pondService; private final IAquUserService aquUserService; private final ISysUserService sysUserService; + private final DeviceMapper deviceMapper; + private final DeviceSwitchMapper deviceSwitchMapper; + private final PondMapper pondMapper; + private final LinkedCtrlMapper linkedCtrlMapper; + private final DeviceErrorCodeMapper deviceErrorCodeMapper; + private final TimingCtrlMapper timingCtrlMapper; /** * 查询塘口管理列表 @@ -140,4 +176,636 @@ public class PondController extends BaseController { @PathVariable Long[] ids) { return toAjax(pondService.deleteWithValidByIds(List.of(ids), true)); } + + /** + * 根据塘口ID查询设备列表 + * 返回数据按设备类型分类: + * - listDetector: 水质检测仪列表(deviceType=1)+ 开启溶氧检测的测控一体机(deviceType=2 && isOxygenUsed=1) + * - listController: 测控一体机列表(deviceType=2),包含开关列表 + * + * @param pondId 塘口ID + */ + @GetMapping("/devices/{pondId}") + public R getDevicesByPondId(@NotNull(message = "塘口ID不能为空") + @PathVariable Long pondId) { + PondDeviceListVo result = new PondDeviceListVo(); + + // 1. 查询塘口下的所有设备 + List devices = deviceMapper.selectList( + new LambdaQueryWrapper() + .eq(Device::getPondId, pondId) + .orderByAsc(Device::getDeviceType) + .orderByDesc(Device::getCreateTime) + ); + + if (devices == null || devices.isEmpty()) { + result.setListDetector(List.of()); + result.setListController(List.of()); + return R.ok(result); + } + + // 2. 收集所有设备ID,用于查询联动控制和故障码 + List deviceIds = devices.stream() + .map(Device::getId) + .collect(Collectors.toList()); + + // 3. 批量查询所有设备的联动控制 + List allLinkedCtrls = linkedCtrlMapper.selectList( + new LambdaQueryWrapper() + .in(LinkedCtrl::getDeviceId, deviceIds) + ); + java.util.Map> linkedCtrlsByDevice = allLinkedCtrls.stream() + .collect(Collectors.groupingBy(LinkedCtrl::getDeviceId)); + + // 4. 批量查询塘口的所有开关 + List allSwitches = deviceSwitchMapper.selectList( + new LambdaQueryWrapper() + .eq(DeviceSwitch::getPondId, pondId) + .orderByAsc(DeviceSwitch::getIndex) + ); + + // 5. 收集开关ID,查询定时控制 + List switchIds = allSwitches.stream() + .map(DeviceSwitch::getId) + .collect(Collectors.toList()); + java.util.Map> timingCtrlsBySwitch = new java.util.HashMap<>(); + if (!switchIds.isEmpty()) { + List allTimingCtrls = timingCtrlMapper.selectList( + new LambdaQueryWrapper() + .in(TimingCtrl::getSwitchId, switchIds) + ); + timingCtrlsBySwitch = allTimingCtrls.stream() + .collect(Collectors.groupingBy(TimingCtrl::getSwitchId)); + } + + // 6. 按设备ID分组开关 + java.util.Map> switchesByDevice = allSwitches.stream() + .collect(Collectors.groupingBy(DeviceSwitch::getDeviceId)); + + // 7. 批量查询故障码 + Set controllerIds = devices.stream() + .filter(d -> d.getDeviceType() != null && d.getDeviceType() == 2) + .map(Device::getId) + .collect(Collectors.toSet()); + List errorCodes = new ArrayList<>(); + if (!controllerIds.isEmpty()) { + errorCodes = deviceErrorCodeMapper.selectList( + new LambdaQueryWrapper() + .in(DeviceErrorCode::getDeviceId, controllerIds) + ); + } + + // 8. 处理探测器列表 (包含: deviceType=1 + deviceType=2且isOxygenUsed=1) + List detectorList = new ArrayList<>(); + for (Device device : devices) { + // 水质检测仪 或 开启溶氧检测的测控一体机 + if ((device.getDeviceType() != null && device.getDeviceType() == 1) + || (device.getDeviceType() != null && device.getDeviceType() == 2 + && device.getIsOxygenUsed() != null && device.getIsOxygenUsed() == 1)) { + + DeviceVo deviceVo = MapstructUtils.convert(device, DeviceVo.class); + + // 设置告警码信息 + if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { + // 告警码信息已在DeviceVo中,无需额外处理 + } else { + deviceVo.setWarnCode(DefineDeviceWarnCode.None); + } + + // 判断是否有联动控制 + deviceVo.setIsLinkCtrl(false); + List linkedCtrls = linkedCtrlsByDevice.get(device.getId()); + if (linkedCtrls != null && !linkedCtrls.isEmpty()) { + for (LinkedCtrl linkedCtrl : linkedCtrls) { + if ((linkedCtrl.getOxyLowerOpen() != null && linkedCtrl.getOxyLowerOpen() == 1) + || (linkedCtrl.getOxyUpperOpen() != null && linkedCtrl.getOxyUpperOpen() == 1)) { + deviceVo.setIsLinkCtrl(true); + break; + } + } + } + + detectorList.add(deviceVo); + } + } + + // 9. 处理控制器列表 (仅测控一体机 deviceType=2) + List controllerList = new ArrayList<>(); + for (Device device : devices) { + if (device.getDeviceType() != null && device.getDeviceType() == 2) { + // 转换为DeviceVo再复制到DeviceWithSwitchVo + DeviceVo baseDeviceVo = MapstructUtils.convert(device, DeviceVo.class); + DeviceWithSwitchVo deviceVo = new DeviceWithSwitchVo(); + BeanUtil.copyProperties(baseDeviceVo, deviceVo); + + // 初始化告警码信息 + PondMode1WarnCodeInfo warnCodeInfo = new PondMode1WarnCodeInfo(); + if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { + warnCodeInfo.setWarnCode(device.getWarnCode()); + warnCodeInfo.setWarnDescription(DefineDeviceWarnCode.toWarnDescription(device.getWarnCode())); + } else { + warnCodeInfo.setWarnCode(DefineDeviceWarnCode.None); + warnCodeInfo.setWarnDescription(""); + } + deviceVo.setWarnCodeInfo(warnCodeInfo); + + // 处理设备级故障码 (switchIndex=0的故障) + StringBuilder errorMessage = new StringBuilder(); + for (DeviceErrorCode errorCode : errorCodes) { + if (!errorCode.getDeviceId().equals(device.getId())) { + continue; + } + if (errorCode.getSwitchIndex() == null || errorCode.getSwitchIndex() != 0) { + continue; + } + if (errorCode.getErrorCode() == null || errorCode.getErrorCode() > DefineDeviceErrorCode.PowerOff) { + continue; + } + + // 电压告警需要开关打开 + boolean isVoltageError = errorCode.getErrorCode() >= DefineDeviceErrorCode.Three_PhaseA_OverVoltage + && errorCode.getErrorCode() <= DefineDeviceErrorCode.Three_PhaseC_UnderVoltage; + + if (device.getVoltageWarnOpen() != null && device.getVoltageWarnOpen() == 1 && isVoltageError + || !isVoltageError) { + String msg = DefineDeviceErrorCode.getErrorMessage(errorCode.getErrorCode()); + if (msg != null && !msg.isEmpty()) { + if (errorMessage.length() > 0 && !errorMessage.toString().contains(msg)) { + errorMessage.append(","); + } + if (!errorMessage.toString().contains(msg)) { + errorMessage.append(msg); + } + } + } + } + deviceVo.setErrorMessage(errorMessage.toString()); + + // 查询该设备的开关列表 + List switches = switchesByDevice.getOrDefault(device.getId(), new ArrayList<>()); + List switchVoList = new ArrayList<>(); + + for (DeviceSwitch sw : switches) { + DeviceSwitchVo switchVo = MapstructUtils.convert(sw, DeviceSwitchVo.class); + + // 判断是否有联动控制 + switchVo.setIsLinkedCtrl(0); + if (sw.getLinkedCtrlId() != null) { + LinkedCtrl linkedCtrl = linkedCtrlMapper.selectById(sw.getLinkedCtrlId()); + if (linkedCtrl != null + && ((linkedCtrl.getOxyLowerOpen() != null && linkedCtrl.getOxyLowerOpen() == 1) + || (linkedCtrl.getOxyUpperOpen() != null && linkedCtrl.getOxyUpperOpen() == 1))) { + switchVo.setIsLinkedCtrl(1); + } + } + + // 判断是否有定时控制 + switchVo.setIsTimingCtrl(false); + List timingCtrls = timingCtrlsBySwitch.get(sw.getId()); + if (timingCtrls != null && !timingCtrls.isEmpty()) { + for (TimingCtrl timingCtrl : timingCtrls) { + if (timingCtrl.getIsOpen() != null && timingCtrl.getIsOpen() == 1) { + switchVo.setIsTimingCtrl(true); + break; + } + } + } + + // 处理开关的故障码 + switchVo.setHasErrorCode(false); + for (DeviceErrorCode errorCode : errorCodes) { + if (!errorCode.getDeviceId().equals(sw.getDeviceId())) { + continue; + } + if (errorCode.getSwitchIndex() == null || !errorCode.getSwitchIndex().equals(sw.getIndex())) { + continue; + } + if (errorCode.getErrorCode() == null || errorCode.getErrorCode() > DefineDeviceErrorCode.PowerOff) { + continue; + } + + // 电流告警范围 + boolean hasElectricError = (errorCode.getErrorCode() >= DefineDeviceErrorCode.Three_Switch1OverElectricA + && errorCode.getErrorCode() <= DefineDeviceErrorCode.Three_Switch4ElectricEmpty) + || (errorCode.getErrorCode() >= DefineDeviceErrorCode.One_Switch1OverElectric + && errorCode.getErrorCode() <= DefineDeviceErrorCode.One_Switch4ElectricEmpty); + + if (sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1 && hasElectricError) { + switchVo.setHasErrorCode(true); + String msg = DefineDeviceErrorCode.getErrorMessage(errorCode.getErrorCode()); + if (msg != null && !msg.isEmpty()) { + if (errorMessage.length() > 0 && !errorMessage.toString().contains(msg)) { + errorMessage.append(","); + } + if (!errorMessage.toString().contains(msg)) { + errorMessage.append(msg); + } + } + } + } + + switchVoList.add(switchVo); + } + + // 更新设备的错误信息(包含开关的错误) + deviceVo.setErrorMessage(errorMessage.toString()); + deviceVo.setListSwitch(switchVoList); + controllerList.add(deviceVo); + } + } + + result.setListDetector(detectorList); + result.setListController(controllerList); + + return R.ok(result); + } + + /** + * 为塘口选择/移除设备或开关 + * 将指定的设备(探测器)和开关分配到塘口,或从塘口移除 + * + * @param request 请求参数(包含塘口ID、设备ID列表、开关ID列表) + */ + @PutMapping("/bind/device") + @Transactional(rollbackFor = Exception.class) + public R selectDeviceOrSwitch( + @Validated @RequestBody PondSelectDeviceOrSwitchBo request) { + + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + + // 查询塘口信息并验证权限 + Pond pond = pondMapper.selectById(request.getPondId()); + if (pond == null || !pond.getUserId().equals(userId)) { + return R.fail("塘口不存在或无权限访问"); + } + + // 处理空列表 + List listDetectorId = request.getListDetectorId() != null ? request.getListDetectorId() : new ArrayList<>(); + List listSwitchId = request.getListSwitchId() != null ? request.getListSwitchId() : new ArrayList<>(); + + // 查询当前塘口已有的设备 + List currentDevices = deviceMapper.selectList( + new LambdaQueryWrapper() + .eq(Device::getPondId, request.getPondId()) + ); + + // 查询当前塘口已有的开关 + List currentSwitches = deviceSwitchMapper.selectList( + new LambdaQueryWrapper() + .eq(DeviceSwitch::getPondId, request.getPondId()) + ); + + // 查询要添加的设备详细信息 + List newDevices = new ArrayList<>(); + if (!listDetectorId.isEmpty()) { + newDevices = deviceMapper.selectBatchIds(listDetectorId); + } + + // 查询要添加的开关详细信息(包含设备信息) + List newSwitches = new ArrayList<>(); + if (!listSwitchId.isEmpty()) { + newSwitches = deviceSwitchMapper.selectList( + new LambdaQueryWrapper() + .in(DeviceSwitch::getId, listSwitchId) + ); + // 查询开关关联的设备信息 + if (!newSwitches.isEmpty()) { + List deviceIds = newSwitches.stream() + .map(DeviceSwitch::getDeviceId) + .distinct() + .collect(Collectors.toList()); + List switchDevices = deviceMapper.selectBatchIds(deviceIds); + java.util.Map deviceMap = switchDevices.stream() + .collect(Collectors.toMap(Device::getId, d -> d)); + // 这里可以用于后续操作记录 + } + } + + // 构建字典:要添加的设备 + java.util.Map dictDeviceAdd = new java.util.HashMap<>(); + for (Device device : newDevices) { + dictDeviceAdd.put(device.getId(), device); + } + + // 构建字典:要添加的开关 + java.util.Map dictSwitchAdd = new java.util.HashMap<>(); + for (DeviceSwitch sw : newSwitches) { + dictSwitchAdd.put(sw.getId(), sw); + } + + // 构建字典:要移除的设备 + java.util.Map dictDeviceRemove = new java.util.HashMap<>(); + Set hashDeviceRemove = new HashSet<>(); + for (Device device : currentDevices) { + dictDeviceRemove.put(device.getId(), device); + hashDeviceRemove.add(device.getId()); + dictDeviceAdd.remove(device.getId()); // 已存在的不算新增 + } + // 从要移除的列表中排除仍要保留的 + for (Long id : listDetectorId) { + dictDeviceRemove.remove(id); + hashDeviceRemove.remove(id); + } + + // 构建字典:要移除的开关 + java.util.Map dictSwitchRemove = new java.util.HashMap<>(); + Set hashSwitchRemove = new HashSet<>(); + for (DeviceSwitch sw : currentSwitches) { + dictSwitchRemove.put(sw.getId(), sw); + hashSwitchRemove.add(sw.getId()); + dictSwitchAdd.remove(sw.getId()); // 已存在的不算新增 + } + // 从要移除的列表中排除仍要保留的 + for (Long id : listSwitchId) { + dictSwitchRemove.remove(id); + hashSwitchRemove.remove(id); + } + + // 1. 添加/更新设备到塘口 + if (!listDetectorId.isEmpty()) { + deviceMapper.update(null, + new LambdaUpdateWrapper() + .in(Device::getId, listDetectorId) + .set(Device::getPondId, request.getPondId()) + ); + } + + // 2. 从塘口移除设备 + if (!hashDeviceRemove.isEmpty()) { + // 删除关联的联动控制 + linkedCtrlMapper.delete( + new LambdaQueryWrapper() + .in(LinkedCtrl::getDeviceId, hashDeviceRemove) + ); + + // 移除设备的塘口关联,并清除告警状态 + deviceMapper.update(null, + new LambdaUpdateWrapper() + .in(Device::getId, hashDeviceRemove) + .set(Device::getPondId, null) + .set(Device::getIsTempWarnExist, 0) + .set(Device::getIsOxygenWarnExist, 0) + ); + } + + // 3. 添加/更新开关到塘口 + if (!listSwitchId.isEmpty()) { + deviceSwitchMapper.update(null, + new LambdaUpdateWrapper() + .in(DeviceSwitch::getId, listSwitchId) + .set(DeviceSwitch::getPondId, request.getPondId()) + ); + } + + // 4. 从塘口移除开关 + if (!hashSwitchRemove.isEmpty()) { + deviceSwitchMapper.update(null, + new LambdaUpdateWrapper() + .in(DeviceSwitch::getId, hashSwitchRemove) + .set(DeviceSwitch::getPondId, null) + .set(DeviceSwitch::getLinkedCtrlId, null) + ); + } + + // TODO: 清除设备报警数据(类似C#中的EventHelper.RemoveDeviceWarnWaitAndNotice) + // for (Long deviceId : hashDeviceRemove) { + // // 清除该设备的报警等待和通知数据 + // } + + // TODO: 操作记录(类似C#中的CacheData.AddMessageOpRecordUser) + // 可以集成到日志系统中记录以下操作: + // 1. 设备添加到塘口: dictDeviceAdd + // 2. 设备从塘口移除: dictDeviceRemove + // 3. 开关添加到塘口: dictSwitchAdd + // 4. 开关从塘口移除: dictSwitchRemove + + return R.ok(); + } + + /** + * 查询用户塘口列表 - 模式1(用于首页展示) + * 返回塘口的设备和开关汇总信息,包括最大水质参数、告警状态、故障信息等 + * + * @param rootUserId 用户ID + * @return 塘口列表 + */ + @GetMapping("/list_mode1") + public R> getPondListMode1( + @RequestParam("rootUserId") Long rootUserId) { + + // 1. 查询用户的所有塘口 + List ponds = pondMapper.selectList( + new LambdaQueryWrapper() + .eq(Pond::getUserId, rootUserId) + .orderByDesc(Pond::getCreateTime) + ); + + List listData = new ArrayList<>(); + if (ponds == null || ponds.isEmpty()) { + return R.ok(listData); + } + + // 提取塘口ID列表 + List pondIds = ponds.stream() + .map(Pond::getId) + .collect(Collectors.toList()); + + // 2. 批量查询所有塘口的设备 + List allDevices = deviceMapper.selectList( + new LambdaQueryWrapper() + .in(Device::getPondId, pondIds) + ); + + // 按塘口ID分组 + java.util.Map> devicesByPond = allDevices.stream() + .collect(Collectors.groupingBy(Device::getPondId)); + + // 3. 批量查询所有塘口的开关 + List allSwitches = deviceSwitchMapper.selectList( + new LambdaQueryWrapper() + .in(DeviceSwitch::getPondId, pondIds) + ); + + // 按塘口ID分组 + java.util.Map> switchesByPond = allSwitches.stream() + .collect(Collectors.groupingBy(DeviceSwitch::getPondId)); + + // 4. 收集所有控制器ID(用于查询故障码) + Set hashSetControllerId = new HashSet<>(); + for (Device device : allDevices) { + if (device.getDeviceType() != null && device.getDeviceType() == 2) { + hashSetControllerId.add(device.getId()); + } + } + for (DeviceSwitch sw : allSwitches) { + if (sw.getDeviceId() != null) { + hashSetControllerId.add(sw.getDeviceId()); + } + } + + // 5. 查询控制器的故障码 + List listErrorCodes = new ArrayList<>(); + if (!hashSetControllerId.isEmpty()) { + listErrorCodes = deviceErrorCodeMapper.selectList( + new LambdaQueryWrapper() + .in(DeviceErrorCode::getDeviceId, hashSetControllerId) + ); + } + + // 6. 查询开关关联的设备(用于获取WarnCode) + Set switchDeviceIds = allSwitches.stream() + .map(DeviceSwitch::getDeviceId) + .collect(Collectors.toSet()); + java.util.Map switchDeviceMap = new java.util.HashMap<>(); + if (!switchDeviceIds.isEmpty()) { + List switchDevices = deviceMapper.selectBatchIds(switchDeviceIds); + switchDeviceMap = switchDevices.stream() + .collect(Collectors.toMap(Device::getId, d -> d)); + } + + // 7. 为每个塘口聚合数据 + for (Pond pond : ponds) { + PublicPondMode1Vo data = new PublicPondMode1Vo(); + data.setId(pond.getId()); + data.setPondName(pond.getPondName()); + + Set hashSetDeviceId = new HashSet<>(); + int warnCode = DefineDeviceWarnCode.None; + + // 获取该塘口的设备列表 + List pondDevices = devicesByPond.getOrDefault(pond.getId(), new ArrayList<>()); + + // 处理设备数据 + for (Device device : pondDevices) { + hashSetDeviceId.add(device.getId()); + + // 聚合告警码 + if (device.getWarnCode() != null + && device.getWarnCode() != DefineDeviceWarnCode.None + && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { + warnCode |= device.getWarnCode(); + } + + // 判断设备是否在线(简化判断,实际应根据业务逻辑) + if (device.getWarnCode() == null + || (device.getWarnCode() != DefineDeviceWarnCode.DeviceDead + && device.getWarnCode() != DefineDeviceWarnCode.DeviceOffline)) { + data.setIsAllDead(false); + } + + // 聚合最大水质参数 + if (device.getValueDissolvedOxygen() != null + && data.getValueDissolvedOxygen() < device.getValueDissolvedOxygen()) { + data.setValueDissolvedOxygen(device.getValueDissolvedOxygen()); + } + if (device.getValueTemperature() != null + && data.getValueTemperature() < device.getValueTemperature()) { + data.setValueTemperature(device.getValueTemperature()); + } + if (device.getValueSaturability() != null + && data.getValueSaturability() < device.getValueSaturability()) { + data.setValueSaturability(device.getValueSaturability()); + } + if (device.getValuePh() != null + && data.getValuePH() < device.getValuePh()) { + data.setValuePH(device.getValuePh()); + } + if (device.getValueSalinity() != null + && data.getValueSalinity() < device.getValueSalinity()) { + data.setValueSalinity(device.getValueSalinity()); + } + + // 处理设备的故障码(电压告警) + if (device.getVoltageWarnOpen() != null && device.getVoltageWarnOpen() == 1) { + for (DeviceErrorCode errorCode : listErrorCodes) { + if (!errorCode.getDeviceId().equals(device.getId())) { + continue; + } + if (errorCode.getErrorCode() == null + || errorCode.getErrorCode() > DefineDeviceErrorCode.PowerOff) { + continue; + } + + // 电压告警范围 + if (errorCode.getErrorCode() >= DefineDeviceErrorCode.Three_PhaseA_OverVoltage + && errorCode.getErrorCode() <= DefineDeviceErrorCode.Three_PhaseC_UnderVoltage) { + String errorMessage = DefineDeviceErrorCode.getErrorMessage(errorCode.getErrorCode()); + if (errorMessage != null && !errorMessage.isEmpty()) { + if (data.getErrorMessage() == null || data.getErrorMessage().isEmpty()) { + data.setErrorMessage(errorMessage); + } else if (!data.getErrorMessage().contains(errorMessage)) { + data.setErrorMessage(data.getErrorMessage() + "," + errorMessage); + } + } + } + } + } + } + + // 获取该塘口的开关列表 + List pondSwitches = switchesByPond.getOrDefault(pond.getId(), new ArrayList<>()); + + // 处理开关数据 + for (DeviceSwitch sw : pondSwitches) { + if (sw.getDeviceId() != null) { + hashSetDeviceId.add(sw.getDeviceId()); + } + + // 聚合开关关联设备的告警码 + Device switchDevice = switchDeviceMap.get(sw.getDeviceId()); + if (switchDevice != null + && switchDevice.getWarnCode() != null + && switchDevice.getWarnCode() != DefineDeviceWarnCode.None + && switchDevice.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { + warnCode |= switchDevice.getWarnCode(); + } + + // 统计开启的开关数量 + if (sw.getIsOpen() != null && sw.getIsOpen() == 1) { + data.setSwitchOpenCount(data.getSwitchOpenCount() + 1); + } + + // 处理开关的故障码(电流告警) + if (sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1) { + for (DeviceErrorCode errorCode : listErrorCodes) { + if (!errorCode.getDeviceId().equals(sw.getDeviceId())) { + continue; + } + if (errorCode.getErrorCode() == null + || errorCode.getErrorCode() > DefineDeviceErrorCode.PowerOff) { + continue; + } + + // 电流告警范围 + boolean hasElectricError = (errorCode.getErrorCode() >= DefineDeviceErrorCode.Three_Switch1OverElectricA + && errorCode.getErrorCode() <= DefineDeviceErrorCode.Three_Switch4ElectricEmpty) + || (errorCode.getErrorCode() >= DefineDeviceErrorCode.One_Switch1OverElectric + && errorCode.getErrorCode() <= DefineDeviceErrorCode.One_Switch4ElectricEmpty); + + if (hasElectricError) { + String errorMessage = DefineDeviceErrorCode.getErrorMessage(errorCode.getErrorCode()); + if (errorMessage != null && !errorMessage.isEmpty()) { + if (data.getErrorMessage() == null || data.getErrorMessage().isEmpty()) { + data.setErrorMessage(errorMessage); + } else if (!data.getErrorMessage().contains(errorMessage)) { + data.setErrorMessage(data.getErrorMessage() + "," + errorMessage); + } + } + } + } + } + } + + // 设置汇总数据 + data.setDeviceCount(hashSetDeviceId.size()); + data.setSwitchCount(pondSwitches.size()); + data.getWarnCodeInfo().setWarnCode(warnCode); + data.getWarnCodeInfo().setWarnDescription(DefineDeviceWarnCode.toWarnDescription(warnCode)); + + listData.add(data); + } + + return R.ok(listData); + } } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/PondSelectDeviceOrSwitchBo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/PondSelectDeviceOrSwitchBo.java new file mode 100644 index 0000000..3462c2d --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/PondSelectDeviceOrSwitchBo.java @@ -0,0 +1,35 @@ +package com.intc.fishery.domain.bo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 塘口选择设备或开关请求对象 + * + * @author intc + * @date 2026-01-12 + */ +@Data +public class PondSelectDeviceOrSwitchBo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 塘口ID + */ + private Long pondId; + + /** + * 探测器(水质检测仪)ID列表 + */ + private List listDetectorId; + + /** + * 开关ID列表 + */ + private List listSwitchId; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceSwitchVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceSwitchVo.java index 8e7b193..d7d5392 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceSwitchVo.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceSwitchVo.java @@ -154,4 +154,14 @@ public class DeviceSwitchVo implements Serializable { private Date updateTime; + /** + * 是否有定时控制 + */ + private Boolean isTimingCtrl; + + /** + * 是否有故障码 + */ + private Boolean hasErrorCode; + } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java index c3ec7c0..79d0f45 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java @@ -341,4 +341,9 @@ public class DeviceVo implements Serializable { private Date updateTime; + /** + * 是否有联动控制 + */ + private Boolean isLinkCtrl; + } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceWithSwitchVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceWithSwitchVo.java new file mode 100644 index 0000000..a7f365b --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceWithSwitchVo.java @@ -0,0 +1,35 @@ +package com.intc.fishery.domain.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.intc.common.json.handler.DoubleSerializer; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; +import java.util.List; + +/** + * 带开关列表的设备视图对象 + * + * @author intc + * @date 2026-01-12 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DeviceWithSwitchVo extends DeviceVo { + + /** + * 开关列表(仅测控一体机有) + */ + private List listSwitch; + + /** + * 故障信息 + */ + private String errorMessage; + + /** + * 告警码信息 + */ + private PondMode1WarnCodeInfo warnCodeInfo; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondDeviceListVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondDeviceListVo.java new file mode 100644 index 0000000..414404f --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondDeviceListVo.java @@ -0,0 +1,30 @@ +package com.intc.fishery.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 塘口设备列表视图对象 + * + * @author intc + * @date 2026-01-12 + */ +@Data +public class PondDeviceListVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 探测器列表(水质检测仪) + */ + private List listDetector; + + /** + * 控制器列表(测控一体机,包含开关列表) + */ + private List listController; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondMode1WarnCodeInfo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondMode1WarnCodeInfo.java new file mode 100644 index 0000000..8f5fd78 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondMode1WarnCodeInfo.java @@ -0,0 +1,27 @@ +package com.intc.fishery.domain.vo; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +/** + * 塘口告警码信息 + * + * @author intc + */ +@Data +public class PondMode1WarnCodeInfo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 告警码 + */ + private Integer warnCode; + + /** + * 告警描述 + */ + private String warnDescription; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceSimpleVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceSimpleVo.java new file mode 100644 index 0000000..081bede --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceSimpleVo.java @@ -0,0 +1,56 @@ +package com.intc.fishery.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 设备简化视图对象 + * + * @author intc + * @date 2026-01-12 + */ +@Data +public class PublicDeviceSimpleVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 设备ID + */ + private Long id; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 设备类型 + */ + private Integer deviceType; + + /** + * 服务到期时间 + */ + private Date deadTime; + + /** + * 溶解氧参数配置开关 + */ + private Integer isOxygenUsed; + + /** + * 塘口信息 + */ + private PublicPondIdNameVo pondInfo; + + /** + * 开关列表 + */ + private List listSwitch; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceSwitchSimpleVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceSwitchSimpleVo.java new file mode 100644 index 0000000..c709261 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceSwitchSimpleVo.java @@ -0,0 +1,39 @@ +package com.intc.fishery.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 设备开关简化视图对象 + * + * @author intc + * @date 2026-01-12 + */ +@Data +public class PublicDeviceSwitchSimpleVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 开关ID + */ + private Long id; + + /** + * 序号 + */ + private Integer index; + + /** + * 开关名称 + */ + private String switchName; + + /** + * 塘口信息 + */ + private PublicPondIdNameVo pondInfo; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicPondIdNameVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicPondIdNameVo.java new file mode 100644 index 0000000..f95b871 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicPondIdNameVo.java @@ -0,0 +1,29 @@ +package com.intc.fishery.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 塘口ID和名称视图对象 + * + * @author intc + * @date 2026-01-12 + */ +@Data +public class PublicPondIdNameVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 塘口ID + */ + private Long id; + + /** + * 塘口名称 + */ + private String pondName; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicPondMode1Vo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicPondMode1Vo.java new file mode 100644 index 0000000..e34b0db --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicPondMode1Vo.java @@ -0,0 +1,98 @@ +package com.intc.fishery.domain.vo; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +/** + * 塘口模式1视图对象 - 用于首页展示 + * + * @author intc + */ +@Data +public class PublicPondMode1Vo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 塘口ID + */ + private Long id; + + /** + * 塘口名称 + */ + private String pondName; + + /** + * 设备数量 + */ + private Integer deviceCount; + + /** + * 开关数量 + */ + private Integer switchCount; + + /** + * 开启的开关数量 + */ + private Integer switchOpenCount; + + /** + * 是否全部设备离线/失联 + */ + private Boolean isAllDead; + + /** + * 溶解氧(最大值) + */ + private Double valueDissolvedOxygen; + + /** + * 水温(最大值) + */ + private Double valueTemperature; + + /** + * 饱和度(最大值) + */ + private Double valueSaturability; + + /** + * PH值(最大值) + */ + private Double valuePH; + + /** + * 盐度(最大值) + */ + private Double valueSalinity; + + /** + * 故障信息 + */ + private String errorMessage; + + /** + * 告警码信息 + */ + private PondMode1WarnCodeInfo warnCodeInfo; + + public PublicPondMode1Vo() { + this.deviceCount = 0; + this.switchCount = 0; + this.switchOpenCount = 0; + this.isAllDead = true; + this.valueDissolvedOxygen = 0.0; + this.valueTemperature = 0.0; + this.valueSaturability = 0.0; + this.valuePH = 0.0; + this.valueSalinity = 0.0; + this.errorMessage = ""; + this.warnCodeInfo = new PondMode1WarnCodeInfo(); + this.warnCodeInfo.setWarnCode(0); + this.warnCodeInfo.setWarnDescription(""); + } +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/PondServiceImpl.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/PondServiceImpl.java index d2a0e68..2f06082 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/PondServiceImpl.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/PondServiceImpl.java @@ -25,6 +25,12 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import com.intc.fishery.mapper.DeviceMapper; +import com.intc.fishery.mapper.DeviceSwitchMapper; +import com.intc.fishery.domain.Device; +import com.intc.fishery.domain.DeviceSwitch; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; /** * 塘口管理Service业务层处理 @@ -41,6 +47,10 @@ public class PondServiceImpl implements IPondService { private final FishMapper fishMapper; + private final DeviceMapper deviceMapper; + + private final DeviceSwitchMapper deviceSwitchMapper; + /** * 查询塘口管理 * @@ -206,6 +216,7 @@ public class PondServiceImpl implements IPondService { /** * 校验并批量删除塘口管理信息 + * 删除时会级联删除关联的设备和开关 * * @param ids 待删除的主键集合 * @param isValid 是否进行有效性校验 @@ -216,6 +227,23 @@ public class PondServiceImpl implements IPondService { if(isValid){ //TODO 做一些业务上的校验,判断是否需要校验 } + + // 删除前先级联删除关联数据 + for (Long pondId : ids) { + // 1. 删除关联的设备 + deviceMapper.delete( + new LambdaQueryWrapper() + .eq(Device::getPondId, pondId) + ); + + // 2. 删除关联的开关 + deviceSwitchMapper.delete( + new LambdaQueryWrapper() + .eq(DeviceSwitch::getPondId, pondId) + ); + } + + // 删除塘口 return baseMapper.deleteByIds(ids) > 0; } diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java index 23294c4..7ed61d4 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java @@ -180,6 +180,14 @@ public class AliyunIotProperties { */ private String controlIntegrated; + /** + * 控制器 ProductKey (别名,指向 controlIntegrated) + * @return ProductKey + */ + public String getController() { + return controlIntegrated; + } + /** * 根据设备类型获取 ProductKey * @param deviceType 1-水质检测仪, 2-控制一体机 diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java index 721195c..839b6a0 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java @@ -9,6 +9,7 @@ import com.intc.fishery.domain.Device; import com.intc.fishery.mapper.DeviceMapper; import com.intc.fishery.mapper.PondMapper; import com.intc.iot.config.AliyunIotProperties; +import com.intc.iot.domain.bo.AddDeviceControllerBo; import com.intc.iot.domain.bo.AddDeviceDetectorBo; import com.intc.iot.domain.bo.DeviceRealtimeDataBo; import com.intc.iot.domain.vo.DeviceRealtimeDataVo; @@ -79,6 +80,9 @@ public class IotController extends BaseController { @Autowired(required = false) private PondMapper pondMapper; + @Autowired(required = false) + private com.intc.fishery.mapper.DeviceSwitchMapper deviceSwitchMapper; + @Operation(summary = "测试接口") @GetMapping("/test") public R test() { @@ -339,23 +343,30 @@ public class IotController extends BaseController { Object detailData = deviceDetail.get("data"); Integer statusCode = 0; // 默认为设备未激活 - if (detailData instanceof Map) { - Map detailMap = (Map) detailData; - Object statusObj = detailMap.get("status"); - - if (statusObj != null) { - String statusStr = statusObj.toString(); - // 根据物联网平台返回的状态转换为前端状态码 - // 0-未激活, 1-在线, 3-离线, 8-禁用 - if ("ONLINE".equalsIgnoreCase(statusStr) || "online".equals(statusStr)) { - statusCode = 1; - } else if ("OFFLINE".equalsIgnoreCase(statusStr) || "offline".equals(statusStr)) { - statusCode = 3; - } else if ("UNACTIVE".equalsIgnoreCase(statusStr) || "unactive".equals(statusStr)) { - statusCode = 0; - } else if ("DISABLE".equalsIgnoreCase(statusStr) || "disable".equals(statusStr)) { - statusCode = 8; + if (detailData != null) { + // 通过反射获取 status + try { + java.lang.reflect.Method getStatusMethod = detailData.getClass().getMethod("getStatus"); + Object statusObj = getStatusMethod.invoke(detailData); + + if (statusObj != null) { + String statusStr = statusObj.toString(); + log.info("从 SDK 对象获取到的状态: {}", statusStr); + + // 根据物联网平台返回的状态转换为前端状态码 + // 0-未激活, 1-在线, 3-离线, 8-禁用 + if ("ONLINE".equalsIgnoreCase(statusStr) || "online".equals(statusStr)) { + statusCode = 1; + } else if ("OFFLINE".equalsIgnoreCase(statusStr) || "offline".equals(statusStr)) { + statusCode = 3; + } else if ("UNACTIVE".equalsIgnoreCase(statusStr) || "unactive".equals(statusStr)) { + statusCode = 0; + } else if ("DISABLE".equalsIgnoreCase(statusStr) || "disable".equals(statusStr)) { + statusCode = 8; + } } + } catch (Exception ex) { + log.error("无法从 SDK 对象中获取 status: {}", ex.getMessage(), ex); } } @@ -674,6 +685,443 @@ public class IotController extends BaseController { // ======================== 设备管理相关接口 ======================== + @Operation(summary = "添加测控一体机") + @PostMapping("/device/add_device_controller") + public R addDeviceController(@RequestBody AddDeviceControllerBo bo) { + try { + if (iotDeviceService == null) { + return R.fail("飞燕平台配置未启用"); + } + if (deviceMapper == null) { + return R.fail("设备数据库服务未启用"); + } + if (deviceSwitchMapper == null) { + return R.fail("开关数据库服务未启用"); + } + + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null) { + return R.fail("未登录或登录已过期"); + } + + // 验证塘口是否存在且属于当前用户 + if (bo.getPondId() != null && bo.getPondId() > 0 && pondMapper != null) { + long count = pondMapper.selectCount( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(com.intc.fishery.domain.Pond::getUserId, userId) + .eq(com.intc.fishery.domain.Pond::getId, bo.getPondId()) + ); + if (count == 0) { + return R.fail("塘口不存在或无权限访问"); + } + } + + // 检查设备是否已被绑定 + Device existDevice = deviceMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(Device::getDeviceType, 2) // 2-测控一体机 + .eq(Device::getSerialNum, bo.getSerialNum()) + ); + if (existDevice != null && existDevice.getUserId() != null) { + return R.fail("该设备号已被绑定"); + } + + // 获取控制器的 ProductKey + String productKey = aliyunIotProperties.getDeviceType().getControlIntegrated(); + if (productKey == null || productKey.isEmpty()) { + return R.fail("未配置测控一体机的 ProductKey"); + } + + // 查询设备基础信息 + Map deviceInfo = iotDeviceService.findDeviceByProductKeyAndName(productKey, bo.getSerialNum()); + if (deviceInfo == null || !Boolean.TRUE.equals(deviceInfo.get("success"))) { + return R.fail("设备不存在"); + } + + // 提取设备数据 + Map data = (Map) deviceInfo.get("data"); + if (data == null || !data.containsKey("deviceList")) { + return R.fail("设备信息格式异常"); + } + + java.util.List deviceList = (java.util.List) data.get("deviceList"); + if (deviceList == null || deviceList.isEmpty()) { + return R.fail("设备不存在"); + } + + Object deviceObj = deviceList.get(0); + String iotId = null; + String status = null; + + // 提取 iotId 和 status + if (deviceObj instanceof Map) { + Map deviceMap = (Map) deviceObj; + iotId = (String) deviceMap.get("iotId"); + Object statusObj = deviceMap.get("status"); + status = statusObj != null ? statusObj.toString() : null; + } else { + try { + iotId = (String) deviceObj.getClass().getMethod("getIotId").invoke(deviceObj); + Object statusObj = deviceObj.getClass().getMethod("getStatus").invoke(deviceObj); + status = statusObj != null ? statusObj.toString() : null; + } catch (Exception ex) { + log.error("无法从设备对象中获取信息: {}", ex.getMessage()); + } + } + + if (iotId == null || iotId.isEmpty()) { + return R.fail("设备 iotId 为空"); + } + + // 检查设备状态 + if (status != null) { + if ("UNACTIVE".equalsIgnoreCase(status)) { + return R.fail("设备未激活"); + } + if ("DISABLE".equalsIgnoreCase(status)) { + return R.fail("设备禁用"); + } + } + + // 查询物模型,确定 rated_voltage 的正确格式 + log.info("查询控制器物模型,ProductKey: {}", productKey); + Map thingModel = iotDeviceService.queryThingModel(productKey); + log.info("物模型查询结果: {}", thingModel); + + // 先查询现有设备属性,了解 rated_voltage 的正确格式 + try { + Map currentProps = iotDeviceService.queryDeviceProperties(iotId); + log.info("当前设备属性: {}", currentProps); + } catch (Exception e) { + log.warn("查询当前设备属性失败: {}", e.getMessage()); + } + + // 设置设备属性 + Map properties = new java.util.HashMap<>(); + + // 设置额定电压 - rated_voltage 是结构体类型 + // 根据C#代码,需要调用 ControllerHelper.GetInputVoltageProperty + // 该方法返回一个特定结构的对象 + Integer voltValue = getVoltageValue(bo.getInputVoltage()); + if (voltValue == null) { + return R.fail("电压参数错误"); + } + + // 暂时跳过 rated_voltage 设置,先设置其他属性 + // properties.put("rated_voltage", ratedVoltageStruct); + + // 如果是三相380V四线,设置输出电压 + if (bo.getInputVoltage() == 4) { + for (int i = 1; i <= 4; i++) { + properties.put("Switch" + i + "_volt", 380); + } + } + + // 清空定时控制数据和设置额定电流 + java.util.List emptyTimerList = new java.util.ArrayList<>(); + Double defaultRatingSwitch = 10.0; // 默认额定电流10A + for (int i = 1; i <= 4; i++) { + properties.put("rating_switch" + i, defaultRatingSwitch); + properties.put("localTimer_switch" + i, emptyTimerList); + } + + // 如果使用溶解氧功能,设置盐度 + if (bo.getIsOxygenUsed()) { + properties.put("salinitySet", bo.getSalinityCompensation()); + } + + String propertiesJson = cn.hutool.json.JSONUtil.toJsonStr(properties); + log.info("准备设置设备属性,iotId: {}, properties: {}", iotId, propertiesJson); + + Map setResult = iotDeviceService.setDeviceProperty(iotId, propertiesJson); + log.info("设置设备属性返回结果: {}", setResult); + + if (setResult == null) { + log.error("设置设备属性返回null"); + return R.fail("设置设备属性失败:返回结果为空"); + } + + if (!Boolean.TRUE.equals(setResult.get("success"))) { + String errorMsg = setResult.get("errorMessage") != null ? + setResult.get("errorMessage").toString() : "未知错误"; + log.error("设置设备属性失败,错误信息: {}", errorMsg); + return R.fail("设置设备属性失败: " + errorMsg); + } + + // 获取设备属性 + Map deviceProperties = iotDeviceService.queryDeviceProperties(iotId); + + // 初始化警告码:默认探头离线且未校准 (0x0081) + int warnCode = 0x0081; + + // 如果设备离线,添加设备离线警告 + if (status != null && "OFFLINE".equalsIgnoreCase(status)) { + warnCode |= 0x0080; // 设备离线 (0x0080) + } + + // 计算设备数量,用于生成设备名称 + long deviceCount = deviceMapper.selectCount( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(Device::getUserId, userId) + .eq(Device::getDeviceType, 2) + ); + + // 创建或更新设备信息 + Date now = new Date(); + boolean isNew = (existDevice == null); + Device device = isNew ? new Device() : existDevice; + + if (isNew) { + device.setIotId(iotId); + device.setSerialNum(bo.getSerialNum()); + device.setDeviceType(2); // 2-测控一体机 + device.setDeadTime(new Date(now.getTime() + 365L * 24 * 60 * 60 * 1000)); // 一年后到期 + } else { + // 检查是否已过期 + if (device.getDeadTime() != null && now.after(device.getDeadTime())) { + warnCode |= 0x0040; // 设备时间到期 (0x0040) + } + } + + // 设置基本信息 + device.setUserId(userId); + device.setDeviceName("控制器" + (deviceCount + 1)); + device.setBindTime(now); + device.setInputVoltage(bo.getInputVoltage()); + device.setVoltageWarnOpen(0); + device.setIsOxygenUsed(bo.getIsOxygenUsed() ? 1 : 0); + + // 设置必填字段默认值(数据库非空约束) + if (device.getValuePh() == null) { + device.setValuePh(0.0); + } + if (device.getIsOxygenWarnExist() == null) { + device.setIsOxygenWarnExist(0); + } + if (device.getTfluorescence() == null) { + device.setTfluorescence(0.0); + } + if (device.getTreference() == null) { + device.setTreference(0.0); + } + if (device.getPhaseDifference() == null) { + device.setPhaseDifference(0.0); + } + + if (bo.getIsOxygenUsed()) { + device.setSalinityCompensation(bo.getSalinityCompensation()); + device.setOxyWarnLower(bo.getOxyWarnLower()); + device.setOxyWarnCallOpen(1); + device.setOxyWarnCallNoDis(1); + + // 如果有塘口且开关列表包含0(设备自身),则设置设备的塘口ID + if (bo.getPondId() != null && bo.getPondId() > 0 + && bo.getListPutPondPartId() != null && bo.getListPutPondPartId().contains(0)) { + device.setPondId(bo.getPondId()); + } else { + device.setPondId(null); + } + } else { + device.setPondId(null); + } + + device.setTempWarnCallOpen(0); + device.setTempWarnCallNoDis(0); + + // 创建开关列表 + java.util.List switches = new java.util.ArrayList<>(); + for (int i = 1; i <= 4; i++) { + com.intc.fishery.domain.DeviceSwitch deviceSwitch = new com.intc.fishery.domain.DeviceSwitch(); + deviceSwitch.setIndex(i); + deviceSwitch.setSwitchName(device.getDeviceName() + "_开关_" + i); + deviceSwitch.setConnectVoltageType(bo.getInputVoltage()); + deviceSwitch.setRateElectricValue(defaultRatingSwitch); + deviceSwitch.setElectricWarnOpen(0); + deviceSwitch.setIsOpen(0); + // 初始化电流和电压默认值,避免数据库非空约束错误 + deviceSwitch.setDetectElectricValue(0.0); + deviceSwitch.setDetectVoltageValue(0.0); + + // 如果有塘口且开关列表包含当前索引,则设置开关的塘口ID + if (bo.getPondId() != null && bo.getPondId() > 0 + && bo.getListPutPondPartId() != null && bo.getListPutPondPartId().contains(i)) { + deviceSwitch.setPondId(bo.getPondId()); + } + + switches.add(deviceSwitch); + } + + // 解析并设置设备属性 + int errorCode = 0; + if (deviceProperties != null && Boolean.TRUE.equals(deviceProperties.get("success"))) { + Object propData = deviceProperties.get("data"); + + if (propData != null) { + try { + java.lang.reflect.Method getListMethod = propData.getClass().getMethod("getList"); + Object listObj = getListMethod.invoke(propData); + + if (listObj instanceof java.util.List) { + java.util.List propList = (java.util.List) listObj; + + for (Object item : propList) { + if (item != null) { + try { + java.lang.reflect.Method getIdentifierMethod = item.getClass().getMethod("getIdentifier"); + java.lang.reflect.Method getValueMethod = item.getClass().getMethod("getValue"); + String attribute = (String) getIdentifierMethod.invoke(item); + Object value = getValueMethod.invoke(item); + + if (attribute != null && value != null) { + switch (attribute) { + case "dissolvedOxygen": // 溶解氧 + device.setValueDissolvedOxygen(Double.parseDouble(value.toString())); + break; + case "currentTemperature": // 温度 + device.setValueTemperature(Double.parseDouble(value.toString())); + break; + case "dosat": // 饱和度 + device.setValueSaturability(Double.parseDouble(value.toString())); + break; + case "errorCode": // 故障码 + try { + errorCode = Integer.parseInt(value.toString()); + } catch (NumberFormatException e) { + log.warn("无法解析故障码: {}", value); + } + break; + case "sensorErrorCode": // 传感器错误码 + try { + int sensorErrorCode = Integer.parseInt(value.toString()); + warnCode |= sensorErrorCode; + } catch (NumberFormatException e) { + log.warn("无法解析传感器错误码: {}", value); + } + break; + case "Tcorrect": // 设备校准状态 + try { + int tcorrect = Integer.parseInt(value.toString()); + if (tcorrect == 1) { + warnCode &= ~0x0001; // 清除未校准标记 + } + } catch (NumberFormatException e) { + log.warn("无法解析校准状态: {}", value); + } + break; + case "ICCID": // 物联网卡号 + device.setIccId(value.toString()); + break; + case "Treference": // 参比值 + device.setTreference(Double.parseDouble(value.toString())); + break; + case "Tfluorescence": // 荧光值 + device.setTfluorescence(Double.parseDouble(value.toString())); + break; + // 开关状态 + case "switch1": + switches.get(0).setIsOpen(Integer.parseInt(value.toString())); + break; + case "switch2": + switches.get(1).setIsOpen(Integer.parseInt(value.toString())); + break; + case "switch3": + switches.get(2).setIsOpen(Integer.parseInt(value.toString())); + break; + case "switch4": + switches.get(3).setIsOpen(Integer.parseInt(value.toString())); + break; + // 开关电压电流 + case "switch1_VoltCur": + parseSwitchVoltCur(value.toString(), switches.get(0)); + break; + case "switch2_VoltCur": + parseSwitchVoltCur(value.toString(), switches.get(1)); + break; + case "switch3_VoltCur": + parseSwitchVoltCur(value.toString(), switches.get(2)); + break; + case "switch4_VoltCur": + parseSwitchVoltCur(value.toString(), switches.get(3)); + break; + } + } + } catch (Exception ex) { + log.error("无法从 SDK 对象中获取属性: {}", ex.getMessage(), ex); + } + } + } + } + } catch (Exception ex) { + log.error("无法从 SDK 对象中获取属性列表: {}", ex.getMessage(), ex); + } + } + } + + // 验证 ICCID 是否存在 + if (device.getIccId() == null || device.getIccId().isEmpty()) { + return R.fail("设备缺少物联网卡号(ICCID)"); + } + + device.setWarnCode(warnCode); + + // 保存到数据库 + if (isNew) { + deviceMapper.insert(device); + log.info("新测控一体机添加成功: userId={}, iotId={}, serialNum={}", userId, iotId, bo.getSerialNum()); + } else { + deviceMapper.updateById(device); + log.info("测控一体机更新成功: userId={}, iotId={}, serialNum={}", userId, iotId, bo.getSerialNum()); + } + + // 保存开关信息 + for (com.intc.fishery.domain.DeviceSwitch deviceSwitch : switches) { + deviceSwitch.setDeviceId(device.getId()); + deviceSwitchMapper.insert(deviceSwitch); + } + + // TODO: 记录绑定历史和故障码(如果需要的话) + + return R.ok(); + } catch (Exception e) { + log.error("添加测控一体机失败: {}", e.getMessage(), e); + return R.fail("添加测控一体机失败: " + e.getMessage()); + } + } + + /** + * 根据输入电压类型获取电压值 + */ + private Integer getVoltageValue(Integer inputVoltage) { + switch (inputVoltage) { + case 1: return 220; + case 2: return 380; + case 3: return 380; + case 4: return 380; + default: return null; + } + } + + /** + * 解析开关电压电流数据 + */ + private void parseSwitchVoltCur(String json, com.intc.fishery.domain.DeviceSwitch deviceSwitch) { + try { + // 清理JSON字符串 + json = json.replace("\"{", "{").replace("}\"", "}").replace("\\", ""); + Map content = cn.hutool.json.JSONUtil.toBean(json, Map.class); + + if (content.containsKey("voltage")) { + deviceSwitch.setDetectVoltageValue(Double.parseDouble(content.get("voltage").toString())); + } + if (content.containsKey("current")) { + deviceSwitch.setDetectElectricValue(Double.parseDouble(content.get("current").toString())); + } + } catch (Exception e) { + log.warn("解析开关电压电流数据失败: {}", e.getMessage()); + } + } + @Operation(summary = "添加设备探测器(水质检测仪)") @PostMapping("/device/add_device_detector") public R addDeviceDetector(@RequestBody AddDeviceDetectorBo bo) { @@ -825,65 +1273,79 @@ public class IotController extends BaseController { // 解析并设置设备属性 if (deviceProperties != null && Boolean.TRUE.equals(deviceProperties.get("success"))) { Object propData = deviceProperties.get("data"); - if (propData instanceof Map) { - Map propMap = (Map) propData; - Object listObj = propMap.get("list"); - if (listObj instanceof java.util.List) { - java.util.List propList = (java.util.List) listObj; - for (Object item : propList) { - if (item instanceof Map) { - Map prop = (Map) item; - String attribute = (String) prop.get("identifier"); - Object value = prop.get("value"); - - if (attribute != null && value != null) { - switch (attribute) { - case "dissolvedOxygen": // 溶解氧 - device.setValueDissolvedOxygen(Double.parseDouble(value.toString())); - break; - case "currentTemperature": // 温度 - device.setValueTemperature(Double.parseDouble(value.toString())); - break; - case "dosat": // 饱和度 - device.setValueSaturability(Double.parseDouble(value.toString())); - break; - case "PH": - device.setValuePh(Double.parseDouble(value.toString())); - break; - case "salinity": // 盐度 - device.setValueSalinity(Double.parseDouble(value.toString())); - break; - case "sensorErrorCode": // 警告码 - try { - int errorCode = Integer.parseInt(value.toString()); - warnCode |= errorCode; - } catch (NumberFormatException e) { - log.warn("无法解析警告码: {}", value); + + if (propData != null) { + // 通过反射获取属性列表 + try { + java.lang.reflect.Method getListMethod = propData.getClass().getMethod("getList"); + Object listObj = getListMethod.invoke(propData); + + if (listObj instanceof java.util.List) { + java.util.List propList = (java.util.List) listObj; + + for (Object item : propList) { + if (item != null) { + // 通过反射获取属性标识符和值 + try { + java.lang.reflect.Method getIdentifierMethod = item.getClass().getMethod("getIdentifier"); + java.lang.reflect.Method getValueMethod = item.getClass().getMethod("getValue"); + String attribute = (String) getIdentifierMethod.invoke(item); + Object value = getValueMethod.invoke(item); + + if (attribute != null && value != null) { + switch (attribute) { + case "dissolvedOxygen": // 溶解氧 + device.setValueDissolvedOxygen(Double.parseDouble(value.toString())); + break; + case "currentTemperature": // 温度 + device.setValueTemperature(Double.parseDouble(value.toString())); + break; + case "dosat": // 饱和度 + device.setValueSaturability(Double.parseDouble(value.toString())); + break; + case "PH": + device.setValuePh(Double.parseDouble(value.toString())); + break; + case "salinity": // 盐度 + device.setValueSalinity(Double.parseDouble(value.toString())); + break; + case "sensorErrorCode": // 警告码 + try { + int errorCode = Integer.parseInt(value.toString()); + warnCode |= errorCode; + } catch (NumberFormatException e) { + log.warn("无法解析警告码: {}", value); + } + break; + case "Tcorrect": // 设备校准状态 + try { + int tcorrect = Integer.parseInt(value.toString()); + if (tcorrect == 1) { + warnCode &= ~0x0001; // 清除未校准标记 + } + } catch (NumberFormatException e) { + log.warn("无法解析校准状态: {}", value); + } + break; + case "ICCID": // 物联网卡号 + device.setIccId(value.toString()); + break; + case "Treference": // 参比值 + device.setTreference(Double.parseDouble(value.toString())); + break; + case "Tfluorescence": // 荧光值 + device.setTfluorescence(Double.parseDouble(value.toString())); + break; } - break; - case "Tcorrect": // 设备校准状态 - try { - int tcorrect = Integer.parseInt(value.toString()); - if (tcorrect == 1) { - warnCode &= ~0x0001; // 清除未校准标记 - } - } catch (NumberFormatException e) { - log.warn("无法解析校准状态: {}", value); - } - break; - case "ICCID": // 物联网卡号 - device.setIccId(value.toString()); - break; - case "Treference": // 参比值 - device.setTreference(Double.parseDouble(value.toString())); - break; - case "Tfluorescence": // 荧光值 - device.setTfluorescence(Double.parseDouble(value.toString())); - break; + } + } catch (Exception ex) { + log.error("无法从 SDK 对象中获取属性: {}", ex.getMessage(), ex); } } } } + } catch (Exception ex) { + log.error("无法从 SDK 对象中获取属性列表: {}", ex.getMessage(), ex); } } } diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/domain/bo/AddDeviceControllerBo.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/domain/bo/AddDeviceControllerBo.java new file mode 100644 index 0000000..242125f --- /dev/null +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/domain/bo/AddDeviceControllerBo.java @@ -0,0 +1,70 @@ +package com.intc.iot.domain.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 添加测控一体机业务对象 + * + * @author intc + */ +@Data +@Schema(description = "添加测控一体机请求对象") +public class AddDeviceControllerBo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 设备编号/序列号 + */ + @Schema(description = "设备编号/序列号") + @NotBlank(message = "设备编号不能为空") + private String serialNum; + + /** + * 塘口ID + */ + @Schema(description = "塘口ID") + private Long pondId; + + /** + * 盐度补偿值 + */ + @Schema(description = "盐度补偿值") + @NotNull(message = "盐度补偿值不能为空") + private Double salinityCompensation; + + /** + * 溶解氧报警下限 + */ + @Schema(description = "溶解氧报警下限") + @NotNull(message = "溶解氧报警下限不能为空") + private Double oxyWarnLower; + + /** + * 输入电压类型(1-单相220V, 2-单相380V, 3-三相380V, 4-三相380V四线) + */ + @Schema(description = "输入电压类型") + @NotNull(message = "输入电压类型不能为空") + private Integer inputVoltage; + + /** + * 是否使用溶解氧功能 + */ + @Schema(description = "是否使用溶解氧功能") + @NotNull(message = "是否使用溶解氧功能不能为空") + private Boolean isOxygenUsed; + + /** + * 开关放置塘口分区ID列表(0表示设备自身,1-4表示开关1-4) + */ + @Schema(description = "开关放置塘口分区ID列表") + private List listPutPondPartId; +} diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/IotDeviceServiceImpl.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/IotDeviceServiceImpl.java index ae76d86..1726e76 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/IotDeviceServiceImpl.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/IotDeviceServiceImpl.java @@ -132,6 +132,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { Map result = new HashMap<>(); result.put("success", response.getSuccess()); result.put("data", response.getData()); + result.put("errorMessage", response.getErrorMessage()); + result.put("code", response.getCode()); + + if (!response.getSuccess()) { + log.error("设置设备属性失败,Code: {}, ErrorMessage: {}", response.getCode(), response.getErrorMessage()); + } + return result; } diff --git a/intc-modules/intc-iot/src/main/resources/application.yml b/intc-modules/intc-iot/src/main/resources/application.yml deleted file mode 100644 index c93db1a..0000000 --- a/intc-modules/intc-iot/src/main/resources/application.yml +++ /dev/null @@ -1,99 +0,0 @@ -# 阿里云生活物联网平台(飞燕平台)配置 -# 使用说明: -# 1. 替换下面的占位符为实际值 -# 2. AccessKey/Secret 建议使用环境变量或配置中心管理 -# 3. 推荐使用 AMQP 服务端订阅接收所有设备数据 -aliyun: - living-iot: - # ========== 基础配置 ========== - # 阿里云 AccessKey ID(必填)- 从阿里云控制台获取 - # 安全提示:请通过环境变量配置,不要在代码中暴露真实密钥 - access-key-id: LTAI5tRnPowmTLjH181nSbsR - # 阿里云 AccessKey Secret(必填)- 从阿里云控制台获取 - # 安全提示:请通过环境变量配置,不要在代码中暴露真实密钥 - access-key-secret: Vh2LoAM1t3XuMUVy2wTWSACJ97kOUW - # 地域节点(必填,如:cn-shanghai) - region-id: cn-shanghai - # 飞燕平台项目ID(Project ID,必填) - project-id: a123nMibvh0q4UnU - # App Key(必填) - app-key: 334224397 - # App Secret(必填) - app-secret: 70de3018ec39423e9ca1e1b6a6a84ad6 - - # ========== AMQP 服务端订阅配置(推荐) ========== - # 说明:AMQP 服务端订阅用于接收所有设备的数据,无需为每个设备配置 MQTT - # 配置步骤: - # 1. 在阿里云IoT控制台创建消费组 - # 路径:IoT控制台 -> 规则引擎 -> 服务端订阅 -> 消费组管理 -> 创建消费组 - # 2. 获取 AMQP 接入点信息 - # 路径:IoT控制台 -> 实例详情 -> 开发配置 -> 服务端订阅 -> 查看 AMQP 接入点 - # 3. 使用工具生成认证信息: - # username: {accessKeyId}|authMode=aksign| - # password: 使用 HMAC-SHA1 签名生成,可使用阿里云提供的在线工具 - amqp: - # 是否启用 AMQP 订阅 - enabled: true - # AMQP 接入点地址(格式:${uid}.iot-amqp.${region}.aliyuncs.com) - # 示例:1234567890.iot-amqp.cn-shanghai.aliyuncs.com - host: 1572610294777992.iot-amqp.cn-shanghai.aliyuncs.com - # AMQP 端口(默认 5672) - port: 5672 - # 虚拟主机(默认为 AccessKey 的前 8 位) - virtual-host: - # 用户名(格式:${accessKeyId}|authMode=aksign|) - username: - # 密码(使用 HMAC-SHA1 签名生成) - password: - # 消费组 ID(在阿里云 IoT 控制台创建) - consumer-group-id: HPaJu3YgmDnUAP43Z7xd000100 - # 连接超时时间(毫秒) - connection-timeout: 30000 - # 自动重连 - auto-reconnect: true - # 预取数量(每次批量拉取的消息数) - prefetch-count: 50 - - # ========== MQTT 配置(可选,用于设备直连) ========== - # 说明:MQTT 适用于单个设备直连,不推荐用于大量设备场景 - # 如需启用,请确保以下配置正确 - # 注:大量设备请使用 AMQP 服务端订阅 - mqtt: - # MQTT Broker 地址(格式:tcp://{ProductKey}.iot-as-mqtt.{RegionId}.aliyuncs.com:1883) - # 示例:tcp://a1Xj9dagTIx.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883 - broker-url: - # 客户端ID - client-id: - # 用户名(格式:{DeviceName}&{ProductKey}) - username: - # 密码(使用 AliyunIotSignUtil.generatePassword() 方法生成) - password: - # 连接超时时间(秒) - connection-timeout: 30 - # 保活时间(秒) - keep-alive-interval: 60 - # 自动重连 - auto-reconnect: true - # 清除会话 - clean-session: true - -# ========== 配置说明 ========== -# -# 1. AMQP 服务端订阅配置步骤: -# a. 登录阿里云 IoT 控制台 -# b. 进入实例管理 -> 选择你的实例 -> 规则引擎 -> 服务端订阅 -# c. 创建消费组(记住消费组 ID) -# d. 查看 AMQP 接入点信息,获取 host 和 virtualHost -# e. 生成认证信息: -# username: {accessKeyId}|authMode=aksign| -# password: 使用 HMAC-SHA1("{accessKeySecret}", "{consumerGroupId}") 计算 -# 可使用阿里云提供的在线签名工具 -# -# 2. AMQP vs MQTT 选择: -# - AMQP:推荐,适用于服务端接收所有设备数据,无需为每个设备配置 -# - MQTT:适用于单个设备直连,需要设备三元组(ProductKey/DeviceName/DeviceSecret) -# -# 3. 关键信息获取路径: -# - AMQP 接入点:IoT控制台 -> 实例详情 -> 开发配置 -> 服务端订阅 -# - 消费组 ID:IoT控制台 -> 规则引擎 -> 服务端订阅 -> 消费组管理 -# - AccessKey:阿里云控制台 -> AccessKey 管理