From 319ebd19adc182611096ef8ce45924e0d4c6cd04 Mon Sep 17 00:00:00 2001
From: yuhaiming <40624989@qq.com>
Date: Tue, 30 Jun 2026 08:12:35 +0800
Subject: [PATCH] =?UTF-8?q?MQTT=E9=80=9A=E4=BF=A1=E4=BF=AE=E6=94=B9=20=20?=
=?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=BA=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/mqtt-embedded-communication.md | 36 ++--
.../src/main/resources/application-dev.yml | 4 +-
.../src/main/resources/application-prod.yml | 20 +--
.../src/main/resources/application.yml | 1 +
.../common/json/config/JacksonConfig.java | 4 +-
water-common/water-common-mqtt/pom.xml | 5 +
.../mqtt/DeviceMqttCommandPublisher.java | 9 +-
water-modules/water-app/pom.xml | 5 +
.../dromara/app/controller/AppController.java | 162 +++++++++++++++++-
.../app/domain/mqtt/DeviceCommand.java | 1 +
.../app/handler/DeviceDataHandler.java | 11 +-
.../app/handler/DeviceRegisterHandler.java | 37 ++--
.../app/service/IDeviceCommandService.java | 18 ++
.../service/impl/AppDeviceServiceImpl.java | 7 +-
.../impl/DeviceCommandServiceImpl.java | 78 ++++++++-
15 files changed, 328 insertions(+), 70 deletions(-)
diff --git a/docs/mqtt-embedded-communication.md b/docs/mqtt-embedded-communication.md
index 7771ac3..d62ac57 100644
--- a/docs/mqtt-embedded-communication.md
+++ b/docs/mqtt-embedded-communication.md
@@ -2,6 +2,8 @@
本文档说明后端服务与嵌入式设备之间的 MQTT Topic、通信方向、Payload 类型和 JSON 数据格式。
+> **Topic 标识变更(v5.6 起)**:除首次注册回复 `registerDeviceNo` 仍按 **MAC 地址** 下发外,其余 MQTT 下行命令 Topic 第一段统一使用**设备编号 deviceNo**。后端上行处理通过 `DeviceIdentityResolver` 兼容 MAC / 设备编号两种入参;下行命令请按 `deviceNo` 订阅 `subscriber/cmd` 或 `subscriber/schedule`。业务字段(payload 中的 `deviceNo`、数据库主键、API 入参)仍是设备编号,未变更。
+
当前配置位置:
- 后端订阅配置:`water-admin/src/main/resources/application.yml`
@@ -14,8 +16,8 @@
| --- | --- |
| Broker | `mqtt.broker-url` |
| QoS | `mqtt.qos`,当前默认 `1` |
-| 设备编号 | Topic 第一段 `{deviceNo}` |
-| Topic 变量 | 文档中的 `{deviceNo}` 替换为真实设备编号,例如 `/01/publish/register` |
+| 设备标识 | 上行 Topic 第一段可为设备 **MAC** 或 `deviceNo`;下行命令 Topic 第一段统一使用 `deviceNo`,仅 `registerDeviceNo` 例外仍使用 MAC |
+| Topic 变量 | 上行文档中的 `{deviceNo}` 表示设备标识占位符;下行文档中的 `{deviceNo}` 即设备编号 |
| JSON 编码 | UTF-8 |
| 时间格式 | `yyyy-MM-dd HH:mm:ss` |
@@ -52,22 +54,19 @@
| Topic | `/{deviceNo}/publish/register` |
| Payload 类型 | JSON |
-后端以 Topic 中的 `{deviceNo}` 作为设备编号,payload 中的 `deviceNo` 即使传入也会被 Topic 覆盖。
+后端以 Topic 中的设备标识作为解析入口,payload 中的 `deviceNo` 即使传入也会被 Topic 覆盖。
示例:
```json
{
"deviceName": "一号浇水设备",
- "status": "1",
- "workStatus": "0",
"powerLevel": "86",
- "wifiName": "office-wifi",
"deviceEm": "WATER-EM-01",
"deviceSn": "SN202606100001",
"fwVer": "1.0.0",
- "macAddress": "AA:BB:CC:DD:EE:FF",
- "bindToken": "123456"
+ "macAddress": "AA:BB:CC:DD:EE:FF"
+
}
```
@@ -76,15 +75,11 @@
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| deviceName | string | 否 | 设备名称 |
-| status | string | 否 | 设备状态:`1` 在线,`0` 离线,`2` 到期,`3` 故障;为空时后端默认 `2` |
-| workStatus | string | 否 | 工作状态:`0` 休息,`1` 工作 |
| powerLevel | string | 否 | 电量 |
-| wifiName | string | 否 | WiFi 名称 |
| deviceEm | string | 否 | 设备型号 |
| deviceSn | string | 否 | 设备序列号 |
| fwVer | string | 否 | 固件版本 |
| macAddress | string | 否 | MAC 地址 |
-| bindToken | string | 否 | 设备绑定令牌 |
## 电量上报
@@ -99,7 +94,9 @@
```json
{
- "powerLevel": "86"
+ "deviceName": "Waterer_01",
+ "powerLevel": "86",
+ "charging": 1
}
```
@@ -193,6 +190,17 @@ receive
}
```
+## 设备编号下发
+
+| 项目 | 内容 |
+| --- | --- |
+| 功能名称 | 设备编号下发 |
+| 通信方向 | 后端发布,设备订阅 |
+| Topic | `/{deviceMac}/subscriber/cmd` |
+| Payload 类型 | JSON |
+
+说明:这是首次注册后的唯一例外,后端按设备 MAC 回传设备编号;其余下行命令都使用 `deviceNo` 作为 Topic 第一段。
+
## 排程任务完成上报
| 项目 | 内容 |
@@ -300,7 +308,7 @@ JSON 示例:
1. 设备启动后发布注册消息到 `/{deviceNo}/publish/register`。
2. 设备定时或电量变化时发布电量到 `/{deviceNo}/publish/power`。
-3. 设备订阅自己的命令 Topic:`/{deviceNo}/subscriber/cmd`。
+3. 设备订阅自己的命令 Topic:`/{deviceNo}/subscriber/cmd` 和 `/{deviceNo}/subscriber/schedule`。
4. 设备收到命令后立即返回 ACK 到 `/{deviceNo}/publish/ack`,推荐返回 JSON 并携带 `commandId`。
5. 排程或按键浇水完成后,分别发布到 `/{deviceNo}/publish/finish/schedule` 或 `/{deviceNo}/publish/finish/key`。
6. 硬件异常时发布到 `/{deviceNo}/publish/error`。
diff --git a/water-admin/src/main/resources/application-dev.yml b/water-admin/src/main/resources/application-dev.yml
index 204a118..32a2d5e 100644
--- a/water-admin/src/main/resources/application-dev.yml
+++ b/water-admin/src/main/resources/application-dev.yml
@@ -47,9 +47,9 @@ spring:
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
- url: jdbc:mysql://localhost:3306/water?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+ url: jdbc:mysql://47.97.217.123:3306/water?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
- password: 123456
+ password: 267a976b31c7a649
# # 从库数据源
# slave:
# lazy: true
diff --git a/water-admin/src/main/resources/application-prod.yml b/water-admin/src/main/resources/application-prod.yml
index 0521879..0b56e5c 100644
--- a/water-admin/src/main/resources/application-prod.yml
+++ b/water-admin/src/main/resources/application-prod.yml
@@ -50,9 +50,9 @@ spring:
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
- url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+ url: jdbc:mysql://localhost:3306/water?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
- password: root
+ password: 267a976b31c7a649
# # 从库数据源
# slave:
# lazy: true
@@ -105,7 +105,7 @@ spring.data:
# 数据库索引
database: 0
# redis 密码必须配置
- password: water123
+ password: waterx123
# 连接超时时间
timeout: 10s
# 是否开启ssl
@@ -136,17 +136,17 @@ redisson:
--- # mail 邮件发送
mail:
- enabled: false
+ enabled: true
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方,遵循RFC-822标准
- from: xxx@163.com
+ from: watexdevice@163.com
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
- user: xxx@163.com
+ user: watexdevice@163.com
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
- pass: xxxxxxxxxx
+ pass: XZVDXjRdVrjiX8N3
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
@@ -175,10 +175,10 @@ sms:
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: alibaba
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
- access-key-id: 您的accessKey
+ access-key-id: LTAI5t7xHqxFKG9DwMgurvrn
# 称为accessSecret有些称之为apiSecret
- access-key-secret: 您的accessKeySecret
- signature: 您的短信签名
+ access-key-secret: nNFjR8YwNJUcMrrtdc4pkpPsHmYtfp
+ signature: 山东春雨润集雨节灌设备
sdk-app-id: 您的sdkAppId
config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
diff --git a/water-admin/src/main/resources/application.yml b/water-admin/src/main/resources/application.yml
index d1354ca..b5c5881 100644
--- a/water-admin/src/main/resources/application.yml
+++ b/water-admin/src/main/resources/application.yml
@@ -59,6 +59,7 @@ mqtt:
enabled: true
interval-ms: 90000
topics:
+ # 订阅/下发主题第一段为设备 MAC(小写、去分隔符),上行业务侧由 DeviceIdentityResolver 解析为设备编号
subscribe:
- /+/publish/finish/schedule #排程任务完成上报
diff --git a/water-common/water-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java b/water-common/water-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java
index 10eb7a1..832e224 100644
--- a/water-common/water-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java
+++ b/water-common/water-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java
@@ -33,9 +33,9 @@ public class JacksonConfig {
public Module registerJavaTimeModule() {
// 全局配置序列化返回 JSON 处理
JavaTimeModule javaTimeModule = new JavaTimeModule();
- javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
+ javaTimeModule.addSerializer(Long.class, ToStringSerializer.instance);
javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
- javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
+ javaTimeModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
diff --git a/water-common/water-common-mqtt/pom.xml b/water-common/water-common-mqtt/pom.xml
index d4809f2..2f98a29 100644
--- a/water-common/water-common-mqtt/pom.xml
+++ b/water-common/water-common-mqtt/pom.xml
@@ -43,5 +43,10 @@
org.dromara
water-app
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
diff --git a/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/DeviceMqttCommandPublisher.java b/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/DeviceMqttCommandPublisher.java
index cd61323..a62dd74 100644
--- a/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/DeviceMqttCommandPublisher.java
+++ b/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/DeviceMqttCommandPublisher.java
@@ -3,6 +3,7 @@ package org.dromara.mqtt;
import lombok.RequiredArgsConstructor;
import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.service.IDeviceCommandPublisher;
+import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.mqtt.config.properties.MqttProperties;
@@ -44,14 +45,18 @@ public class DeviceMqttCommandPublisher implements IDeviceCommandPublisher {
}
private String buildCommandTopic(String deviceNo) {
+ if (StringUtils.isBlank(deviceNo)) {
+ throw new ServiceException("命令下发缺少设备编号,无法拼接主题");
+ }
+ String device = deviceNo.trim().toLowerCase();
String prefix = mqttProperties.getTopics().getPublishPrefix();
if (StringUtils.isBlank(prefix)) {
- return "/" + deviceNo + "/subscriber/cmd";
+ return "/" + device + "/subscriber/cmd";
}
String normalizedPrefix = prefix.startsWith("/") ? prefix : "/" + prefix;
if (normalizedPrefix.endsWith("/")) {
normalizedPrefix = normalizedPrefix.substring(0, normalizedPrefix.length() - 1);
}
- return normalizedPrefix + "/" + deviceNo + "/subscriber/cmd";
+ return normalizedPrefix + "/" + device + "/subscriber/cmd";
}
}
diff --git a/water-modules/water-app/pom.xml b/water-modules/water-app/pom.xml
index 92da839..e44f8cb 100644
--- a/water-modules/water-app/pom.xml
+++ b/water-modules/water-app/pom.xml
@@ -123,6 +123,11 @@
org.dromara
water-system
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java b/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java
index eef0c96..9b84ad5 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java
@@ -14,7 +14,6 @@ import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.bo.AppWateringLogBo;
-import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.domain.vo.*;
import org.dromara.app.service.*;
import org.dromara.app.service.impl.AppScheduleServiceImpl;
@@ -37,11 +36,16 @@ import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysUserBo;
+import org.dromara.system.domain.vo.SysOssUploadVo;
+import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.domain.vo.UserInfoVo;
+import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.ISysUserService;
+import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
import java.util.*;
@@ -61,6 +65,7 @@ public class AppController extends BaseController {
private final ISysUserService userService;
private final IAppWateringLogService appWateringLogService;
private final IDeviceCommandService deviceCommandService;
+ private final ISysOssService ossService;
/**
*查询设备
@@ -243,10 +248,15 @@ public class AppController extends BaseController {
continue;
}
assertDeviceOwned(deviceNo);
+ if (appSchedulingDeviceService.queryByScheduleIdAndDeviceNo(scheduleId, deviceNo) != null) {
+ continue;
+ }
AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
schedulingDeviceBo.setDeviceNo(deviceNo);
schedulingDeviceBo.setScheduleId(scheduleId);
- appSchedulingDeviceService.insertByBo(schedulingDeviceBo);
+ if (appSchedulingDeviceService.insertByBo(schedulingDeviceBo)) {
+ deviceCommandService.sendScheduleBindCommand(deviceNo, buildScheduleBindPayload(scheduleId, deviceNo));
+ }
}
return R.ok();
} catch (Exception e) {
@@ -277,6 +287,47 @@ public class AppController extends BaseController {
.toList();
}
+ private Map buildScheduleBindPayload(Long scheduleId, String deviceNo) {
+ // AppScheduleVo schedule = getOwnedSchedule(scheduleId);
+ List details = appScheduleDetailService.queryByScheduleIdByStatus(scheduleId);
+
+// Map schedulePayload = new HashMap<>();
+// schedulePayload.put("id", schedule.getId());
+// schedulePayload.put("name", schedule.getName());
+// schedulePayload.put("status", schedule.getStatus());
+
+ Map payload = new HashMap<>();
+ // payload.put("cmd", -1);
+ payload.put("deviceNo", deviceNo);
+ // payload.put("schedule", schedulePayload);
+ payload.put("details", toScheduleDetailPayload(details));
+ return payload;
+ // return toScheduleDetailPayload(details);
+ }
+
+ private List