Procházet zdrojové kódy

1、构建通用的Web端->后端消息->设备MQTT,双向json通信

liweimin před 2 týdny
rodič
revize
03409598e3

+ 23 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/annotation/JsonCmdDownHandler.java

@@ -0,0 +1,23 @@
+package com.ruoyi.device.mqtt.annotation;
+
+import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.*;
+
+/**
+ * JSON Body 业务命令 下行处理器(按 {@link CmdTypeEnum cmdDownType} 注册)
+ *
+ * @author lwm
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface JsonCmdDownHandler
+{
+    /**
+     * 下行业务命令类型
+     */
+    CmdTypeEnum cmdType();
+}

+ 0 - 27
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/annotation/JsonCmdHandler.java

@@ -1,27 +0,0 @@
-package com.ruoyi.device.mqtt.annotation;
-
-import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
-import org.springframework.stereotype.Component;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * JSON Body 业务命令处理器(按 {@link CmdTypeEnum} 注册)
- *
- * @author lwm
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-@Component
-public @interface JsonCmdHandler
-{
-    /**
-     * 业务命令类型
-     */
-    CmdTypeEnum cmdType();
-}

+ 23 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/annotation/JsonCmdUpHandler.java

@@ -0,0 +1,23 @@
+package com.ruoyi.device.mqtt.annotation;
+
+import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.*;
+
+/**
+ * JSON Body 业务命令 上行处理器(按 {@link CmdTypeEnum cmdUpType} 注册)
+ *
+ * @author lwm
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface JsonCmdUpHandler
+{
+    /**
+     * 上行业务命令类型
+     */
+    CmdTypeEnum cmdType();
+}

+ 98 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/JsonCmdHandlerManager.java

@@ -0,0 +1,98 @@
+package com.ruoyi.device.mqtt.handler;
+
+import com.ruoyi.device.mqtt.annotation.JsonCmdDownHandler;
+import com.ruoyi.device.mqtt.annotation.JsonCmdUpHandler;
+import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
+import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdUpHandler;
+import com.ruoyi.device.mqtt.handler.encoder.json.IJsonCmdDownHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 按 {@link CmdTypeEnum} 将 JSON 上行/下行分发
+ *
+ * @author lwm
+ */
+@Component
+public class JsonCmdHandlerManager implements ApplicationListener<ContextRefreshedEvent>
+{
+    private static final Logger log = LoggerFactory.getLogger(JsonCmdHandlerManager.class);
+
+    private final Map<String, IJsonCmdUpHandler> UP_HANDLER_BY_UP_KEY = new HashMap<>();
+    private final Map<String, IJsonCmdUpHandler> UP_HANDLER_BY_BASE_KEY = new HashMap<>();
+
+    private final Map<String, IJsonCmdDownHandler> DOWN_HANDLER_BY_DOWN_KEY = new HashMap<>();
+    private final Map<String, IJsonCmdDownHandler> DOWN_HANDLER_BY_BASE_KEY = new HashMap<>();
+
+    /**
+     * 监听spring容器启动完成,将所有JsonCmdHandler缓存起来
+     *
+     * @param event 上下文
+     */
+    @Override
+    public void onApplicationEvent(ContextRefreshedEvent event)
+    {
+        ApplicationContext ctx = event.getApplicationContext();
+
+        Map<String, Object> jsonCmdUpHandler = ctx.getBeansWithAnnotation(JsonCmdUpHandler.class);
+        for (Object bean : jsonCmdUpHandler.values())
+        {
+            JsonCmdUpHandler annotation = bean.getClass().getAnnotation(JsonCmdUpHandler.class);
+            CmdTypeEnum cmdTypeEnum = annotation.cmdType();
+            UP_HANDLER_BY_UP_KEY.put(cmdTypeEnum.getCmdUpType(), (IJsonCmdUpHandler) bean);
+            UP_HANDLER_BY_BASE_KEY.put(cmdTypeEnum.getCmdType(), (IJsonCmdUpHandler) bean);
+            log.info("加载 JSON 命令上行处理器 upKey={}, baseKey={}, handler={}",
+                    cmdTypeEnum.getCmdUpType(), cmdTypeEnum.getCmdType(), bean.getClass().getSimpleName());
+        }
+
+        Map<String, Object> jsonCmdDownHandler = ctx.getBeansWithAnnotation(JsonCmdDownHandler.class);
+        for (Object bean : jsonCmdDownHandler.values())
+        {
+            JsonCmdDownHandler annotation = bean.getClass().getAnnotation(JsonCmdDownHandler.class);
+            CmdTypeEnum cmdTypeEnum = annotation.cmdType();
+            DOWN_HANDLER_BY_DOWN_KEY.put(cmdTypeEnum.getCmdDownType(), (IJsonCmdDownHandler) bean);
+            DOWN_HANDLER_BY_BASE_KEY.put(cmdTypeEnum.getCmdType(), (IJsonCmdDownHandler) bean);
+            log.info("加载 JSON 命令下行处理器 downKey={}, baseKey={}, handler={}",
+                    cmdTypeEnum.getCmdDownType(), cmdTypeEnum.getCmdType(), bean.getClass().getSimpleName());
+        }
+    }
+
+    /**
+     * 按 upKey 获取上行处理器
+     */
+    public IJsonCmdUpHandler getUpHandlerByUpKey(String upKey)
+    {
+        return UP_HANDLER_BY_UP_KEY.get(upKey);
+    }
+
+    /**
+     * 按 baseKey 获取上行处理器
+     */
+    public IJsonCmdUpHandler getUpHandlerByBaseKey(String baseKey)
+    {
+        return UP_HANDLER_BY_BASE_KEY.get(baseKey);
+    }
+
+    /**
+     * 按 downKey 获取下行处理器
+     */
+    public IJsonCmdDownHandler getDownHandlerByDownKey(String downKey)
+    {
+        return DOWN_HANDLER_BY_DOWN_KEY.get(downKey);
+    }
+
+    /**
+     * 按 baseKey 获取下行处理器
+     */
+    public IJsonCmdDownHandler getDownHandlerByBaseKey(String baseKey)
+    {
+        return DOWN_HANDLER_BY_BASE_KEY.get(baseKey);
+    }
+}

