From 18c51d40be82435289ba3be6bd5f8e15fdf786f7 Mon Sep 17 00:00:00 2001
From: cl <1442464845@qq.com>
Date: 星期六, 04 四月 2026 00:26:56 +0800
Subject: [PATCH] mqtt
---
src/main/java/com/zy/common/config/ControllerResAdvice.java | 96 +++-
.local/iot-mqtt/asrs-iot-client-mqttsa21wi8dwvkjf1datsiotcn-north-1amazonawscomcn8883/.lck | 0
src/main/java/com/zy/iot/service/impl/IotDbConfigServiceImpl.java | 132 ++++++
src/main/resources/iot-certs/AmazonRootCA1.pem | 20 +
src/main/java/com/zy/integration/iot/handler/impl/IotInboundMessageHandlerImpl.java | 42 +
src/test/java/com/zy/integration/iot/IotSupportTest.java | 15
src/main/resources/iot-certs/device-certificate.pem.crt | 20 +
src/main/resources/sql/20260403_iot_mqtt_sys_config.sql | 9
src/main/java/com/zy/iot/config/IotProperties.java | 61 ++
src/main/java/com/zy/integration/iot/paho/MqttsNetworkModuleFactory.java | 50 ++
src/main/resources/iot-certs/device-public.pem.key | 9
src/main/java/com/zy/integration/iot/paho/MqttNetworkModuleFactory.java | 50 ++
src/main/java/com/zy/integration/iot/task/IotPendingPublishScheduler.java | 6
src/main/webapp/views/iotMqttOutbound/iotMqttOutbound.html | 112 +++++
src/main/resources/iot-certs/device-private.pem.key | 27 +
src/main/java/com/zy/iot/controller/IotMqttOutboundController.java | 89 ++++
src/main/java/com/zy/iot/service/IotDbConfigService.java | 17
src/main/java/com/zy/iot/constant/IotSysConfigCodes.java | 16
src/main/java/com/zy/integration/iot/client/IotMqttClient.java | 3
src/main/resources/application.yml | 32 +
src/main/java/com/zy/iot/controller/IotMqttAdminController.java | 97 +++++
src/main/resources/META-INF/services/org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory | 2
src/main/webapp/static/js/iotMqttOutbound/iotMqttOutbound.js | 233 ++++++++++++
23 files changed, 1,074 insertions(+), 64 deletions(-)
diff --git a/.local/iot-mqtt/asrs-iot-client-mqttsa21wi8dwvkjf1datsiotcn-north-1amazonawscomcn8883/.lck b/.local/iot-mqtt/asrs-iot-client-mqttsa21wi8dwvkjf1datsiotcn-north-1amazonawscomcn8883/.lck
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.local/iot-mqtt/asrs-iot-client-mqttsa21wi8dwvkjf1datsiotcn-north-1amazonawscomcn8883/.lck
diff --git a/src/main/java/com/zy/common/config/ControllerResAdvice.java b/src/main/java/com/zy/common/config/ControllerResAdvice.java
index 3661d36..ae7410a 100644
--- a/src/main/java/com/zy/common/config/ControllerResAdvice.java
+++ b/src/main/java/com/zy/common/config/ControllerResAdvice.java
@@ -47,40 +47,80 @@
if (serverHttpRequest instanceof ServletServerHttpRequest) {
HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
Object appAuth = request.getAttribute("appAuth");
- if (appAuth != null) {
- if (o instanceof R) {
- String appkey = request.getHeader("appkey");
- Object reqCache = request.getAttribute("cache");
- if (!Cools.isEmpty(appkey)) {
- boolean success = String.valueOf(((R) o).get("code")).equalsIgnoreCase("200");
- if (success){
- // 淇濆瓨鎺ュ彛鏃ュ織
- apiLogService.save(
- String.valueOf(appAuth),
- request.getRequestURI(),
- appkey,
- IpTools.gainRealIp(request),
- reqCache==null?"": JSON.toJSONString(reqCache),
- JSON.toJSONString(o),
- success
- );
- } else {
- beforeBodyWriteCallApiLogSave(
- String.valueOf(appAuth),
- request.getRequestURI(),
- appkey,
- IpTools.gainRealIp(request),
- reqCache==null?"": JSON.toJSONString(reqCache),
- JSON.toJSONString(o),
- success);
- }
- }
+ if (appAuth != null && o instanceof R) {
+ String appkey = request.getHeader("appkey");
+ if (Cools.isEmpty(appkey)) {
+ appkey = "-";
+ }
+ Object reqCache = request.getAttribute("cache");
+ boolean success = String.valueOf(((R) o).get("code")).equalsIgnoreCase("200");
+ if (success) {
+ apiLogService.save(
+ String.valueOf(appAuth),
+ request.getRequestURI(),
+ appkey,
+ IpTools.gainRealIp(request),
+ reqCache == null ? "" : JSON.toJSONString(reqCache),
+ JSON.toJSONString(o),
+ success
+ );
+ } else {
+ beforeBodyWriteCallApiLogSave(
+ String.valueOf(appAuth),
+ request.getRequestURI(),
+ appkey,
+ IpTools.gainRealIp(request),
+ reqCache == null ? "" : JSON.toJSONString(reqCache),
+ JSON.toJSONString(o),
+ success);
+ }
+ } else if (o instanceof R && isInboundThirdPartyUri(request.getRequestURI())) {
+ String appkey = request.getHeader("appkey");
+ if (Cools.isEmpty(appkey)) {
+ appkey = "-";
+ }
+ Object reqCache = request.getAttribute("cache");
+ boolean success = String.valueOf(((R) o).get("code")).equalsIgnoreCase("200");
+ String ns = inboundNamespace(request.getRequestURI());
+ if (success) {
+ apiLogService.save(
+ ns,
+ request.getRequestURI(),
+ appkey,
+ IpTools.gainRealIp(request),
+ reqCache == null ? "" : JSON.toJSONString(reqCache),
+ JSON.toJSONString(o),
+ success
+ );
+ } else {
+ beforeBodyWriteCallApiLogSave(
+ ns,
+ request.getRequestURI(),
+ appkey,
+ IpTools.gainRealIp(request),
+ reqCache == null ? "" : JSON.toJSONString(reqCache),
+ JSON.toJSONString(o),
+ success);
}
}
}
return o;
}
+ private static boolean isInboundThirdPartyUri(String uri) {
+ if (uri == null) {
+ return false;
+ }
+ return uri.contains("/open/asrs") || uri.contains("/wcs/openapi/report");
+ }
+
+ private static String inboundNamespace(String uri) {
+ if (uri != null && uri.contains("/wcs/openapi/report")) {
+ return "WCS鍥炲啓";
+ }
+ return "寮�鏀炬帴鍙�";
+ }
+
public void beforeBodyWriteCallApiLogSave(String name, String url, String appkey, String ip, String request, String response, boolean success) {
ApiLogService apiLogService = SpringUtils.getBean(ApiLogService.class);
String memo = response;
diff --git a/src/main/java/com/zy/integration/iot/client/IotMqttClient.java b/src/main/java/com/zy/integration/iot/client/IotMqttClient.java
index 1904797..67e1a07 100644
--- a/src/main/java/com/zy/integration/iot/client/IotMqttClient.java
+++ b/src/main/java/com/zy/integration/iot/client/IotMqttClient.java
@@ -7,4 +7,7 @@
void publish(String topic, String payload) throws Exception;
boolean isConnected();
+
+ /** 閲嶆柊璇诲簱骞舵柇寮�/閲嶅缓杩炴帴锛岀敤浜庡悗鍙版敼閰嶇疆鍚庣敓鏁堛�� */
+ void reconnectFromDbConfig();
}
diff --git a/src/main/java/com/zy/integration/iot/handler/impl/IotInboundMessageHandlerImpl.java b/src/main/java/com/zy/integration/iot/handler/impl/IotInboundMessageHandlerImpl.java
index 1d21e4b..bf6290d 100644
--- a/src/main/java/com/zy/integration/iot/handler/impl/IotInboundMessageHandlerImpl.java
+++ b/src/main/java/com/zy/integration/iot/handler/impl/IotInboundMessageHandlerImpl.java
@@ -1,13 +1,15 @@
package com.zy.integration.iot.handler.impl;
import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONException;
import com.core.common.Cools;
import com.zy.integration.iot.biz.IotInstructionService;
import com.zy.integration.iot.handler.IotInboundMessageHandler;
import com.zy.integration.iot.publish.IotPublishService;
-import com.zy.iot.config.IotProperties;
+import com.zy.iot.service.IotDbConfigService;
import com.zy.iot.entity.IotFeedbackMessage;
import com.zy.iot.entity.IotInstructionMessage;
+import com.zy.iot.entity.IotTopicConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -21,7 +23,7 @@
public class IotInboundMessageHandlerImpl implements IotInboundMessageHandler {
@Autowired
- private IotProperties iotProperties;
+ private IotDbConfigService iotDbConfigService;
@Autowired
private IotInstructionService iotInstructionService;
@Autowired
@@ -32,17 +34,30 @@
*/
@Override
public void handleMessage(String topic, String payload) {
- if (!iotProperties.isEnabled() || Cools.isEmpty(topic) || payload == null) {
+ if (!iotDbConfigService.isMqttEnabled() || Cools.isEmpty(topic) || payload == null) {
return;
}
try {
+ IotTopicConfig topics = iotDbConfigService.getEffectiveTopics();
Long recordId = null;
- if (topic.equals(iotProperties.getTopics().getEgressStow())) {
- recordId = iotInstructionService.handleStowInstruction(JSON.parseObject(payload, IotInstructionMessage.class), topic, payload);
- } else if (topic.equals(iotProperties.getTopics().getEgressPick())) {
- recordId = iotInstructionService.handlePickInstruction(JSON.parseObject(payload, IotInstructionMessage.class), topic, payload);
- } else if (topic.equals(iotProperties.getTopics().getEgressFeedback())) {
- iotInstructionService.handleFeedbackAck(JSON.parseObject(payload, IotFeedbackMessage.class), topic, payload);
+ if (topic.equals(topics.getEgressStow())) {
+ IotInstructionMessage stow = parseJsonPayload(payload, topic, IotInstructionMessage.class);
+ if (stow == null) {
+ return;
+ }
+ recordId = iotInstructionService.handleStowInstruction(stow, topic, payload);
+ } else if (topic.equals(topics.getEgressPick())) {
+ IotInstructionMessage pick = parseJsonPayload(payload, topic, IotInstructionMessage.class);
+ if (pick == null) {
+ return;
+ }
+ recordId = iotInstructionService.handlePickInstruction(pick, topic, payload);
+ } else if (topic.equals(topics.getEgressFeedback())) {
+ IotFeedbackMessage ack = parseJsonPayload(payload, topic, IotFeedbackMessage.class);
+ if (ack == null) {
+ return;
+ }
+ iotInstructionService.handleFeedbackAck(ack, topic, payload);
} else {
log.warn("ignore unknown iot topic={}, payload={}", topic, payload);
}
@@ -54,4 +69,13 @@
log.error("handle iot inbound message failed, topic={}, payload={}", topic, payload, e);
}
}
+
+ private <T> T parseJsonPayload(String payload, String topic, Class<T> type) {
+ try {
+ return JSON.parseObject(payload, type);
+ } catch (JSONException e) {
+ log.warn("IoT MQTT 闈炲悎娉� JSON topic={} payload={}", topic, payload, e);
+ return null;
+ }
+ }
}
diff --git a/src/main/java/com/zy/integration/iot/paho/MqttNetworkModuleFactory.java b/src/main/java/com/zy/integration/iot/paho/MqttNetworkModuleFactory.java
new file mode 100644
index 0000000..7c65bbf
--- /dev/null
+++ b/src/main/java/com/zy/integration/iot/paho/MqttNetworkModuleFactory.java
@@ -0,0 +1,50 @@
+package com.zy.integration.iot.paho;
+
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.internal.NetworkModule;
+import org.eclipse.paho.client.mqttv3.internal.TCPNetworkModuleFactory;
+import org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Paho 浠呭唴缃� ssl/tcp/ws/wss锛岄�氳繃 SPI 澧炲姞 mqtt锛屼笌 tcp 绛変环寤鸿繛銆�
+ */
+public final class MqttNetworkModuleFactory implements NetworkModuleFactory {
+
+ private static final TCPNetworkModuleFactory DELEGATE = new TCPNetworkModuleFactory();
+
+ @Override
+ public Set<String> getSupportedUriSchemes() {
+ return Collections.singleton("mqtt");
+ }
+
+ @Override
+ public void validateURI(URI uri) throws IllegalArgumentException {
+ DELEGATE.validateURI(toTcpUri(uri));
+ }
+
+ @Override
+ public NetworkModule createNetworkModule(URI uri, MqttConnectOptions options, String clientId) throws MqttException {
+ return DELEGATE.createNetworkModule(toTcpUri(uri), options, clientId);
+ }
+
+ private static URI toTcpUri(URI uri) {
+ try {
+ return new URI(
+ "tcp",
+ uri.getRawUserInfo(),
+ uri.getHost(),
+ uri.getPort(),
+ uri.getRawPath(),
+ uri.getRawQuery(),
+ uri.getRawFragment());
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/zy/integration/iot/paho/MqttsNetworkModuleFactory.java b/src/main/java/com/zy/integration/iot/paho/MqttsNetworkModuleFactory.java
new file mode 100644
index 0000000..540a42e
--- /dev/null
+++ b/src/main/java/com/zy/integration/iot/paho/MqttsNetworkModuleFactory.java
@@ -0,0 +1,50 @@
+package com.zy.integration.iot.paho;
+
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.internal.NetworkModule;
+import org.eclipse.paho.client.mqttv3.internal.SSLNetworkModuleFactory;
+import org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Paho 浠呭唴缃� ssl/tcp/ws/wss锛岄�氳繃 SPI 澧炲姞 mqtts锛屼笌 ssl 绛変环寤鸿繛銆�
+ */
+public final class MqttsNetworkModuleFactory implements NetworkModuleFactory {
+
+ private static final SSLNetworkModuleFactory DELEGATE = new SSLNetworkModuleFactory();
+
+ @Override
+ public Set<String> getSupportedUriSchemes() {
+ return Collections.singleton("mqtts");
+ }
+
+ @Override
+ public void validateURI(URI uri) throws IllegalArgumentException {
+ DELEGATE.validateURI(toSslUri(uri));
+ }
+
+ @Override
+ public NetworkModule createNetworkModule(URI uri, MqttConnectOptions options, String clientId) throws MqttException {
+ return DELEGATE.createNetworkModule(toSslUri(uri), options, clientId);
+ }
+
+ private static URI toSslUri(URI uri) {
+ try {
+ return new URI(
+ "ssl",
+ uri.getRawUserInfo(),
+ uri.getHost(),
+ uri.getPort(),
+ uri.getRawPath(),
+ uri.getRawQuery(),
+ uri.getRawFragment());
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/zy/integration/iot/task/IotPendingPublishScheduler.java b/src/main/java/com/zy/integration/iot/task/IotPendingPublishScheduler.java
index 813d184..27eaa34 100644
--- a/src/main/java/com/zy/integration/iot/task/IotPendingPublishScheduler.java
+++ b/src/main/java/com/zy/integration/iot/task/IotPendingPublishScheduler.java
@@ -1,7 +1,7 @@
package com.zy.integration.iot.task;
import com.zy.integration.iot.publish.IotPublishService;
-import com.zy.iot.config.IotProperties;
+import com.zy.iot.service.IotDbConfigService;
import com.zy.iot.entity.IotPublishRecord;
import com.zy.iot.service.IotPublishRecordService;
import lombok.extern.slf4j.Slf4j;
@@ -20,7 +20,7 @@
public class IotPendingPublishScheduler {
@Autowired
- private IotProperties iotProperties;
+ private IotDbConfigService iotDbConfigService;
@Autowired
private IotPublishRecordService iotPublishRecordService;
@Autowired
@@ -31,7 +31,7 @@
*/
@Scheduled(cron = "0/5 * * * * ? ")
private void execute() {
- if (!iotProperties.isEnabled()) {
+ if (!iotDbConfigService.isMqttEnabled()) {
return;
}
List<IotPublishRecord> records = iotPublishRecordService.selectPendingPublishes(50);
diff --git a/src/main/java/com/zy/iot/config/IotProperties.java b/src/main/java/com/zy/iot/config/IotProperties.java
index 42585ce..24b0ac5 100644
--- a/src/main/java/com/zy/iot/config/IotProperties.java
+++ b/src/main/java/com/zy/iot/config/IotProperties.java
@@ -1,12 +1,16 @@
package com.zy.iot.config;
import com.zy.iot.entity.IotTopicConfig;
+import lombok.AccessLevel;
import lombok.Data;
+import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
+import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
@Data
@Component
@@ -43,27 +47,70 @@
private Map<String, Integer> pickStationMappings = new LinkedHashMap<>();
+ @Getter(AccessLevel.NONE)
+ private volatile String resolvedClientIdCache;
+
public String getResolvedClientId() {
- if (clientId != null && clientId.trim().length() > 0) {
- return clientId.trim();
+ if (resolvedClientIdCache != null) {
+ return resolvedClientIdCache;
}
- return thingName;
+ synchronized (this) {
+ if (resolvedClientIdCache != null) {
+ return resolvedClientIdCache;
+ }
+ String base = null;
+ if (clientId != null && clientId.trim().length() > 0) {
+ base = clientId.trim();
+ } else if (thingName != null && thingName.trim().length() > 0) {
+ base = thingName.trim();
+ }
+ if (base == null) {
+ return null;
+ }
+ resolvedClientIdCache = base + "-" + newClientIdSuffix();
+ return resolvedClientIdCache;
+ }
+ }
+
+ private static String newClientIdSuffix() {
+ long n = ThreadLocalRandom.current().nextLong() & 0xFFFFFFFFL;
+ return Long.toHexString(n);
}
public boolean isTlsEnabled() {
- String serverUri = getServerUri();
- return serverUri != null && serverUri.startsWith("ssl://");
+ return isTlsMqttScheme(getServerUri());
}
+ /** 涓� Paho 寤鸿繛 URI锛氬畬鏁村墠缂� mqtts/mqtt/wss/ws/ssl/tcp 鍘熸牱锛涗粎涓绘満鍚嶆椂榛樿 mqtts 鎴� mqtt銆� */
public String getServerUri() {
if (endpoint == null || endpoint.trim().length() == 0) {
return null;
}
String trimmed = endpoint.trim();
- if (trimmed.startsWith("ssl://") || trimmed.startsWith("tcp://")) {
+ if (hasExplicitBrokerScheme(trimmed)) {
return trimmed;
}
int resolvedPort = port == null ? 8883 : port;
- return "ssl://" + trimmed + ":" + resolvedPort;
+ String scheme = resolvedPort == 1883 ? "mqtt://" : "mqtts://";
+ return scheme + trimmed + ":" + resolvedPort;
+ }
+
+ private static boolean hasExplicitBrokerScheme(String s) {
+ String lower = s.toLowerCase(Locale.ROOT);
+ return lower.startsWith("mqtts://") || lower.startsWith("mqtt://")
+ || lower.startsWith("ssl://") || lower.startsWith("tcp://")
+ || lower.startsWith("wss://") || lower.startsWith("ws://");
+ }
+
+ private static boolean isTlsMqttScheme(String serverUri) {
+ if (serverUri == null || serverUri.isEmpty()) {
+ return false;
+ }
+ int p = serverUri.indexOf("://");
+ if (p <= 0) {
+ return false;
+ }
+ String scheme = serverUri.substring(0, p).toLowerCase(Locale.ROOT);
+ return "mqtts".equals(scheme) || "ssl".equals(scheme) || "wss".equals(scheme);
}
}
diff --git a/src/main/java/com/zy/iot/constant/IotSysConfigCodes.java b/src/main/java/com/zy/iot/constant/IotSysConfigCodes.java
new file mode 100644
index 0000000..5ee793a
--- /dev/null
+++ b/src/main/java/com/zy/iot/constant/IotSysConfigCodes.java
@@ -0,0 +1,16 @@
+package com.zy.iot.constant;
+
+public final class IotSysConfigCodes {
+
+ private IotSysConfigCodes() {
+ }
+
+ public static final String MQTT_ENABLED = "iot.mqtt.enabled";
+
+ public static final String TOPIC_EGRESS_STOW = "iot.topic.egress_stow";
+ public static final String TOPIC_EGRESS_PICK = "iot.topic.egress_pick";
+ public static final String TOPIC_EGRESS_FEEDBACK = "iot.topic.egress_feedback";
+ public static final String TOPIC_INGRESS_STOW = "iot.topic.ingress_stow";
+ public static final String TOPIC_INGRESS_PICK = "iot.topic.ingress_pick";
+ public static final String TOPIC_INGRESS_FEEDBACK = "iot.topic.ingress_feedback";
+}
diff --git a/src/main/java/com/zy/iot/controller/IotMqttAdminController.java b/src/main/java/com/zy/iot/controller/IotMqttAdminController.java
new file mode 100644
index 0000000..4887f01
--- /dev/null
+++ b/src/main/java/com/zy/iot/controller/IotMqttAdminController.java
@@ -0,0 +1,97 @@
+package com.zy.iot.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.core.annotations.ManagerAuth;
+import com.core.common.Cools;
+import com.core.common.R;
+import com.zy.common.web.BaseController;
+import com.zy.integration.iot.client.IotMqttClient;
+import com.zy.iot.constant.IotSysConfigCodes;
+import com.zy.iot.service.IotDbConfigService;
+import com.zy.system.entity.Config;
+import com.zy.system.service.ConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 鍚庡彴锛欼oT MQTT 寮�鍏充笌 topic 瀛� sys_config锛岄噸杩炲悗璁㈤槄鐢熸晥銆�
+ */
+@RestController
+public class IotMqttAdminController extends BaseController {
+
+ @Autowired
+ private ConfigService configService;
+ @Autowired
+ private IotDbConfigService iotDbConfigService;
+ @Autowired
+ private IotMqttClient iotMqttClient;
+
+ @RequestMapping(value = "/iotMqttAdmin/config/auth", method = RequestMethod.GET)
+ @ManagerAuth(memo = "IoT MQTT 閰嶇疆鏌ヨ")
+ public R getConfig() {
+ Map<String, Object> data = new LinkedHashMap<>(iotDbConfigService.snapshotForAdmin());
+ data.put("mqttConnected", iotMqttClient.isConnected());
+ return R.ok(data);
+ }
+
+ @RequestMapping(value = "/iotMqttAdmin/config/save/auth", method = RequestMethod.POST)
+ @ManagerAuth(memo = "IoT MQTT 閰嶇疆淇濆瓨")
+ public R saveConfig(@RequestBody JSONObject body) {
+ if (body == null) {
+ return R.error("鍙傛暟涓虹┖");
+ }
+ if (body.containsKey("enabled")) {
+ Boolean en = body.getBoolean("enabled");
+ upsert(IotSysConfigCodes.MQTT_ENABLED, "IoT MQTT 鎬诲紑鍏�", en == null ? "false" : String.valueOf(en));
+ }
+ JSONObject topics = body.getJSONObject("topics");
+ if (topics != null) {
+ putTopicIfPresent(topics, "egressStow", IotSysConfigCodes.TOPIC_EGRESS_STOW, "IoT topic egress stow");
+ putTopicIfPresent(topics, "egressPick", IotSysConfigCodes.TOPIC_EGRESS_PICK, "IoT topic egress pick");
+ putTopicIfPresent(topics, "egressFeedback", IotSysConfigCodes.TOPIC_EGRESS_FEEDBACK, "IoT topic egress feedback");
+ putTopicIfPresent(topics, "ingressStow", IotSysConfigCodes.TOPIC_INGRESS_STOW, "IoT topic ingress stow");
+ putTopicIfPresent(topics, "ingressPick", IotSysConfigCodes.TOPIC_INGRESS_PICK, "IoT topic ingress pick");
+ putTopicIfPresent(topics, "ingressFeedback", IotSysConfigCodes.TOPIC_INGRESS_FEEDBACK, "IoT topic ingress feedback");
+ }
+ iotDbConfigService.refreshCache();
+ return R.ok();
+ }
+
+ private void putTopicIfPresent(JSONObject topics, String key, String code, String name) {
+ if (!topics.containsKey(key)) {
+ return;
+ }
+ String v = topics.getString(key);
+ if (v == null) {
+ return;
+ }
+ upsert(code, name, v.trim());
+ }
+
+ @RequestMapping(value = "/iotMqttAdmin/reconnect/auth", method = RequestMethod.POST)
+ @ManagerAuth(memo = "IoT MQTT 閲嶈繛")
+ public R reconnect() {
+ iotMqttClient.reconnectFromDbConfig();
+ return R.ok();
+ }
+
+ private void upsert(String code, String name, String value) {
+ if (Cools.isEmpty(code)) {
+ return;
+ }
+ Config existing = configService.selectConfigByCode(code);
+ if (existing != null) {
+ existing.setValue(value);
+ existing.setStatus((short) 1);
+ configService.updateById(existing);
+ } else {
+ configService.insert(new Config(name, code, value, (short) 1, (short) 1));
+ }
+ }
+}
diff --git a/src/main/java/com/zy/iot/controller/IotMqttOutboundController.java b/src/main/java/com/zy/iot/controller/IotMqttOutboundController.java
new file mode 100644
index 0000000..cd59619
--- /dev/null
+++ b/src/main/java/com/zy/iot/controller/IotMqttOutboundController.java
@@ -0,0 +1,89 @@
+package com.zy.iot.controller;
+
+import com.baomidou.mybatisplus.mapper.EntityWrapper;
+import com.baomidou.mybatisplus.plugins.Page;
+import com.core.annotations.ManagerAuth;
+import com.core.common.Cools;
+import com.core.common.R;
+import com.zy.common.web.BaseController;
+import com.zy.integration.iot.publish.IotPublishService;
+import com.zy.iot.service.IotDbConfigService;
+import com.zy.iot.constant.IotConstants;
+import com.zy.iot.entity.IotPublishRecord;
+import com.zy.iot.service.IotPublishRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 鍚庡彴锛氫粎鍑虹珯 MQTT锛圵MS 鍙戝嚭锛夛紝鏀寔鍒楄〃涓庢墜鍔ㄩ噸鍙戙��
+ */
+@RestController
+public class IotMqttOutboundController extends BaseController {
+
+ @Autowired
+ private IotDbConfigService iotDbConfigService;
+ @Autowired
+ private IotPublishRecordService iotPublishRecordService;
+ @Autowired
+ private IotPublishService iotPublishService;
+
+ @RequestMapping(value = "/iotMqttOutbound/list/auth")
+ @ManagerAuth(memo = "IoT MQTT 鍙戦�佽褰�")
+ public R list(@RequestParam(defaultValue = "1") Integer curr,
+ @RequestParam(defaultValue = "16") Integer limit,
+ @RequestParam(required = false) String instruction_id,
+ @RequestParam(required = false) String publish_status,
+ @RequestParam(required = false) String publish_topic,
+ @RequestParam(required = false) String container_id) {
+ EntityWrapper<IotPublishRecord> wrapper = new EntityWrapper<>();
+ wrapper.eq("direction", IotConstants.DIRECTION_OUTBOUND);
+ wrapper.isNotNull("publish_topic");
+ wrapper.isNotNull("publish_payload");
+ if (!Cools.isEmpty(instruction_id)) {
+ wrapper.like("instruction_id", instruction_id);
+ }
+ if (!Cools.isEmpty(publish_status)) {
+ wrapper.eq("publish_status", publish_status);
+ }
+ if (!Cools.isEmpty(publish_topic)) {
+ wrapper.like("publish_topic", publish_topic);
+ }
+ if (!Cools.isEmpty(container_id)) {
+ wrapper.like("container_id", container_id);
+ }
+ wrapper.orderBy("create_time", false);
+ return R.ok(iotPublishRecordService.selectPage(new Page<>(curr, limit), wrapper));
+ }
+
+ @RequestMapping(value = "/iotMqttOutbound/resend/auth")
+ @ManagerAuth(memo = "IoT MQTT 鍙戦�侀噸鍙�")
+ public R resend(@RequestParam Long id) {
+ if (id == null) {
+ return R.error("缂哄皯 id");
+ }
+ if (!iotDbConfigService.isMqttEnabled()) {
+ return R.error("IoT 鏈紑鍚�");
+ }
+ IotPublishRecord record = iotPublishRecordService.selectById(id);
+ if (record == null) {
+ return R.error("璁板綍涓嶅瓨鍦�");
+ }
+ if (!IotConstants.DIRECTION_OUTBOUND.equals(record.getDirection())) {
+ return R.error("浠呮敮鎸佸嚭绔欒褰曢噸鍙�");
+ }
+ if (Cools.isEmpty(record.getPublishTopic()) || Cools.isEmpty(record.getPublishPayload())) {
+ return R.error("鏃犲彂閫佷富棰樻垨娑堟伅浣�");
+ }
+ iotPublishService.publishRecordNow(id);
+ record = iotPublishRecordService.selectById(id);
+ if (record != null && IotConstants.PUBLISH_STATUS_SUCCESS.equals(record.getPublishStatus())) {
+ return R.ok();
+ }
+ String err = record != null && !Cools.isEmpty(record.getErrorMessage())
+ ? record.getErrorMessage()
+ : "鍙戦�佸け璐�";
+ return R.error(err);
+ }
+}
diff --git a/src/main/java/com/zy/iot/service/IotDbConfigService.java b/src/main/java/com/zy/iot/service/IotDbConfigService.java
new file mode 100644
index 0000000..3186a18
--- /dev/null
+++ b/src/main/java/com/zy/iot/service/IotDbConfigService.java
@@ -0,0 +1,17 @@
+package com.zy.iot.service;
+
+import com.zy.iot.entity.IotTopicConfig;
+
+import java.util.Map;
+
+public interface IotDbConfigService {
+
+ void refreshCache();
+
+ boolean isMqttEnabled();
+
+ IotTopicConfig getEffectiveTopics();
+
+ /** 褰撳墠鐢熸晥寮�鍏充笌 topic锛屼緵鍚庡彴琛ㄥ崟灞曠ず銆� */
+ Map<String, Object> snapshotForAdmin();
+}
diff --git a/src/main/java/com/zy/iot/service/impl/IotDbConfigServiceImpl.java b/src/main/java/com/zy/iot/service/impl/IotDbConfigServiceImpl.java
new file mode 100644
index 0000000..81e8ec9
--- /dev/null
+++ b/src/main/java/com/zy/iot/service/impl/IotDbConfigServiceImpl.java
@@ -0,0 +1,132 @@
+package com.zy.iot.service.impl;
+
+import com.core.common.Cools;
+import com.zy.iot.config.IotProperties;
+import com.zy.iot.constant.IotSysConfigCodes;
+import com.zy.iot.entity.IotTopicConfig;
+import com.zy.iot.service.IotDbConfigService;
+import com.zy.system.entity.Config;
+import com.zy.system.service.ConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Service
+public class IotDbConfigServiceImpl implements IotDbConfigService {
+
+ @Autowired
+ private ConfigService configService;
+ @Autowired
+ private IotProperties iotProperties;
+
+ private volatile Snapshot snapshot;
+
+ @PostConstruct
+ public void init() {
+ refreshCache();
+ }
+
+ @Override
+ public synchronized void refreshCache() {
+ Snapshot s = new Snapshot();
+ s.enabled = resolveEnabled();
+ s.topics = resolveTopics();
+ this.snapshot = s;
+ }
+
+ @Override
+ public boolean isMqttEnabled() {
+ Snapshot s = snapshot;
+ return s != null && s.enabled;
+ }
+
+ @Override
+ public IotTopicConfig getEffectiveTopics() {
+ Snapshot s = snapshot;
+ if (s == null || s.topics == null) {
+ return copyTopics(iotProperties.getTopics());
+ }
+ return copyTopics(s.topics);
+ }
+
+ @Override
+ public Map<String, Object> snapshotForAdmin() {
+ Snapshot s = snapshot;
+ if (s == null) {
+ refreshCache();
+ s = snapshot;
+ }
+ Map<String, Object> root = new LinkedHashMap<>();
+ root.put("enabled", s != null && s.enabled);
+ Map<String, String> topics = new LinkedHashMap<>();
+ IotTopicConfig t = s != null && s.topics != null ? s.topics : iotProperties.getTopics();
+ topics.put("egressStow", t.getEgressStow());
+ topics.put("egressPick", t.getEgressPick());
+ topics.put("egressFeedback", t.getEgressFeedback());
+ topics.put("ingressStow", t.getIngressStow());
+ topics.put("ingressPick", t.getIngressPick());
+ topics.put("ingressFeedback", t.getIngressFeedback());
+ root.put("topics", topics);
+ return root;
+ }
+
+ private boolean resolveEnabled() {
+ Config c = configService.selectConfigByCode(IotSysConfigCodes.MQTT_ENABLED);
+ if (c != null && isConfigActive(c) && !Cools.isEmpty(c.getValue())) {
+ String v = c.getValue().trim();
+ if ("1".equals(v) || "true".equalsIgnoreCase(v) || "yes".equalsIgnoreCase(v)) {
+ return true;
+ }
+ if ("0".equals(v) || "false".equalsIgnoreCase(v) || "no".equalsIgnoreCase(v)) {
+ return false;
+ }
+ }
+ return iotProperties.isEnabled();
+ }
+
+ private IotTopicConfig resolveTopics() {
+ IotTopicConfig yml = iotProperties.getTopics();
+ IotTopicConfig t = new IotTopicConfig();
+ t.setEgressStow(orDb(IotSysConfigCodes.TOPIC_EGRESS_STOW, yml.getEgressStow()));
+ t.setEgressPick(orDb(IotSysConfigCodes.TOPIC_EGRESS_PICK, yml.getEgressPick()));
+ t.setEgressFeedback(orDb(IotSysConfigCodes.TOPIC_EGRESS_FEEDBACK, yml.getEgressFeedback()));
+ t.setIngressStow(orDb(IotSysConfigCodes.TOPIC_INGRESS_STOW, yml.getIngressStow()));
+ t.setIngressPick(orDb(IotSysConfigCodes.TOPIC_INGRESS_PICK, yml.getIngressPick()));
+ t.setIngressFeedback(orDb(IotSysConfigCodes.TOPIC_INGRESS_FEEDBACK, yml.getIngressFeedback()));
+ return t;
+ }
+
+ private String orDb(String code, String ymlDefault) {
+ Config c = configService.selectConfigByCode(code);
+ if (c != null && isConfigActive(c) && !Cools.isEmpty(c.getValue())) {
+ return c.getValue().trim();
+ }
+ return ymlDefault;
+ }
+
+ private static boolean isConfigActive(Config c) {
+ return c.getStatus() != null && c.getStatus() == 1;
+ }
+
+ private static IotTopicConfig copyTopics(IotTopicConfig src) {
+ if (src == null) {
+ return new IotTopicConfig();
+ }
+ IotTopicConfig c = new IotTopicConfig();
+ c.setEgressStow(src.getEgressStow());
+ c.setEgressPick(src.getEgressPick());
+ c.setEgressFeedback(src.getEgressFeedback());
+ c.setIngressStow(src.getIngressStow());
+ c.setIngressPick(src.getIngressPick());
+ c.setIngressFeedback(src.getIngressFeedback());
+ return c;
+ }
+
+ private static final class Snapshot {
+ private boolean enabled;
+ private IotTopicConfig topics;
+ }
+}
diff --git a/src/main/resources/META-INF/services/org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory b/src/main/resources/META-INF/services/org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory
new file mode 100644
index 0000000..92b7f71
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory
@@ -0,0 +1,2 @@
+com.zy.integration.iot.paho.MqttsNetworkModuleFactory
+com.zy.integration.iot.paho.MqttNetworkModuleFactory
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 0a20941..9c0b543 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -105,12 +105,14 @@
switch: true
status-sync:
enabled: true
+ # 澶辫触鏃舵槸鍚︽墦 WARN/ERROR锛堟湰鍦版棤 WCS 鏃跺彲璁� false锛岄渶鎺掓煡鏃跺啀寮�锛�
+ log-on-failure: true
initial-delay: 10000
fixed-delay: 5000
method: POST
# 鍦板潃
address:
- URL: https://127.0.0.1:9090/wcs
+ URL: http://127.0.0.1:9090/wcs
#鍏ュ簱浠诲姟涓嬪彂鍦板潃
createInTask : /openapi/createInTask
#鍑哄簱浠诲姟涓嬪彂鍦板潃
@@ -121,14 +123,18 @@
stopOutTask: /openapi/cancelTaskBatch
#璁惧鐘舵�佽幏鍙栧湴鍧�
getDeviceStatus: /openapi/deviceStatus
+ #鎸� WMS 浠诲姟鍙锋煡璇� WCS 鏄惁宸叉帴鏀朵换鍔�
+ queryTask: /openapi/queryTask
# AWS IoT 瀵规帴寮�鍏充笌 topic/璇佷功閰嶇疆銆�
# 榛樿鍏抽棴锛屽彧鏈夐厤缃綈 endpoint 鍜岃瘉涔﹁矾寰勫悗鎵嶄細灏濊瘯寤鸿繛銆�
iot:
- enabled: true
- endpoint: tcp://192.168.100.170:1883
- port: 1883
+ enabled: false
+# endpoint: 192.168.100.170
+# port: 1883
+ endpoint: a21wi8dwvkjf1d.ats.iot.cn-north-1.amazonaws.com.cn
+ port: 8883
thingName: asrs-iot-client
clientId: asrs-iot-client
cleanSession: false
@@ -136,21 +142,21 @@
keepAliveSeconds: 60
connectionTimeoutSeconds: 10
persistenceDir: .local/iot-mqtt
- caCertPath:
- clientCertPath:
- privateKeyPath:
+ caCertPath: D:/work/work-zy/gsl/zy-wms-gsl/src/main/resources/iot-certs/AmazonRootCA1.pem
+ clientCertPath: D:/work/work-zy/gsl/zy-wms-gsl/src/main/resources/iot-certs/device-certificate.pem.crt
+ privateKeyPath: D:/work/work-zy/gsl/zy-wms-gsl/src/main/resources/iot-certs/device-private.pem.key
topics:
#浜氶┈閫婂彂閫佺粍鎵樼粰wms,鍦ㄦ墭鐩樹笂鏀跺埌绾哥鍚庯紝鍙戝竷鍫嗗灈鎸囦护浠ュ皢鎵樼洏鍙戦�佸埌浠撳簱銆�
- egressStow: glenn/instruction/icna/egress/asrs/stow
+ egressStow: glenn/instruction/icng/egress/asrs/stow
#浜氶┈閫婂彂閫佸嚭搴撴寚浠ょ粰wms,鍚姩鎷i�夎姹傚悗锛屽彂甯冩嫞閫夋寚浠や互寮曞ASRS浠庢枡绠变腑鍙栬揣銆�
- egressPick: glenn/instruction/icna/egress/asrs/pick
+ egressPick: glenn/instruction/icng/egress/asrs/pick
#wms鍏ュ簱瀹屾垚鍙戦�佺粰浜氶┈閫婏紝ASRS瀹為檯鎷栨嫿鎵樼洏鍒版枡绠卞悗锛屽彂甯冨爢鍨涘姩浣滀互鍚屾BPS鏁版嵁銆�
- egressFeedback: glenn/instruction/icna/egress/asrs/feedback
+ egressFeedback: glenn/instruction/icng/egress/asrs/feedback
#wms鍑哄簱瀹屾垚鍙戦�佺粰浜氶┈閫婏紝鍦ˋSRS瀹為檯浠庢枡绠卞彇璐у悗锛屽彂甯冩嫞閫夊姩浣渟z XBPS鏁版嵁銆�
- ingressStow: glenn/instruction/icna/ingress/asrs/stow
+ ingressStow: glenn/instruction/icng/ingress/asrs/stow
#ASRS 鎺ユ敹鍒� XBPS 鎸囦护鍚庯紝鍚戝弽棣圶BPS 鍙戦�佸搷搴斻��
- ingressPick: glenn/instruction/icna/ingress/asrs/pick
+ ingressPick: glenn/instruction/icng/ingress/asrs/pick
#鍦╔BPs鏀跺埌ASRS鎿嶄綔鍚庯紝鍚戝弽棣圓SRS鍙戦�佸搷搴�
- ingressFeedback: glenn/instruction/icna/ingress/asrs/feedback
+ ingressFeedback: glenn/instruction/icng/ingress/asrs/feedback
pickStationMappings:
ASRSOutbound1: 101
diff --git a/src/main/resources/iot-certs/AmazonRootCA1.pem b/src/main/resources/iot-certs/AmazonRootCA1.pem
new file mode 100644
index 0000000..61ae256
--- /dev/null
+++ b/src/main/resources/iot-certs/AmazonRootCA1.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
+ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
+b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
+MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
+b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
+ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
+9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
+IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
+VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
+93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
+jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
+A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
+U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
+N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
+o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
+5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
+rqXRfboQnoZsG4q5WTP468SQvvG5
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/src/main/resources/iot-certs/device-certificate.pem.crt b/src/main/resources/iot-certs/device-certificate.pem.crt
new file mode 100644
index 0000000..bfe7540
--- /dev/null
+++ b/src/main/resources/iot-certs/device-certificate.pem.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDWTCCAkGgAwIBAgIUadE+uh4j8msPlgfGjldjbBLP+tMwDQYJKoZIhvcNAQEL
+BQAwTTFLMEkGA1UECwxCQW1hem9uIFdlYiBTZXJ2aWNlcyBPPUFtYXpvbi5jb20g
+SW5jLiBMPVNlYXR0bGUgU1Q9V2FzaGluZ3RvbiBDPVVTMB4XDTI2MDMyNTIxNDIx
+M1oXDTQ5MTIzMTIzNTk1OVowHjEcMBoGA1UEAwwTQVdTIElvVCBDZXJ0aWZpY2F0
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOdorxd56BY+uJqANr5B
+N+zz+XeJFXd2ou6e30gG1URBomsMWZts0bhqfG+GyrqaZ0KFi6Z1yhbk0DfSir5e
+FxPtfG5Rvo05V9RjsjGrSh3LeeZnTSgcGHimtwfz5D72j6nuxCdOiEpctLYyjxqM
+whe02xOT+AuS82Q2kCurCrBlsxyzRh3HKJ0xT2/lohDgP1IDbxKTYJBOm1nvRRqa
+GmFab+BRAIjT1Y4G7cs2EXmE23ykkEPZ6TSXMij20lN9VDF3fUUuBI5G7rcq9HfS
+v1lQzVwF9NmbGfRNWobHwrvT7G9d74UXWmFfdrnzowaWRh2OtCRG7WlowHkDGVuL
+AuMCAwEAAaNgMF4wHwYDVR0jBBgwFoAUzEvKR0Elhk6it4OonQyGA0e2EkowHQYD
+VR0OBBYEFHyFYPxyk/7aYGwrS0S8Ai6uYgS/MAwGA1UdEwEB/wQCMAAwDgYDVR0P
+AQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQBgPB5FZp8n6B3PNUuUV7HVbDrq
+QAAYX18NQ7+vVOAnW6Yfisggqe8l3cC6ZLiGHpJ90bLmPTAiBr+tHJLLhJFi3e0w
+WygplWlRdv3AbGEdQjMRAV86y0gx8LJZfLDzeG1K9HCj6p859Ugr3BbViFUz1YWh
+rrFPnzsALBrem+J++2L834dFC+jFmSEWwBhRchRFEkJc7Iu0+dskOAlpIpJHw2X+
+8xLIqhpPLbjWTv5LMWPrBXoH4dBvw6oTXLJLY0sn1OBZTobRJ+KRmCVZDC/Ircu/
+dABftIoCMQw53ilPNQCpP0Qe1ecAGZxxQXvdrdR+AVJPdDhCYfkXvj1NVS3h
+-----END CERTIFICATE-----
diff --git a/src/main/resources/iot-certs/device-private.pem.key b/src/main/resources/iot-certs/device-private.pem.key
new file mode 100644
index 0000000..6c78314
--- /dev/null
+++ b/src/main/resources/iot-certs/device-private.pem.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA52ivF3noFj64moA2vkE37PP5d4kVd3ai7p7fSAbVREGiawxZ
+m2zRuGp8b4bKuppnQoWLpnXKFuTQN9KKvl4XE+18blG+jTlX1GOyMatKHct55mdN
+KBwYeKa3B/PkPvaPqe7EJ06ISly0tjKPGozCF7TbE5P4C5LzZDaQK6sKsGWzHLNG
+HcconTFPb+WiEOA/UgNvEpNgkE6bWe9FGpoaYVpv4FEAiNPVjgbtyzYReYTbfKSQ
+Q9npNJcyKPbSU31UMXd9RS4Ejkbutyr0d9K/WVDNXAX02ZsZ9E1ahsfCu9Psb13v
+hRdaYV92ufOjBpZGHY60JEbtaWjAeQMZW4sC4wIDAQABAoIBAQCJWPbjZjW8Tknf
+Wc4kKi15dG1S54hYOZAHNUCtTXDzbElsZA4jU/k+DeYBg+17x/0V3JHAoRTrda+o
+EkzLJKlp6ID8MYR56dkZdHrlRBdfi8+0UwfWkKZtpfXowHdub4VhhRfjhJccG94e
+be/GAFmLHIsTGbYVmIjhqAj2AjT78J/rA4Mvv9ng2qgXwb7WQgbauhpAZcG8cfYa
+Z4sO4PJ8S4VSAxuoJYFY0A+3fYARfvxEO8jsrnqeRSGwjsWGPt9xzqbnJ/hcwFP9
+4O7JYwJ8aqMEEGlx4FmcAt0qjz4E3qhNqPUMjyVpQcZObDoYFKOzHDHOsBxd+WXo
+/+01U5IRAoGBAPQ4WRAmFs82IIG8B3OFX70TVd9qRwCqPi6bNysi60LX/0Bf7qTP
+6NAnD9rxmyJJ1PVmCgmcdLH8CnF7ntyXAx5QsFxJ/XHZQ5KEa7FDo0aTFm4GqduR
+MHBPEy5mlKgum72soFpbGG9mH4mXXOhR0xkYuRQUg+i13cmeX5HLBczdAoGBAPKS
+JHCtf1jidjch71QtRze56dOPZQOgmAJqNEf+lIVXRbhHCCTQED+UNYAhDp+jUFpR
+yE+oR/xx3+11XCHFjpaXssL2OMAPjee1JKm9F18woJjc2LMPW6XCk6hHjZ6Ltued
+VlSQplwjTxewR+81tj0KGagmmXhcXkcku3iTxDK/AoGBAKOIB7tUhfmCmQnGSocE
+TDNjeyD7HUhItxKmRK7R1w8Pa5BDrJ0XyyF2xpspJWQ0ZDFefmIpLcrwpl2PFbVI
+OYJXLYDe2qMdhK3blfFBBVgArghG1f58nh7WFFYBwpFLhGXh7g4S6a3OiFetzzyR
+bfVkJKpZgmqVPUoAjqYleGDRAoGBAJZVvqHa1UcYK13l+Tb5TN8bqPBGObuyxyMQ
+AVDxVckCGqKn20M9dCSDTVkYo8CKbd1cPEIqMFsjlD3N84i2sLViVRcBlJBr023c
+VVmhaJ/FOnMixGbNSOaFng+4MOwm+Pe5Cm0krQYDmBw9U4fMiSJxZQ9SxODUllWP
+TWTgZ8NvAoGAKmveOulE1ykt26FLfemjEuZIHCT+BBhILHHBRWKW4uT2YBdreiCZ
+DMDN2suu7gGf30grmE/DoFUfu1m7Ser15pwf/AMkZgTErLF8xB0hoYWbSqIGOBUa
+uiLjUKVT6O7Q+YY5wiKv7p/BQaF+jmXkW4kwhE1L7dueEkl0zFPUguk=
+-----END RSA PRIVATE KEY-----
diff --git a/src/main/resources/iot-certs/device-public.pem.key b/src/main/resources/iot-certs/device-public.pem.key
new file mode 100644
index 0000000..dd8936c
--- /dev/null
+++ b/src/main/resources/iot-certs/device-public.pem.key
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA52ivF3noFj64moA2vkE3
+7PP5d4kVd3ai7p7fSAbVREGiawxZm2zRuGp8b4bKuppnQoWLpnXKFuTQN9KKvl4X
+E+18blG+jTlX1GOyMatKHct55mdNKBwYeKa3B/PkPvaPqe7EJ06ISly0tjKPGozC
+F7TbE5P4C5LzZDaQK6sKsGWzHLNGHcconTFPb+WiEOA/UgNvEpNgkE6bWe9FGpoa
+YVpv4FEAiNPVjgbtyzYReYTbfKSQQ9npNJcyKPbSU31UMXd9RS4Ejkbutyr0d9K/
+WVDNXAX02ZsZ9E1ahsfCu9Psb13vhRdaYV92ufOjBpZGHY60JEbtaWjAeQMZW4sC
+4wIDAQAB
+-----END PUBLIC KEY-----
diff --git a/src/main/resources/sql/20260403_iot_mqtt_sys_config.sql b/src/main/resources/sql/20260403_iot_mqtt_sys_config.sql
new file mode 100644
index 0000000..6685cc6
--- /dev/null
+++ b/src/main/resources/sql/20260403_iot_mqtt_sys_config.sql
@@ -0,0 +1,9 @@
+-- IoT MQTT锛歴ys_config 缂栫爜璇存槑锛堟棤鍒欒蛋 application.yml 鐨� iot.enabled / topics锛�
+-- iot.mqtt.enabled 鍊� true/false锛宻tatus=1 鐢熸晥
+-- iot.topic.egress_stow / egress_pick / egress_feedback / ingress_stow / ingress_pick / ingress_feedback
+
+IF NOT EXISTS (SELECT 1 FROM sys_config WHERE code = N'iot.mqtt.enabled')
+BEGIN
+ INSERT INTO sys_config (name, code, value, type, status)
+ VALUES (N'IoT MQTT 鎬诲紑鍏�', N'iot.mqtt.enabled', N'false', 1, 1);
+END;
diff --git a/src/main/webapp/static/js/iotMqttOutbound/iotMqttOutbound.js b/src/main/webapp/static/js/iotMqttOutbound/iotMqttOutbound.js
new file mode 100644
index 0000000..9bd0603
--- /dev/null
+++ b/src/main/webapp/static/js/iotMqttOutbound/iotMqttOutbound.js
@@ -0,0 +1,233 @@
+var pageCurr;
+var tableIns;
+
+function escHtml(s) {
+ if (s == null || s === '') {
+ return '';
+ }
+ return $('<div/>').text(String(s)).html();
+}
+
+function payloadPreview(s) {
+ var t = s == null ? '' : String(s);
+ if (t.length <= 100) {
+ return escHtml(t);
+ }
+ return escHtml(t.substring(0, 100)) + '鈥�';
+}
+
+layui.use(['table', 'form'], function () {
+ var table = layui.table;
+ var $ = layui.jquery;
+ var layer = layui.layer;
+ var form = layui.form;
+
+ function loadIotMqttConfig() {
+ $.ajax({
+ url: baseUrl + '/iotMqttAdmin/config/auth',
+ headers: {token: localStorage.getItem('token')},
+ dataType: 'json',
+ success: function (res) {
+ if (res.code === 403) {
+ top.location.href = baseUrl + "/";
+ return;
+ }
+ if (res.code !== 200 || !res.data) {
+ return;
+ }
+ var d = res.data;
+ var tp = d.topics || {};
+ form.val('iotMqttCfgForm', {
+ enabled: d.enabled ? 'on' : '',
+ egressStow: tp.egressStow || '',
+ egressPick: tp.egressPick || '',
+ egressFeedback: tp.egressFeedback || '',
+ ingressStow: tp.ingressStow || '',
+ ingressPick: tp.ingressPick || '',
+ ingressFeedback: tp.ingressFeedback || ''
+ });
+ $('#iotMqttConnState').text(d.mqttConnected ? '褰撳墠锛氬凡杩炴帴' : '褰撳墠锛氭湭杩炴帴');
+ form.render();
+ }
+ });
+ }
+
+ loadIotMqttConfig();
+ $('#iotMqttReloadCfg').on('click', function () {
+ loadIotMqttConfig();
+ });
+ $('#iotMqttReconnect').on('click', function () {
+ $.ajax({
+ url: baseUrl + '/iotMqttAdmin/reconnect/auth',
+ headers: {token: localStorage.getItem('token')},
+ type: 'POST',
+ dataType: 'json',
+ success: function (res) {
+ if (res.code === 200) {
+ layer.msg('宸叉墽琛岄噸杩�');
+ loadIotMqttConfig();
+ } else {
+ layer.msg(res.msg || '閲嶈繛澶辫触', {icon: 2});
+ }
+ },
+ error: function () {
+ layer.msg('璇锋眰澶辫触', {icon: 2});
+ }
+ });
+ });
+ form.on('submit(iotMqttSave)', function () {
+ var v = form.val('iotMqttCfgForm');
+ var payload = {
+ enabled: v.enabled === 'on',
+ topics: {
+ egressStow: v.egressStow || '',
+ egressPick: v.egressPick || '',
+ egressFeedback: v.egressFeedback || '',
+ ingressStow: v.ingressStow || '',
+ ingressPick: v.ingressPick || '',
+ ingressFeedback: v.ingressFeedback || ''
+ }
+ };
+ $.ajax({
+ url: baseUrl + '/iotMqttAdmin/config/save/auth',
+ headers: {token: localStorage.getItem('token'), 'Content-Type': 'application/json'},
+ type: 'POST',
+ data: JSON.stringify(payload),
+ dataType: 'json',
+ success: function (res) {
+ if (res.code === 200) {
+ layer.msg('宸蹭繚瀛橈紝璇风偣銆岄噸杩� MQTT銆嶄娇璁㈤槄鐢熸晥');
+ loadIotMqttConfig();
+ } else {
+ layer.msg(res.msg || '淇濆瓨澶辫触', {icon: 2});
+ }
+ },
+ error: function () {
+ layer.msg('璇锋眰澶辫触', {icon: 2});
+ }
+ });
+ return false;
+ });
+
+ tableIns = table.render({
+ elem: '#iotMqttOutbound',
+ headers: {token: localStorage.getItem('token')},
+ url: baseUrl + '/iotMqttOutbound/list/auth',
+ page: true,
+ limit: 16,
+ limits: [16, 30, 50, 100],
+ cellMinWidth: 50,
+ cols: [[
+ {field: 'id', title: 'ID', width: 80, sort: true, align: 'center'}
+ , {field: 'instructionId', title: 'instructionId', align: 'center', width: 200}
+ , {field: 'messageType', title: '绫诲瀷', align: 'center', width: 100}
+ , {field: 'publishTopic', title: '鍙戦�佷富棰�', align: 'center', minWidth: 200}
+ , {
+ field: 'publishPayload',
+ title: '鍙戦�佹秷鎭綋(鎽樿)',
+ align: 'left',
+ minWidth: 280,
+ templet: function (d) {
+ return payloadPreview(d.publishPayload);
+ }
+ }
+ , {field: 'publishStatus', title: '鍙戝竷鐘舵��', align: 'center', width: 120}
+ , {field: 'errorMessage', title: '閿欒淇℃伅', align: 'center', minWidth: 160}
+ , {field: 'createTime', title: '鍒涘缓鏃堕棿', align: 'center', width: 170}
+ , {field: 'updateTime', title: '鏇存柊鏃堕棿', align: 'center', width: 170}
+ , {fixed: 'right', title: '鎿嶄綔', align: 'center', toolbar: '#iotMqttOutboundOperate', width: 150}
+ ]],
+ request: {
+ pageName: 'curr',
+ pageSize: 'limit'
+ },
+ parseData: function (res) {
+ return {
+ 'code': res.code,
+ 'msg': res.msg,
+ 'count': res.data.total,
+ 'data': res.data.records
+ };
+ },
+ response: {
+ statusCode: 200
+ },
+ done: function (res, curr) {
+ if (res.code === 403) {
+ top.location.href = baseUrl + "/";
+ }
+ pageCurr = curr;
+ if (typeof limit === 'function') {
+ limit();
+ }
+ }
+ });
+
+ form.on('submit(search)', function () {
+ var searchData = {};
+ $.each($('#search-box [name]').serializeArray(), function () {
+ searchData[this.name] = this.value;
+ });
+ tableIns.reload({
+ where: searchData,
+ page: {curr: 1},
+ done: function (res, curr) {
+ if (res.code === 403) {
+ top.location.href = baseUrl + "/";
+ }
+ pageCurr = curr;
+ if (typeof limit === 'function') {
+ limit();
+ }
+ }
+ });
+ return false;
+ });
+
+ form.on('submit(reset)', function () {
+ $('#search-box [name]').val('');
+ form.render();
+ tableIns.reload({
+ where: {},
+ page: {curr: 1}
+ });
+ return false;
+ });
+
+ table.on('tool(iotMqttOutbound)', function (obj) {
+ var data = obj.data;
+ if (obj.event === 'payload') {
+ layer.open({
+ type: 1,
+ title: '鍙戦�佹秷鎭綋',
+ area: ['720px', '520px'],
+ shadeClose: true,
+ content: '<div style="padding:12px;"><pre style="white-space:pre-wrap;word-break:break-all;margin:0;">'
+ + escHtml(data.publishPayload) + '</pre></div>'
+ });
+ } else if (obj.event === 'resend') {
+ layer.confirm('纭畾閲嶅彂璇ユ潯 MQTT锛�', function (index) {
+ $.ajax({
+ url: baseUrl + '/iotMqttOutbound/resend/auth',
+ headers: {token: localStorage.getItem('token')},
+ type: 'POST',
+ data: {id: data.id},
+ dataType: 'json',
+ success: function (res) {
+ layer.close(index);
+ if (res.code === 200) {
+ layer.msg('閲嶅彂鎴愬姛');
+ tableIns.reload({page: {curr: pageCurr || 1}});
+ } else {
+ layer.msg(res.msg || '閲嶅彂澶辫触', {icon: 2});
+ }
+ },
+ error: function () {
+ layer.close(index);
+ layer.msg('璇锋眰澶辫触', {icon: 2});
+ }
+ });
+ });
+ }
+ });
+});
diff --git a/src/main/webapp/views/iotMqttOutbound/iotMqttOutbound.html b/src/main/webapp/views/iotMqttOutbound/iotMqttOutbound.html
new file mode 100644
index 0000000..a786d77
--- /dev/null
+++ b/src/main/webapp/views/iotMqttOutbound/iotMqttOutbound.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>IoT MQTT 鍙戦�佽褰�</title>
+ <meta name="renderer" content="webkit">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+ <link rel="stylesheet" href="../../static/layui/css/layui.css" media="all">
+ <link rel="stylesheet" href="../../static/css/cool.css" media="all">
+ <link rel="stylesheet" href="../../static/css/common.css" media="all">
+</head>
+<body>
+
+<div class="layui-card" style="margin:8px;">
+ <div class="layui-card-header">MQTT 閰嶇疆锛坰ys_config锛屾敼 topic/寮�鍏冲悗璇峰厛淇濆瓨鍐嶇偣閲嶈繛锛�</div>
+ <div class="layui-card-body layui-form" lay-filter="iotMqttCfgForm">
+ <div class="layui-form-item">
+ <label class="layui-form-label">鎬诲紑鍏�</label>
+ <div class="layui-input-block">
+ <input type="checkbox" name="enabled" lay-skin="switch" lay-text="寮�|鍏�">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label">egressStow</label>
+ <div class="layui-input-block">
+ <input type="text" name="egressStow" autocomplete="off" class="layui-input" placeholder="璁㈤槄锛歑BPS鈫扺MS 缁勬墭">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label">egressPick</label>
+ <div class="layui-input-block">
+ <input type="text" name="egressPick" autocomplete="off" class="layui-input" placeholder="璁㈤槄锛氭嫞閫夋寚浠�">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label">egressFeedback</label>
+ <div class="layui-input-block">
+ <input type="text" name="egressFeedback" autocomplete="off" class="layui-input" placeholder="璁㈤槄锛歠eedback">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label">ingressStow</label>
+ <div class="layui-input-block">
+ <input type="text" name="ingressStow" autocomplete="off" class="layui-input" placeholder="鍙戝竷锛氬叆搴撳畬鎴�">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label">ingressPick</label>
+ <div class="layui-input-block">
+ <input type="text" name="ingressPick" autocomplete="off" class="layui-input" placeholder="鍙戝竷锛氬嚭搴撳畬鎴�">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label">ingressFeedback</label>
+ <div class="layui-input-block">
+ <input type="text" name="ingressFeedback" autocomplete="off" class="layui-input" placeholder="鍙戝竷锛歠eedback">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <div class="layui-input-block">
+ <button class="layui-btn layui-btn-primary" type="button" id="iotMqttReloadCfg">閲嶆柊鍔犺浇</button>
+ <button class="layui-btn" lay-submit lay-filter="iotMqttSave">淇濆瓨</button>
+ <button type="button" class="layui-btn layui-btn-normal" id="iotMqttReconnect">閲嶈繛 MQTT</button>
+ <span id="iotMqttConnState" style="margin-left:12px;color:#666;"></span>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div id="search-box" class="layui-form layui-card-header">
+ <div class="layui-inline">
+ <div class="layui-input-inline">
+ <input class="layui-input" type="text" name="instruction_id" placeholder="instructionId" autocomplete="off">
+ </div>
+ </div>
+ <div class="layui-inline">
+ <div class="layui-input-inline">
+ <input class="layui-input" type="text" name="publish_status" placeholder="鍙戝竷鐘舵�� PENDING/SUCCESS/FAILURE..." autocomplete="off">
+ </div>
+ </div>
+ <div class="layui-inline">
+ <div class="layui-input-inline">
+ <input class="layui-input" type="text" name="publish_topic" placeholder="鍙戦�佷富棰�" autocomplete="off">
+ </div>
+ </div>
+ <div class="layui-inline">
+ <div class="layui-input-inline">
+ <input class="layui-input" type="text" name="container_id" placeholder="瀹瑰櫒鍙�" autocomplete="off">
+ </div>
+ </div>
+ <div id="data-search-btn" class="layui-btn-container layui-form-item" style="display: inline-block">
+ <button id="search" class="layui-btn layui-btn-primary layui-btn-radius" lay-submit lay-filter="search">鎼滅储</button>
+ <button id="reset" class="layui-btn layui-btn-primary layui-btn-radius" lay-submit lay-filter="reset">閲嶇疆</button>
+ </div>
+</div>
+
+<table class="layui-hide" id="iotMqttOutbound" lay-filter="iotMqttOutbound"></table>
+
+<script type="text/html" id="iotMqttOutboundOperate">
+ <a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">娑堟伅浣�</a>
+ <a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="resend">閲嶅彂</a>
+</script>
+
+<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/iotMqttOutbound/iotMqttOutbound.js" charset="utf-8"></script>
+
+</body>
+</html>
diff --git a/src/test/java/com/zy/integration/iot/IotSupportTest.java b/src/test/java/com/zy/integration/iot/IotSupportTest.java
index b93cd14..63f3180 100644
--- a/src/test/java/com/zy/integration/iot/IotSupportTest.java
+++ b/src/test/java/com/zy/integration/iot/IotSupportTest.java
@@ -14,11 +14,18 @@
properties.setPort(8883);
properties.setThingName("thing-a");
- Assertions.assertEquals("ssl://example-ats.iot.cn-north-1.amazonaws.com.cn:8883", properties.getServerUri());
- Assertions.assertEquals("thing-a", properties.getResolvedClientId());
+ Assertions.assertEquals("mqtts://example-ats.iot.cn-north-1.amazonaws.com.cn:8883", properties.getServerUri());
+ String thingId = properties.getResolvedClientId();
+ Assertions.assertNotNull(thingId);
+ Assertions.assertTrue(thingId.startsWith("thing-a-"));
+ Assertions.assertEquals(thingId, properties.getResolvedClientId());
- properties.setClientId("client-b");
- Assertions.assertEquals("client-b", properties.getResolvedClientId());
+ IotProperties withClient = new IotProperties();
+ withClient.setClientId("client-b");
+ String cid = withClient.getResolvedClientId();
+ Assertions.assertNotNull(cid);
+ Assertions.assertTrue(cid.startsWith("client-b-"));
+ Assertions.assertEquals(cid, withClient.getResolvedClientId());
}
@Test
--
Gitblit v1.9.1