改用自建emqx

This commit is contained in:
2025-06-05 17:34:07 +08:00
parent 9227eb07a2
commit f132b557df
4 changed files with 67 additions and 38 deletions

View File

@@ -125,3 +125,33 @@
- 修复了 `charging_web_app``my-sessions` 页面 (`charging_web_app/src/app/(authenticated)/my-sessions/page.tsx`) 获取用户充电记录列表的接口调用。 - 修复了 `charging_web_app``my-sessions` 页面 (`charging_web_app/src/app/(authenticated)/my-sessions/page.tsx`) 获取用户充电记录列表的接口调用。
- 问题:原请求路径为 `/session/my/list/page`,缺少了 API 代理前缀 `/api` - 问题:原请求路径为 `/session/my/list/page`,缺少了 API 代理前缀 `/api`
- 解决:将请求路径修改为 `/api/session/my/list/page`,确保请求能正确通过 Next.js 代理到后端服务。 - 解决:将请求路径修改为 `/api/session/my/list/page`,确保请求能正确通过 Next.js 代理到后端服务。
## YYYY-MM-DD (请替换为今天的实际日期)
- **管理员会话管理功能方案调整**:
- 根据决策,从《第五阶段:管理员会话管理功能开发方案》(`springboot-init-main/doc/development_stages/stage_5_admin_session_management.md`) 中移除了管理员直接删除充电会话的功能。
- 主要移除内容包括相关的后端 Service 方法、Controller 接口定义以及前端对应的操作按钮和API调用封装。
- 此举旨在降低误操作风险并保留完整的数据记录。
- **前端管理员控制台界面调整**:
- 从管理员控制台页面 (`charging_web_app/src/app/(authenticated)/admin/dashboard/page.tsx`) 移除了"会话管理"导航卡片按钮,以匹配后端不再提供管理员直接删除会话功能的调整。
## YYYY-MM-DD (请替换为今天的实际日期)
- **MQTT 服务器域名更新**:
- 将后端 (`springboot-init-main/src/main/resources/application.yml`) 和单片机 (`mqtt_esp32_client/mqtt_esp32_client.ino`) 的 MQTT Broker 地址从 `broker.emqx.io` 更新为 `yuyun-hk1.stormrain.cn`
- 确保设备与自建 MQTT 服务器的通信正常进行。
## YYYY-MM-DD (请替换为今天的实际日期)
- **修复后端 MQTT 消息处理逻辑**:
- 修正 `springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java`,确保在机器人完成 `MOVE_TO_SPOT` 任务后,能够正确调用 `ChargingSessionService.handleRobotArrival` 方法更新充电会话状态。
- **具体变更**
-`MqttMessageHandler` 中注入 `ChargingSessionService`
-`handleRobotStatusUpdate` 方法中,当接收到 `COMPLETED` 状态的 `RobotTask` 消息,并且该任务的 `commandType``MOVE_TO_SPOT` 时,调用 `chargingSessionService.handleRobotArrival`,传入相关的会话 ID 和任务 ID。
## YYYY-MM-DD (请替换为今天的实际日期)
- **修复单片机 MQTT ACK 消息格式**:
- 修改 `mqtt_esp32_client/mqtt_esp32_client.ino`,使其发送的 ACK 消息符合后端 `MqttMessageHandlerImpl` 的期望格式。
- **具体变更**
- 删除旧的 `publish_ack_message` 函数。
- 修改 `publish_status_update` 函数,使其只发送通用状态更新和心跳消息。
- 新增一个 `publish_ack_message` 函数,用于构建包含 `command_ack`(指令中文描述)、`task_id`(数字类型)和 `success`(布尔类型)的 JSON 结构。
-`STOP_CHARGE` 指令的 ACK 中添加 `energy_kwh``duration_s` 字段。
- 更新 `callback` 函数中所有 `publish_ack_message` 的调用,以使用新的函数签名和参数。

View File