+ 2 - 2
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/IJsonCmdHandler.java → ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/IJsonCmdUpHandler.java

@@ -6,11 +6,11 @@ import com.ruoyi.device.mqtt.vo.CommonHeader;
 import com.ruoyi.device.mqtt.vo.CommonTopic;
 
 /**
- * JSON Body 按 cmdType 分发的业务处理器
+ * JSON Body 按 cmdUpType 分发的业务处理器
  *
  * @author lwm
  */
-public interface IJsonCmdHandler
+public interface IJsonCmdUpHandler
 {
     /**
      * @param topic     上行 topic 解析结果

+ 3 - 2
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/JsonBodyDecoder.java

@@ -7,6 +7,7 @@ import com.ruoyi.device.mqtt.annotation.ConsumerHandler;
 import com.ruoyi.device.mqtt.domain.BaseJsonBody;
 import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
 import com.ruoyi.device.mqtt.enums.MsgTypeEnum;
+import com.ruoyi.device.mqtt.handler.JsonCmdHandlerManager;
 import com.ruoyi.device.mqtt.handler.decoder.AbstractDecoder;
 import com.ruoyi.device.mqtt.vo.CommonHeader;
 import com.ruoyi.device.mqtt.vo.CommonTopic;
@@ -81,10 +82,10 @@ public class JsonBodyDecoder extends AbstractDecoder<BaseJsonBody>
         }
 
         // 4、进行业务处理
-        IJsonCmdHandler handler = jsonCmdHandlerManager.getHandlerByUpKey(cmd.getCmdUpType());
+        IJsonCmdUpHandler handler = jsonCmdHandlerManager.getUpHandlerByUpKey(cmd.getCmdUpType());
         if (handler == null)
         {
-            handler = jsonCmdHandlerManager.getHandlerByBaseKey(cmd.getCmdType());
+            handler = jsonCmdHandlerManager.getUpHandlerByBaseKey(cmd.getCmdType());
         }
         if (handler == null)
         {

+ 0 - 65
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/JsonCmdHandlerManager.java

@@ -1,65 +0,0 @@
-package com.ruoyi.device.mqtt.handler.decoder.json;
-
-import com.ruoyi.device.mqtt.annotation.JsonCmdHandler;
-import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationListener;
-import org.springframework.context.event.ContextRefreshedEvent;
-import org.springframework.stereotype.Component;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * 按 {@link CmdTypeEnum} 将 JSON 上行分发给各 {@link IJsonCmdHandler}
- *
- * @author lwm
- */
-@Component
-public class JsonCmdHandlerManager implements ApplicationListener<ContextRefreshedEvent>
-{
-    private static final Logger log = LoggerFactory.getLogger(JsonCmdHandlerManager.class);
-
-    private final Map<String, IJsonCmdHandler> HANDLER_BY_UP_KEY = new HashMap<>();
-    private final Map<String, IJsonCmdHandler> HANDLER_BY_BASE_KEY = new HashMap<>();
-
-    /**
-     * 监听spring容器启动完成,将所有JsonCmdHandler缓存起来
-     *
-     * @param event 上下文
-     */
-    @Override
-    public void onApplicationEvent(ContextRefreshedEvent event)
-    {
-        ApplicationContext ctx = event.getApplicationContext();
-
-        Map<String, Object> jsonCmdHandler = ctx.getBeansWithAnnotation(JsonCmdHandler.class);
-        for (Object bean : jsonCmdHandler.values())
-        {
-            JsonCmdHandler annotation = bean.getClass().getAnnotation(JsonCmdHandler.class);
-            CmdTypeEnum cmdTypeEnum = annotation.cmdType();
-            HANDLER_BY_UP_KEY.put(cmdTypeEnum.getCmdUpType(), (IJsonCmdHandler) bean);
-            HANDLER_BY_BASE_KEY.put(cmdTypeEnum.getCmdType(), (IJsonCmdHandler) bean);
-            log.info("加载 JSON 命令处理器 upKey={}, baseKey={}, handler={}",
-                    cmdTypeEnum.getCmdUpType(), cmdTypeEnum.getCmdType(), bean.getClass().getSimpleName());
-        }
-    }
-
-    /**
-     * 按 upKey 获取处理器
-     */
-    public IJsonCmdHandler getHandlerByUpKey(String upKey)
-    {
-        return HANDLER_BY_UP_KEY.get(upKey);
-    }
-
-    /**
-     * 按 baseKey 获取处理器
-     */
-    public IJsonCmdHandler getHandlerByBaseKey(String baseKey)
-    {
-        return HANDLER_BY_BASE_KEY.get(baseKey);
-    }
-}

+ 4 - 4
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/service/DeviceLoginService.java

@@ -7,13 +7,13 @@ import com.ruoyi.common.enums.UserStatus;
 import com.ruoyi.common.utils.SecurityUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.device.domain.model.TsbUserDeviceBind;
-import com.ruoyi.device.mqtt.annotation.JsonCmdHandler;
+import com.ruoyi.device.mqtt.annotation.JsonCmdUpHandler;
 import com.ruoyi.device.mqtt.constants.MqttConstants;
 import com.ruoyi.device.mqtt.domain.BaseJsonBody;
 import com.ruoyi.device.mqtt.domain.decoder.DeviceLoginRequest;
 import com.ruoyi.device.mqtt.domain.encoder.DeviceLoginResponse;
 import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
-import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdHandler;
+import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdUpHandler;
 import com.ruoyi.device.mqtt.vo.CommonHeader;
 import com.ruoyi.device.mqtt.vo.CommonTopic;
 import com.ruoyi.device.service.ITsbUserDeviceService;
@@ -34,8 +34,8 @@ import java.util.stream.Collectors;
  *
  * @author lwm
  */
-@JsonCmdHandler(cmdType = CmdTypeEnum.TSB_LOGIN)
-public class DeviceLoginService implements IJsonCmdHandler
+@JsonCmdUpHandler(cmdType = CmdTypeEnum.TSB_LOGIN)
+public class DeviceLoginService implements IJsonCmdUpHandler
 {
     private static final Logger log = LoggerFactory.getLogger(DeviceLoginService.class);
 

+ 4 - 4
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/service/DevicePtService.java

@@ -6,13 +6,13 @@ import com.ruoyi.common.utils.DateUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.device.domain.entity.TsbDevice;
 import com.ruoyi.device.mapper.TsbDeviceMapper;
-import com.ruoyi.device.mqtt.annotation.JsonCmdHandler;
+import com.ruoyi.device.mqtt.annotation.JsonCmdUpHandler;
 import com.ruoyi.device.mqtt.constants.MqttConstants;
 import com.ruoyi.device.mqtt.domain.BaseJsonBody;
 import com.ruoyi.device.mqtt.domain.decoder.DevicePtRequest;
 import com.ruoyi.device.mqtt.domain.encoder.DevicePtResponse;
 import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
-import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdHandler;
+import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdUpHandler;
 import com.ruoyi.device.mqtt.vo.CommonHeader;
 import com.ruoyi.device.mqtt.vo.CommonTopic;
 import jakarta.annotation.Resource;
@@ -24,8 +24,8 @@ import org.slf4j.LoggerFactory;
  *
  * @author lwm
  */
-@JsonCmdHandler(cmdType = CmdTypeEnum.TSB_PT)
-public class DevicePtService implements IJsonCmdHandler
+@JsonCmdUpHandler(cmdType = CmdTypeEnum.TSB_PT)
+public class DevicePtService implements IJsonCmdUpHandler
 {
     private static final Logger log = LoggerFactory.getLogger(DevicePtService.class);
 

+ 26 - 7
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/decoder/json/service/TaxDataUpService.java

@@ -1,36 +1,55 @@
 package com.ruoyi.device.mqtt.handler.decoder.json.service;
 
 import com.alibaba.fastjson2.JSON;
-import com.ruoyi.device.mqtt.annotation.JsonCmdHandler;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.device.mqtt.annotation.JsonCmdUpHandler;
 import com.ruoyi.device.mqtt.domain.BaseJsonBody;
 import com.ruoyi.device.mqtt.domain.decoder.TaxDataUp;
 import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
-import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdHandler;
+import com.ruoyi.device.mqtt.handler.decoder.json.IJsonCmdUpHandler;
 import com.ruoyi.device.mqtt.vo.CommonHeader;
 import com.ruoyi.device.mqtt.vo.CommonTopic;
+import com.ruoyi.device.websocket.TsbWebSocketService;
+import jakarta.annotation.Resource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * 报税口上行数据
+ * 报税口上行:common:tax:up
+ * <p>
+ * 解析设备页面数据并推送到 WebSocket(无 MQTT 下行应答)。
  *
  * @author lwm
  */
-@JsonCmdHandler(cmdType = CmdTypeEnum.COMMON_TAX)
-public class TaxDataUpService implements IJsonCmdHandler
+@JsonCmdUpHandler(cmdType = CmdTypeEnum.COMMON_TAX)
+public class TaxDataUpService implements IJsonCmdUpHandler
 {
     private static final Logger log = LoggerFactory.getLogger(TaxDataUpService.class);
 
+    @Resource
+    private TsbWebSocketService tsbWebSocketService;
+
     @Override
     public BaseJsonBody handle(CommonTopic topic, CommonHeader header, String bodyJson, CmdTypeEnum cmd)
     {
-        TaxDataUp taxDataUp = JSON.parseObject(bodyJson, TaxDataUp.class);
+        // 1、JSON 反序列化(保留原始 key,避免未传字段被补成 null 推到前端)
+        // 因为 只传几个字段,就改变几个字段:部分字段为null,就清空显示
+        JSONObject bodyObj = JSON.parseObject(bodyJson);
+        if (bodyObj == null)
+        {
+            log.warn("报税口报文体解析失败, deviceSn={}", topic.getDeviceSn());
+            return BaseJsonBody.fail(topic.getDeviceType(), topic.getDeviceSn(), cmd.getCmdDownType(), "报税口报文体解析失败");
+        }
+        TaxDataUp taxDataUp = bodyObj.to(TaxDataUp.class);
         if (taxDataUp == null)
         {
-            log.warn("报税口报文体解析失败");
+            log.warn("报税口报文体解析失败, deviceSn={}", topic.getDeviceSn());
             return BaseJsonBody.fail(topic.getDeviceType(), topic.getDeviceSn(), cmd.getCmdDownType(), "报税口报文体解析失败");
         }
 
+        // 2、推送 Web 端
+        tsbWebSocketService.pushPageSyncFromDevice(cmd.getCmdType(), bodyObj);
+        // 3、无需下行应答
         return null;
     }
 }

+ 21 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/encoder/json/IJsonCmdDownHandler.java

@@ -0,0 +1,21 @@
+package com.ruoyi.device.mqtt.handler.encoder.json;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.device.domain.model.TsbUserDeviceBind;
+import com.ruoyi.device.mqtt.domain.BaseJsonBody;
+import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
+
+/**
+ * JSON Body 按 cmdDownType 分发的业务处理器
+ *
+ * @author lwm
+ */
+public interface IJsonCmdDownHandler
+{
+    /**
+     * @param bind      绑定信息
+     * @param data      业务数据
+     * @param cmd       已匹配的下行命令枚举
+     */
+    BaseJsonBody handle(TsbUserDeviceBind bind, JSONObject data, CmdTypeEnum cmd);
+}

+ 39 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/encoder/json/service/TaxDataDownService.java

@@ -0,0 +1,39 @@
+package com.ruoyi.device.mqtt.handler.encoder.json.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.device.domain.model.TsbUserDeviceBind;
+import com.ruoyi.device.mqtt.annotation.JsonCmdDownHandler;
+import com.ruoyi.device.mqtt.domain.BaseJsonBody;
+import com.ruoyi.device.mqtt.domain.encoder.TaxDataDown;
+import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
+import com.ruoyi.device.mqtt.handler.encoder.json.IJsonCmdDownHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 报税口下行:common:tax:down
+ * <p>
+ * 收到 WebSocket页面数据并推送到 设备 MQTT 下行应答
+ *
+ * @author lwm
+ */
+@JsonCmdDownHandler(cmdType = CmdTypeEnum.COMMON_TAX)
+public class TaxDataDownService implements IJsonCmdDownHandler
+{
+    private static final Logger log = LoggerFactory.getLogger(TaxDataDownService.class);
+
+    @Override
+    public BaseJsonBody handle(TsbUserDeviceBind bind, JSONObject data, CmdTypeEnum cmd)
+    {
+        TaxDataDown taxDataDown = data.to(TaxDataDown.class);
+        if (taxDataDown == null)
+        {
+            log.warn("报税口报文体解析失败, deviceSn={}", bind.getDeviceSn());
+            return null;
+        }
+        taxDataDown.setDeviceType(bind.getDeviceType());
+        taxDataDown.setDeviceSn(bind.getDeviceSn());
+        taxDataDown.setCmdType(cmd.getCmdDownType());
+        return taxDataDown;
+    }
+}

+ 20 - 8
ruoyi-device/src/main/java/com/ruoyi/device/websocket/TsbWebSocketService.java

@@ -7,12 +7,13 @@ import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.device.domain.model.TsbUserDeviceBind;
 import com.ruoyi.device.mapper.TsbUserDeviceMapper;
 import com.ruoyi.device.mqtt.domain.BaseJsonBody;
-import com.ruoyi.device.mqtt.domain.encoder.TaxDataDown;
 import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
 import com.ruoyi.device.mqtt.enums.MsgTypeEnum;
 import com.ruoyi.device.mqtt.handler.DeviceOnlineManager;
 import com.ruoyi.device.mqtt.handler.HandlerManager;
+import com.ruoyi.device.mqtt.handler.JsonCmdHandlerManager;
 import com.ruoyi.device.mqtt.handler.encoder.IEncoder;
+import com.ruoyi.device.mqtt.handler.encoder.json.IJsonCmdDownHandler;
 import com.ruoyi.device.mqtt.util.MsgHandlerUtil;
 import com.ruoyi.device.websocket.model.TsbWebSocketMessage;
 import com.ruoyi.framework.web.service.TokenService;
@@ -45,6 +46,9 @@ public class TsbWebSocketService
     private HandlerManager handlerManager;
 
     @Resource
+    private JsonCmdHandlerManager jsonCmdHandlerManager;
+
+    @Resource
     private DeviceOnlineManager deviceOnlineManager;
 
     /**
@@ -126,12 +130,20 @@ public class TsbWebSocketService
             return;
         }
 
-        // 3、发送消息
-        TaxDataDown down = tsbWebSocketMessage.getData() == null ?
-                new TaxDataDown() : tsbWebSocketMessage.getData().toJavaObject(TaxDataDown.class);
-        down.setDeviceType(bind.getDeviceType());
-        down.setDeviceSn(bind.getDeviceSn());
-        down.setCmdType(cmdType.getCmdDownType());
+        // 3、Web data → MQTT 下行体(保留 JSONObject 全部字段)
+        IJsonCmdDownHandler handler = jsonCmdHandlerManager.getDownHandlerByDownKey(cmdType.getCmdDownType());
+        if (handler == null)
+        {
+            handler = jsonCmdHandlerManager.getDownHandlerByBaseKey(cmdType.getCmdType());
+        }
+        if (handler == null)
+        {
+            log.warn("未注册 JSON 命令处理器 cmd={}", cmdType);
+            TsbWebSocketUsers.sendMessageToUserByText(session,
+                    TsbWebSocketMessage.fail(tsbWebSocketMessage.getCmdType(), "未实现的服务处理器"));
+            return;
+        }
+        BaseJsonBody baseJsonBody = handler.handle(bind, tsbWebSocketMessage.getData(), cmdType);
         log.info("Web端 -> 设备 MQTT 同步消息, userId={}, deviceSn={}", bind.getUserId(), bind.getDeviceSn());
         String key = MsgHandlerUtil.getEncoderKey(MsgTypeEnum.JSON_BODY);
         IEncoder<BaseJsonBody> encoder = (IEncoder<BaseJsonBody>) handlerManager.getEncoder(key);
@@ -142,7 +154,7 @@ public class TsbWebSocketService
         }
         log.info("MQTT 下行发送, cmdType={}, deviceType={}, deviceSn={}",
                 cmdType.getCmdType(), bind.getDeviceType(), bind.getDeviceSn());
-        encoder.encode(down);
+        encoder.encode(baseJsonBody);
     }
 
     /**