diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/opc/OpcUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/opc/OpcUtils.java index bd6146d6d0..a9e6d816d3 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/opc/OpcUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/opc/OpcUtils.java @@ -1,8 +1,11 @@ package cn.iocoder.yudao.framework.common.util.opc; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; +import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig; +import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder; import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider; import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider; +import org.eclipse.milo.opcua.stack.client.DiscoveryClient; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; @@ -10,7 +13,11 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; +import java.net.InetAddress; +import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Predicate; /** @@ -37,9 +44,6 @@ public class OpcUtils { return connect(url, null, null, timeoutSeconds); } - /** - * 连接OPC UA服务器(支持匿名和用户名密码认证) - */ public static boolean connect(String url, String username, String password, int timeoutSeconds) { if (isConnected) { System.out.println(LOG_PREFIX + "客户端已连接,无需重复连接"); @@ -51,24 +55,105 @@ public class OpcUtils { try { System.out.println(LOG_PREFIX + "正在连接到OPC UA服务器: " + url); + // 提取主机和端口 + final String targetHost = extractHostFromUrl(url); + final int targetPort = extractPortFromUrl(url); + final String path = extractPathFromUrl(url); + + System.out.println(LOG_PREFIX + "目标主机: " + targetHost + ", 端口: " + targetPort + ", 路径: " + path); + + // 将主机名解析为IP地址 + final String ipAddress = resolveToIpAddress(targetHost); + System.out.println(LOG_PREFIX + "解析为IP地址: " + ipAddress); + if (username != null && password != null && !username.isEmpty()) { System.out.println(LOG_PREFIX + "使用用户名密码认证: " + username); - Predicate endpointPredicate = e -> - "http://opcfoundation.org/UA/SecurityPolicy#None".equals(e.getSecurityPolicyUri()); - - client = OpcUaClient.create(url, endpoints -> - endpoints.stream() - .filter(endpointPredicate) - .findFirst(), - configBuilder -> configBuilder - .setIdentityProvider(new UsernameProvider(username, password)) - .setRequestTimeout(UInteger.valueOf(timeoutSeconds * 1000L)) - .build() - ); + // 用户名密码认证 + client = OpcUaClient.create(url, endpoints -> { + if (endpoints == null || endpoints.isEmpty()) { + System.err.println(LOG_PREFIX + "服务器未返回任何端点"); + return Optional.empty(); + } + + System.out.println(LOG_PREFIX + "发现端点数量: " + endpoints.size()); + + // 查找无安全策略的端点 + for (EndpointDescription endpoint : endpoints) { + String endpointUrl = endpoint.getEndpointUrl(); + System.out.println(LOG_PREFIX + "检查端点: " + endpointUrl + + " | 安全策略: " + endpoint.getSecurityPolicyUri()); + + if ("http://opcfoundation.org/UA/SecurityPolicy#None".equals(endpoint.getSecurityPolicyUri())) { + // 修正端点URL,强制使用IP地址 + String correctedUrl = forceIpAddressEndpoint(endpointUrl, ipAddress, targetPort, path); + System.out.println(LOG_PREFIX + "强制使用IP地址: " + endpointUrl + " -> " + correctedUrl); + + return Optional.of(new EndpointDescription( + correctedUrl, + endpoint.getServer(), + endpoint.getServerCertificate(), + endpoint.getSecurityMode(), + endpoint.getSecurityPolicyUri(), + endpoint.getUserIdentityTokens(), + endpoint.getTransportProfileUri(), + endpoint.getSecurityLevel() + )); + } + } + + System.err.println(LOG_PREFIX + "未找到无安全策略的端点"); + return Optional.empty(); + }, configBuilder -> configBuilder + .setIdentityProvider(new UsernameProvider(username, password)) + .setRequestTimeout(UInteger.valueOf(timeoutSeconds * 1000L)) + .build()); } else { System.out.println(LOG_PREFIX + "使用匿名认证"); - client = OpcUaClient.create(url); + + // 对于匿名认证,手动发现端点并修正 + List endpoints = DiscoveryClient.getEndpoints(url).get(timeoutSeconds, TimeUnit.SECONDS); + + if (endpoints == null || endpoints.isEmpty()) { + System.err.println(LOG_PREFIX + "服务器未返回任何端点"); + return false; + } + + // 查找无安全策略的端点 + Optional selectedEndpoint = Optional.empty(); + for (EndpointDescription endpoint : endpoints) { + if ("http://opcfoundation.org/UA/SecurityPolicy#None".equals(endpoint.getSecurityPolicyUri())) { + selectedEndpoint = Optional.of(endpoint); + break; + } + } + + if (!selectedEndpoint.isPresent()) { + System.err.println(LOG_PREFIX + "未找到无安全策略的端点"); + return false; + } + + EndpointDescription endpoint = selectedEndpoint.get(); + String correctedUrl = forceIpAddressEndpoint(endpoint.getEndpointUrl(), ipAddress, targetPort, path); + System.out.println(LOG_PREFIX + "强制使用IP地址: " + endpoint.getEndpointUrl() + " -> " + correctedUrl); + + EndpointDescription correctedEndpoint = new EndpointDescription( + correctedUrl, + endpoint.getServer(), + endpoint.getServerCertificate(), + endpoint.getSecurityMode(), + endpoint.getSecurityPolicyUri(), + endpoint.getUserIdentityTokens(), + endpoint.getTransportProfileUri(), + endpoint.getSecurityLevel() + ); + + OpcUaClientConfigBuilder configBuilder = OpcUaClientConfig.builder() + .setEndpoint(correctedEndpoint) + .setIdentityProvider(new AnonymousProvider()) + .setRequestTimeout(UInteger.valueOf(timeoutSeconds * 1000L)); + + client = OpcUaClient.create(configBuilder.build()); } client.connect().get(timeoutSeconds, TimeUnit.SECONDS); @@ -84,10 +169,321 @@ public class OpcUtils { } catch (Exception e) { System.err.println(LOG_PREFIX + "连接失败: " + e.getMessage()); + e.printStackTrace(); return false; } } + /** + * 将主机名解析为IP地址 + */ + private static String resolveToIpAddress(String host) { + try { + // 如果已经是IP地址,直接返回 + if (host.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")) { + return host; + } + + // 尝试解析主机名 + InetAddress address = InetAddress.getByName(host); + return address.getHostAddress(); + + } catch (Exception e) { + System.err.println(LOG_PREFIX + "解析主机名失败: " + host + ", 错误: " + e.getMessage()); + return host; // 返回原始值 + } + } + + /** + * 强制使用IP地址构建端点URL + */ + private static String forceIpAddressEndpoint(String endpointUrl, String ipAddress, int defaultPort, String defaultPath) { + try { + // 提取端口和路径 + int port = extractPortFromUrl(endpointUrl); + if (port <= 0) { + port = defaultPort; + } + + String path = extractPathFromUrl(endpointUrl); + if (path.isEmpty()) { + path = defaultPath; + } + + return String.format("opc.tcp://%s:%d%s", ipAddress, port, path); + + } catch (Exception e) { + System.err.println(LOG_PREFIX + "构建IP端点URL失败: " + e.getMessage()); + return endpointUrl; + } + } + + + + + + /** + * 从URL中提取主机地址 + */ + private static String extractHostFromUrl(String url) { + try { + // 格式: opc.tcp://host:port/path + if (url == null || url.trim().isEmpty()) { + return "localhost"; + } + + String withoutProtocol = url.replace("opc.tcp://", ""); + // 处理IPv6地址 + if (withoutProtocol.startsWith("[")) { + // IPv6地址格式: [2001:db8::1]:4840/path + int closeBracket = withoutProtocol.indexOf("]"); + if (closeBracket > 0) { + String host = withoutProtocol.substring(1, closeBracket); + return host; + } + } + + // 普通IPv4或主机名 + String[] parts = withoutProtocol.split("[:/]"); + if (parts.length > 0 && !parts[0].isEmpty()) { + return parts[0]; + } + } catch (Exception e) { + System.err.println(LOG_PREFIX + "提取主机地址失败: " + e.getMessage()); + } + return "localhost"; + } + + /** + * 从URL中提取端口 + */ + private static int extractPortFromUrl(String url) { + try { + if (url == null || url.trim().isEmpty()) { + return 4840; // 默认OPC UA端口 + } + + String withoutProtocol = url.replace("opc.tcp://", ""); + + // 查找端口号 + int portStart = -1; + int portEnd = -1; + + if (withoutProtocol.startsWith("[")) { + // IPv6地址: [2001:db8::1]:4840/path + int closeBracket = withoutProtocol.indexOf("]"); + if (closeBracket > 0) { + portStart = withoutProtocol.indexOf(":", closeBracket); + } + } else { + // IPv4或主机名: host:4840/path + int firstColon = withoutProtocol.indexOf(":"); + if (firstColon > 0) { + portStart = firstColon; + } + } + + if (portStart > 0) { + portStart++; // 跳过冒号 + portEnd = withoutProtocol.indexOf("/", portStart); + if (portEnd < 0) { + portEnd = withoutProtocol.length(); + } + + String portStr = withoutProtocol.substring(portStart, portEnd); + if (portStr.matches("\\d+")) { + return Integer.parseInt(portStr); + } + } + + } catch (Exception e) { + System.err.println(LOG_PREFIX + "提取端口失败: " + e.getMessage()); + } + return 4840; // 默认OPC UA端口 + } + + /** + * 从URL中提取路径 + */ + private static String extractPathFromUrl(String url) { + try { + if (url == null || url.trim().isEmpty()) { + return ""; + } + + String withoutProtocol = url.replace("opc.tcp://", ""); + + // 查找路径开始位置 + int pathStart = -1; + + if (withoutProtocol.startsWith("[")) { + // IPv6地址: [2001:db8::1]:4840/path + int closeBracket = withoutProtocol.indexOf("]"); + if (closeBracket > 0) { + // 检查是否有端口 + int colonAfterBracket = withoutProtocol.indexOf(":", closeBracket); + if (colonAfterBracket > 0) { + // 有端口: [2001:db8::1]:4840/path + int slashAfterPort = withoutProtocol.indexOf("/", colonAfterBracket); + if (slashAfterPort > 0) { + return withoutProtocol.substring(slashAfterPort); + } + } else { + // 无端口: [2001:db8::1]/path + int slashAfterBracket = withoutProtocol.indexOf("/", closeBracket); + if (slashAfterBracket > 0) { + return withoutProtocol.substring(slashAfterBracket); + } + } + } + } else { + // IPv4或主机名 + // 先找主机名结束位置 + int hostEnd = withoutProtocol.indexOf(":"); + if (hostEnd < 0) { + // 无端口: host/path + hostEnd = withoutProtocol.indexOf("/"); + if (hostEnd > 0) { + return withoutProtocol.substring(hostEnd); + } + } else { + // 有端口: host:port/path + int portEnd = withoutProtocol.indexOf("/", hostEnd); + if (portEnd > 0) { + return withoutProtocol.substring(portEnd); + } + } + } + + } catch (Exception e) { + System.err.println(LOG_PREFIX + "提取路径失败: " + e.getMessage()); + } + return ""; + } + + /** + * 从URL中提取协议 + */ + private static String extractProtocolFromUrl(String url) { + try { + if (url == null || url.trim().isEmpty()) { + return "opc.tcp"; + } + + int protocolEnd = url.indexOf("://"); + if (protocolEnd > 0) { + return url.substring(0, protocolEnd); + } + } catch (Exception e) { + System.err.println(LOG_PREFIX + "提取协议失败: " + e.getMessage()); + } + return "opc.tcp"; + } + + /** + * 构建完整的URL + */ + private static String buildUrl(String protocol, String host, int port, String path) { + StringBuilder url = new StringBuilder(); + + if (protocol == null || protocol.isEmpty()) { + protocol = "opc.tcp"; + } + url.append(protocol).append("://"); + + // 处理IPv6地址 + if (host.contains(":")) { + url.append("[").append(host).append("]"); + } else { + url.append(host); + } + + if (port > 0) { + url.append(":").append(port); + } + + if (path != null && !path.isEmpty()) { + if (!path.startsWith("/")) { + url.append("/"); + } + url.append(path); + } + + return url.toString(); + } + + /** + * 修正端点URL,强制使用指定的主机和端口 + */ + private static String correctEndpointUrl(String endpointUrl, String targetHost, int targetPort) { + if (endpointUrl == null || endpointUrl.trim().isEmpty()) { + return endpointUrl; + } + + try { + // 提取原URL的各个部分 + String protocol = extractProtocolFromUrl(endpointUrl); + String originalHost = extractHostFromUrl(endpointUrl); + int originalPort = extractPortFromUrl(endpointUrl); + String path = extractPathFromUrl(endpointUrl); + + // 决定使用哪个主机 + String useHost = targetHost; + int usePort = (targetPort > 0) ? targetPort : originalPort; + + // 如果原端口无效,使用默认端口 + if (usePort <= 0) { + usePort = 4840; + } + + // 构建修正后的URL + return buildUrl(protocol, useHost, usePort, path); + + } catch (Exception e) { + System.err.println(LOG_PREFIX + "修正端点URL失败: " + e.getMessage()); + return endpointUrl; + } + } + + + + + + /** + * 检查是否为本地地址 + */ + private static boolean isLocalAddress(String url) { + if (url == null) return false; + + return url.contains("127.0.0.1") || + url.contains("localhost") || + url.contains("127.0.1.1") || + url.contains("[::1]") || + url.startsWith("opc.tcp://localhost") || + url.startsWith("opc.tcp://127."); + } + + /** + * 修正端点URL中的本地地址 + */ + private static String correctEndpointUrl(String endpointUrl, String targetHost) { + if (endpointUrl == null) return endpointUrl; + + // 替换各种本地地址表示形式 + String corrected = endpointUrl + .replace("127.0.0.1", targetHost) + .replace("localhost", targetHost) + .replace("127.0.1.1", targetHost) + .replace("[::1]", targetHost); + + // 如果还有localhost:端口的形式 + if (corrected.contains("localhost:")) { + corrected = corrected.replace("localhost", targetHost); + } + + return corrected; + } + /** * 断开连接 */ diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java index 14e376905e..515f463900 100644 --- a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java @@ -42,15 +42,15 @@ public class SchedulerManager { Integer retryCount, Integer retryInterval) throws SchedulerException { validateScheduler(); - String jobKeyName = jobHandlerName+ "_"+ jobId; - JobKey jobKey = JobKey.jobKey(jobKeyName, DEFAULT_GROUP); +// String jobKeyName = jobHandlerName+ "_"+ jobId; +// JobKey jobKey = JobKey.jobKey(jobKeyName, DEFAULT_GROUP); // 创建 JobDetail 对象 JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class) .usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId) .usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName) - .withIdentity(jobKey).build(); + .withIdentity(jobHandlerName).build(); // 创建 Trigger 对象 - Trigger trigger = this.buildTrigger(jobKey, jobHandlerParam, cronExpression, retryCount, retryInterval); + Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); // 新增 Job 调度 scheduler.scheduleJob(jobDetail, trigger); } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java index 87a2158466..73938e40f4 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java @@ -48,9 +48,9 @@ public class JobServiceImpl implements JobService { public Long createJob(JobSaveReqVO createReqVO) throws SchedulerException { validateCronExpression(createReqVO.getCronExpression()); // 1.1 校验唯一性 -// if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { -// throw exception(JOB_HANDLER_EXISTS); -// } + if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { + throw exception(JOB_HANDLER_EXISTS); + } // 1.2 校验 JobHandler 是否存在 validateJobHandlerExists(createReqVO.getHandlerName()); @@ -64,7 +64,7 @@ public class JobServiceImpl implements JobService { schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), createReqVO.getRetryCount(), createReqVO.getRetryInterval()); // 3.2 更新 JobDO - JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.STOP.getStatus()).build(); + JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build(); jobMapper.updateById(updateObj); return job.getId(); } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 9635d6bb73..c358cd025f 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -36,6 +36,7 @@ public interface ErrorCodeConstants { //======================================数据采集相关================================================= ErrorCode DEVICE_MODEL_NOT_EXISTS = new ErrorCode(1_003_000_000, "采集设备模型不存在"); ErrorCode DEVICE_ID_MODEL_NOT_EXISTS = new ErrorCode(1_003_000_000, "该设备模型ID不能为空"); + ErrorCode POINT_ID_MODEL_NOT_EXISTS = new ErrorCode(1_003_000_000, "该点位参数ID不能为空"); ErrorCode DEVICE_MODEL_ATTRIBUTE_NOT_EXISTS = new ErrorCode(1_003_000_000, "采集设备模型点位不存在"); ErrorCode DEVICE_ATTRIBUTE_NOT_EXISTS = new ErrorCode(1_003_000_000, "设备属性不存在"); @@ -49,6 +50,7 @@ public interface ErrorCodeConstants { ErrorCode OPC_CLOSE_CONNECT_FAILURE= new ErrorCode(1_003_000_000, "OPC断开连接失败"); ErrorCode OPC_PARAMETER_DOES_NOT_EXIST= new ErrorCode(1_003_000_000, "连接关闭参数不存在"); ErrorCode CREATE_TDENGINE_FAILURE= new ErrorCode(1_003_000_000, "创建Tdengine子表失败"); + ErrorCode RECIPE_NOT_EXISTS = new ErrorCode(1_003_000_000, "配方主不存在"); ErrorCode RECIPE_POINT_NOT_EXISTS = new ErrorCode(1_003_000_000, "配方点位配置表(配方与设备点位关联)不存在"); ErrorCode RECIPE_TYPE_NOT_EXISTS = new ErrorCode(1_003_000_000, "配方类型表(基础字典)不存在"); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java index e91aab8955..3f3fca4045 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/DeviceController.java @@ -5,22 +5,16 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.common.util.opc.OpcUtils; import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO; -import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; import cn.iocoder.yudao.module.infra.service.job.JobService; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.devicemodelattribute.vo.DeviceModelAttributePageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodelattribute.DeviceModelAttributeDO; import cn.iocoder.yudao.module.iot.dal.devicecontactmodel.DeviceContactModelDO; import cn.iocoder.yudao.module.iot.service.device.DeviceService; import cn.iocoder.yudao.module.iot.service.device.TDengineService; -import com.alibaba.fastjson.JSON; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.core.JsonProcessingException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -194,7 +188,6 @@ public class DeviceController { } - // ==================== 子表(设备属性) ==================== @GetMapping("/device-attribute/page") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicemodelattribute/DeviceModelAttributeController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicemodelattribute/DeviceModelAttributeController.java index fd27c9cf42..a7b0bd6057 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicemodelattribute/DeviceModelAttributeController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/devicemodelattribute/DeviceModelAttributeController.java @@ -98,4 +98,16 @@ public class DeviceModelAttributeController { BeanUtils.toBean(list, DeviceModelAttributeRespVO.class)); } + + @GetMapping("/operationAnalysisDetails") + @Operation(summary = "设备运行参数分析-详情") +// @Parameter(name = "deviceId,modelId", description = "设备id,点位参数id", required = true) + @PreAuthorize("@ss.hasPermission('iot:device-model-attribute:query')") + public CommonResult>> operationAnalysisDetails(@RequestParam("deviceId") Long deviceId, + @RequestParam("modelId") Long modelId, + @RequestParam(name = "collectionStartTime", required = false ) String collectionStartTime, + @RequestParam(name = "collectionEndTime", required = false ) String collectionEndTime) { + List> deviceModelAttribute = deviceModelAttributeService.operationAnalysisDetails(deviceId,modelId,collectionStartTime,collectionEndTime); + return success(deviceModelAttribute); + } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java index 6769e217cc..17d931fbae 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceService.java @@ -4,18 +4,12 @@ import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DevicePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.LineDeviceRequestVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.LineDeviceRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.devicemodelattribute.vo.DeviceModelAttributePageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodelattribute.DeviceModelAttributeDO; import cn.iocoder.yudao.module.iot.dal.devicecontactmodel.DeviceContactModelDO; import com.fasterxml.jackson.core.JsonProcessingException; -import org.springframework.web.bind.annotation.RequestParam; import javax.validation.Valid; import java.util.Collection; @@ -128,4 +122,5 @@ public interface DeviceService { Map>> singleDevice(Long deviceId) throws JsonProcessingException; List> historyRecord(Long deviceId,String collectionStartTime, String collectionEndTime); + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java index c7194e7df3..a0686fd5fa 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java @@ -6,12 +6,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.opc.OpcUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DevicePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.LineDeviceRequestVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.LineDeviceRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.devicemodelattribute.vo.DeviceModelAttributePageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.mqttdatarecord.vo.MqttDataRecordPageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.deviceattributetype.DeviceAttributeTypeDO; @@ -25,22 +21,16 @@ import cn.iocoder.yudao.module.iot.dal.mysql.devicecontactmodel.DeviceContactMod import cn.iocoder.yudao.module.iot.dal.mysql.devicemodel.DeviceModelMapper; import cn.iocoder.yudao.module.iot.dal.mysql.devicemodelattribute.DeviceModelAttributeMapper; import cn.iocoder.yudao.module.iot.dal.mysql.mqttdatarecord.MqttDataRecordMapper; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO; -import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper; import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceAttributeMapper; + import com.alibaba.fastjson.JSON; -import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; @@ -52,7 +42,6 @@ import javax.annotation.Resource; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -95,6 +84,7 @@ public class DeviceServiceImpl implements DeviceService { @Resource private DeviceAttributeTypeMapper deviceAttributeTypeMapper; + @Override @Transactional(rollbackFor = Exception.class) public DeviceDO createDevice(DeviceSaveReqVO createReqVO) { @@ -220,7 +210,8 @@ public class DeviceServiceImpl implements DeviceService { public PageResult getDeviceAttributePage(PageParam pageReqVO, DeviceContactModelPageReqVO deviceModelAttributePageReqVO) { // 参数校验 if (deviceModelAttributePageReqVO.getDeviceId() == null) { - throw new IllegalArgumentException("关联设备模型ID不能为空"); + throw exception(DEVICE_ID_MODEL_NOT_EXISTS); + } // 查询设备信息,只查询一次 @@ -378,21 +369,19 @@ public class DeviceServiceImpl implements DeviceService { deviceModelAttributeLambdaQueryWrapper.eq(DeviceContactModelDO::getDeviceId,createReqVO.getId()); List deviceContactModelDOS = deviceContactModelMapper.selectList(deviceModelAttributeLambdaQueryWrapper); - //连接后查询5次保存到数据库 - for (int i = 0; i < 3; i++) { - if (deviceContactModelDOS != null && deviceContactModelDOS.size() > 0){ - for (DeviceContactModelDO deviceContactModelDO : deviceContactModelDOS) { - Object addressValue = OpcUtils.readValue(deviceContactModelDO.getAddress() != null ? deviceContactModelDO.getAddress() : ""); - deviceContactModelDO.setAddressValue(addressValue); - } - String json = JSON.toJSONString(deviceContactModelDOS); - tdengineService.insertDeviceData(createReqVO.getId(),json); + if (deviceContactModelDOS != null && deviceContactModelDOS.size() > 0){ + for (DeviceContactModelDO deviceContactModelDO : deviceContactModelDOS) { + Object addressValue = OpcUtils.readValue(deviceContactModelDO.getAddress() != null ? deviceContactModelDO.getAddress() : ""); + deviceContactModelDO.setAddressValue(addressValue); } - + String json = JSON.toJSONString(deviceContactModelDOS); + tdengineService.insertDeviceData(createReqVO.getId(),json); } + + }else { throw exception(OPC_CONNECT_FAILURE_DOES_NOT_EXIST); } @@ -632,6 +621,7 @@ public class DeviceServiceImpl implements DeviceService { } + private void validateDeviceAttributeExists(Long id) { if (deviceAttributeMapper.selectById(id) == null) { throw exception(DEVICE_ATTRIBUTE_NOT_EXISTS); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeService.java index 59bf320a49..d30c4d1eed 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeService.java @@ -52,4 +52,5 @@ public interface DeviceModelAttributeService { */ PageResult getDeviceModelAttributePage(DeviceModelAttributePageReqVO pageReqVO); + List> operationAnalysisDetails(Long deviceId, Long modelId,String collectionStartTime, String collectionEndTime); } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java index 7a1d794afd..207f60c22f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/devicemodelattribute/DeviceModelAttributeServiceImpl.java @@ -1,13 +1,23 @@ package cn.iocoder.yudao.module.iot.service.devicemodelattribute; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; import cn.iocoder.yudao.module.iot.dal.dataobject.deviceattributetype.DeviceAttributeTypeDO; import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodel.DeviceModelDO; +import cn.iocoder.yudao.module.iot.dal.mysql.deviceattributetype.DeviceAttributeTypeMapper; +import cn.iocoder.yudao.module.iot.service.device.TDengineService; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; import java.util.*; +import java.util.stream.Collectors; + import cn.iocoder.yudao.module.iot.controller.admin.devicemodelattribute.vo.*; import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodelattribute.DeviceModelAttributeDO; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -31,6 +41,12 @@ public class DeviceModelAttributeServiceImpl implements DeviceModelAttributeServ @Resource private DeviceModelAttributeMapper deviceModelAttributeMapper; + @Resource + private TDengineService tdengineService; + + @Resource + private DeviceAttributeTypeMapper deviceAttributeTypeMapper; + @Override public Long createDeviceModelAttribute(DeviceModelAttributeSaveReqVO createReqVO) { // 插入 @@ -76,5 +92,90 @@ public class DeviceModelAttributeServiceImpl implements DeviceModelAttributeServ return deviceModelAttributeMapper.selectPage(pageReqVO); } + @Override + public List> operationAnalysisDetails(Long deviceId, Long modelId,String collectionStartTime, String collectionEndTime) { + if(deviceId == null){ + throw exception(DEVICE_ID_DOES_NOT_EXIST); + } + if(modelId == null){ + throw exception(POINT_ID_MODEL_NOT_EXISTS); + } + + List> resultList = new ArrayList<>(); + + try { + // 1. 获取设备数据列表 + List> deviceDataList = tdengineService.getstDeviceDataOrderByTimeDesc(deviceId,collectionStartTime,collectionEndTime); + + // 2. 获取属性类型映射 + Map idToNameMap = deviceAttributeTypeMapper.selectList() + .stream() + .collect(Collectors.toMap( + DeviceAttributeTypeDO::getId, + DeviceAttributeTypeDO::getName + )); + + // 3. 遍历每个时间点的数据 + for (Map deviceData : deviceDataList) { + String queryDataJson = (String) deviceData.get("queryData"); + Timestamp timestamp = (Timestamp) deviceData.get("timestamp"); + SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); + String formattedTime = sdf.format(timestamp); // 例如 "26-01-06 14:30:45" + if (StringUtils.isNotBlank(queryDataJson) && timestamp != null) { + List> dataList = new ObjectMapper().readValue( + queryDataJson, + new TypeReference>>() {} + ); + + // 按属性类型分组 + Map>> groupedData = new LinkedHashMap<>(); + + for (Map data : dataList) { + if (((Integer) data.get("id")).longValue() == modelId){ + + + String attributeTypeName = "其他"; + String typeStr = (String) data.get("attributeType"); + if (typeStr != null) { + try { + attributeTypeName = typeStr; + } catch (Exception e) { + attributeTypeName = "未知"; + } + } + + Map simplifiedData = new HashMap<>(); + simplifiedData.put("addressValue", data.get("addressValue")); + simplifiedData.put("attributeName", data.get("attributeName")); + + groupedData + .computeIfAbsent(attributeTypeName, k -> new ArrayList<>()) + .add(simplifiedData); + } + } + + // 创建当前时间点的Map + Map timePointData = new LinkedHashMap<>(); + + // 添加属性分组 + for (Map.Entry>> entry : groupedData.entrySet()) { + timePointData.put(entry.getKey(), entry.getValue()); + } + + // 添加收集时间 + timePointData.put("collectTime", formattedTime); + + resultList.add(timePointData); + } + } + + } catch (Exception e) { + throw new RuntimeException("处理设备数据时发生异常", e); + } + + + return resultList; + } + } \ No newline at end of file diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/OrganizationController.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/OrganizationController.java index e4c3b32ad5..5e977f66fc 100644 --- a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/OrganizationController.java +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/OrganizationController.java @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.LineAnalysisTreeDTO; import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationListReqVO; import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationRespVO; import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationSaveReqVO; @@ -12,6 +13,7 @@ import cn.iocoder.yudao.module.mes.service.organization.OrganizationService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -92,4 +94,14 @@ public class OrganizationController { organizationService.buildVOList(list)); } + + @GetMapping("/deviceParameterAnalysis") + @Operation(summary = "设备运行参数分析") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult> deviceParameterAnalysis(@RequestParam(value = "keyword", required = false) String keyword) { + List list = organizationService.deviceParameterAnalysis(keyword); + return success(list); + } + + } \ No newline at end of file diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/vo/LineAnalysisTreeDTO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/vo/LineAnalysisTreeDTO.java new file mode 100644 index 0000000000..25b89dda3c --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/vo/LineAnalysisTreeDTO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.mes.controller.admin.organization.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "管理后台 - 产线数据运行分析 树形结构") +public class LineAnalysisTreeDTO { + + @Schema(description = "产线节点列表") + private List lines = new ArrayList<>(); + + // 产线节点 + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class LineNode { + private Long id; // 产线ID + private String name; // 产线名称 + private List equipments = new ArrayList<>(); + } + + // 设备节点 + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class EquipmentNode { + private Long id; // 设备ID + private String name; // 设备名称 + private List parameters = new ArrayList<>(); + } + + // 参数节点 + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ParameterNode { + private Long id; // 参数ID + private String name; // 参数名称 (对应attributeName) + private String code; // 参数编码 (对应attributeCode) + private String type; // 参数类型 (对应attributeType) + private String dataType;// 数据类型 + private String unit; // 单位 + private Double ratio; // 倍率 + } +} \ No newline at end of file diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/vo/OrganizationRespVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/vo/OrganizationRespVO.java index e3263b8bd0..6d62fecdff 100644 --- a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/vo/OrganizationRespVO.java +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/organization/vo/OrganizationRespVO.java @@ -12,7 +12,7 @@ import java.time.LocalDateTime; @Schema(description = "管理后台 - 产线工位 Response VO") @Data @ExcelIgnoreUnannotated -public class OrganizationRespVO { + public class OrganizationRespVO { @Schema(description = "组织id", requiredMode = Schema.RequiredMode.REQUIRED, example = "9560") @ExcelProperty("组织id") diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationService.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationService.java index 1b5c895087..26d3fa7f00 100644 --- a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationService.java +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationService.java @@ -2,9 +2,7 @@ package cn.iocoder.yudao.module.mes.service.organization; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO; -import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutItemDO; -import cn.iocoder.yudao.module.mes.controller.admin.feedingrecord.vo.FeedingRecordRespVO; +import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.LineAnalysisTreeDTO; import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationListReqVO; import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationRespVO; import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationSaveReqVO; @@ -81,4 +79,6 @@ public interface OrganizationService { if(ids.isEmpty())return new HashMap<>(); return convertMap(getOrganizationVOList(ids), OrganizationDO::getId); } + + List deviceParameterAnalysis(String keyword); } \ No newline at end of file diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationServiceImpl.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationServiceImpl.java index 9cd5785bd1..275fb05b43 100644 --- a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationServiceImpl.java +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/organization/OrganizationServiceImpl.java @@ -4,18 +4,13 @@ import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO; -import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO; -import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutItemDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; +import cn.iocoder.yudao.module.iot.dal.devicecontactmodel.DeviceContactModelDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper; +import cn.iocoder.yudao.module.iot.dal.mysql.devicecontactmodel.DeviceContactModelMapper; import cn.iocoder.yudao.module.iot.service.device.DeviceService; -import cn.iocoder.yudao.module.mes.controller.admin.feedingrecord.vo.FeedingRecordRespVO; -import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrgClassEnum; -import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationListReqVO; -import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationRespVO; -import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.OrganizationSaveReqVO; +import cn.iocoder.yudao.module.mes.controller.admin.organization.vo.*; import cn.iocoder.yudao.module.mes.controller.admin.orgworker.vo.OrgWorkerPageReqVO; -import cn.iocoder.yudao.module.mes.dal.dataobject.machine.MachineComponentDO; import cn.iocoder.yudao.module.mes.dal.dataobject.organization.OrganizationDO; import cn.iocoder.yudao.module.mes.dal.dataobject.orgworker.OrgWorkerDO; import cn.iocoder.yudao.module.mes.dal.mysql.organization.OrganizationMapper; @@ -24,6 +19,7 @@ import cn.iocoder.yudao.module.mes.dal.redis.no.MesNoRedisDAO; import cn.iocoder.yudao.module.mes.service.machine.MachineComponentService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -31,6 +27,7 @@ import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -55,7 +52,10 @@ public class OrganizationServiceImpl implements OrganizationService { private MesNoRedisDAO noRedisDAO; @Resource private AdminUserApi adminUserApi; - + @Resource + private DeviceContactModelMapper deviceContactModelMapper; + @Resource + private DeviceMapper deviceMapper; @Override public Long createOrganization(OrganizationSaveReqVO createReqVO) { // 校验父组织id的有效性 @@ -255,4 +255,320 @@ public class OrganizationServiceImpl implements OrganizationService { List list = organizationMapper.selectBatchIds(ids); return list; } + + @Override + public List deviceParameterAnalysis(String keyword) { + + //1. 获取产线集合 + OrganizationListReqVO organizationListReqVO = new OrganizationListReqVO(); + List organizationDOS = getOrganizationList(organizationListReqVO); + List organizationRespVOS = buildVOList(organizationDOS); + if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(organizationRespVOS)) { + return Collections.emptyList(); + } + + // 2. 获取所有有machineId的产线 + List machineIds = organizationRespVOS.stream() + .map(OrganizationRespVO::getMachineId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // 3. 根据machineId查询设备 + List allDevices = Collections.emptyList(); + if (!machineIds.isEmpty()) { + LambdaQueryWrapper deviceWrapper = new LambdaQueryWrapper<>(); + deviceWrapper.in(DeviceDO::getId, machineIds) + .eq(DeviceDO::getIsEnable, true); + allDevices = deviceMapper.selectList(deviceWrapper); + } + + // 4. 获取所有参数 + List allParameters = getAllParameters(allDevices); + + + // 5. 构建映射关系 + Map deviceById = allDevices.stream() + .collect(Collectors.toMap(DeviceDO::getId, Function.identity())); + + // 6. 构建设备按产线分组的映射 + Map> devicesByLineId = new HashMap<>(); + for (OrganizationRespVO line : organizationRespVOS) { + if (line.getMachineId() != null) { + DeviceDO device = deviceById.get(line.getMachineId()); + if (device != null) { + devicesByLineId.computeIfAbsent(line.getId(), k -> new ArrayList<>()) + .add(device); + } + } + } + + Map> paramsByDeviceId = allParameters.stream() + .filter(param -> param.getDeviceId() != null) + .collect(Collectors.groupingBy(DeviceContactModelDO::getDeviceId)); + + // 7. 构建树结构 + return buildTreeStructureWithKeyword(organizationRespVOS, devicesByLineId, paramsByDeviceId,keyword); + + } + private List buildTreeStructureWithKeyword(List lines, + Map> devicesByLineId, + Map> paramsByDeviceId, + String keyword) { + + // 统一处理关键词 + boolean hasKeyword = StringUtils.isNotBlank(keyword); + String lowerKeyword = hasKeyword ? keyword.toLowerCase() : ""; + + // 记录匹配情况 + Map lineMatchedMap = new HashMap<>(); + Map deviceMatchedMap = new HashMap<>(); + + // 第一遍:分析匹配情况 + for (OrganizationRespVO line : lines) { + // 产线是否匹配 + boolean lineMatch = !hasKeyword || + (line.getName() != null && line.getName().toLowerCase().contains(lowerKeyword)); + lineMatchedMap.put(line.getId(), lineMatch); + + // 检查该产线的设备 + List lineDevices = devicesByLineId.getOrDefault(line.getId(), new ArrayList<>()); + for (DeviceDO device : lineDevices) { + // 设备是否匹配 + boolean deviceMatch = !hasKeyword || + (device.getDeviceName() != null && + device.getDeviceName().toLowerCase().contains(lowerKeyword)); + deviceMatchedMap.put(device.getId(), deviceMatch); + } + } + + // 第二遍:构建树结构 + return lines.stream().map(line -> { + boolean lineMatch = lineMatchedMap.getOrDefault(line.getId(), false); + + // 关键修改:如果产线匹配,跳过后续的设备过滤逻辑 + if (hasKeyword && !lineMatch) { + // 检查产线下是否有匹配的设备或参数 + boolean hasMatchingDeviceOrParam = checkIfLineHasMatchingDeviceOrParam( + line, devicesByLineId, paramsByDeviceId, lowerKeyword, deviceMatchedMap); + if (!hasMatchingDeviceOrParam) { + return null; + } + } + + // 获取该产线的设备 + List lineDevices = devicesByLineId.getOrDefault(line.getId(), new ArrayList<>()); + + // 构建设备节点 + List equipmentNodes = lineDevices.stream() + .map(device -> { + boolean deviceMatch = deviceMatchedMap.getOrDefault(device.getId(), false); + + // 关键修改:如果产线匹配,保留所有设备 + if (lineMatch) { + // 产线匹配时,保留该设备 + List deviceParams = paramsByDeviceId.getOrDefault(device.getId(), new ArrayList<>()); + + // 产线匹配时,显示所有参数 + List parameterNodes = deviceParams.stream() + .map(this::buildParameterNode) + .collect(Collectors.toList()); + + return LineAnalysisTreeDTO.EquipmentNode.builder() + .id(device.getId()) + .name(device.getDeviceName()) + .parameters(parameterNodes) + .build(); + } + + // 以下为原有逻辑:产线不匹配时的处理 + if (hasKeyword && !deviceMatch) { + boolean hasMatchingParam = checkIfDeviceHasMatchingParam( + device, paramsByDeviceId, lowerKeyword); + if (!hasMatchingParam) { + return null; + } + } + + List deviceParams = paramsByDeviceId.getOrDefault(device.getId(), new ArrayList<>()); + + // 构建参数节点 + List parameterNodes = deviceParams.stream() + .filter(param -> { + if (!hasKeyword) { + return true; // 没有关键词,显示所有 + } + + // 有关键词时: + // 如果设备匹配,显示所有参数 + if (deviceMatch) { + return true; + } + + // 否则,只显示匹配的参数 + return isParameterMatch(param, lowerKeyword); + }) + .map(this::buildParameterNode) + .collect(Collectors.toList()); + + return LineAnalysisTreeDTO.EquipmentNode.builder() + .id(device.getId()) + .name(device.getDeviceName()) + .parameters(parameterNodes) + .build(); + }) + .filter(Objects::nonNull) + .filter(equipmentNode -> !lineMatch || !equipmentNode.getParameters().isEmpty()) // 修改过滤条件 + .collect(Collectors.toList()); + + // 构建设备节点 + return LineAnalysisTreeDTO.LineNode.builder() + .id(line.getId()) + .name(line.getName()) + .equipments(equipmentNodes) + .build(); + }) + .filter(Objects::nonNull) + .filter(lineNode -> !hasKeyword || lineMatchedMap.getOrDefault(lineNode.getId(), false) || !lineNode.getEquipments().isEmpty()) + .collect(Collectors.toList()); + } + + /** + * 检查产线下是否有匹配的设备或参数 + */ + private boolean checkIfLineHasMatchingDeviceOrParam(OrganizationRespVO line, + Map> devicesByLineId, + Map> paramsByDeviceId, + String lowerKeyword, + Map deviceMatchedMap) { + + List lineDevices = devicesByLineId.getOrDefault(line.getId(), new ArrayList<>()); + + for (DeviceDO device : lineDevices) { + // 检查设备是否匹配 + boolean deviceMatch = deviceMatchedMap.getOrDefault(device.getId(), false); + if (deviceMatch) { + return true; + } + + // 检查设备下的参数是否匹配 + List deviceParams = paramsByDeviceId.getOrDefault(device.getId(), new ArrayList<>()); + boolean hasMatchingParam = deviceParams.stream() + .anyMatch(param -> isParameterMatch(param, lowerKeyword)); + if (hasMatchingParam) { + return true; + } + } + + return false; + } + + /** + * 检查设备下是否有匹配的参数 + */ + private boolean checkIfDeviceHasMatchingParam(DeviceDO device, + Map> paramsByDeviceId, + String lowerKeyword) { + + List deviceParams = paramsByDeviceId.getOrDefault(device.getId(), new ArrayList<>()); + return deviceParams.stream() + .anyMatch(param -> isParameterMatch(param, lowerKeyword)); + } + private List buildTreeStructure(List lines, + Map> devicesByLineId, + Map> paramsByDeviceId) { + return lines.stream().map(line -> { + LineAnalysisTreeDTO.LineNode lineNode = LineAnalysisTreeDTO.LineNode.builder() + .id(line.getId()) + .name(line.getName()) + .build(); + + // 获取该产线的设备 + List lineDevices = devicesByLineId.getOrDefault(line.getId(), new ArrayList<>()); + + // 构建设备节点 + List equipmentNodes = lineDevices.stream() + .map(device -> buildEquipmentNode(device, paramsByDeviceId)) + .collect(Collectors.toList()); + + lineNode.setEquipments(equipmentNodes); + return lineNode; + }).collect(Collectors.toList()); + } + + /** + * 判断参数是否匹配关键词 + */ + private boolean isParameterMatch(DeviceContactModelDO param, String lowerKeyword) { + if (StringUtils.isBlank(lowerKeyword)) { + return true; + } + + return (param.getAttributeName() != null && param.getAttributeName().toLowerCase().contains(lowerKeyword)) || + (param.getAttributeCode() != null && param.getAttributeCode().toLowerCase().contains(lowerKeyword)); + } + + + /** + * 构建设备节点 + */ + private LineAnalysisTreeDTO.EquipmentNode buildEquipmentNode(DeviceDO device, + Map> paramsByDeviceId) { + LineAnalysisTreeDTO.EquipmentNode equipmentNode = LineAnalysisTreeDTO.EquipmentNode.builder() + .id(device.getId()) + .name(device.getDeviceName()) + .build(); + + // 获取设备参数 + List deviceParams = + paramsByDeviceId.getOrDefault(device.getId(), new ArrayList<>()); + + // 构建参数节点 + List parameterNodes = deviceParams.stream() + .map(this::buildParameterNode) + .collect(Collectors.toList()); + + equipmentNode.setParameters(parameterNodes); + return equipmentNode; + } + + /** + * 构建参数节点 + */ + private LineAnalysisTreeDTO.ParameterNode buildParameterNode(DeviceContactModelDO param) { + return LineAnalysisTreeDTO.ParameterNode.builder() + .id(param.getId()) + .name(param.getAttributeName()) + .code(param.getAttributeCode()) + .type(param.getAttributeType()) + .dataType(param.getDataType()) + .unit(param.getDataUnit()) + .ratio(param.getRatio()) + .build(); + } + + + private List getAllParameters(List devices) { + + if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(devices)) { + return Collections.emptyList(); + } + + List deviceIds = devices.stream() + .map(DeviceDO::getId) + .collect(Collectors.toList()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(DeviceContactModelDO::getDeviceId, deviceIds) + .orderByAsc(DeviceContactModelDO::getSort); // 按排序字段排序 + + List result = deviceContactModelMapper.selectList(wrapper); + + // 调试日志 + System.out.println("查询条件: 设备IDs=" + deviceIds); + System.out.println("查询结果数量: " + result.size()); + + return result; + } + + } \ No newline at end of file