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