@@ -4,11 +4,11 @@
// ----------- 设备配置 (需要为您自己的设备和环境修改) ----------- // ----------- 设备配置 (需要为您自己的设备和环境修改) -----------
// WiFi // WiFi
const char *ssid = "UFI_DB50CD"; // 请输入您的 Wi-Fi 名称 const char *ssid = "WEIHANG3718"; // 请输入您的 Wi-Fi 名称
const char *password = "1349534012"; // 请输入您的 Wi-Fi 密码 const char *password = "@05Tu146"; // 请输入您的 Wi-Fi 密码
// MQTT Broker // MQTT Broker
const char *mqtt_broker = "broker.emqx.io"; // 您的 MQTT Broker 地址 const char *mqtt_broker = "yuyun-hk1.stormrain.cn"; // 您的 MQTT Broker 地址
const char *mqtt_username = "emqx"; // 您的 MQTT 用户名 (如果需要) const char *mqtt_username = "emqx"; // 您的 MQTT 用户名 (如果需要)
const char *mqtt_password = "public"; // 您的 MQTT 密码 (如果需要) const char *mqtt_password = "public"; // 您的 MQTT 密码 (如果需要)
const int mqtt_port = 1883; // 您的 MQTT 端口 const int mqtt_port = 1883; // 您的 MQTT 端口
@@ -130,7 +130,7 @@ void callback(char *topic, byte *payload, unsigned int length) {
if (cmdType == nullptr || taskId == nullptr) { if (cmdType == nullptr || taskId == nullptr) {
Serial.println("指令JSON缺少 commandType/command 或 taskId 字段。"); Serial.println("指令JSON缺少 commandType/command 或 taskId 字段。");
publish_ack_message(taskId, false, "Command JSON invalid (missing commandType/command or taskId)", nullptr); publish_ack_message(0, "指令解析失败", false, "Command JSON invalid (missing commandType/command or taskId)");
return; return;
} }
@@ -170,7 +170,7 @@ void callback(char *topic, byte *payload, unsigned int length) {
chargeStartTimeMillis = millis(); chargeStartTimeMillis = millis();
Serial.println("状态更新: CHARGING (已到达目标车位 " + currentSpotId + ",视为开始充电)"); Serial.println("状态更新: CHARGING (已到达目标车位 " + currentSpotId + ",视为开始充电)");
publish_ack_message(taskId, true, "Robot arrived at spot and started charging (simulated)", currentSpotId.c_str()); publish_ack_message(taskId, "移动到指定点", true, "Robot arrived at spot and started charging (simulated)");
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr);
@@ -196,7 +196,7 @@ void callback(char *topic, byte *payload, unsigned int length) {
currentSessionId = ""; currentSessionId = "";
} }
Serial.println("模拟: 充电已启动。会话ID: " + currentSessionId + ", 车位: " + currentSpotId); Serial.println("模拟: 充电已启动。会话ID: " + currentSessionId + ", 车位: " + currentSpotId);
publish_ack_message(taskId, true, "Charging started successfully", currentSessionId.c_str()); publish_ack_message(taskId, "开始充电", true, "Charging started successfully");
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr);
} else if (strcmp(cmdType, "STOP_CHARGE") == 0) { } else if (strcmp(cmdType, "STOP_CHARGE") == 0) {
@@ -214,7 +214,7 @@ void callback(char *topic, byte *payload, unsigned int length) {
currentSessionId = ""; currentSessionId = "";
// 在ACK中上报准确的充电时长如果需要的话可以通过修改 publish_ack_message 或在 message 字段中添加 // 在ACK中上报准确的充电时长如果需要的话可以通过修改 publish_ack_message 或在 message 字段中添加
// For now, the generic ACK is sent. // For now, the generic ACK is sent.
publish_ack_message(taskId, true, ("Charging stopped. Duration: " + String(chargeDuration) + "s").c_str(), previousSessionId.c_str()); publish_ack_message(taskId, "停止充电", true, ("Charging stopped. Duration: " + String(chargeDuration) + "s").c_str(), currentEnergyConsumed, chargeDuration);
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr);
} }
// Add other commandType handling here if needed, e.g., "QUERY_STATUS" // Add other commandType handling here if needed, e.g., "QUERY_STATUS"
@@ -225,7 +225,7 @@ void callback(char *topic, byte *payload, unsigned int length) {
// } // }
else { else {
Serial.println("未知指令 commandType: " + String(cmdType)); Serial.println("未知指令 commandType: " + String(cmdType));
publish_ack_message(taskId, false, ("Unknown commandType: " + String(cmdType)).c_str(), nullptr); publish_ack_message(taskId, "未知指令", false, ("Unknown commandType: " + String(cmdType)).c_str());
} }
Serial.println("-----------------------"); Serial.println("-----------------------");
} }
@@ -323,18 +323,30 @@ void publish_heartbeat() {
lastHeartbeatTime = millis(); lastHeartbeatTime = millis();
} }
// Simplified ACK message function // 新增ACK消息发布函数以符合后端期望的格式
void publish_ack_message(const char* taskId, bool success, const char* message, const char* contextInfo) { void publish_ack_message(long taskId, const char* commandAckStr, bool success, const char* message, float energyKwh = -1.0f, int durationSeconds = -1) {
if (!taskId || strlen(taskId) == 0) { StaticJsonDocument<256> doc; // 调整大小以适应所有字段
Serial.println("无法发送ACK: taskId 为空");
return; doc["robotUid"] = spotUid;
doc["command_ack"] = commandAckStr; // 指令的中文描述
doc["task_id"] = taskId; // 任务ID (数字类型)
doc["success"] = success; // 成功状态 (布尔类型)
if (message && strlen(message) > 0) {
doc["message"] = message;
} }
// Use the main publish_status_update function formatted as an ACK
// For contextInfo, we can pass spotId if relevant, or sessionId if that's what backend expects for ACKs. // 针对 STOP_CHARGE 指令的额外字段
// The 'true' indicates it's an ACK. if (strcmp(commandAckStr, "停止充电") == 0) {
// The ackErrorCode field in publish_status_update will be set to "SUCCESS_ACK" or "FAILURE_ACK" if (energyKwh >= 0) {
// 根据用户要求ACK中的errorCode也暂时简化或移除。如果保留确保含义清晰。 doc["energy_kwh"] = energyKwh;
publish_status_update(true, taskId, success ? "SUCCESS" : "FAILURE", message, nullptr, contextInfo); }
if (durationSeconds >= 0) {
doc["duration_s"] = durationSeconds;
}
}
publish_message(topic_uplink_to_backend, doc, "ACK");
} }
void setup() { void setup() {

View File

@@ -14,9 +14,6 @@
* **更新 (Update)**: * **更新 (Update)**:
* **谨慎引入**。主要考虑场景:对异常会话进行状态纠正(例如,系统未能自动更新的会话,管理员可手动标记为 `ERROR``CANCELLED_BY_SYSTEM`)。 * **谨慎引入**。主要考虑场景:对异常会话进行状态纠正(例如,系统未能自动更新的会话,管理员可手动标记为 `ERROR``CANCELLED_BY_SYSTEM`)。
* 所有手动更新操作必须记录详细的操作审计日志(操作人、时间、原因、变更前后状态)。 * 所有手动更新操作必须记录详细的操作审计日志(操作人、时间、原因、变更前后状态)。
* **删除 (Delete)**:
* **谨慎引入**。通常仅支持逻辑删除,用于隐藏无效或测试产生的会话数据。
* 物理删除会话记录通常不推荐,以保证数据完整性和可追溯性。
* **创建 (Create)**: * **创建 (Create)**:
* 管理员**不直接创建**充电会话。会话由用户通过前端发起,或由系统在特定业务流程中自动创建。 * 管理员**不直接创建**充电会话。会话由用户通过前端发起,或由系统在特定业务流程中自动创建。
@@ -81,10 +78,6 @@
* 更新会话状态、`update_time` * 更新会话状态、`update_time`
* 记录操作日志(可考虑引入操作日志表或利用现有日志框架)。 * 记录操作日志(可考虑引入操作日志表或利用现有日志框架)。
* `adminUserId` 用于审计。 * `adminUserId` 用于审计。
* **Delete (删除)**:
* `boolean adminMarkSessionAsDeleted(Long sessionId, Long adminUserId)`:
* 逻辑删除,设置 `is_deleted = 1`
* 记录操作日志。
### 2.3. Controller 层 (`com.yupi.project.controller.ChargingSessionAdminController`) ### 2.3. Controller 层 (`com.yupi.project.controller.ChargingSessionAdminController`)
* `POST /api/admin/session/list/page`: (已存在,需审视是否需要修改请求体和响应体以满足 `ChargingSessionAdminQueryRequest``Page<ChargingSessionAdminVO>`) * `POST /api/admin/session/list/page`: (已存在,需审视是否需要修改请求体和响应体以满足 `ChargingSessionAdminQueryRequest``Page<ChargingSessionAdminVO>`)
@@ -98,9 +91,6 @@
* 调用 `chargingSessionService.adminUpdateSessionStatus(sessionId, newStatus, reason, adminUserId)` * 调用 `chargingSessionService.adminUpdateSessionStatus(sessionId, newStatus, reason, adminUserId)`
* `adminUserId` 从当前登录用户上下文获取。 * `adminUserId` 从当前登录用户上下文获取。
* 权限: `@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)` * 权限: `@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)`
* `DELETE /api/admin/session/{sessionId}`:
* 调用 `chargingSessionService.adminMarkSessionAsDeleted(sessionId, adminUserId)`
* 权限: `@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)`
## 3. 前端实现 (`charging_web_app`) ## 3. 前端实现 (`charging_web_app`)
@@ -128,7 +118,6 @@
* **操作列**: * **操作列**:
* "详情"按钮:点击打开会话详情模态框。 * "详情"按钮:点击打开会话详情模态框。
* "修改状态"按钮 (如果实现该功能):打开状态修改模态框。 * "修改状态"按钮 (如果实现该功能):打开状态修改模态框。
* "删除"按钮 (如果实现该功能):触发删除确认。
* **查询筛选**: * **查询筛选**:
* 表单元素对应 `ChargingSessionAdminQueryRequest` 中的筛选字段。 * 表单元素对应 `ChargingSessionAdminQueryRequest` 中的筛选字段。
* "查询"按钮触发 API 请求,更新表格数据。 * "查询"按钮触发 API 请求,更新表格数据。
@@ -139,9 +128,6 @@
* **修改状态 (Modal)** (如果实现): * **修改状态 (Modal)** (如果实现):
* 表单包含可选的新状态和操作原因。 * 表单包含可选的新状态和操作原因。
* 提交时调用后端 `/api/admin/session/{sessionId}/status` 接口。 * 提交时调用后端 `/api/admin/session/{sessionId}/status` 接口。
* **删除会话** (如果实现):
* 使用 `Popconfirm` 提示。
* 确认后调用后端 `DELETE /api/admin/session/{sessionId}` 接口。
### 3.4. API 服务层 ### 3.4. API 服务层
*`charging_web_app/src/services/` 目录下创建 `sessionAdminService.ts` (或类似名称)。 *`charging_web_app/src/services/` 目录下创建 `sessionAdminService.ts` (或类似名称)。
@@ -149,13 +135,14 @@
* `fetchAdminSessions(query: ChargingSessionAdminQueryRequest): Promise<BaseResponse<Page<ChargingSessionAdminVO>>>` * `fetchAdminSessions(query: ChargingSessionAdminQueryRequest): Promise<BaseResponse<Page<ChargingSessionAdminVO>>>`
* `fetchAdminSessionDetails(sessionId: number): Promise<BaseResponse<ChargingSessionAdminVO>>` * `fetchAdminSessionDetails(sessionId: number): Promise<BaseResponse<ChargingSessionAdminVO>>`
* `updateAdminSessionStatus(sessionId: number, data: ChargingSessionAdminUpdateStatusRequest): Promise<BaseResponse<any>>` * `updateAdminSessionStatus(sessionId: number, data: ChargingSessionAdminUpdateStatusRequest): Promise<BaseResponse<any>>`
* `deleteAdminSession(sessionId: number): Promise<BaseResponse<any>>`
### 3.5. 状态管理 (React Context / Zustand / Redux Toolkit - 根据项目现有方式) ### 3.5. 状态管理 (React Context / Zustand / Redux Toolkit - 根据项目现有方式)
* 管理会话列表数据、加载状态、错误信息。 * 管理会话列表数据、加载状态、错误信息。
* 管理分页参数 (`current`, `pageSize`, `total`)。 * 管理分页参数 (`current`, `pageSize`, `total`)。
* 管理筛选表单的条件。 * 管理筛选表单的条件。
* 管理模态框的显示/隐藏状态及当前操作的会话数据。 * 管理模态框的显示/隐藏状态及当前操作的会话数据。
* 状态更新操作的业务逻辑和数据一致性。
* 并发操作下的数据准确性(如果适用)。
## 4. 数据库变更 ## 4. 数据库变更
* 当前 `charging_session` 表结构基本满足需求。 * 当前 `charging_session` 表结构基本满足需求。
@@ -166,12 +153,12 @@
* 各 API 接口的参数校验、权限控制。 * 各 API 接口的参数校验、权限控制。
* 筛选条件(特别是组合条件和边界值)的正确性。 * 筛选条件(特别是组合条件和边界值)的正确性。
* 分页和排序逻辑的正确性。 * 分页和排序逻辑的正确性。
* 状态更新和删除操作的业务逻辑和数据一致性。 * 状态更新操作的业务逻辑和数据一致性。
* 并发操作下的数据准确性(如果适用)。 * 并发操作下的数据准确性(如果适用)。
* **前端**: * **前端**:
* 页面数据能否正确加载和展示。 * 页面数据能否正确加载和展示。
* 查询、分页、排序功能是否按预期工作。 * 查询、分页、排序功能是否按预期工作。
* 操作按钮(详情、修改、删除)的交互和功能是否正常。 * 操作按钮(详情、修改状态)的交互和功能是否正常。
* 表单校验和用户提示。 * 表单校验和用户提示。
* 不同屏幕尺寸下的响应式布局。 * 不同屏幕尺寸下的响应式布局。
* **整体**: * **整体**:

View File

@@ -51,7 +51,7 @@ logging:
# MQTT Configurations # MQTT Configurations
# =================================================================== # ===================================================================
mqtt: mqtt:
broker-url: tcp://broker.emqx.io:1883 broker-url: tcp://yuyun-hk1.stormrain.cn:1883
username: # Public broker, no credentials specified for connection username: # Public broker, no credentials specified for connection
password: # Public broker, no credentials specified for connection password: # Public broker, no credentials specified for connection
client-id-prefix: backend-yupi-mqtt-power- # Unique client ID prefix for our project client-id-prefix: backend-yupi-mqtt-power- # Unique client ID prefix for our project