修复充电请求流程
This commit is contained in:
33
LogBook.md
33
LogBook.md
@@ -135,23 +135,20 @@
|
|||||||
- 从管理员控制台页面 (`charging_web_app/src/app/(authenticated)/admin/dashboard/page.tsx`) 移除了"会话管理"导航卡片按钮,以匹配后端不再提供管理员直接删除会话功能的调整。
|
- 从管理员控制台页面 (`charging_web_app/src/app/(authenticated)/admin/dashboard/page.tsx`) 移除了"会话管理"导航卡片按钮,以匹配后端不再提供管理员直接删除会话功能的调整。
|
||||||
|
|
||||||
## YYYY-MM-DD (请替换为今天的实际日期)
|
## YYYY-MM-DD (请替换为今天的实际日期)
|
||||||
- **MQTT 服务器域名更新**:
|
- 新建 MG995 舵机测试项目 `mg995_servo_test`。
|
||||||
- 将后端 (`springboot-init-main/src/main/resources/application.yml`) 和单片机 (`mqtt_esp32_client/mqtt_esp32_client.ino`) 的 MQTT Broker 地址从 `broker.emqx.io` 更新为 `yuyun-hk1.stormrain.cn`。
|
- 目标:测试 MG995 舵机与 ESP32-S3 的集成。
|
||||||
- 确保设备与自建 MQTT 服务器的通信正常进行。
|
- 硬件配置:
|
||||||
|
- 主控:ESP32-S3
|
||||||
|
- 舵机:MG995
|
||||||
|
- 连接引脚:舵机信号线连接到 GPIO 18。
|
||||||
|
- 在 `mg995_servo_test/` 目录下创建了 `mg995_servo_test.ino` 文件,实现了舵机在 0-180 度之间往复运动的测试程序。
|
||||||
|
|
||||||
## YYYY-MM-DD (请替换为今天的实际日期)
|
## YYYY-MM-DD (请替换为今天的实际日期)
|
||||||
- **修复后端 MQTT 消息处理逻辑**:
|
- 新建 Arduino Nano 的 MG995 舵机测试项目 `mg995_nano_test`。
|
||||||
- 修正 `springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java`,确保在机器人完成 `MOVE_TO_SPOT` 任务后,能够正确调用 `ChargingSessionService.handleRobotArrival` 方法更新充电会话状态。
|
- 目标:将舵机控制方案切换到 Arduino Nano 平台。
|
||||||
- **具体变更**:
|
- 硬件配置:
|
||||||
- 在 `MqttMessageHandler` 中注入 `ChargingSessionService`。
|
- 主控:Arduino Nano
|
||||||
- 在 `handleRobotStatusUpdate` 方法中,当接收到 `COMPLETED` 状态的 `RobotTask` 消息,并且该任务的 `commandType` 为 `MOVE_TO_SPOT` 时,调用 `chargingSessionService.handleRobotArrival`,传入相关的会话 ID 和任务 ID。
|
- 舵机:MG995
|
||||||
|
- 连接引脚:舵机信号线连接到 Nano 的 D9 PWM 引脚。
|
||||||
## YYYY-MM-DD (请替换为今天的实际日期)
|
- 在 `mg995_nano_test/` 目录下创建了 `mg995_nano_test.ino` 文件,使用标准 `Servo.h` 库实现舵机往复运动。
|
||||||
- **修复单片机 MQTT ACK 消息格式**:
|
- **重要提示**:同样需要为 MG995 舵机提供独立的 5V 外部电源,并与 Nano 共地。
|
||||||
- 修改 `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` 的调用,以使用新的函数签名和参数。
|
|
||||||
@@ -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(0, "指令解析失败", false, "Command JSON invalid (missing commandType/command or taskId)");
|
publish_ack_message(taskId, false, "Command JSON invalid (missing commandType/command or taskId)", nullptr);
|
||||||
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)");
|
publish_ack_message(taskId, true, "Robot arrived at spot and started charging (simulated)", currentSpotId.c_str());
|
||||||
|
|
||||||
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");
|
publish_ack_message(taskId, true, "Charging started successfully", currentSessionId.c_str());
|
||||||
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(), currentEnergyConsumed, chargeDuration);
|
publish_ack_message(taskId, true, ("Charging stopped. Duration: " + String(chargeDuration) + "s").c_str(), previousSessionId.c_str());
|
||||||
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());
|
publish_ack_message(taskId, false, ("Unknown commandType: " + String(cmdType)).c_str(), nullptr);
|
||||||
}
|
}
|
||||||
Serial.println("-----------------------");
|
Serial.println("-----------------------");
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,13 @@ void publish_status_update(bool isAckOrTaskUpdate, const char* ackTaskId, const
|
|||||||
doc["robotUid"] = spotUid;
|
doc["robotUid"] = spotUid;
|
||||||
|
|
||||||
if (isAckOrTaskUpdate) {
|
if (isAckOrTaskUpdate) {
|
||||||
if (ackTaskId) doc["taskId"] = ackTaskId;
|
if (ackTaskId) {
|
||||||
|
// 关键修复:同时提供 taskId 和 activeTaskId 字段
|
||||||
|
// taskId 用于 robot_task 表的状态更新
|
||||||
|
// activeTaskId 用于 charging_session 的业务流程推进
|
||||||
|
doc["taskId"] = ackTaskId;
|
||||||
|
doc["activeTaskId"] = ackTaskId;
|
||||||
|
}
|
||||||
if (ackStatus) doc["status"] = ackStatus;
|
if (ackStatus) doc["status"] = ackStatus;
|
||||||
if (ackMessage) doc["message"] = ackMessage;
|
if (ackMessage) doc["message"] = ackMessage;
|
||||||
// 根据用户要求,ACK中不发送errorCode
|
// 根据用户要求,ACK中不发送errorCode
|
||||||
@@ -323,30 +329,18 @@ void publish_heartbeat() {
|
|||||||
lastHeartbeatTime = millis();
|
lastHeartbeatTime = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增ACK消息发布函数,以符合后端期望的格式
|
// Simplified ACK message function
|
||||||
void publish_ack_message(long taskId, const char* commandAckStr, bool success, const char* message, float energyKwh = -1.0f, int durationSeconds = -1) {
|
void publish_ack_message(const char* taskId, bool success, const char* message, const char* contextInfo) {
|
||||||
StaticJsonDocument<256> doc; // 调整大小以适应所有字段
|
if (!taskId || strlen(taskId) == 0) {
|
||||||
|
Serial.println("无法发送ACK: taskId 为空");
|
||||||
doc["robotUid"] = spotUid;
|
return;
|
||||||
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
|
||||||
// 针对 STOP_CHARGE 指令的额外字段
|
// For contextInfo, we can pass spotId if relevant, or sessionId if that's what backend expects for ACKs.
|
||||||
if (strcmp(commandAckStr, "停止充电") == 0) {
|
// The 'true' indicates it's an ACK.
|
||||||
if (energyKwh >= 0) {
|
// The ackErrorCode field in publish_status_update will be set to "SUCCESS_ACK" or "FAILURE_ACK"
|
||||||
doc["energy_kwh"] = energyKwh;
|
// 根据用户要求,ACK中的errorCode也暂时简化或移除。如果保留,确保含义清晰。
|
||||||
}
|
publish_status_update(true, taskId, success ? "COMPLETED" : "FAILED", message, nullptr, contextInfo);
|
||||||
if (durationSeconds >= 0) {
|
|
||||||
doc["duration_s"] = durationSeconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
publish_message(topic_uplink_to_backend, doc, "ACK");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class RobotStatusMessage {
|
public class RobotStatusMessage {
|
||||||
|
private Long taskId;
|
||||||
|
private String status;
|
||||||
|
private String message;
|
||||||
|
private String errorCode;
|
||||||
|
private String robotUid;
|
||||||
|
private String actualRobotStatus;
|
||||||
|
private String location;
|
||||||
|
private Integer batteryLevel;
|
||||||
|
|
||||||
// Fields for Task ACK / specific message context
|
// 关键修复:添加此字段以从MQTT消息中正确接收任务ID
|
||||||
private Long taskId; // ID of the task this message might be an ACK for
|
private Long activeTaskId;
|
||||||
private String status; // Status related to the taskId (e.g., "PROCESSING", "COMPLETED", "FAILED")
|
|
||||||
private String message; // General message, or error message for a task
|
|
||||||
private String errorCode; // Error code for a task failure
|
|
||||||
|
|
||||||
// Fields for general robot status reporting (heartbeat, unsolicited status)
|
|
||||||
private String robotUid; // UID of the robot sending this status
|
|
||||||
private String actualRobotStatus; // The robot's current operational status (e.g., from RobotStatusEnum)
|
|
||||||
private String location; // Robot's current location
|
|
||||||
private Integer batteryLevel; // Robot's current battery level
|
|
||||||
private Long activeTaskId; // ID of the task the robot is currently busy with (if any)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,25 @@ public enum CommandTypeEnum {
|
|||||||
return ackValue;
|
return ackValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 value 获取枚举
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static CommandTypeEnum getEnumByValue(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (CommandTypeEnum anEnum : CommandTypeEnum.values()) {
|
||||||
|
if (anEnum.value.equals(value)) {
|
||||||
|
return anEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ackValue获取枚举
|
* 根据ackValue获取枚举
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,10 +6,18 @@ import com.yupi.project.common.ErrorCode;
|
|||||||
import com.yupi.project.exception.BusinessException;
|
import com.yupi.project.exception.BusinessException;
|
||||||
import com.yupi.project.mapper.ChargingRobotMapper;
|
import com.yupi.project.mapper.ChargingRobotMapper;
|
||||||
import com.yupi.project.model.entity.ChargingRobot;
|
import com.yupi.project.model.entity.ChargingRobot;
|
||||||
|
import com.yupi.project.model.entity.ParkingSpot;
|
||||||
|
import com.yupi.project.model.entity.RobotTask;
|
||||||
|
import com.yupi.project.model.enums.ParkingSpotStatusEnum;
|
||||||
import com.yupi.project.model.enums.RobotStatusEnum;
|
import com.yupi.project.model.enums.RobotStatusEnum;
|
||||||
import com.yupi.project.service.ChargingRobotService;
|
import com.yupi.project.service.ChargingRobotService;
|
||||||
|
import com.yupi.project.service.ParkingSpotService;
|
||||||
|
import com.yupi.project.service.RobotTaskService;
|
||||||
|
import com.yupi.project.service.ChargingSessionService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -27,6 +35,19 @@ import java.util.List;
|
|||||||
public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, ChargingRobot>
|
public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, ChargingRobot>
|
||||||
implements ChargingRobotService {
|
implements ChargingRobotService {
|
||||||
|
|
||||||
|
private final ParkingSpotService parkingSpotService;
|
||||||
|
private final RobotTaskService robotTaskService;
|
||||||
|
private final ChargingSessionService chargingSessionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ChargingRobotServiceImpl(ParkingSpotService parkingSpotService,
|
||||||
|
RobotTaskService robotTaskService,
|
||||||
|
@Lazy ChargingSessionService chargingSessionService) {
|
||||||
|
this.parkingSpotService = parkingSpotService;
|
||||||
|
this.robotTaskService = robotTaskService;
|
||||||
|
this.chargingSessionService = chargingSessionService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus) {
|
public ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus) {
|
||||||
@@ -57,8 +78,8 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
|
|||||||
@Transactional
|
@Transactional
|
||||||
public boolean updateRobotStatus(String robotUID, RobotStatusEnum status, String location,
|
public boolean updateRobotStatus(String robotUID, RobotStatusEnum status, String location,
|
||||||
Integer batteryLevel, Long currentTaskId, Date lastHeartbeatTime) {
|
Integer batteryLevel, Long currentTaskId, Date lastHeartbeatTime) {
|
||||||
if (StringUtils.isBlank(robotUID) || status == null) {
|
if (StringUtils.isBlank(robotUID)) {
|
||||||
log.warn("更新机器人状态失败:robotUID 或 status 为空。robotUID: {}, status: {}", robotUID, status);
|
log.warn("更新机器人状态失败:robotUID 为空。");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +93,32 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
|
|||||||
robotToUpdate.setId(robot.getId());
|
robotToUpdate.setId(robot.getId());
|
||||||
boolean changed = false;
|
boolean changed = false;
|
||||||
|
|
||||||
|
RobotStatusEnum oldStatus = RobotStatusEnum.getEnumByValue(robot.getStatus());
|
||||||
|
|
||||||
|
if (status == RobotStatusEnum.CHARGING && oldStatus != RobotStatusEnum.CHARGING) {
|
||||||
|
if (currentTaskId != null) {
|
||||||
|
RobotTask task = robotTaskService.getById(currentTaskId);
|
||||||
|
if (task != null && task.getRelatedSessionId() != null) {
|
||||||
|
log.info("机器人 {} 状态变更为 CHARGING,触发会话 {} 的到达/充电开始逻辑。", robotUID, task.getRelatedSessionId());
|
||||||
|
chargingSessionService.handleRobotArrival(task.getRelatedSessionId(), currentTaskId);
|
||||||
|
} else {
|
||||||
|
log.warn("机器人 {} 状态变为 CHARGING,但找不到关联的任务或任务未关联会话。TaskId: {}", robotUID, currentTaskId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("机器人 {} 状态变为 CHARGING,但上报信息中缺少 currentTaskId,无法推进业务流程。", robotUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldStatus == RobotStatusEnum.CHARGING && status != RobotStatusEnum.CHARGING) {
|
||||||
|
if (StringUtils.isNotBlank(robot.getCurrentLocation())) {
|
||||||
|
ParkingSpot spotToRelease = parkingSpotService.findBySpotUid(robot.getCurrentLocation());
|
||||||
|
if (spotToRelease != null && spotToRelease.getStatus().equals(ParkingSpotStatusEnum.CHARGING.getValue())) {
|
||||||
|
log.info("机器人 {} 停止充电,释放车位 {}", robotUID, robot.getCurrentLocation());
|
||||||
|
parkingSpotService.releaseSpot(spotToRelease.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (status != null && !status.getValue().equals(robot.getStatus())) {
|
if (status != null && !status.getValue().equals(robot.getStatus())) {
|
||||||
robotToUpdate.setStatus(status.getValue());
|
robotToUpdate.setStatus(status.getValue());
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -98,8 +145,10 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
log.info("机器人 {} 的状态与数据库一致 (状态: {}, 位置: {}, 电量: {}, 任务ID: {}, 心跳: {}),无需更新。",
|
if(status == oldStatus){
|
||||||
|
log.info("机器人 {} 的状态与数据库一致 (状态: {}, 位置: {}, 电量: {}, 任务ID: {}, 心跳: {}),无需更新。",
|
||||||
robotUID, status, location, batteryLevel, currentTaskId, lastHeartbeatTime);
|
robotUID, status, location, batteryLevel, currentTaskId, lastHeartbeatTime);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.yupi.project.service.impl;
|
package com.yupi.project.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.yupi.project.common.ErrorCode;
|
import com.yupi.project.common.ErrorCode;
|
||||||
import com.yupi.project.exception.BusinessException;
|
import com.yupi.project.exception.BusinessException;
|
||||||
@@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
@@ -142,6 +144,20 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
|
|||||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建机器人移动任务失败");
|
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建机器人移动任务失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关键修复:将任务ID关联到机器人实例上,并持久化到数据库
|
||||||
|
log.info("将任务 {} 关联到机器人 {}", moveTask.getId(), assignedRobot.getRobotUid());
|
||||||
|
boolean robotUpdated = chargingRobotService.update(
|
||||||
|
new UpdateWrapper<ChargingRobot>()
|
||||||
|
.eq("id", assignedRobot.getId())
|
||||||
|
.set("current_task_id", moveTask.getId())
|
||||||
|
);
|
||||||
|
if (!robotUpdated) {
|
||||||
|
// 如果更新失败,这是严重的内部错误,需要回滚所有操作
|
||||||
|
log.error("为会话 {} 关联机器人任务ID失败!机器人ID: {}", sessionId, assignedRobot.getId());
|
||||||
|
// 抛出异常以触发 @Transactional 回滚
|
||||||
|
throw new BusinessException(ErrorCode.OPERATION_ERROR, "关联机器人任务失败");
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 更新充电会话信息
|
// 4. 更新充电会话信息
|
||||||
session.setRobotId(assignedRobot.getId());
|
session.setRobotId(assignedRobot.getId());
|
||||||
session.setRobotUidSnapshot(assignedRobot.getRobotUid());
|
session.setRobotUidSnapshot(assignedRobot.getRobotUid());
|
||||||
@@ -253,32 +269,58 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
|
|||||||
log.warn("处理充电结束事件失败,会话不存在: sessionId={}", sessionId);
|
log.warn("处理充电结束事件失败,会话不存在: sessionId={}", sessionId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 校验状态
|
// 校验会话状态
|
||||||
if (!ChargingSessionStatusEnum.CHARGING_STARTED.getValue().equals(session.getStatus()) ||
|
List<String> validPreviousStates = Arrays.asList(
|
||||||
!robotTaskId.equals(session.getRelatedRobotTaskId())) {
|
ChargingSessionStatusEnum.CHARGING_STARTED.getValue()
|
||||||
log.warn("处理充电结束事件失败,会话状态 ({}) 或任务ID ({}) 不匹配 (期望任务ID: {}) for session {}",
|
);
|
||||||
session.getStatus(), robotTaskId, session.getRelatedRobotTaskId(), sessionId);
|
if (!validPreviousStates.contains(session.getStatus())) {
|
||||||
|
log.warn("处理充电结束事件失败,会话状态 ({}) 不正确 for session {}", session.getStatus(), sessionId);
|
||||||
|
// 如果已经是COMPLETED,直接返回成功,避免重复处理
|
||||||
|
if (session.getStatus().equals(ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("充电结束,开始处理会话 {} 的收尾工作. 关联任务ID: {}", sessionId, robotTaskId);
|
||||||
|
|
||||||
|
// 1. 更新会话状态和核心数据
|
||||||
session.setStatus(ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue());
|
session.setStatus(ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue());
|
||||||
session.setChargeEndTime(new Date());
|
session.setChargeEndTime(new Date());
|
||||||
session.setEnergyConsumedKwh(energyConsumedKwh);
|
session.setEnergyConsumedKwh(energyConsumedKwh); // 从机器人实际上报或系统估算
|
||||||
session.setTotalDurationSeconds(durationSeconds);
|
|
||||||
session.setUpdateTime(new Date());
|
|
||||||
boolean updated = this.updateById(session);
|
|
||||||
|
|
||||||
if (updated) {
|
// 如果外部传入了时长,使用它;否则自己计算
|
||||||
log.info("充电结束,会话 {} 状态更新为 CHARGING_COMPLETED. 电量:{} kWh, 时长:{}s. 关联任务ID: {}",
|
if (durationSeconds > 0) {
|
||||||
sessionId, energyConsumedKwh, durationSeconds, robotTaskId);
|
session.setTotalDurationSeconds(durationSeconds);
|
||||||
// 释放机器人和车位
|
} else if (session.getChargeStartTime() != null) {
|
||||||
chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.IDLE, null, null, null, new Date());
|
long durationMillis = System.currentTimeMillis() - session.getChargeStartTime().getTime();
|
||||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null); // 车位变为空闲
|
session.setTotalDurationSeconds((int) (durationMillis / 1000));
|
||||||
|
|
||||||
// 执行计费和最终化会话逻辑
|
|
||||||
this.calculateCostAndFinalizeSession(sessionId);
|
|
||||||
}
|
}
|
||||||
return updated;
|
session.setUpdateTime(new Date());
|
||||||
|
|
||||||
|
boolean updated = this.updateById(session);
|
||||||
|
if (!updated) {
|
||||||
|
log.error("更新会话 {} 状态为COMPLETED失败", sessionId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 释放资源
|
||||||
|
// 释放机器人,使其变为空闲
|
||||||
|
if(session.getRobotId() != null) {
|
||||||
|
chargingRobotService.releaseRobot(session.getRobotId());
|
||||||
|
log.info("已释放机器人 {} for session {}", session.getRobotUidSnapshot(), sessionId);
|
||||||
|
}
|
||||||
|
// 释放车位
|
||||||
|
if(session.getSpotId() != null) {
|
||||||
|
parkingSpotService.releaseSpot(session.getSpotId());
|
||||||
|
log.info("已释放车位 {} for session {}", session.getSpotUidSnapshot(), sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 进行计费
|
||||||
|
log.info("开始为会话 {} 进行计费...", sessionId);
|
||||||
|
calculateCostAndFinalizeSession(sessionId);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -66,43 +66,49 @@ public class MqttMessageHandler implements IMqttMessageListener {
|
|||||||
if (taskNewStatus == null) {
|
if (taskNewStatus == null) {
|
||||||
log.error("Invalid task status '{}' received for task {} from robot {}. Payload: {}",
|
log.error("Invalid task status '{}' received for task {} from robot {}. Payload: {}",
|
||||||
statusMessage.getStatus(), statusMessage.getTaskId(), robotUIDFromTopicSource, payloadJson);
|
statusMessage.getStatus(), statusMessage.getTaskId(), robotUIDFromTopicSource, payloadJson);
|
||||||
return;
|
// Do not return, general status might still need processing.
|
||||||
}
|
|
||||||
boolean taskUpdated; // Defined outside switch
|
|
||||||
switch (taskNewStatus) {
|
|
||||||
case PROCESSING:
|
|
||||||
taskUpdated = robotTaskService.markTaskAsProcessing(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
|
|
||||||
break;
|
|
||||||
case COMPLETED:
|
|
||||||
taskUpdated = robotTaskService.markTaskAsCompleted(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
|
|
||||||
break;
|
|
||||||
case FAILED:
|
|
||||||
String combinedTaskErrorMessage = statusMessage.getErrorCode() != null ?
|
|
||||||
statusMessage.getErrorCode() + ": " + statusMessage.getMessage() :
|
|
||||||
statusMessage.getMessage();
|
|
||||||
taskUpdated = robotTaskService.markTaskAsFailed(statusMessage.getTaskId(), combinedTaskErrorMessage, new Date());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log.warn("Received unhandled RobotTaskStatusEnum: {} for task {} from robot {}. Ignoring.",
|
|
||||||
taskNewStatus, statusMessage.getTaskId(), robotUIDFromTopicSource);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (taskUpdated) {
|
|
||||||
log.info("Successfully updated task {} to status {} for robot {} based on MQTT message.",
|
|
||||||
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource);
|
|
||||||
} else {
|
} else {
|
||||||
log.warn("Failed to update task {} to status {} for robot {} (or task not found/invalid state transition). Message: {}",
|
boolean taskUpdated; // Defined outside switch
|
||||||
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource, statusMessage.getMessage());
|
switch (taskNewStatus) {
|
||||||
|
case PROCESSING:
|
||||||
|
taskUpdated = robotTaskService.markTaskAsProcessing(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
|
||||||
|
break;
|
||||||
|
case COMPLETED:
|
||||||
|
taskUpdated = robotTaskService.markTaskAsCompleted(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
|
||||||
|
break;
|
||||||
|
case FAILED:
|
||||||
|
String combinedTaskErrorMessage = statusMessage.getErrorCode() != null ?
|
||||||
|
statusMessage.getErrorCode() + ": " + statusMessage.getMessage() :
|
||||||
|
statusMessage.getMessage();
|
||||||
|
taskUpdated = robotTaskService.markTaskAsFailed(statusMessage.getTaskId(), combinedTaskErrorMessage, new Date());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn("Received unhandled RobotTaskStatusEnum: {} for task {} from robot {}. Ignoring.",
|
||||||
|
taskNewStatus, statusMessage.getTaskId(), robotUIDFromTopicSource);
|
||||||
|
taskUpdated = false; // To prevent logging success
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (taskUpdated) {
|
||||||
|
log.info("Successfully updated task {} to status {} for robot {} based on MQTT message.",
|
||||||
|
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource);
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to update task {} to status {} for robot {} (or task not found/invalid state transition). Message: {}",
|
||||||
|
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource, statusMessage.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (statusMessage.getActualRobotStatus() != null) {
|
|
||||||
|
// Scenario 2: Message contains a general robot status update.
|
||||||
|
// This is now a separate "if", not an "else if", so it can execute even if a taskId was present.
|
||||||
|
// This is crucial for ACK messages that also convey a change in the robot's main status (e.g., from MOVING to CHARGING).
|
||||||
|
if (statusMessage.getActualRobotStatus() != null) {
|
||||||
log.debug("Handling general status update from robot {}", robotUIDFromTopicSource);
|
log.debug("Handling general status update from robot {}", robotUIDFromTopicSource);
|
||||||
|
|
||||||
String actualRobotUIDToUse = StringUtils.isNotBlank(statusMessage.getRobotUid()) ? statusMessage.getRobotUid() : robotUIDFromTopicSource;
|
String actualRobotUIDToUse = StringUtils.isNotBlank(statusMessage.getRobotUid()) ? statusMessage.getRobotUid() : robotUIDFromTopicSource;
|
||||||
if (StringUtils.isBlank(actualRobotUIDToUse)) {
|
if (StringUtils.isBlank(actualRobotUIDToUse)) {
|
||||||
log.warn("Cannot determine a valid robot UID for general status update (from topic: {}, from message body: {}). Ignoring update.",
|
log.warn("Cannot determine a valid robot UID for general status update (from topic: {}, from message body: {}). Ignoring update.",
|
||||||
robotUIDFromTopicSource, statusMessage.getRobotUid());
|
robotUIDFromTopicSource, statusMessage.getRobotUid());
|
||||||
return;
|
return; // Critical to have a UID, so we can return here.
|
||||||
}
|
}
|
||||||
|
|
||||||
RobotStatusEnum robotStatus = RobotStatusEnum.getEnumByValue(statusMessage.getActualRobotStatus());
|
RobotStatusEnum robotStatus = RobotStatusEnum.getEnumByValue(statusMessage.getActualRobotStatus());
|
||||||
@@ -120,7 +126,8 @@ public class MqttMessageHandler implements IMqttMessageListener {
|
|||||||
|
|
||||||
chargingRobotService.updateRobotStatus(actualRobotUIDToUse, robotStatus, location, batteryLevel, currentRobotTask, new Date());
|
chargingRobotService.updateRobotStatus(actualRobotUIDToUse, robotStatus, location, batteryLevel, currentRobotTask, new Date());
|
||||||
}
|
}
|
||||||
else {
|
// A final check for completely empty/un-actionable messages.
|
||||||
|
else if (statusMessage.getTaskId() == null) {
|
||||||
log.warn("RobotStatusMessage for robot {} has no taskId and no general status fields. Payload: {}. Ignoring.",
|
log.warn("RobotStatusMessage for robot {} has no taskId and no general status fields. Payload: {}. Ignoring.",
|
||||||
robotUIDFromTopicSource, payloadJson);
|
robotUIDFromTopicSource, payloadJson);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import com.yupi.project.exception.BusinessException;
|
|||||||
import com.yupi.project.model.entity.RobotTask;
|
import com.yupi.project.model.entity.RobotTask;
|
||||||
import com.yupi.project.model.enums.CommandTypeEnum;
|
import com.yupi.project.model.enums.CommandTypeEnum;
|
||||||
import com.yupi.project.model.enums.RobotTaskStatusEnum;
|
import com.yupi.project.model.enums.RobotTaskStatusEnum;
|
||||||
|
import com.yupi.project.service.ChargingSessionService;
|
||||||
import com.yupi.project.service.RobotTaskService;
|
import com.yupi.project.service.RobotTaskService;
|
||||||
import com.yupi.project.mapper.RobotTaskMapper;
|
import com.yupi.project.mapper.RobotTaskMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -27,6 +30,13 @@ import java.util.List;
|
|||||||
public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask>
|
public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask>
|
||||||
implements RobotTaskService {
|
implements RobotTaskService {
|
||||||
|
|
||||||
|
private final ChargingSessionService chargingSessionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public RobotTaskServiceImpl(@Lazy ChargingSessionService chargingSessionService) {
|
||||||
|
this.chargingSessionService = chargingSessionService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPendingOrSentTask(String robotId) {
|
public boolean hasPendingOrSentTask(String robotId) {
|
||||||
if (robotId == null) {
|
if (robotId == null) {
|
||||||
@@ -288,35 +298,26 @@ public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean markTaskAsCompleted(Long taskId, Date ackTime, String message) {
|
public boolean markTaskAsCompleted(Long taskId, Date ackTime, String message) {
|
||||||
if (taskId == null) {
|
|
||||||
log.error("Cannot mark task as completed: taskId is null.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
RobotTask task = this.getById(taskId);
|
RobotTask task = this.getById(taskId);
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
log.warn("Cannot mark task as completed: Task with ID {} not found.", taskId);
|
log.warn("尝试将任务标记为完成失败,任务不存在: {}", taskId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow transition from PENDING, SENT, or PROCESSING to COMPLETED
|
task.setStatus(RobotTaskStatusEnum.COMPLETED);
|
||||||
if (task.getStatus() != RobotTaskStatusEnum.PENDING && task.getStatus() != RobotTaskStatusEnum.SENT && task.getStatus() != RobotTaskStatusEnum.PROCESSING) {
|
task.setAckTime(ackTime);
|
||||||
log.warn("Cannot mark task {} as COMPLETED. Current status is {}, expected PENDING, SENT or PROCESSING.", taskId, task.getStatus());
|
boolean updated = this.updateById(task);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
RobotTask updateTask = new RobotTask();
|
|
||||||
updateTask.setId(taskId);
|
|
||||||
updateTask.setStatus(RobotTaskStatusEnum.COMPLETED);
|
|
||||||
if (ackTime != null) {
|
|
||||||
updateTask.setAckTime(ackTime);
|
|
||||||
}
|
|
||||||
updateTask.setErrorMessage(message); // Can be a success message or null
|
|
||||||
|
|
||||||
boolean updated = this.updateById(updateTask);
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
log.info("Marked RobotTask with ID: {} as COMPLETED{}.", taskId, (message != null ? " with message: " + message : ""));
|
log.info("任务 {} 已成功标记为 COMPLETED", taskId);
|
||||||
} else {
|
|
||||||
log.error("Failed to mark RobotTask with ID: {} as COMPLETED.", taskId);
|
// 关键修复:检查任务类型,并触发相应的会话状态推进
|
||||||
|
CommandTypeEnum commandType = task.getCommandType();
|
||||||
|
if (commandType == CommandTypeEnum.STOP_CHARGE && task.getRelatedSessionId() != null) {
|
||||||
|
log.info("检测到 STOP_CHARGE 任务完成,开始处理会话 {} 的结束流程...", task.getRelatedSessionId());
|
||||||
|
// 此处可以从 message 中解析出耗电量和时长,但为简化,我们让 handleChargingEnd 内部自己计算
|
||||||
|
chargingSessionService.handleChargingEnd(task.getRelatedSessionId(), taskId, null, 0);
|
||||||
|
}
|
||||||
|
// 可以为其他任务类型(如 MOVE_TO_SPOT)添加类似逻辑,但当前只处理STOP_CHARGE
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user