修复充电请求流程

This commit is contained in:
2025-06-07 16:28:02 +08:00
parent f132b557df
commit fb27dc6f29
8 changed files with 243 additions and 137 deletions

View File

@@ -4,18 +4,15 @@ import lombok.Data;
@Data
public class RobotStatusMessage {
// Fields for Task ACK / specific message context
private Long taskId; // ID of the task this message might be an ACK for
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)
}
private Long taskId;
private String status;
private String message;
private String errorCode;
private String robotUid;
private String actualRobotStatus;
private String location;
private Integer batteryLevel;
// 关键修复添加此字段以从MQTT消息中正确接收任务ID
private Long activeTaskId;
}

View File

@@ -27,6 +27,25 @@ public enum CommandTypeEnum {
public String getAckValue() {
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获取枚举
@@ -45,4 +64,4 @@ public enum CommandTypeEnum {
}
return null;
}
}
}

View File

@@ -6,10 +6,18 @@ import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.mapper.ChargingRobotMapper;
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.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 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.transaction.annotation.Transactional;
@@ -27,6 +35,19 @@ import java.util.List;
public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, ChargingRobot>
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
@Transactional
public ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus) {
@@ -57,8 +78,8 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
@Transactional
public boolean updateRobotStatus(String robotUID, RobotStatusEnum status, String location,
Integer batteryLevel, Long currentTaskId, Date lastHeartbeatTime) {
if (StringUtils.isBlank(robotUID) || status == null) {
log.warn("更新机器人状态失败robotUID 或 status 为空。robotUID: {}, status: {}", robotUID, status);
if (StringUtils.isBlank(robotUID)) {
log.warn("更新机器人状态失败robotUID 为空。");
return false;
}
@@ -72,6 +93,32 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
robotToUpdate.setId(robot.getId());
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())) {
robotToUpdate.setStatus(status.getValue());
changed = true;
@@ -98,8 +145,10 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
}
if (!changed) {
log.info("机器人 {} 的状态与数据库一致 (状态: {}, 位置: {}, 电量: {}, 任务ID: {}, 心跳: {}),无需更新。",
if(status == oldStatus){
log.info("机器人 {} 的状态与数据库一致 (状态: {}, 位置: {}, 电量: {}, 任务ID: {}, 心跳: {}),无需更新。",
robotUID, status, location, batteryLevel, currentTaskId, lastHeartbeatTime);
}
return true;
}

View File

@@ -1,6 +1,7 @@
package com.yupi.project.service.impl;
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.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
@@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@@ -142,6 +144,20 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
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. 更新充电会话信息
session.setRobotId(assignedRobot.getId());
session.setRobotUidSnapshot(assignedRobot.getRobotUid());
@@ -253,32 +269,58 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
log.warn("处理充电结束事件失败,会话不存在: sessionId={}", sessionId);
return false;
}
// 校验状态
if (!ChargingSessionStatusEnum.CHARGING_STARTED.getValue().equals(session.getStatus()) ||
!robotTaskId.equals(session.getRelatedRobotTaskId())) {
log.warn("处理充电结束事件失败,会话状态 ({}) 或任务ID ({}) 不匹配 (期望任务ID: {}) for session {}",
session.getStatus(), robotTaskId, session.getRelatedRobotTaskId(), sessionId);
// 校验会话状态
List<String> validPreviousStates = Arrays.asList(
ChargingSessionStatusEnum.CHARGING_STARTED.getValue()
);
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;
}
log.info("充电结束,开始处理会话 {} 的收尾工作. 关联任务ID: {}", sessionId, robotTaskId);
// 1. 更新会话状态和核心数据
session.setStatus(ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue());
session.setChargeEndTime(new Date());
session.setEnergyConsumedKwh(energyConsumedKwh);
session.setTotalDurationSeconds(durationSeconds);
session.setUpdateTime(new Date());
boolean updated = this.updateById(session);
session.setEnergyConsumedKwh(energyConsumedKwh); // 从机器人实际上报或系统估算
if (updated) {
log.info("充电结束,会话 {} 状态更新为 CHARGING_COMPLETED. 电量:{} kWh, 时长:{}s. 关联任务ID: {}",
sessionId, energyConsumedKwh, durationSeconds, robotTaskId);
// 释放机器人和车位
chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.IDLE, null, null, null, new Date());
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null); // 车位变为空闲
// 执行计费和最终化会话逻辑
this.calculateCostAndFinalizeSession(sessionId);
// 如果外部传入了时长,使用它;否则自己计算
if (durationSeconds > 0) {
session.setTotalDurationSeconds(durationSeconds);
} else if (session.getChargeStartTime() != null) {
long durationMillis = System.currentTimeMillis() - session.getChargeStartTime().getTime();
session.setTotalDurationSeconds((int) (durationMillis / 1000));
}
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

View File

@@ -66,43 +66,49 @@ public class MqttMessageHandler implements IMqttMessageListener {
if (taskNewStatus == null) {
log.error("Invalid task status '{}' received for task {} from robot {}. Payload: {}",
statusMessage.getStatus(), statusMessage.getTaskId(), robotUIDFromTopicSource, payloadJson);
return;
}
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);
// Do not return, general status might still need processing.
} 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());
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);
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);
String actualRobotUIDToUse = StringUtils.isNotBlank(statusMessage.getRobotUid()) ? statusMessage.getRobotUid() : robotUIDFromTopicSource;
if (StringUtils.isBlank(actualRobotUIDToUse)) {
log.warn("Cannot determine a valid robot UID for general status update (from topic: {}, from message body: {}). Ignoring update.",
robotUIDFromTopicSource, statusMessage.getRobotUid());
return;
return; // Critical to have a UID, so we can return here.
}
RobotStatusEnum robotStatus = RobotStatusEnum.getEnumByValue(statusMessage.getActualRobotStatus());
@@ -120,7 +126,8 @@ public class MqttMessageHandler implements IMqttMessageListener {
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.",
robotUIDFromTopicSource, payloadJson);
}

View File

@@ -7,9 +7,12 @@ import com.yupi.project.exception.BusinessException;
import com.yupi.project.model.entity.RobotTask;
import com.yupi.project.model.enums.CommandTypeEnum;
import com.yupi.project.model.enums.RobotTaskStatusEnum;
import com.yupi.project.service.ChargingSessionService;
import com.yupi.project.service.RobotTaskService;
import com.yupi.project.mapper.RobotTaskMapper;
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.transaction.annotation.Transactional;
@@ -27,6 +30,13 @@ import java.util.List;
public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask>
implements RobotTaskService {
private final ChargingSessionService chargingSessionService;
@Autowired
public RobotTaskServiceImpl(@Lazy ChargingSessionService chargingSessionService) {
this.chargingSessionService = chargingSessionService;
}
@Override
public boolean hasPendingOrSentTask(String robotId) {
if (robotId == null) {
@@ -288,35 +298,26 @@ public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask
@Override
@Transactional
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);
if (task == null) {
log.warn("Cannot mark task as completed: Task with ID {} not found.", taskId);
log.warn("尝试将任务标记为完成失败,任务不存在: {}", taskId);
return false;
}
// Allow transition from PENDING, SENT, or PROCESSING to COMPLETED
if (task.getStatus() != RobotTaskStatusEnum.PENDING && task.getStatus() != RobotTaskStatusEnum.SENT && task.getStatus() != RobotTaskStatusEnum.PROCESSING) {
log.warn("Cannot mark task {} as COMPLETED. Current status is {}, expected PENDING, SENT or PROCESSING.", taskId, task.getStatus());
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);
task.setStatus(RobotTaskStatusEnum.COMPLETED);
task.setAckTime(ackTime);
boolean updated = this.updateById(task);
if (updated) {
log.info("Marked RobotTask with ID: {} as COMPLETED{}.", taskId, (message != null ? " with message: " + message : ""));
} else {
log.error("Failed to mark RobotTask with ID: {} as COMPLETED.", taskId);
log.info("任务 {} 已成功标记为 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;
}