第三阶段核心业务开发完成

This commit is contained in:
2025-05-17 21:51:04 +08:00
parent 5ea13d6dea
commit 53f7fee73a
50 changed files with 3465 additions and 153 deletions

View File

@@ -11,14 +11,73 @@
## 2. 后端开发详解
1. **数据库初始化**:
* **创建 `charging_robot`, `parking_spot`, `charging_session` 表**: 执行 `development_plan.md` 中定义的相应 DDL 语句。
* **创建 `charging_robot`, `parking_spot`, `charging_session` 表**: 执行以下 DDL 语句。
```sql
-- (DDL for charging_robot)
CREATE TABLE `charging_robot` (...) COMMENT='充电机器人表';
-- (DDL for parking_spot)
CREATE TABLE `parking_spot` (...) COMMENT='车位表';
-- (DDL for charging_session)
CREATE TABLE `charging_session` (...) COMMENT='充电记录表';
-- DDL for charging_robot
CREATE TABLE `charging_robot` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
`robot_uid` VARCHAR(100) NOT NULL UNIQUE COMMENT '机器人唯一标识符 (例如,硬件序列号)',
`status` VARCHAR(50) NOT NULL DEFAULT 'IDLE' COMMENT '机器人状态 (IDLE, MOVING, CHARGING, ERROR, MAINTENANCE, OFFLINE)',
`current_location` VARCHAR(255) COMMENT '当前位置描述 (例如坐标或区域ID)',
`battery_level` TINYINT UNSIGNED COMMENT '电池电量百分比 (0-100)',
`current_task_id` BIGINT COMMENT '当前执行的任务ID (关联 robot_task.id)',
`last_heartbeat_time` DATETIME COMMENT '最后心跳时间',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
`is_deleted` TINYINT(1) DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)'
) COMMENT='充电机器人表';
-- (索引建议,可根据实际查询需求调整)
-- CREATE INDEX `idx_robot_uid` ON `charging_robot` (`robot_uid`);
-- CREATE INDEX `idx_robot_status` ON `charging_robot` (`status`);
-- DDL for parking_spot
CREATE TABLE `parking_spot` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
`spot_uid` VARCHAR(100) NOT NULL UNIQUE COMMENT '车位唯一标识符 (例如,车位号)',
`status` VARCHAR(50) NOT NULL DEFAULT 'AVAILABLE' COMMENT '车位状态 (AVAILABLE, OCCUPIED_BY_CAR, RESERVED, CHARGING, MAINTENANCE, UNAVAILABLE)',
`location_description` VARCHAR(255) COMMENT '位置描述 (例如,楼层、区域)',
`robot_assignable` BOOLEAN DEFAULT TRUE COMMENT '此车位是否可指派机器人前往',
`current_session_id` BIGINT COMMENT '当前占用此车位的充电会话ID (关联 charging_session.id)',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
`is_deleted` TINYINT(1) DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)'
) COMMENT='车位表';
-- (索引建议)
-- CREATE INDEX `idx_spot_uid` ON `parking_spot` (`spot_uid`);
-- CREATE INDEX `idx_spot_status` ON `parking_spot` (`status`);
-- DDL for charging_session
CREATE TABLE `charging_session` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID充电会话唯一标识',
`user_id` BIGINT NOT NULL COMMENT '用户ID (关联 user.id)',
`robot_id` BIGINT COMMENT '服务的机器人ID (关联 charging_robot.id)',
`robot_uid_snapshot` VARCHAR(100) COMMENT '服务机器人的UID快照 (冗余字段,方便查询历史)',
`spot_id` BIGINT NOT NULL COMMENT '车位ID (关联 parking_spot.id)',
`spot_uid_snapshot` VARCHAR(100) NOT NULL COMMENT '车位UID快照 (冗余字段)',
`status` VARCHAR(50) NOT NULL DEFAULT 'REQUESTED' COMMENT '充电会话状态 (REQUESTED, ROBOT_ASSIGNED, ROBOT_MOVING, CHARGING_STARTED, CHARGING_IN_PROGRESS, COMPLETED, CANCELLED_BY_USER, CANCELLED_BY_SYSTEM, ERROR, ROBOT_TASK_TIMEOUT)',
`request_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '用户请求充电时间',
`robot_assigned_time` DATETIME COMMENT '机器人分配时间',
`robot_arrival_time` DATETIME COMMENT '机器人到达车位时间',
`charge_start_time` DATETIME COMMENT '充电开始时间',
`charge_end_time` DATETIME COMMENT '充电结束时间',
`total_duration_seconds` INT COMMENT '总充电时长 (秒)',
`energy_consumed_kwh` DECIMAL(10, 3) COMMENT '消耗电量 (kWh)',
`cost` DECIMAL(10, 2) COMMENT '本次充电费用 (元)',
`payment_status` VARCHAR(50) DEFAULT 'PENDING' COMMENT '支付状态 (PENDING, PAID, FAILED)',
`error_code` VARCHAR(100) COMMENT '错误码 (如果会话出错)',
`error_message` TEXT COMMENT '错误信息',
`related_robot_task_id` BIGINT COMMENT '启动本次会话或关键步骤的 RobotTask ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
`is_deleted` TINYINT(1) DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)'
) COMMENT='充电记录表';
-- (外键约束和索引建议)
-- ALTER TABLE `charging_session` ADD CONSTRAINT `fk_session_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`);
-- CREATE INDEX `idx_session_user` ON `charging_session` (`user_id`);
-- ... (其他索引)
```
2. **机器人与车位管理 (可选,主要面向管理员)**:
* **Entity**: 创建 `ChargingRobot.java`, `ParkingSpot.java`。

View File

@@ -97,6 +97,11 @@
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version> <!-- 使用稳定版本 -->
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,126 @@
/*
Navicat Premium Data Transfer
Source Server : yuyun
Source Server Type : MySQL
Source Server Version : 50744
Source Host : yuyun-us1.stormrain.cn:3306
Source Schema : mqtt_power
Target Server Type : MySQL
Target Server Version : 50744
File Encoding : 65001
Date: 17/05/2025 21:14:59
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for charging_robot
-- ----------------------------
DROP TABLE IF EXISTS `charging_robot`;
CREATE TABLE `charging_robot` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`robot_uid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '机器人唯一标识符 (例如,硬件序列号)',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'IDLE' COMMENT '机器人状态 (IDLE, MOVING, CHARGING, ERROR, MAINTENANCE, OFFLINE)',
`current_location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '当前位置描述 (例如坐标或区域ID)',
`battery_level` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '电池电量百分比 (0-100)',
`current_task_id` bigint(20) NULL DEFAULT NULL COMMENT '当前执行的任务ID (关联 robot_task.id)',
`last_heartbeat_time` datetime NULL DEFAULT NULL COMMENT '最后心跳时间',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `robot_uid`(`robot_uid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '充电机器人表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for charging_session
-- ----------------------------
DROP TABLE IF EXISTS `charging_session`;
CREATE TABLE `charging_session` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID充电会话唯一标识',
`user_id` bigint(20) NOT NULL COMMENT '用户ID (关联 user.id)',
`robot_id` bigint(20) NULL DEFAULT NULL COMMENT '服务的机器人ID (关联 charging_robot.id)',
`robot_uid_snapshot` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '服务机器人的UID快照 (冗余字段,方便查询历史)',
`spot_id` bigint(20) NOT NULL COMMENT '车位ID (关联 parking_spot.id)',
`spot_uid_snapshot` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '车位UID快照 (冗余字段)',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'REQUESTED' COMMENT '充电会话状态 (REQUESTED, ROBOT_ASSIGNED, ROBOT_MOVING, CHARGING_STARTED, CHARGING_IN_PROGRESS, COMPLETED, CANCELLED_BY_USER, CANCELLED_BY_SYSTEM, ERROR, ROBOT_TASK_TIMEOUT)',
`request_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '用户请求充电时间',
`robot_assigned_time` datetime NULL DEFAULT NULL COMMENT '机器人分配时间',
`robot_arrival_time` datetime NULL DEFAULT NULL COMMENT '机器人到达车位时间',
`charge_start_time` datetime NULL DEFAULT NULL COMMENT '充电开始时间',
`charge_end_time` datetime NULL DEFAULT NULL COMMENT '充电结束时间',
`total_duration_seconds` int(11) NULL DEFAULT NULL COMMENT '总充电时长 (秒)',
`energy_consumed_kwh` decimal(10, 3) NULL DEFAULT NULL COMMENT '消耗电量 (kWh)',
`cost` decimal(10, 2) NULL DEFAULT NULL COMMENT '本次充电费用 (元)',
`payment_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT 'PENDING' COMMENT '支付状态 (PENDING, PAID, FAILED)',
`error_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '错误码 (如果会话出错)',
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL COMMENT '错误信息',
`related_robot_task_id` bigint(20) NULL DEFAULT NULL COMMENT '启动本次会话或关键步骤的 RobotTask ID',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '充电记录表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for parking_spot
-- ----------------------------
DROP TABLE IF EXISTS `parking_spot`;
CREATE TABLE `parking_spot` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`spot_uid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '车位唯一标识符 (例如,车位号)',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'AVAILABLE' COMMENT '车位状态 (AVAILABLE, OCCUPIED_BY_CAR, RESERVED, CHARGING, MAINTENANCE, UNAVAILABLE)',
`location_description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '位置描述 (例如,楼层、区域)',
`robot_assignable` tinyint(1) NULL DEFAULT 1 COMMENT '此车位是否可指派机器人前往',
`current_session_id` bigint(20) NULL DEFAULT NULL COMMENT '当前占用此车位的充电会话ID (关联 charging_session.id)',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `spot_uid`(`spot_uid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '车位表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for robot_task
-- ----------------------------
DROP TABLE IF EXISTS `robot_task`;
CREATE TABLE `robot_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`robot_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '机器人ID',
`command_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '命令类型 (MOVE_TO_SPOT, START_CHARGE, STOP_CHARGE, QUERY_STATUS)',
`command_payload` text CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL COMMENT '命令参数 (JSON格式)',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'PENDING' COMMENT '任务状态 (PENDING, SENT, ACKNOWLEDGED_SUCCESS, ACKNOWLEDGED_FAILURE, TIMED_OUT)',
`sent_time` datetime NULL DEFAULT NULL COMMENT '命令发送时间',
`ack_time` datetime NULL DEFAULT NULL COMMENT '命令确认时间',
`related_session_id` bigint(20) NULL DEFAULT NULL COMMENT '关联的充电会话ID (可选)',
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL COMMENT '失败或超时的错误信息',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_delete` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_robot_status`(`robot_id`, `status`) USING BTREE COMMENT '机器人和状态索引,便于查询'
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '机器人指令任务表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '密码 (加密存储)',
`role` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'user' COMMENT '角色 (user/admin)',
`balance` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '账户余额',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
`isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除(0-未删, 1-已删)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,155 @@
package com.yupi.project.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.DeleteRequest;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.constant.UserConstant;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.exception.ThrowUtils;
import com.yupi.project.model.dto.charging_robot.ChargingRobotAddRequest;
import com.yupi.project.model.dto.charging_robot.ChargingRobotQueryRequest;
import com.yupi.project.model.dto.charging_robot.ChargingRobotUpdateRequest;
import com.yupi.project.model.entity.ChargingRobot;
import com.yupi.project.model.enums.RobotStatusEnum;
import com.yupi.project.service.ChargingRobotService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 充电机器人管理接口 (管理员权限)
*/
@RestController
@RequestMapping("/admin/robot")
@Slf4j
@Api(tags = "充电机器人管理接口 (管理员)")
public class ChargingRobotAdminController {
@Resource
private ChargingRobotService chargingRobotService;
@ApiOperation("添加充电机器人")
@PostMapping("/add")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addChargingRobot(@RequestBody ChargingRobotAddRequest addRequest, HttpServletRequest request) {
if (addRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 参数校验
if (StringUtils.isBlank(addRequest.getRobotUid())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "机器人UID不能为空");
}
if (addRequest.getStatus() == null || RobotStatusEnum.getEnumByValue(addRequest.getStatus()) == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的机器人状态");
}
ChargingRobot robot = chargingRobotService.registerRobot(addRequest.getRobotUid(), RobotStatusEnum.getEnumByValue(addRequest.getStatus()));
if (robot == null || robot.getId() == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加机器人失败");
}
// 可以选择性地更新其他字段,如初始电量等
if (addRequest.getBatteryLevel() != null) {
chargingRobotService.updateBatteryLevel(robot.getId(), addRequest.getBatteryLevel());
}
if (StringUtils.isNotBlank(addRequest.getCurrentLocation())) {
ChargingRobot updateRobot = new ChargingRobot();
updateRobot.setId(robot.getId());
updateRobot.setCurrentLocation(addRequest.getCurrentLocation());
chargingRobotService.updateById(updateRobot);
}
return ResultUtils.success(robot.getId());
}
@ApiOperation("删除充电机器人")
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> deleteChargingRobot(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean b = chargingRobotService.removeById(deleteRequest.getId());
return ResultUtils.success(b);
}
@ApiOperation("更新充电机器人信息")
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateChargingRobot(@RequestBody ChargingRobotUpdateRequest updateRequest, HttpServletRequest request) {
if (updateRequest == null || updateRequest.getId() == null || updateRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
ChargingRobot chargingRobot = new ChargingRobot();
BeanUtils.copyProperties(updateRequest, chargingRobot);
if (StringUtils.isNotBlank(updateRequest.getStatus()) && RobotStatusEnum.getEnumByValue(updateRequest.getStatus()) == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的机器人状态值");
}
if (updateRequest.getBatteryLevel() != null && (updateRequest.getBatteryLevel() < 0 || updateRequest.getBatteryLevel() > 100)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的电池电量值");
}
boolean result = chargingRobotService.updateById(chargingRobot);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
return ResultUtils.success(true);
}
@ApiOperation("根据ID获取充电机器人信息")
@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<ChargingRobot> getChargingRobotById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
ChargingRobot chargingRobot = chargingRobotService.getById(id);
ThrowUtils.throwIf(chargingRobot == null, ErrorCode.NOT_FOUND_ERROR);
return ResultUtils.success(chargingRobot);
}
@ApiOperation("分页获取充电机器人列表")
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<ChargingRobot>> listChargingRobotsByPage(@RequestBody ChargingRobotQueryRequest queryRequest, HttpServletRequest request) {
if (queryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long current = queryRequest.getCurrent();
long size = queryRequest.getPageSize();
Page<ChargingRobot> page = new Page<>(current, size);
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(queryRequest.getRobotUid())) {
queryWrapper.like("robot_uid", queryRequest.getRobotUid());
}
if (StringUtils.isNotBlank(queryRequest.getStatus())) {
queryWrapper.eq("status", queryRequest.getStatus());
}
if (StringUtils.isNotBlank(queryRequest.getSortField())) {
queryWrapper.orderBy(true, queryRequest.getSortOrder().equals("ascend"), queryRequest.getSortField());
}
chargingRobotService.page(page, queryWrapper);
return ResultUtils.success(page);
}
@ApiOperation("获取所有机器人状态类型")
@GetMapping("/status/types")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<List<String>> getRobotStatusTypes() {
List<String> statusTypes = Arrays.stream(RobotStatusEnum.values())
.map(RobotStatusEnum::getValue)
.collect(Collectors.toList());
return ResultUtils.success(statusTypes);
}
}

View File

@@ -0,0 +1,83 @@
package com.yupi.project.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.constant.UserConstant;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
import com.yupi.project.model.entity.ChargingSession;
import com.yupi.project.model.entity.User;
import com.yupi.project.model.vo.ChargingSessionVO;
import com.yupi.project.service.ChargingSessionService;
import com.yupi.project.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* 充电会话管理接口 (管理员权限)
*/
@RestController
@RequestMapping("/admin/session")
@Slf4j
@Api(tags = "充电会话管理接口 (管理员)")
public class ChargingSessionAdminController {
@Resource
private ChargingSessionService chargingSessionService;
@Resource
private UserService userService; // 可能需要用于填充用户信息
@ApiOperation("管理员分页获取所有充电会话列表")
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<ChargingSessionVO>> listAllChargingSessionsByPage(@RequestBody ChargingSessionQueryRequest queryRequest, HttpServletRequest request) {
if (queryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 管理员查询不强制设置userId
long current = queryRequest.getCurrent();
long size = queryRequest.getPageSize();
Page<ChargingSession> sessionPage = chargingSessionService.page(new Page<>(current, size),
chargingSessionService.getQueryWrapper(queryRequest));
Page<ChargingSessionVO> sessionVOPage = new Page<>(sessionPage.getCurrent(), sessionPage.getSize(), sessionPage.getTotal());
// 转换VO并填充用户信息
List<ChargingSessionVO> voList = sessionPage.getRecords().stream().map(session -> {
ChargingSessionVO vo = ChargingSessionVO.objToVo(session);
if (session.getUserId() != null) {
User user = userService.getById(session.getUserId());
if (user != null) {
vo.setUser(user); // UserVO 转换会在 setUser 方法内进行
}
}
// TODO: 填充机器人和车位快照对应的实体信息 (如果VO中需要)
return vo;
}).collect(Collectors.toList());
sessionVOPage.setRecords(voList);
return ResultUtils.success(sessionVOPage);
}
// TODO: 管理员可能需要的其他接口,例如:
// - 管理员手动取消某个会话 (不同于用户取消)
// - 查看特定会话的详细日志或关联任务
// - 重试某个失败的支付等
}

View File

@@ -0,0 +1,136 @@
package com.yupi.project.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.exception.ThrowUtils;
import com.yupi.project.model.dto.charging_session.ChargingRequest;
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
import com.yupi.project.model.dto.charging_session.PaymentRequest;
import com.yupi.project.model.entity.ChargingSession;
import com.yupi.project.model.entity.User;
import com.yupi.project.model.enums.ChargingSessionStatusEnum;
import com.yupi.project.model.vo.ChargingSessionVO;
import com.yupi.project.service.ChargingSessionService;
import com.yupi.project.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* 充电会话接口 (用户权限)
*/
@RestController
@RequestMapping("/session")
@Slf4j
@Api(tags = "充电会话接口 (用户)")
public class ChargingSessionController {
@Resource
private ChargingSessionService chargingSessionService;
@Resource
private UserService userService;
@ApiOperation("用户请求充电")
@PostMapping("/request")
@AuthCheck // 需要登录
public BaseResponse<Long> requestCharging(@RequestBody ChargingRequest chargingRequest, HttpServletRequest request) {
if (chargingRequest == null || chargingRequest.getSpotId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数错误必须指定车位ID");
}
User loginUser = userService.getCurrentUser(request);
ChargingSession session = chargingSessionService.requestCharging(loginUser, chargingRequest);
return ResultUtils.success(session.getId());
}
@ApiOperation("用户获取自己的充电会话列表 (分页)")
@PostMapping("/my/list/page")
@AuthCheck // 需要登录
public BaseResponse<Page<ChargingSessionVO>> listMyChargingSessionsByPage(@RequestBody ChargingSessionQueryRequest queryRequest, HttpServletRequest request) {
if (queryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getCurrentUser(request);
queryRequest.setUserId(loginUser.getId()); // 强制查询当前用户的
long current = queryRequest.getCurrent();
long size = queryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<ChargingSession> sessionPage = chargingSessionService.page(new Page<>(current, size),
chargingSessionService.getQueryWrapper(queryRequest));
Page<ChargingSessionVO> sessionVOPage = new Page<>(sessionPage.getCurrent(), sessionPage.getSize(), sessionPage.getTotal());
List<ChargingSessionVO> voList = sessionPage.getRecords().stream().map(ChargingSessionVO::objToVo).collect(Collectors.toList());
sessionVOPage.setRecords(voList);
return ResultUtils.success(sessionVOPage);
}
@ApiOperation("用户获取单个充电会话详情")
@GetMapping("/get")
@AuthCheck
public BaseResponse<ChargingSessionVO> getChargingSessionById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getCurrentUser(request);
ChargingSession session = chargingSessionService.getById(id);
if (session == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 权限校验,只能看自己的
if (!session.getUserId().equals(loginUser.getId())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
return ResultUtils.success(ChargingSessionVO.objToVo(session));
}
@ApiOperation("用户取消充电会话")
@PostMapping("/cancel")
@AuthCheck
public BaseResponse<Boolean> cancelChargingSession(@RequestBody PaymentRequest cancelRequest, HttpServletRequest request) { // 复用PaymentRequest只取sessionId
if (cancelRequest == null || cancelRequest.getSessionId() == null || cancelRequest.getSessionId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getCurrentUser(request);
boolean result = chargingSessionService.cancelChargingSession(cancelRequest.getSessionId(), loginUser.getId(), "用户主动取消", ChargingSessionStatusEnum.CANCELLED_BY_USER);
return ResultUtils.success(result);
}
@ApiOperation("用户请求停止当前充电")
@PostMapping("/stop")
@AuthCheck
public BaseResponse<Boolean> stopCharging(@RequestBody PaymentRequest stopRequest, HttpServletRequest request) { // 复用PaymentRequest只取sessionId
if (stopRequest == null || stopRequest.getSessionId() == null || stopRequest.getSessionId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getCurrentUser(request);
boolean result = chargingSessionService.stopChargingByUser(stopRequest.getSessionId(), loginUser.getId());
return ResultUtils.success(result);
}
@ApiOperation("用户支付充电费用")
@PostMapping("/pay")
@AuthCheck
public BaseResponse<Boolean> payChargingSession(@RequestBody PaymentRequest paymentRequest, HttpServletRequest request) {
if (paymentRequest == null || paymentRequest.getSessionId() == null || paymentRequest.getSessionId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getCurrentUser(request);
boolean result = chargingSessionService.processPayment(paymentRequest.getSessionId(), loginUser.getId());
return ResultUtils.success(result);
}
}

View File

@@ -0,0 +1,149 @@
package com.yupi.project.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.DeleteRequest;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.constant.UserConstant;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.exception.ThrowUtils;
import com.yupi.project.model.dto.parking_spot.ParkingSpotAddRequest;
import com.yupi.project.model.dto.parking_spot.ParkingSpotQueryRequest;
import com.yupi.project.model.dto.parking_spot.ParkingSpotUpdateRequest;
import com.yupi.project.model.entity.ParkingSpot;
import com.yupi.project.model.enums.ParkingSpotStatusEnum;
import com.yupi.project.service.ParkingSpotService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 车位管理接口 (管理员权限)
*/
@RestController
@RequestMapping("/admin/spot")
@Slf4j
@Api(tags = "车位管理接口 (管理员)")
public class ParkingSpotAdminController {
@Resource
private ParkingSpotService parkingSpotService;
@ApiOperation("添加车位")
@PostMapping("/add")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addParkingSpot(@RequestBody ParkingSpotAddRequest addRequest, HttpServletRequest request) {
if (addRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
if (StringUtils.isBlank(addRequest.getSpotUid())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "车位UID不能为空");
}
// robotAssignable 默认为true如果 DTO 中是 Boolean则 null 会被处理
ParkingSpot spot = parkingSpotService.addParkingSpot(
addRequest.getSpotUid(),
addRequest.getLocationDescription(),
addRequest.getRobotAssignable() == null ? true : addRequest.getRobotAssignable()
);
if (spot == null || spot.getId() == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加车位失败");
}
return ResultUtils.success(spot.getId());
}
@ApiOperation("删除车位")
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> deleteParkingSpot(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 检查车位是否正在使用等逻辑可以在Service层实现这里直接删除
boolean b = parkingSpotService.removeById(deleteRequest.getId());
return ResultUtils.success(b);
}
@ApiOperation("更新车位信息")
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateParkingSpot(@RequestBody ParkingSpotUpdateRequest updateRequest, HttpServletRequest request) {
if (updateRequest == null || updateRequest.getId() == null || updateRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
ParkingSpot parkingSpot = new ParkingSpot();
BeanUtils.copyProperties(updateRequest, parkingSpot);
if (StringUtils.isNotBlank(updateRequest.getStatus()) && ParkingSpotStatusEnum.getEnumByValue(updateRequest.getStatus()) == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的车位状态值");
}
boolean result = parkingSpotService.updateById(parkingSpot);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
return ResultUtils.success(true);
}
@ApiOperation("根据ID获取车位信息")
@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<ParkingSpot> getParkingSpotById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
ParkingSpot parkingSpot = parkingSpotService.getById(id);
ThrowUtils.throwIf(parkingSpot == null, ErrorCode.NOT_FOUND_ERROR);
return ResultUtils.success(parkingSpot);
}
@ApiOperation("分页获取车位列表")
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<ParkingSpot>> listParkingSpotsByPage(@RequestBody ParkingSpotQueryRequest queryRequest, HttpServletRequest request) {
if (queryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long current = queryRequest.getCurrent();
long size = queryRequest.getPageSize();
Page<ParkingSpot> page = new Page<>(current, size);
QueryWrapper<ParkingSpot> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(queryRequest.getSpotUid())) {
queryWrapper.like("spot_uid", queryRequest.getSpotUid());
}
if (StringUtils.isNotBlank(queryRequest.getStatus())) {
queryWrapper.eq("status", queryRequest.getStatus());
}
if (queryRequest.getRobotAssignable() != null) {
queryWrapper.eq("robot_assignable", queryRequest.getRobotAssignable());
}
if (StringUtils.isNotBlank(queryRequest.getSortField())) {
queryWrapper.orderBy(true, queryRequest.getSortOrder().equals("ascend"), queryRequest.getSortField());
}
parkingSpotService.page(page, queryWrapper);
return ResultUtils.success(page);
}
@ApiOperation("获取所有车位状态类型")
@GetMapping("/status/types")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<List<String>> getParkingSpotStatusTypes() {
List<String> statusTypes = Arrays.stream(ParkingSpotStatusEnum.values())
.map(ParkingSpotStatusEnum::getValue)
.collect(Collectors.toList());
return ResultUtils.success(statusTypes);
}
}

View File

@@ -0,0 +1,45 @@
package com.yupi.project.exception;
import com.yupi.project.common.ErrorCode;
/**
* 抛异常工具类
*
*/
public final class ThrowUtils {
/**
* 条件成立则抛异常
*
* @param condition
* @param runtimeException
*/
public static void throwIf(boolean condition, RuntimeException runtimeException) {
if (condition) {
throw runtimeException;
}
}
/**
* 条件成立则抛异常
*
* @param condition
* @param errorCode
*/
public static void throwIf(boolean condition, ErrorCode errorCode) {
throwIf(condition, new BusinessException(errorCode));
}
/**
* 条件成立则抛异常
*
* @param condition
* @param errorCode
* @param message
*/
public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
throwIf(condition, new BusinessException(errorCode, message));
}
private ThrowUtils() {}
}

View File

@@ -0,0 +1,14 @@
package com.yupi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yupi.project.model.entity.ChargingRobot;
/**
* @author Yupi
* @description 针对表【charging_robot(充电机器人表)】的数据库操作Mapper
* @createDate 2023-12-03 10:00:00
* @Entity com.yupi.project.model.entity.ChargingRobot
*/
public interface ChargingRobotMapper extends BaseMapper<ChargingRobot> {
}

View File

@@ -0,0 +1,14 @@
package com.yupi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yupi.project.model.entity.ChargingSession;
/**
* @author Yupi
* @description 针对表【charging_session(充电记录表)】的数据库操作Mapper
* @createDate 2023-12-03 10:00:00
* @Entity com.yupi.project.model.entity.ChargingSession
*/
public interface ChargingSessionMapper extends BaseMapper<ChargingSession> {
}

View File

@@ -0,0 +1,14 @@
package com.yupi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yupi.project.model.entity.ParkingSpot;
/**
* @author Yupi
* @description 针对表【parking_spot(车位表)】的数据库操作Mapper
* @createDate 2023-12-03 10:00:00
* @Entity com.yupi.project.model.entity.ParkingSpot
*/
public interface ParkingSpotMapper extends BaseMapper<ParkingSpot> {
}

View File

@@ -2,6 +2,8 @@ package com.yupi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yupi.project.model.entity.User;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
/**
* @description 针对表【user(用户表)】的数据库操作Mapper
@@ -10,4 +12,11 @@ import com.yupi.project.model.entity.User;
*/
public interface UserMapper extends BaseMapper<User> {
/**
* 更新用户余额
* @param userId 用户ID
* @param amountChange 要变更的金额 (元), 正数表示增加,负数表示减少
* @return 影响的行数
*/
int updateUserBalance(@Param("userId") Long userId, @Param("amountChange") BigDecimal amountChange);
}

View File

@@ -0,0 +1,32 @@
package com.yupi.project.model.dto.charging_robot;
import lombok.Data;
import java.io.Serializable;
/**
* 添加充电机器人请求
*/
@Data
public class ChargingRobotAddRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 机器人唯一标识符
*/
private String robotUid;
/**
* 初始状态 (来自 RobotStatusEnum 的 value)
*/
private String status;
/**
* 初始电池电量百分比 (0-100)
*/
private Integer batteryLevel;
/**
* 初始当前位置描述
*/
private String currentLocation;
}

View File

@@ -0,0 +1,27 @@
package com.yupi.project.model.dto.charging_robot;
import com.yupi.project.common.PageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 充电机器人查询请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ChargingRobotQueryRequest extends PageRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 机器人唯一标识符 (模糊查询)
*/
private String robotUid;
/**
* 状态 (精确查询, 来自 RobotStatusEnum 的 value)
*/
private String status;
// 可以根据需要添加其他查询条件,如电量范围等
}

View File

@@ -0,0 +1,39 @@
package com.yupi.project.model.dto.charging_robot;
import lombok.Data;
import java.io.Serializable;
/**
* 更新充电机器人请求
*/
@Data
public class ChargingRobotUpdateRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 机器人ID (主键)
*/
private Long id;
/**
* 机器人唯一标识符 (一般不允许修改,但提供以便查询)
*/
private String robotUid;
/**
* 状态 (来自 RobotStatusEnum 的 value)
*/
private String status;
/**
* 当前位置描述
*/
private String currentLocation;
/**
* 电池电量百分比 (0-100)
*/
private Integer batteryLevel;
// currentTaskId 不建议直接通过此接口修改,应由业务流程驱动
}

View File

@@ -0,0 +1,28 @@
package com.yupi.project.model.dto.charging_session;
import lombok.Data;
import java.io.Serializable;
/**
* 用户发起充电请求的 DTO
*/
@Data
public class ChargingRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 请求充电的车位ID (parking_spot.id)
*/
private Long spotId;
/**
* 用户期望充电时长 (分钟) - 可选,系统也可有默认值或按需充电
*/
private Integer expectedDurationMinutes;
// 可以根据需求添加其他参数,例如:
// private String vehiclePlate; // 车牌号,用于某些特定场景
// private String preferredRobotType; // 偏好的机器人类型 (如果支持多种)
}

View File

@@ -0,0 +1,62 @@
package com.yupi.project.model.dto.charging_session;
import com.yupi.project.common.PageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.List;
/**
* 充电会话查询请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ChargingSessionQueryRequest extends PageRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 会话ID
*/
private Long id;
/**
* 用户ID (管理员接口可能需要)
*/
private Long userId;
/**
* 机器人ID
*/
private Long robotId;
/**
* 车位ID
*/
private Long spotId;
/**
* 机器人UID快照 (模糊查询)
*/
private String robotUidSnapshot;
/**
* 车位UID快照 (模糊查询)
*/
private String spotUidSnapshot;
/**
* 会话状态 (精确查询, 来自 ChargingSessionStatusEnum 的 value)
*/
private String status;
/**
* 多个会话状态 (用于查询)
*/
private List<String> orStatusList;
/**
* 支付状态 (精确查询, 来自 PaymentStatusEnum 的 value)
*/
private String paymentStatus;
}

View File

@@ -0,0 +1,17 @@
package com.yupi.project.model.dto.charging_session;
import lombok.Data;
import java.io.Serializable;
/**
* 支付/取消请求 (主要用于传递sessionId)
*/
@Data
public class PaymentRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电会话ID
*/
private Long sessionId;
}

View File

@@ -5,9 +5,17 @@ import lombok.Data;
@Data
public class RobotStatusMessage {
private Long taskId;
private String status; // e.g., "PROCESSING", "COMPLETED", "FAILED"
private String message;
private String errorCode;
// 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)
}

View File

@@ -0,0 +1,27 @@
package com.yupi.project.model.dto.parking_spot;
import lombok.Data;
import java.io.Serializable;
/**
* 添加车位请求
*/
@Data
public class ParkingSpotAddRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 车位唯一标识符
*/
private String spotUid;
/**
* 位置描述
*/
private String locationDescription;
/**
* 此车位是否可指派机器人前往 (默认为 true)
*/
private Boolean robotAssignable;
}

View File

@@ -0,0 +1,31 @@
package com.yupi.project.model.dto.parking_spot;
import com.yupi.project.common.PageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 车位查询请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ParkingSpotQueryRequest extends PageRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 车位唯一标识符 (模糊查询)
*/
private String spotUid;
/**
* 状态 (精确查询, 来自 ParkingSpotStatusEnum 的 value)
*/
private String status;
/**
* 此车位是否可指派机器人前往 (精确查询)
*/
private Boolean robotAssignable;
}

View File

@@ -0,0 +1,39 @@
package com.yupi.project.model.dto.parking_spot;
import lombok.Data;
import java.io.Serializable;
/**
* 更新车位请求
*/
@Data
public class ParkingSpotUpdateRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 车位ID (主键)
*/
private Long id;
/**
* 车位唯一标识符
*/
private String spotUid;
/**
* 状态 (来自 ParkingSpotStatusEnum 的 value)
*/
private String status;
/**
* 位置描述
*/
private String locationDescription;
/**
* 此车位是否可指派机器人前往
*/
private Boolean robotAssignable;
// currentSessionId 不建议直接通过此接口修改,应由业务流程驱动
}

View File

@@ -0,0 +1,71 @@
package com.yupi.project.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 充电机器人表
*
* @TableName charging_robot
*/
@TableName(value = "charging_robot")
@Data
public class ChargingRobot implements Serializable {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 机器人唯一标识符 (例如,硬件序列号)
*/
private String robotUid;
/**
* 机器人状态 (IDLE, MOVING, CHARGING, ERROR, MAINTENANCE, OFFLINE)
*/
private String status;
/**
* 当前位置描述 (例如坐标或区域ID)
*/
private String currentLocation;
/**
* 电池电量百分比 (0-100)
*/
private Integer batteryLevel;
/**
* 当前执行的任务ID (关联 robot_task.id)
*/
private Long currentTaskId;
/**
* 最后心跳时间
*/
private Date lastHeartbeatTime;
/**
* 注册时间
*/
private Date createTime;
/**
* 最后更新时间
*/
private Date updateTime;
/**
* 逻辑删除标志 (0:未删, 1:已删)
*/
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,132 @@
package com.yupi.project.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 充电记录表
*
* @TableName charging_session
*/
@TableName(value = "charging_session")
@Data
public class ChargingSession implements Serializable {
/**
* 主键ID充电会话唯一标识
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户ID (关联 user.id)
*/
private Long userId;
/**
* 服务的机器人ID (关联 charging_robot.id)
*/
private Long robotId;
/**
* 服务机器人的UID快照
*/
private String robotUidSnapshot;
/**
* 车位ID (关联 parking_spot.id)
*/
private Long spotId;
/**
* 车位UID快照
*/
private String spotUidSnapshot;
/**
* 充电会话状态
*/
private String status;
/**
* 用户请求充电时间
*/
private Date requestTime;
/**
* 机器人分配时间
*/
private Date robotAssignedTime;
/**
* 机器人到达车位时间
*/
private Date robotArrivalTime;
/**
* 充电开始时间
*/
private Date chargeStartTime;
/**
* 充电结束时间
*/
private Date chargeEndTime;
/**
* 总充电时长 (秒)
*/
private Integer totalDurationSeconds;
/**
* 消耗电量 (kWh)
*/
private BigDecimal energyConsumedKwh;
/**
* 本次充电费用 (元)
*/
private BigDecimal cost;
/**
* 支付状态
*/
private String paymentStatus;
/**
* 错误码
*/
private String errorCode;
/**
* 错误信息
*/
private String errorMessage;
/**
* 关联的RobotTask ID
*/
private Long relatedRobotTaskId;
/**
* 记录创建时间
*/
private Date createTime;
/**
* 记录更新时间
*/
private Date updateTime;
/**
* 逻辑删除标志 (0:未删, 1:已删)
*/
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,66 @@
package com.yupi.project.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 车位表
*
* @TableName parking_spot
*/
@TableName(value = "parking_spot")
@Data
public class ParkingSpot implements Serializable {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 车位唯一标识符 (例如,车位号)
*/
private String spotUid;
/**
* 车位状态 (AVAILABLE, OCCUPIED_BY_CAR, RESERVED, CHARGING, MAINTENANCE, UNAVAILABLE)
*/
private String status;
/**
* 位置描述 (例如,楼层、区域)
*/
private String locationDescription;
/**
* 此车位是否可指派机器人前往
*/
private Boolean robotAssignable;
/**
* 当前占用此车位的充电会话ID (关联 charging_session.id)
*/
private Long currentSessionId;
/**
* 创建时间
*/
private Date createTime;
/**
* 最后更新时间
*/
private Date updateTime;
/**
* 逻辑删除标志 (0:未删, 1:已删)
*/
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -17,34 +17,42 @@ public class RobotTask implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("robot_id")
private String robotId;
// Store enum as String in DB, but handle as Enum in Java
// Consider using MyBatis-Plus EnumTypeHandler.VARCHAR if direct mapping is problematic,
// or ensure the String value from the enum is used for persistence.
@TableField("command_type")
private CommandTypeEnum commandType;
@TableField("command_payload")
private String commandPayload;
// Store enum as String in DB
@TableField("status")
private RobotTaskStatusEnum status;
@TableField("sent_time")
private Date sentTime;
@TableField("ack_time")
private Date ackTime;
private Long relatedSessionId;
@TableField("related_session_id")
private Long relatedSessionId; // 关联的充电会话ID (对应数据库表中的related_session_id)
@TableField("error_message")
private String errorMessage;
@TableField(fill = FieldFill.INSERT)
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@TableLogic // If soft delete is desired, though not specified in DDL
@TableField(select = false) // By default, don't select the delete flag unless explicitly queried
@TableField(value = "is_delete", select = false) // By default, don't select the delete flag unless explicitly queried
private Integer isDelete; // Common practice for @TableLogic
private static final long serialVersionUID = 1L;

View File

@@ -0,0 +1,48 @@
package com.yupi.project.model.enums;
import lombok.Getter;
/**
* 充电会话状态枚举
*/
@Getter
public enum ChargingSessionStatusEnum {
REQUESTED("REQUESTED", "已请求"), // 用户发起充电请求
ROBOT_ASSIGNED("ROBOT_ASSIGNED", "机器人已分配"), // 系统已分配机器人
ROBOT_EN_ROUTE("ROBOT_EN_ROUTE", "机器人前往中"), // 机器人正在前往车位 (MQTT: move ack)
ROBOT_ARRIVED("ROBOT_ARRIVED", "机器人已到达"), // 机器人已到达指定车位 (MQTT: arrive ack)
CHARGING_STARTED("CHARGING_STARTED", "充电进行中"), // 机器人开始充电 (MQTT: start_charge ack)
CHARGING_COMPLETED("CHARGING_COMPLETED", "充电已完成"), // 机器人完成充电 (MQTT: stop_charge ack)
PAYMENT_PENDING("PAYMENT_PENDING", "待支付"), // 充电完成,等待用户支付
PAID("PAID", "已支付"), // 用户已支付
CANCELLED_BY_USER("CANCELLED_BY_USER", "用户取消"), // 用户在特定阶段取消
CANCELLED_BY_SYSTEM("CANCELLED_BY_SYSTEM", "系统取消"), // 例如,无可用机器人,机器人故障等
ERROR("ERROR", "会话错误"); // 充电过程中发生未知错误
private final String value;
private final String description;
ChargingSessionStatusEnum(String value, String description) {
this.value = value;
this.description = description;
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static ChargingSessionStatusEnum getEnumByValue(String value) {
if (value == null) {
return null;
}
for (ChargingSessionStatusEnum anEnum : ChargingSessionStatusEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
}

View File

@@ -13,10 +13,36 @@ public enum CommandTypeEnum {
QUERY_STATUS("QUERY_STATUS", "查询状态");
private final String value;
private final String description;
private final String ackValue;
CommandTypeEnum(String value, String description) {
CommandTypeEnum(String value, String ackValue) {
this.value = value;
this.description = description;
this.ackValue = ackValue;
}
public String getValue() {
return value;
}
public String getAckValue() {
return ackValue;
}
/**
* 根据ackValue获取枚举
*
* @param ackValue
* @return
*/
public static CommandTypeEnum fromAck(String ackValue) {
if (ackValue == null) {
return null;
}
for (CommandTypeEnum anEnum : CommandTypeEnum.values()) {
if (ackValue.equalsIgnoreCase(anEnum.getAckValue())) {
return anEnum;
}
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
package com.yupi.project.model.enums;
import lombok.Getter;
/**
* 车位状态枚举
*/
@Getter
public enum ParkingSpotStatusEnum {
AVAILABLE("AVAILABLE", "可用"),
OCCUPIED_BY_CAR("OCCUPIED_BY_CAR", "有车占用"),
RESERVED("RESERVED", "已预订"),
CHARGING("CHARGING", "充电中"), // 指此车位正在进行充电服务
MAINTENANCE("MAINTENANCE", "维护中"),
UNAVAILABLE("UNAVAILABLE", "不可用");
private final String value;
private final String description;
ParkingSpotStatusEnum(String value, String description) {
this.value = value;
this.description = description;
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static ParkingSpotStatusEnum getEnumByValue(String value) {
if (value == null) {
return null;
}
for (ParkingSpotStatusEnum anEnum : ParkingSpotStatusEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
}

View File

@@ -0,0 +1,42 @@
package com.yupi.project.model.enums;
import lombok.Getter;
/**
* 支付状态枚举
*/
@Getter
public enum PaymentStatusEnum {
PENDING("PENDING", "待支付"),
PAID("PAID", "已支付"),
FAILED("FAILED", "支付失败"),
REFUNDED("REFUNDED", "已退款"),
NOT_REQUIRED("NOT_REQUIRED", "无需支付"); // 例如免费活动或内部测试
private final String value;
private final String description;
PaymentStatusEnum(String value, String description) {
this.value = value;
this.description = description;
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static PaymentStatusEnum getEnumByValue(String value) {
if (value == null) {
return null;
}
for (PaymentStatusEnum anEnum : PaymentStatusEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
package com.yupi.project.model.enums;
import lombok.Getter;
/**
* 机器人状态枚举
*/
@Getter
public enum RobotStatusEnum {
IDLE("IDLE", "空闲"),
MOVING("MOVING", "移动中"),
CHARGING("CHARGING", "充电服务中"),
ERROR("ERROR", "故障"),
MAINTENANCE("MAINTENANCE", "维护中"),
OFFLINE("OFFLINE", "离线");
private final String value;
private final String description;
RobotStatusEnum(String value, String description) {
this.value = value;
this.description = description;
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static RobotStatusEnum getEnumByValue(String value) {
if (value == null) {
return null;
}
for (RobotStatusEnum anEnum : RobotStatusEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
}

View File

@@ -36,4 +36,22 @@ public enum RobotTaskStatusEnum {
.findFirst()
.orElse(null);
}
/**
* 判断是否为最终状态 (COMPLETED, FAILED, TIMED_OUT)
* @param status 要检查的状态
* @return 如果是最终状态则为 true
*/
public static boolean isFinalStatus(RobotTaskStatusEnum status) {
if (status == null) return false;
return status == COMPLETED || status == FAILED || status == TIMED_OUT;
}
/**
* 判断当前枚举实例是否为最终状态
* @return 如果是最终状态则为 true
*/
public boolean isFinalStatus() {
return this == COMPLETED || this == FAILED || this == TIMED_OUT;
}
}

View File

@@ -0,0 +1,97 @@
package com.yupi.project.model.vo;
import com.yupi.project.model.entity.ChargingSession;
import com.yupi.project.model.entity.User;
import com.yupi.project.model.enums.ChargingSessionStatusEnum;
import com.yupi.project.model.enums.PaymentStatusEnum;
import com.yupi.project.model.vo.UserVO;
import lombok.Data;
import org.springframework.beans.BeanUtils;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 充电会话视图对象
*/
@Data
public class ChargingSessionVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long userId;
private UserVO user; // 关联的用户信息 (脱敏后)
private Long robotId;
// private ChargingRobotVO robot; // 关联的机器人信息 (可选)
private String robotUidSnapshot;
private Long spotId;
// private ParkingSpotVO spot; // 关联的车位信息 (可选)
private String spotUidSnapshot;
private String status;
private String statusText; // 状态文本描述
private Date requestTime;
private Date robotAssignedTime;
private Date robotArrivalTime;
private Date chargeStartTime;
private Date chargeEndTime;
private Integer totalDurationSeconds;
private BigDecimal energyConsumedKwh;
private BigDecimal cost;
private String paymentStatus;
private String paymentStatusText; // 支付状态文本描述
private String errorCode;
private String errorMessage;
private Long relatedRobotTaskId;
private Date createTime;
private Date updateTime;
/**
* 包装类转对象
*
* @param vo
* @return
*/
public static ChargingSession voToObj(ChargingSessionVO vo) {
if (vo == null) {
return null;
}
ChargingSession obj = new ChargingSession();
BeanUtils.copyProperties(vo, obj);
return obj;
}
/**
* 对象转包装类
*
* @param obj
* @return
*/
public static ChargingSessionVO objToVo(ChargingSession obj) {
if (obj == null) {
return null;
}
ChargingSessionVO vo = new ChargingSessionVO();
BeanUtils.copyProperties(obj, vo);
// 根据枚举值设置 statusText 和 paymentStatusText
if (obj.getStatus() != null) {
ChargingSessionStatusEnum statusEnum = ChargingSessionStatusEnum.getEnumByValue(obj.getStatus());
if (statusEnum != null) {
vo.setStatusText(statusEnum.getDescription());
}
}
if (obj.getPaymentStatus() != null) {
PaymentStatusEnum paymentStatusEnum = PaymentStatusEnum.getEnumByValue(obj.getPaymentStatus());
if (paymentStatusEnum != null) {
vo.setPaymentStatusText(paymentStatusEnum.getDescription());
}
}
return vo;
}
// 如果需要关联 UserVO 等,可以添加 set方法
public void setUser(User user) {
if (user != null) {
this.user = UserVO.objToVo(user); // 假设 UserVO 也有类似的转换方法
}
}
}

View File

@@ -0,0 +1,88 @@
package com.yupi.project.model.vo;
import com.yupi.project.model.entity.User;
import lombok.Data;
import org.springframework.beans.BeanUtils;
import java.io.Serializable;
import java.util.Date;
/**
* 用户视图
*
*/
@Data
public class UserVO implements Serializable {
/**
* id
*/
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 用户简介
*/
private String userProfile;
/**
* 用户角色: user/admin/ban
*/
private String userRole;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 余额 (分)
*/
private Long balance; // 新增余额字段与User实体对应
private static final long serialVersionUID = 1L;
/**
* 包装类转对象
*
* @param userVO
* @return
*/
public static User voToObj(UserVO userVO) {
if (userVO == null) {
return null;
}
User user = new User();
BeanUtils.copyProperties(userVO, user);
return user;
}
/**
* 对象转包装类
*
* @param user
* @return
*/
public static UserVO objToVo(User user) {
if (user == null) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
// 注意:此处不应复制敏感信息如 userPassword
return userVO;
}
}

View File

@@ -0,0 +1,17 @@
package com.yupi.project.mqtt.handler;
/**
* MQTT消息处理器接口
*/
public interface MqttMessageHandler {
/**
* 处理来自MQTT的状态更新消息
*
* @param topic 消息主题
* @param payload 消息内容
*/
void handleStatusUpdate(String topic, String payload);
// 可以根据需要添加其他类型的消息处理方法
}

View File

@@ -0,0 +1,111 @@
package com.yupi.project.mqtt.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
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.model.enums.ChargingSessionStatusEnum;
import com.yupi.project.service.ChargingSessionService;
import com.yupi.project.service.RobotTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;
@Component
@Slf4j
public class MqttMessageHandlerImpl implements MqttMessageHandler {
@Resource
private RobotTaskService robotTaskService;
@Resource
private ChargingSessionService chargingSessionService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handleStatusUpdate(String topic, String payload) {
log.info("收到MQTT状态消息 - Topic: {}, Payload: {}", topic, payload);
try {
// 1. 解析Payload
Map<String, Object> payloadMap = objectMapper.readValue(payload, Map.class);
String robotUid = extractRobotUidFromTopic(topic);
String commandTypeStr = (String) payloadMap.get("command_ack");
CommandTypeEnum commandType = CommandTypeEnum.fromAck(commandTypeStr);
Long originalTaskId = ((Number) payloadMap.get("task_id")).longValue();
boolean success = (Boolean) payloadMap.getOrDefault("success", false);
String message = (String) payloadMap.getOrDefault("message", "");
if (robotUid == null || commandType == null || originalTaskId == null) {
log.error("MQTT状态消息解析失败robotUid, commandType,或taskId为空. Topic: {}, Payload: {}", topic, payload);
return;
}
// 2. 更新RobotTask状态
RobotTask task = robotTaskService.getById(originalTaskId);
if (task == null) {
log.warn("未找到与MQTT消息关联的机器人任务: taskId={}, robotUid={}", originalTaskId, robotUid);
return;
}
if (!RobotTaskStatusEnum.SENT.getValue().equals(task.getStatus())){
log.warn("任务 {} 状态为 {}, 非SENT状态不再处理ACK消息。", originalTaskId, task.getStatus());
return;
}
robotTaskService.markTaskAsAcknowledged(originalTaskId, success, success ? null : message, new Date());
log.info("机器人任务 {} (robotUid: {}) 已确认为: {}, 消息: '{}'", originalTaskId, robotUid, success ? "成功" : "失败", message);
// 3. 如果任务成功,则根据任务类型更新 ChargingSession
if (success) {
Long sessionId = task.getRelatedSessionId();
if (sessionId == null) {
log.warn("机器人任务 {} 成功但未关联充电会话ID不处理会话状态更新。", originalTaskId);
return;
}
switch (commandType) {
case MOVE_TO_SPOT:
log.info("机器人 {} 到达车位指令已确认 (任务ID: {}), 更新会话 {} 状态。", robotUid, originalTaskId, sessionId);
chargingSessionService.handleRobotArrival(sessionId, originalTaskId);
break;
case START_CHARGE:
log.info("机器人 {} 开始充电指令已确认 (任务ID: {}), 更新会话 {} 状态。", robotUid, originalTaskId, sessionId);
chargingSessionService.handleChargingStart(sessionId, originalTaskId);
break;
case STOP_CHARGE:
BigDecimal energyConsumed = payloadMap.containsKey("energy_kwh") ? new BigDecimal(payloadMap.get("energy_kwh").toString()) : BigDecimal.ZERO;
int durationSeconds = payloadMap.containsKey("duration_s") ? ((Number) payloadMap.get("duration_s")).intValue() : 0;
log.info("机器人 {} 停止充电指令已确认 (任务ID: {}), 更新会话 {} 状态。电量: {}kWh, 时长: {}s",
robotUid, originalTaskId, sessionId, energyConsumed, durationSeconds);
chargingSessionService.handleChargingEnd(sessionId, originalTaskId, energyConsumed, durationSeconds);
break;
default:
log.info("收到机器人任务 {} (类型:{}) 的ACK但此类型不直接更新充电会话。", originalTaskId, commandType);
break;
}
} else {
Long sessionId = task.getRelatedSessionId();
if (sessionId != null) {
log.warn("机器人任务 {} (类型:{}) 失败,将关联的会话 {} 标记为错误。错误: {}", originalTaskId, commandType, sessionId, message);
chargingSessionService.cancelChargingSession(sessionId, null,
"机器人任务失败: " + commandType + " - " + message,
ChargingSessionStatusEnum.ERROR);
}
}
} catch (Exception e) {
log.error("处理MQTT状态消息时发生错误 - Topic: {}, Payload: {}", topic, payload, e);
}
}
private String extractRobotUidFromTopic(String topic) {
String[] parts = topic.split("/");
if (parts.length > 0) {
return parts[parts.length - 1];
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
import com.yupi.project.model.entity.RobotTask;
import com.yupi.project.service.ChargingSessionService;
import com.yupi.project.service.RobotTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
@Component
@Slf4j
public class TaskTimeoutHandler {
@Resource
private RobotTaskService robotTaskService;
@Resource
private ChargingSessionService chargingSessionService;
@Value("${mqtt.task.timeoutSeconds:300}")
private int taskTimeoutSeconds;
@Scheduled(fixedRateString = "${mqtt.task.timeoutCheckRateMs:60000}")
public void checkForTimedOutTasks() {
log.debug("开始检查超时的机器人任务 (超时阈值: {}s)", taskTimeoutSeconds);
List<RobotTask> timedOutTasks = robotTaskService.findAndMarkTimedOutTasks(taskTimeoutSeconds);
if (timedOutTasks.isEmpty()) {
log.debug("没有发现超时的机器人任务。");
return;
}
log.warn("发现 {} 个超时的机器人任务。", timedOutTasks.size());
for (RobotTask task : timedOutTasks) {
log.warn("任务ID: {} (RobotUID: {}, Command: {}, Status: {}, SentTime: {}) 已超时。",
task.getId(), task.getRobotId(), task.getCommandType(), task.getStatus(), task.getSentTime());
if (task.getRelatedSessionId() != null) {
log.info("任务 {} 超时与充电会话 {} 相关联,将通知会话服务处理。", task.getId(), task.getRelatedSessionId());
try {
chargingSessionService.handleSessionTaskTimeout(task.getId());
} catch (Exception e) {
log.error("调用 chargingSessionService.handleSessionTaskTimeout 失败 for task ID {}: ", task.getId(), e);
}
} else {
log.warn("任务 {} 超时,但未关联任何充电会话。", task.getId());
}
}
}
}

View File

@@ -0,0 +1,89 @@
package com.yupi.project.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yupi.project.model.entity.ChargingRobot;
import com.yupi.project.model.enums.RobotStatusEnum;
import java.util.Date;
import java.util.List;
/**
* @author Yupi
* @description 针对表【charging_robot(充电机器人表)】的数据库操作Service
* @createDate 2023-12-03 10:00:00
*/
public interface ChargingRobotService extends IService<ChargingRobot> {
/**
* 注册一个新的充电机器人
*
* @param robotUid 机器人唯一ID
* @param initialStatus 初始状态
* @return 创建的机器人实体如果已存在则返回null或抛出异常
*/
ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus);
/**
* 根据机器人UID查找机器人
*
* @param robotUid 机器人唯一ID
* @return 机器人实体未找到则返回null
*/
ChargingRobot findByRobotUid(String robotUid);
/**
* 更新机器人电量
*
* @param robotId 机器人DB主键ID
* @param batteryLevel 电量百分比
* @return 是否更新成功
*/
boolean updateBatteryLevel(Long robotId, Integer batteryLevel);
/**
* 更新机器人心跳时间
*
* @param robotId 机器人DB主键ID
* @param heartbeatTime 心跳时间
* @return 是否更新成功
*/
boolean updateHeartbeatTime(Long robotId, Date heartbeatTime);
/**
* 查找指定状态的机器人列表
*
* @param status 机器人状态
* @return 机器人列表
*/
List<ChargingRobot> findRobotsByStatus(RobotStatusEnum status);
/**
* 分配一个空闲的机器人用于执行任务
* (此方法需要考虑并发和选择策略)
*
* @return 分配到的机器人实体若无空闲则返回null
*/
ChargingRobot assignIdleRobot();
/**
* 释放机器人(例如任务完成或取消后)
*
* @param robotId 机器人ID
* @return 操作是否成功
*/
boolean releaseRobot(Long robotId);
/**
* 更新机器人的实时状态。
* 此方法用于处理机器人通过MQTT等方式上报的常规状态信息如心跳
*
* @param robotUID 机器人唯一标识符
* @param status 机器人上报的当前状态 (枚举)
* @param location 机器人上报的当前位置 (可选)
* @param batteryLevel 机器人上报的电池电量 (可选, 0-100)
* @param currentTaskId 机器人当前正在执行的任务ID (可选, 如果上报了)
* @param lastHeartbeatTime 收到此状态消息的时间,可视为最后心跳时间
* @return 更新是否成功
*/
boolean updateRobotStatus(String robotUID, RobotStatusEnum status, String location, Integer batteryLevel, Long currentTaskId, Date lastHeartbeatTime);
}

View File

@@ -0,0 +1,134 @@
package com.yupi.project.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yupi.project.model.dto.charging_session.ChargingRequest;
import com.yupi.project.model.entity.ChargingSession;
import com.yupi.project.model.entity.User;
import com.yupi.project.model.enums.ChargingSessionStatusEnum;
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
import java.math.BigDecimal;
import java.util.List;
/**
* @author Yupi
* @description 针对表【charging_session(充电记录表)】的数据库操作Service
* @createDate 2023-12-03 10:00:00
*/
public interface ChargingSessionService extends IService<ChargingSession> {
/**
* 用户请求充电
*
* @param currentUser 当前登录用户
* @param chargingRequest 充电请求参数 (如 车位ID等)
* @return 创建的充电会话实体
*/
ChargingSession requestCharging(User currentUser, ChargingRequest chargingRequest);
/**
* 系统处理充电请求,分配机器人
* (通常在 requestCharging 内部被调用,或由异步任务触发)
*
* @param sessionId 充电会话ID
* @return 更新后的充电会话实体或null如果无法分配
*/
ChargingSession assignRobotToSession(Long sessionId);
/**
* 更新充电会话状态 (机器人已到达)
*
* @param sessionId 充电会话ID
* @param robotTaskId 关联的机器人任务ID (例如,机器人移动任务)
* @return 是否成功
*/
boolean handleRobotArrival(Long sessionId, Long robotTaskId);
/**
* 更新充电会话状态 (充电开始)
*
* @param sessionId 充电会话ID
* @param robotTaskId 关联的机器人任务ID (例如,开始充电任务)
* @return 是否成功
*/
boolean handleChargingStart(Long sessionId, Long robotTaskId);
/**
* 更新充电会话状态 (充电结束)
*
* @param sessionId 充电会话ID
* @param robotTaskId 关联的机器人任务ID (例如,结束充电任务)
* @param energyConsumedKwh 消耗电量
* @param durationSeconds 充电时长
* @return 是否成功
*/
boolean handleChargingEnd(Long sessionId, Long robotTaskId, BigDecimal energyConsumedKwh, int durationSeconds);
/**
* 计算费用并更新会话 (在充电结束后调用)
*
* @param sessionId 充电会话ID
* @return 是否成功
*/
boolean calculateCostAndFinalizeSession(Long sessionId);
/**
* 用户支付充电费用
*
* @param sessionId 充电会话ID
* @param userId 用户ID (用于校验)
* @return 是否成功
*/
boolean processPayment(Long sessionId, Long userId);
/**
* 取消充电会话
*
* @param sessionId 充电会话ID
* @param userId (可选) 如果是用户发起的取消则为用户ID
* @param reason 取消原因/发起方
* @return 是否成功
*/
boolean cancelChargingSession(Long sessionId, Long userId, String reason, ChargingSessionStatusEnum cancelStatus);
/**
* 根据用户ID查找其充电记录
*
* @param userId 用户ID
* @return 充电会话列表
*/
List<ChargingSession> findUserSessions(Long userId);
/**
* 根据机器人任务ID查找关联的充电会话
* @param robotTaskId 机器人任务ID
* @return 充电会话
*/
ChargingSession findSessionByRobotTaskId(Long robotTaskId);
/**
* 处理充电会话相关的机器人任务超时
* @param robotTaskId 超时的机器人任务ID
*/
void handleSessionTaskTimeout(Long robotTaskId);
/**
* 用户请求停止当前正在进行的充电
* (会向机器人发送停止指令,等待机器人确认后再完成计费)
*
* @param sessionId 充电会话ID
* @param userId 用户ID (用于校验)
* @return 是否成功发起停止请求 (不代表已完成)
*/
boolean stopChargingByUser(Long sessionId, Long userId);
/**
* 获取查询包装类
*
* @param queryRequest
* @return
*/
QueryWrapper<ChargingSession> getQueryWrapper(ChargingSessionQueryRequest queryRequest);
}

View File

@@ -0,0 +1,79 @@
package com.yupi.project.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yupi.project.model.entity.ParkingSpot;
import com.yupi.project.model.enums.ParkingSpotStatusEnum;
import java.util.List;
/**
* @author Yupi
* @description 针对表【parking_spot(车位表)】的数据库操作Service
* @createDate 2023-12-03 10:00:00
*/
public interface ParkingSpotService extends IService<ParkingSpot> {
/**
* 添加新的车位信息
*
* @param spotUid 车位唯一ID
* @param locationDescription 位置描述
* @param robotAssignable 是否可指派机器人
* @return 创建的车位实体
*/
ParkingSpot addParkingSpot(String spotUid, String locationDescription, boolean robotAssignable);
/**
* 根据车位UID查找车位
*
* @param spotUid 车位唯一ID
* @return 车位实体未找到则返回null
*/
ParkingSpot findBySpotUid(String spotUid);
/**
* 更新车位状态
*
* @param spotId 车位DB主键ID
* @param newStatus 新状态
* @param currentSessionId (可选) 当前关联的充电会话ID
* @return 是否更新成功
*/
boolean updateParkingSpotStatus(Long spotId, ParkingSpotStatusEnum newStatus, Long currentSessionId);
/**
* 查找所有可用的、可指派机器人的车位
*
* @return 可用车位列表
*/
List<ParkingSpot> findAvailableAndAssignableSpots();
/**
* 占用一个车位(例如,用户预订或开始充电会话时)
*
* @param spotId 车位ID
* @param sessionId 关联的充电会话ID
* @param targetStatus 目标状态 (如 OCCUPIED_BY_CAR, RESERVED, CHARGING)
* @return 操作是否成功
*/
boolean occupySpot(Long spotId, Long sessionId, ParkingSpotStatusEnum targetStatus);
/**
* 释放一个车位 (例如,充电结束或取消预订)
*
* @param spotId 车位ID
* @return 操作是否成功
*/
boolean releaseSpot(Long spotId);
/**
* 更新车位状态及当前关联的充电会话ID。
*
* @param spotUID 车位唯一标识符
* @param newStatus 新的车位状态
* @param currentSessionId 当前占用此车位的充电会话ID (如果车位变为空闲,则应传入 null)
* @return 更新是否成功
*/
boolean updateSpotStatus(String spotUID, ParkingSpotStatusEnum newStatus, Long currentSessionId);
}

View File

@@ -92,14 +92,13 @@ public interface RobotTaskService extends IService<RobotTask> {
boolean markTaskAsCompleted(Long taskId, Date ackTime, String message);
/**
* 将任务标记为失败
* 标记任务为失败
*
* @param taskId 任务ID
* @param ackTime 失败确认时间 (可选)
* @param errorCode 错误码 (可选)
* @param errorMessage 错误信息 (可选)
* @return 是否成功标记
* @param taskId 任务ID
* @param errorMessage 错误信息
* @param failedTime 失败时间
* @return 是否成功
*/
boolean markTaskAsFailed(Long taskId, Date ackTime, String errorCode, String errorMessage);
boolean markTaskAsFailed(Long taskId, String errorMessage, Date failedTime);
}

View File

@@ -57,20 +57,22 @@ public interface UserService extends IService<User> {
User getSafetyUser(User originUser);
/**
* 扣减用户余额 (需要保证线程安全和数据一致性)
* 扣减用户余额
*
* @param userId 用户ID
* @param amount 扣减金额 (正数)
* @return 操作是否成功
* @param amount 扣减金额 (注意这里假设amount是正数)
* @return 操作是否成功 (例如,余额不足则失败)
*/
boolean deductBalance(Long userId, BigDecimal amount);
boolean decreaseBalance(Long userId, BigDecimal amount);
/**
* 增加用户余额 (需要保证线程安全和数据一致性)
* 增加用户余额
*
* @param userId 用户ID
* @param amount 增加金额 (正数)
* @param amount 增加金额
* @return 操作是否成功
*/
boolean addBalance(Long userId, BigDecimal amount);
boolean increaseBalance(Long userId, BigDecimal amount);
/**
* 获取用户列表 (仅管理员)

View File

@@ -0,0 +1,189 @@
package com.yupi.project.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
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.enums.RobotStatusEnum;
import com.yupi.project.service.ChargingRobotService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
/**
* @author Yupi
* @description 针对表【charging_robot(充电机器人表)】的数据库操作Service实现
* @createDate 2023-12-03 10:00:00
*/
@Service
@Slf4j
public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, ChargingRobot>
implements ChargingRobotService {
@Override
@Transactional
public ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus) {
if (findByRobotUid(robotUid) != null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "机器人UID已存在: " + robotUid);
}
ChargingRobot robot = new ChargingRobot();
robot.setRobotUid(robotUid);
robot.setStatus(initialStatus.getValue());
robot.setCreateTime(new Date());
robot.setUpdateTime(new Date());
boolean saved = this.save(robot);
if (!saved) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "机器人注册失败");
}
log.info("机器人注册成功: {}", robot);
return robot;
}
@Override
public ChargingRobot findByRobotUid(String robotUid) {
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("robot_uid", robotUid);
return this.getOne(queryWrapper);
}
@Override
@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);
return false;
}
ChargingRobot robot = this.findByRobotUid(robotUID);
if (robot == null) {
log.warn("更新机器人状态失败:未找到 UID 为 {} 的机器人。", robotUID);
return false;
}
ChargingRobot robotToUpdate = new ChargingRobot();
robotToUpdate.setId(robot.getId());
boolean changed = false;
if (status != null && !status.getValue().equals(robot.getStatus())) {
robotToUpdate.setStatus(status.getValue());
changed = true;
}
if (location != null && !location.equals(robot.getCurrentLocation())) {
robotToUpdate.setCurrentLocation(location);
changed = true;
}
if (batteryLevel != null && !batteryLevel.equals(robot.getBatteryLevel())) {
robotToUpdate.setBatteryLevel(batteryLevel);
changed = true;
}
if (currentTaskId != null && !currentTaskId.equals(robot.getCurrentTaskId())) {
robotToUpdate.setCurrentTaskId(currentTaskId);
changed = true;
}
if (lastHeartbeatTime != null && (robot.getLastHeartbeatTime() == null || !lastHeartbeatTime.equals(robot.getLastHeartbeatTime()))){
robotToUpdate.setLastHeartbeatTime(lastHeartbeatTime);
changed = true;
}
if (!changed) {
log.info("机器人 {} 的状态与数据库一致 (状态: {}, 位置: {}, 电量: {}, 任务ID: {}, 心跳: {}),无需更新。",
robotUID, status, location, batteryLevel, currentTaskId, lastHeartbeatTime);
return true;
}
boolean success = this.updateById(robotToUpdate);
if (success) {
log.info("成功更新机器人 {} 的状态。新状态: {}, 位置: {}, 电量: {}, 当前任务ID: {}, 心跳: {}",
robotUID, status, location, batteryLevel, currentTaskId, lastHeartbeatTime);
} else {
log.error("更新机器人 {} 的状态失败 (数据库操作失败)。", robotUID);
}
return success;
}
@Override
public boolean updateBatteryLevel(Long robotId, Integer batteryLevel) {
ChargingRobot robot = this.getById(robotId);
if (robot == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "机器人不存在");
}
if (batteryLevel < 0 || batteryLevel > 100) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的电池电量值");
}
robot.setBatteryLevel(batteryLevel);
robot.setUpdateTime(new Date());
return this.updateById(robot);
}
@Override
public boolean updateHeartbeatTime(Long robotId, Date heartbeatTime) {
ChargingRobot robot = this.getById(robotId);
if (robot == null) {
// 心跳通常来自已注册机器人,如果找不到可以只记录日志不抛异常,或根据策略决定
log.warn("尝试更新心跳失败,机器人不存在: {}", robotId);
return false;
}
robot.setLastHeartbeatTime(heartbeatTime);
robot.setUpdateTime(new Date()); // 可选,心跳更新是否算作整体记录更新
return this.updateById(robot);
}
@Override
public List<ChargingRobot> findRobotsByStatus(RobotStatusEnum status) {
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", status.getValue());
return this.list(queryWrapper);
}
@Override
@Transactional // 确保原子性
public ChargingRobot assignIdleRobot() {
// 简单策略:查找第一个空闲的机器人并尝试锁定
// 后续可优化:考虑负载均衡、机器人位置、电量等因素
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", RobotStatusEnum.IDLE.getValue())
.orderByAsc("update_time") // 尝试获取最近更新为空闲的,或根据其他策略排序
.last("FOR UPDATE"); // 行级锁,防止并发问题
List<ChargingRobot> idleRobots = this.list(queryWrapper);
if (idleRobots.isEmpty()) {
log.warn("没有可用的空闲机器人");
return null;
}
ChargingRobot assignedRobot = idleRobots.get(0); // 取第一个
// 更新机器人状态为MOVING或ASSIGNED并关联任务如果此时有任务ID
// 这里暂时只更新为MOVING具体任务ID关联由调用方处理
boolean success = updateRobotStatus(assignedRobot.getRobotUid(), RobotStatusEnum.MOVING, null, null, null, null);
if (success) {
log.info("成功分配机器人: {} (ID: {})", assignedRobot.getRobotUid(), assignedRobot.getId());
return assignedRobot;
} else {
log.error("分配机器人 {} 失败,未能更新其状态", assignedRobot.getRobotUid());
// 可能需要重试或选择其他机器人
throw new BusinessException(ErrorCode.OPERATION_ERROR, "分配机器人失败,无法更新状态");
}
}
@Override
public boolean releaseRobot(Long robotId) {
ChargingRobot robot = getById(robotId);
if (robot == null) {
log.warn("尝试释放机器人失败,机器人不存在: {}", robotId);
return false;
}
// 将机器人状态设置为空闲并清除当前任务ID
return updateRobotStatus(robot.getRobotUid(), RobotStatusEnum.IDLE, null, null, null, null); // 第二个参数传null以清除任务ID
}
}

View File

@@ -0,0 +1,582 @@
package com.yupi.project.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.mapper.ChargingSessionMapper;
import com.yupi.project.model.dto.charging_session.ChargingRequest;
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
import com.yupi.project.model.entity.*;
import com.yupi.project.model.enums.*;
import com.yupi.project.service.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author Yupi
* @description 针对表【charging_session(充电记录表)】的数据库操作Service实现
* @createDate 2023-12-03 10:00:00
*/
@Service
@Slf4j
public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMapper, ChargingSession>
implements ChargingSessionService {
@Resource
private ParkingSpotService parkingSpotService;
@Resource
private ChargingRobotService chargingRobotService;
@Resource
private UserService userService;
@Resource
private RobotTaskService robotTaskService; // 用于创建和关联机器人任务
@Resource
@Lazy // 避免循环依赖 MqttService -> ChargingSessionService -> MqttService (通过TaskTimeoutHandler)
private MqttService mqttService; // 用于发送指令给机器人
// 计费相关配置 (后续可以移到配置类或数据库)
@Value("${charging.price.perHour:10.0}") // 每小时10元
private BigDecimal pricePerHour;
@Value("${charging.minChargeAmount:0.5}") // 最低消费0.5元
private BigDecimal minChargeAmount;
@Override
@Transactional
public ChargingSession requestCharging(User currentUser, ChargingRequest chargingRequest) {
if (currentUser == null || chargingRequest == null || chargingRequest.getSpotId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数错误");
}
// 1. 校验用户账户状态,例如余额是否充足(如果需要预付费或押金)
// (此处简化,暂不校验余额)
// 2. 校验车位是否存在且可用
ParkingSpot spot = parkingSpotService.getById(chargingRequest.getSpotId());
if (spot == null || !spot.getRobotAssignable() || !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "车位不可用或不存在");
}
// 3. 创建充电会话记录
ChargingSession session = new ChargingSession();
session.setUserId(currentUser.getId());
session.setSpotId(spot.getId());
session.setSpotUidSnapshot(spot.getSpotUid());
session.setStatus(ChargingSessionStatusEnum.REQUESTED.getValue());
session.setRequestTime(new Date());
session.setPaymentStatus(PaymentStatusEnum.PENDING.getValue()); // 初始为待支付
session.setCreateTime(new Date());
session.setUpdateTime(new Date());
boolean saved = this.save(session);
if (!saved) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建充电会话失败");
}
log.info("用户 {} 请求在车位 {} (ID:{}) 开始充电会话 (ID:{})", currentUser.getId(), spot.getSpotUid(), spot.getId(), session.getId());
// 4. 尝试分配机器人 (可以同步或异步)
// 此处为简化,采用同步调用。实际项目中可能需要异步处理并通知用户分配结果。
ChargingSession updatedSession = assignRobotToSession(session.getId());
if (updatedSession == null) {
// 如果没有可用机器人,标记会话为系统取消
this.cancelChargingSession(session.getId(), null, "无可用机器人", ChargingSessionStatusEnum.CANCELLED_BY_SYSTEM);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "无可用机器人,充电请求失败");
}
return updatedSession;
}
@Override
@Transactional
public ChargingSession assignRobotToSession(Long sessionId) {
ChargingSession session = this.getById(sessionId);
if (session == null || !ChargingSessionStatusEnum.REQUESTED.getValue().equals(session.getStatus())) {
log.warn("分配机器人失败,会话不存在或状态不正确: sessionId={}", sessionId);
return null; // 或抛出异常
}
// 1. 查找并分配一个空闲机器人
ChargingRobot assignedRobot = chargingRobotService.assignIdleRobot(); // assignIdleRobot 内部已将机器人状态改为MOVING
if (assignedRobot == null) {
log.warn("分配机器人失败,没有空闲机器人可供会话 {} 使用", sessionId);
return null;
}
// 2. 更新车位状态为"已预订"或"机器人前往中"
boolean spotOccupied = parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.RESERVED, session.getId());
if (!spotOccupied) {
log.error("分配机器人后,占用车位 {} (UID: {}) 失败 for session {}", session.getSpotId(), session.getSpotUidSnapshot(), sessionId);
// 需要回滚机器人状态
chargingRobotService.releaseRobot(assignedRobot.getId());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "占用车位失败");
}
// 3. 创建机器人任务:移动到指定车位
// Payload 需要根据与硬件的约定来构建
String moveToSpotPayload = String.format("{\"command\":\"MOVE\", \"target_spot_uid\":\"%s\"}", session.getSpotUidSnapshot());
RobotTask moveTask = robotTaskService.createTask(assignedRobot.getRobotUid(), CommandTypeEnum.MOVE_TO_SPOT, moveToSpotPayload, sessionId);
if (moveTask == null) {
log.error("为会话 {} 创建机器人移动任务失败", sessionId);
chargingRobotService.releaseRobot(assignedRobot.getId());
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建机器人移动任务失败");
}
// 4. 更新充电会话信息
session.setRobotId(assignedRobot.getId());
session.setRobotUidSnapshot(assignedRobot.getRobotUid());
session.setStatus(ChargingSessionStatusEnum.ROBOT_ASSIGNED.getValue());
session.setRobotAssignedTime(new Date());
session.setRelatedRobotTaskId(moveTask.getId()); // 关联当前任务
session.setUpdateTime(new Date());
this.updateById(session);
// 5. 发送MQTT指令给机器人
try {
mqttService.sendCommand(assignedRobot.getRobotUid(), CommandTypeEnum.MOVE_TO_SPOT, moveToSpotPayload, moveTask.getId());
robotTaskService.markTaskAsSent(moveTask.getId(), new Date());
log.info("已向机器人 {} 发送移动指令 for session {}, 任务ID: {}", assignedRobot.getRobotUid(), sessionId, moveTask.getId());
} catch (Exception e) {
log.error("发送MQTT移动指令失败 for session {}, 任务ID: {}: ", sessionId, moveTask.getId(), e);
// 此处可能需要更复杂的错误处理,如重试、标记任务失败等
// 为了简化,如果发送失败,也回滚,并标记会话错误
robotTaskService.markTaskAsFailed(moveTask.getId(), "MQTT发送失败: " + e.getMessage(), new Date());
chargingRobotService.releaseRobot(assignedRobot.getId());
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
session.setStatus(ChargingSessionStatusEnum.ERROR.getValue());
session.setErrorMessage("分配机器人并发送指令失败");
this.updateById(session);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "发送MQTT指令失败");
}
log.info("成功为会话 {} 分配机器人 {} (任务ID: {})", sessionId, assignedRobot.getRobotUid(), moveTask.getId());
return session;
}
@Override
@Transactional
public boolean handleRobotArrival(Long sessionId, Long robotTaskId) {
ChargingSession session = this.getById(sessionId);
if (session == null) {
log.warn("处理机器人到达事件失败,会话不存在: sessionId={}", sessionId);
return false;
}
// 校验会话状态和关联的任务ID
if (!ChargingSessionStatusEnum.ROBOT_ASSIGNED.getValue().equals(session.getStatus()) ||
!robotTaskId.equals(session.getRelatedRobotTaskId())) {
log.warn("处理机器人到达事件失败,会话状态 ({}) 或任务ID ({}) 不匹配 (期望任务ID: {}) for session {}",
session.getStatus(), robotTaskId, session.getRelatedRobotTaskId(), sessionId);
return false;
}
session.setStatus(ChargingSessionStatusEnum.ROBOT_ARRIVED.getValue());
session.setRobotArrivalTime(new Date());
session.setUpdateTime(new Date());
// 接下来可以创建"开始充电"的机器人任务,或等待用户指令/自动开始
// 此处简化认为到达后即可准备开始充电相关任务由MQTT回调触发创建
boolean updated = this.updateById(session);
if(updated) {
log.info("机器人已到达,会话 {} 状态更新为 ROBOT_ARRIVED. 关联任务ID: {}", sessionId, robotTaskId);
// 可选更新机器人状态为IDLE在车位旁等待或 CHARGING_READY
chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.IDLE, null, null, null, new Date());
// 可选:更新车位状态为 CHARGING (如果机器人到达即代表车位被用于充电)
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.CHARGING, session.getId());
}
return updated;
}
@Override
@Transactional
public boolean handleChargingStart(Long sessionId, Long robotTaskId) {
ChargingSession session = this.getById(sessionId);
if (session == null) {
log.warn("处理充电开始事件失败,会话不存在: sessionId={}", sessionId);
return false;
}
// 校验状态,应该在 ROBOT_ARRIVED 之后
if (!ChargingSessionStatusEnum.ROBOT_ARRIVED.getValue().equals(session.getStatus()) ||
!robotTaskId.equals(session.getRelatedRobotTaskId())) {
log.warn("处理充电开始事件失败,会话状态 ({}) 或任务ID ({}) 不匹配 (期望任务ID: {}) for session {}",
session.getStatus(), robotTaskId, session.getRelatedRobotTaskId(), sessionId);
return false;
}
session.setStatus(ChargingSessionStatusEnum.CHARGING_STARTED.getValue());
session.setChargeStartTime(new Date());
session.setUpdateTime(new Date());
boolean updated = this.updateById(session);
if (updated) {
log.info("充电开始,会话 {} 状态更新为 CHARGING_STARTED. 关联任务ID: {}", sessionId, robotTaskId);
// 更新机器人状态为 CHARGING
chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.CHARGING, null, null, robotTaskId, new Date());
// 确保车位状态为 CHARGING
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.CHARGING, session.getId());
}
return updated;
}
@Override
@Transactional
public boolean handleChargingEnd(Long sessionId, Long robotTaskId, BigDecimal energyConsumedKwh, int durationSeconds) {
ChargingSession session = this.getById(sessionId);
if (session == null) {
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);
return false;
}
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);
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);
}
return updated;
}
@Override
@Transactional
public boolean calculateCostAndFinalizeSession(Long sessionId) {
ChargingSession session = this.getById(sessionId);
if (session == null || session.getChargeEndTime() == null || session.getChargeStartTime() == null) {
log.error("计算费用失败,会话 {} 不存在或充电时间不完整", sessionId);
return false;
}
long durationMillis = session.getChargeEndTime().getTime() - session.getChargeStartTime().getTime();
double hours = (double) durationMillis / TimeUnit.HOURS.toMillis(1);
// 费用 = 时长(小时) * 每小时单价
BigDecimal cost = pricePerHour.multiply(BigDecimal.valueOf(hours)).setScale(2, RoundingMode.HALF_UP);
// 应用最低消费
if (cost.compareTo(minChargeAmount) < 0) {
cost = minChargeAmount;
}
session.setCost(cost);
session.setStatus(ChargingSessionStatusEnum.PAYMENT_PENDING.getValue()); // 更新为待支付
session.setPaymentStatus(PaymentStatusEnum.PENDING.getValue());
session.setUpdateTime(new Date());
boolean updated = this.updateById(session);
if(updated){
log.info("会话 {} 费用计算完成: {}元. 状态更新为 PAYMENT_PENDING", sessionId, cost);
// 此处可以触发通知用户支付
}
// 在计费完成后如果会话已标记为COMPLETED但尚未支付并且车位状态不是AVAILABLE则将其设置为空闲
// 这主要覆盖了 handleChargingEnd 中可能未完全释放车位的情况(例如,如果计费失败)
// 或者如果 calculateCostAndFinalizeSession 是独立调用的。
String sessionStatus = session.getStatus();
String paymentStatus = session.getPaymentStatus();
if (ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue().equals(sessionStatus) ||
PaymentStatusEnum.PAID.getValue().equals(paymentStatus) ||
PaymentStatusEnum.FAILED.getValue().equals(paymentStatus)) {
ParkingSpot spot = parkingSpotService.getById(session.getSpotId());
if (spot != null && !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
log.info("在 finalizeSession for session {} 时,车位 {} (UID: {}) 状态为 {},将其更新为 AVAILABLE。",
sessionId, spot.getId(), spot.getSpotUid(), spot.getStatus());
parkingSpotService.updateSpotStatus(spot.getSpotUid(), ParkingSpotStatusEnum.AVAILABLE, null);
}
}
return updated;
}
@Override
@Transactional
public boolean processPayment(Long sessionId, Long userId) {
ChargingSession session = this.getById(sessionId);
if (session == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "充电会话不存在");
}
if (!session.getUserId().equals(userId)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权操作他人充电记录");
}
if (!ChargingSessionStatusEnum.PAYMENT_PENDING.getValue().equals(session.getStatus()) ||
!PaymentStatusEnum.PENDING.getValue().equals(session.getPaymentStatus())) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "会话状态非待支付,无法处理支付");
}
BigDecimal cost = session.getCost();
if (cost == null || cost.compareTo(BigDecimal.ZERO) <= 0) {
log.info("会话 {} 费用为0或无效标记为已支付 (无需支付)", sessionId);
session.setPaymentStatus(PaymentStatusEnum.PAID.getValue());
session.setStatus(ChargingSessionStatusEnum.PAID.getValue()); // 主状态也更新为已支付
session.setUpdateTime(new Date());
return this.updateById(session);
}
// 模拟扣款
boolean paymentSuccess = userService.decreaseBalance(userId, cost);
if (!paymentSuccess) {
log.warn("用户 {} 为会话 {} 支付 {}元 失败,余额不足或操作失败", userId, sessionId, cost);
session.setPaymentStatus(PaymentStatusEnum.FAILED.getValue());
// 可以考虑是否将会话状态改为ERROR或保持PAYMENT_PENDING
this.updateById(session);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "支付失败,余额不足或系统错误");
}
session.setPaymentStatus(PaymentStatusEnum.PAID.getValue());
session.setStatus(ChargingSessionStatusEnum.PAID.getValue()); // 主状态也更新为已支付
session.setUpdateTime(new Date());
boolean updated = this.updateById(session);
if (updated) {
log.info("用户 {} 为会话 {} 成功支付 {}元", userId, sessionId, cost);
}
return updated;
}
@Override
@Transactional
public boolean cancelChargingSession(Long sessionId, Long userId, String reason, ChargingSessionStatusEnum newStatus) {
ChargingSession session = this.getById(sessionId);
if (session == null) {
log.warn("尝试取消会话 {} 失败: 会话不存在", sessionId);
return false;
}
// 检查是否可以取消 (例如,不能取消已完成或已支付的会话)
ChargingSessionStatusEnum currentStatus = ChargingSessionStatusEnum.getEnumByValue(session.getStatus());
if (currentStatus == ChargingSessionStatusEnum.CHARGING_COMPLETED ||
currentStatus == ChargingSessionStatusEnum.PAYMENT_PENDING ||
currentStatus == ChargingSessionStatusEnum.PAID ||
currentStatus == ChargingSessionStatusEnum.CANCELLED_BY_SYSTEM ||
currentStatus == ChargingSessionStatusEnum.CANCELLED_BY_USER) {
log.warn("会话 {} 当前状态 {} 不可取消", sessionId, currentStatus);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "当前状态不可取消");
}
if (userId != null && !session.getUserId().equals(userId)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权取消他人会话");
}
session.setStatus(newStatus.getValue());
session.setErrorMessage("会话取消: " + reason);
session.setChargeEndTime(new Date()); // 标记一个结束时间
session.setUpdateTime(new Date());
boolean updated = this.updateById(session);
if (updated) {
log.info("充电会话 {} 已被取消,原因: {}. 操作者ID(若有): {}", sessionId, reason, userId);
// 如果机器人已分配或在执行任务,需要处理机器人和车位状态
if (session.getRobotId() != null) {
chargingRobotService.releaseRobot(session.getRobotId());
// 如果有关联的MQTT任务可能需要发送取消指令或标记任务为取消
if (session.getRelatedRobotTaskId() != null) {
RobotTask task = robotTaskService.getById(session.getRelatedRobotTaskId());
if (task != null &&
(task.getStatus() == RobotTaskStatusEnum.PENDING || task.getStatus() == RobotTaskStatusEnum.SENT || task.getStatus() == RobotTaskStatusEnum.PROCESSING)) {
robotTaskService.markTaskAsFailed(task.getId(), "充电会话被取消: " + reason, new Date());
log.info("因会话 {} 取消,关联的机器人任务 {} 已标记为失败。", sessionId, task.getId());
}
}
}
// 释放车位 (如果它被此会话占用)
ParkingSpot spot = parkingSpotService.findBySpotUid(session.getSpotUidSnapshot());
if (spot != null && sessionId.equals(spot.getCurrentSessionId()) && !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
log.info("取消会话 {} 时,释放车位 {} (UID: {})。", sessionId, spot.getId(), spot.getSpotUid());
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
}
}
return updated;
}
@Override
public List<ChargingSession> findUserSessions(Long userId) {
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
queryWrapper.orderByDesc("request_time");
return this.list(queryWrapper);
}
@Override
public ChargingSession findSessionByRobotTaskId(Long robotTaskId) {
if (robotTaskId == null) return null;
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("related_robot_task_id", robotTaskId);
// 通常一个任务只关联一个会话的特定阶段
return this.getOne(queryWrapper, false); // false: 不抛异常如果找到多个
}
@Override
@Transactional
public void handleSessionTaskTimeout(Long robotTaskId) {
log.warn("处理机器人任务超时任务ID: {}", robotTaskId);
RobotTask task = robotTaskService.getById(robotTaskId);
if (task == null) {
log.error("任务超时处理失败:任务 {} 未找到", robotTaskId);
return;
}
ChargingSession session = findSessionByRobotTaskId(robotTaskId);
if (session == null) {
log.warn("任务 {} 超时,但未找到关联的充电会话。可能任务与会话解耦或已被处理。", robotTaskId);
// 如果任务不直接关联会话,或者会话已结束,可能不需要进一步操作。
// 但如果机器人仍占用,需要释放
ChargingRobot robot = chargingRobotService.findByRobotUid(task.getRobotId()); // task.getRobotId()是robotUid
if(robot != null && !RobotStatusEnum.IDLE.getValue().equals(robot.getStatus())){
log.info("超时任务 {} 的机器人 {} 将被释放。", robotTaskId, robot.getRobotUid());
chargingRobotService.releaseRobot(robot.getId());
}
return;
}
log.info("任务 {} (类型: {}) 超时关联会话ID: {}", robotTaskId, task.getCommandType(), session.getId());
// 根据当前会话状态和任务类型决定如何处理
// 例如:如果是移动任务超时,可能意味着机器人未能到达
// 如果是充电任务超时,可能意味着充电异常
// 此处统一将会话标记为错误,并释放资源
session.setStatus(ChargingSessionStatusEnum.ERROR.getValue());
session.setErrorMessage("机器人任务超时 (TaskID: " + robotTaskId + ", Type: " + task.getCommandType() + ")");
session.setChargeEndTime(new Date()); // 标记一个结束时间
session.setUpdateTime(new Date());
this.updateById(session);
if (session.getRobotId() != null) {
chargingRobotService.releaseRobot(session.getRobotId());
}
if (session.getSpotId() != null) {
parkingSpotService.releaseSpot(session.getSpotId());
}
log.warn("会话 {} 因任务 {} 超时而被标记为错误并释放了资源。", session.getId(), robotTaskId);
// 如果会话被标记为错误或取消,并且之前占用了车位,则释放车位
if (session != null && session.getSpotUidSnapshot() != null &&
(ChargingSessionStatusEnum.ERROR.getValue().equals(session.getStatus()) ||
ChargingSessionStatusEnum.CANCELLED_BY_SYSTEM.getValue().equals(session.getStatus()) ||
ChargingSessionStatusEnum.CANCELLED_BY_USER.getValue().equals(session.getStatus()))) {
ParkingSpot spot = parkingSpotService.findBySpotUid(session.getSpotUidSnapshot());
if (spot != null && session.getId().equals(spot.getCurrentSessionId()) && !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
log.info("任务 {} 超时导致会话 {} 结束,释放车位 {} (UID: {})。", robotTaskId, session.getId(), spot.getId(), spot.getSpotUid());
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
}
}
}
@Override
public QueryWrapper<ChargingSession> getQueryWrapper(ChargingSessionQueryRequest queryRequest) {
if (queryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
}
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
Long id = queryRequest.getId();
Long userId = queryRequest.getUserId();
Long robotId = queryRequest.getRobotId();
Long spotId = queryRequest.getSpotId();
String robotUidSnapshot = queryRequest.getRobotUidSnapshot();
String spotUidSnapshot = queryRequest.getSpotUidSnapshot();
String status = queryRequest.getStatus();
List<String> orStatusList = queryRequest.getOrStatusList();
String paymentStatus = queryRequest.getPaymentStatus();
String sortField = queryRequest.getSortField();
String sortOrder = queryRequest.getSortOrder();
queryWrapper.eq(id != null && id > 0, "id", id);
queryWrapper.eq(userId != null && userId > 0, "user_id", userId);
queryWrapper.eq(robotId != null && robotId > 0, "robot_id", robotId);
queryWrapper.eq(spotId != null && spotId > 0, "spot_id", spotId);
queryWrapper.like(StringUtils.isNotBlank(robotUidSnapshot), "robot_uid_snapshot", robotUidSnapshot);
queryWrapper.like(StringUtils.isNotBlank(spotUidSnapshot), "spot_uid_snapshot", spotUidSnapshot);
queryWrapper.eq(StringUtils.isNotBlank(status), "status", status);
queryWrapper.eq(StringUtils.isNotBlank(paymentStatus), "payment_status", paymentStatus);
if (CollectionUtils.isNotEmpty(orStatusList)) {
queryWrapper.and(qw -> {
for (String orStatus : orStatusList) {
qw.or().eq("status", orStatus);
}
});
}
queryWrapper.orderBy(StringUtils.isNotBlank(sortField),
sortOrder.equals(com.yupi.project.constant.CommonConstant.SORT_ORDER_ASC), sortField);
return queryWrapper;
}
@Override
@Transactional
public boolean stopChargingByUser(Long sessionId, Long userId) {
ChargingSession session = this.getById(sessionId);
if (session == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "充电会话不存在");
}
if (!session.getUserId().equals(userId)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权操作他人充电记录");
}
if (!ChargingSessionStatusEnum.CHARGING_STARTED.getValue().equals(session.getStatus())) {
log.warn("用户 {} 尝试停止一个非充电中状态的会话 {} (当前状态: {})", userId, sessionId, session.getStatus());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "当前会话并非正在充电状态,无法停止");
}
if (StringUtils.isBlank(session.getRobotUidSnapshot())) {
log.error("会话 {} 缺少机器人UID快照无法发送停止指令", sessionId);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "会话数据异常缺少机器人UID快照");
}
String stopChargePayload = String.format("{\"command\":\"STOP_CHARGE\", \"session_id\":%d}", sessionId);
RobotTask stopTask = robotTaskService.createTask(session.getRobotUidSnapshot(), CommandTypeEnum.STOP_CHARGE, stopChargePayload, sessionId);
if (stopTask == null) {
log.error("为会话 {} 创建机器人停止充电任务失败", sessionId);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建机器人停止任务失败");
}
session.setRelatedRobotTaskId(stopTask.getId());
session.setUpdateTime(new Date());
this.updateById(session);
boolean mqttSent = false;
try {
mqttSent = mqttService.sendCommand(session.getRobotUidSnapshot(), CommandTypeEnum.STOP_CHARGE, stopChargePayload, stopTask.getId());
} catch (Exception e) {
log.error("发送MQTT停止充电指令失败 for session {}, 任务ID: {}: {}", sessionId, stopTask.getId(), e.getMessage(), e);
robotTaskService.markTaskAsFailed(stopTask.getId(), "MQTT发送失败: " + e.getMessage(), new Date());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "发送MQTT停止指令失败: " + e.getMessage());
}
if (!mqttSent) {
log.error("发送MQTT停止充电指令失败 for session {}, 任务ID: {}", sessionId, stopTask.getId());
robotTaskService.markTaskAsFailed(stopTask.getId(), "MQTT发送失败", new Date());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "发送MQTT停止指令失败");
}
log.info("用户 {} 已请求停止充电会话 {}。等待机器人确认。", userId, sessionId);
return true;
}
}

View File

@@ -3,13 +3,16 @@ package com.yupi.project.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yupi.project.config.properties.MqttProperties;
import com.yupi.project.model.dto.mqtt.RobotStatusMessage;
import com.yupi.project.model.enums.RobotTaskStatusEnum;
import com.yupi.project.model.enums.RobotStatusEnum; // For robot's own status
import com.yupi.project.model.enums.RobotTaskStatusEnum; // For task status from message
import com.yupi.project.service.RobotTaskService;
import com.yupi.project.service.ChargingRobotService; // Added import
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.stereotype.Component;
import org.apache.commons.lang3.StringUtils;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@@ -20,6 +23,7 @@ import java.util.Date;
public class MqttMessageHandler implements IMqttMessageListener {
private final RobotTaskService robotTaskService;
private final ChargingRobotService chargingRobotService; // Added final for RequiredArgsConstructor
private final MqttProperties mqttProperties;
private final ObjectMapper objectMapper; // For JSON parsing
@@ -36,72 +40,94 @@ public class MqttMessageHandler implements IMqttMessageListener {
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
log.info("MQTT Message Arrived. Topic: {}, QoS: {}, Payload: {}", topic, message.getQos(), payload);
// Extract robotId from topic. Example: "robot/status/+/robot123" or "robot/command_response/robot123"
// This logic might need adjustment based on the exact topic structure.
String[] topicParts = topic.split("/");
if (topicParts.length == 0) {
log.error("Received message on invalid topic format: {}", topic);
return;
}
String robotId = topicParts[topicParts.length - 1]; // Assuming robotId is the last part
// Determine message type based on topic structure (e.g., status update vs command response)
// For now, let's assume all messages on subscribed topics are status updates.
// A more robust solution would use different listeners for different topic patterns or inspect the payload.
String robotUID = topicParts[topicParts.length - 1];
if (topic.startsWith(mqttProperties.getStatusTopicBase())) {
handleRobotStatusUpdate(robotId, payload);
handleRobotStatusUpdate(robotUID, payload);
} else {
log.warn("Received message on unhandled topic structure: {}. Ignoring.", topic);
}
}
private void handleRobotStatusUpdate(String robotId, String payloadJson) {
private void handleRobotStatusUpdate(String robotUIDFromTopicSource, String payloadJson) {
try {
RobotStatusMessage statusMessage = objectMapper.readValue(payloadJson, RobotStatusMessage.class);
log.info("Parsed RobotStatusMessage for robot {}: {}", robotId, statusMessage);
log.info("Parsed RobotStatusMessage for robot {}: {}", robotUIDFromTopicSource, statusMessage);
if (statusMessage.getTaskId() == null) {
log.warn("RobotStatusMessage for robot {} does not contain a taskId. Payload: {}", robotId, payloadJson);
// Optionally, handle general status updates not tied to a specific task
return;
}
// Scenario 1: Message is related to a specific task (has taskId)
if (statusMessage.getTaskId() != null) {
log.debug("Handling task-specific status update for task ID: {} from robot {}", statusMessage.getTaskId(), robotUIDFromTopicSource);
RobotTaskStatusEnum taskNewStatus = RobotTaskStatusEnum.fromValue(statusMessage.getStatus());
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);
} 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) {
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;
}
// Update the RobotTask based on the status message
RobotTaskStatusEnum newStatus = RobotTaskStatusEnum.fromValue(statusMessage.getStatus());
if (newStatus == null) {
log.error("Invalid status '{}' received from robot {}. Payload: {}", statusMessage.getStatus(), robotId, payloadJson);
return;
}
RobotStatusEnum robotStatus = RobotStatusEnum.getEnumByValue(statusMessage.getActualRobotStatus());
if (robotStatus == null) {
log.warn("Received unknown status value '{}' from robot {}. Message: {}",
statusMessage.getActualRobotStatus(), actualRobotUIDToUse, statusMessage);
}
boolean updated;
switch (newStatus) {
case PROCESSING:
updated = robotTaskService.markTaskAsProcessing(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
break;
case COMPLETED:
updated = robotTaskService.markTaskAsCompleted(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
break;
case FAILED:
updated = robotTaskService.markTaskAsFailed(statusMessage.getTaskId(), new Date(), statusMessage.getErrorCode(), statusMessage.getMessage());
break;
default:
log.warn("Received unhandled RobotTaskStatusEnum: {} for task {} from robot {}. Ignoring.",
newStatus, statusMessage.getTaskId(), robotId);
return; // Don't attempt to update with SENT or PENDING from robot
}
String location = statusMessage.getLocation();
Integer batteryLevel = statusMessage.getBatteryLevel();
Long currentRobotTask = statusMessage.getActiveTaskId();
if (updated) {
log.info("Successfully updated task {} to status {} for robot {} based on MQTT message.",
statusMessage.getTaskId(), newStatus, robotId);
} else {
log.warn("Failed to update task {} to status {} for robot {} (or task not found/invalid state transition). " +
"Message: {}", statusMessage.getTaskId(), newStatus, robotId, statusMessage.getMessage());
log.info("Processing general status update for robot {}: Status={}, Location={}, Battery={}, ActiveTaskID={}. (Original topic UID: {})",
actualRobotUIDToUse, robotStatus, location, batteryLevel, currentRobotTask, robotUIDFromTopicSource);
chargingRobotService.updateRobotStatus(actualRobotUIDToUse, robotStatus, location, batteryLevel, currentRobotTask, new Date());
}
else {
log.warn("RobotStatusMessage for robot {} has no taskId and no general status fields. Payload: {}. Ignoring.",
robotUIDFromTopicSource, payloadJson);
}
} catch (Exception e) {
log.error("Error processing robot status update for robot {}. Payload: {}. Error: {}",
robotId, payloadJson, e.getMessage(), e);
robotUIDFromTopicSource, payloadJson, e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,170 @@
package com.yupi.project.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.mapper.ParkingSpotMapper;
import com.yupi.project.model.entity.ParkingSpot;
import com.yupi.project.model.enums.ParkingSpotStatusEnum;
import com.yupi.project.service.ParkingSpotService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
/**
* @author Yupi
* @description 针对表【parking_spot(车位表)】的数据库操作Service实现
* @createDate 2023-12-03 10:00:00
*/
@Service
@Slf4j
public class ParkingSpotServiceImpl extends ServiceImpl<ParkingSpotMapper, ParkingSpot>
implements ParkingSpotService {
@Override
@Transactional
public ParkingSpot addParkingSpot(String spotUid, String locationDescription, boolean robotAssignable) {
if (findBySpotUid(spotUid) != null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "车位UID已存在: " + spotUid);
}
ParkingSpot spot = new ParkingSpot();
spot.setSpotUid(spotUid);
spot.setLocationDescription(locationDescription);
spot.setRobotAssignable(robotAssignable);
spot.setStatus(ParkingSpotStatusEnum.AVAILABLE.getValue()); // 默认可用
spot.setCreateTime(new Date());
spot.setUpdateTime(new Date());
boolean saved = this.save(spot);
if (!saved) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加车位失败");
}
log.info("车位添加成功: {}", spot);
return spot;
}
@Override
public ParkingSpot findBySpotUid(String spotUid) {
QueryWrapper<ParkingSpot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("spot_uid", spotUid);
return this.getOne(queryWrapper);
}
@Override
@Transactional
public boolean updateParkingSpotStatus(Long spotId, ParkingSpotStatusEnum newStatus, Long currentSessionId) {
ParkingSpot spot = this.getById(spotId);
if (spot == null) {
log.warn("旧版 updateParkingSpotStatus: 车位不存在, spotId: {}", spotId);
// throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "车位不存在");
// 为了与新方法的行为保持一致新方法如果UID找不到会返回false这里也返回false
return false;
}
// 调用新的基于UID的方法
return this.updateSpotStatus(spot.getSpotUid(), newStatus, currentSessionId);
}
@Override
public List<ParkingSpot> findAvailableAndAssignableSpots() {
QueryWrapper<ParkingSpot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", ParkingSpotStatusEnum.AVAILABLE.getValue());
queryWrapper.eq("robot_assignable", true);
return this.list(queryWrapper);
}
@Override
@Transactional
public boolean occupySpot(Long spotId, Long sessionId, ParkingSpotStatusEnum targetStatus) {
ParkingSpot spot = this.getById(spotId);
if (spot == null) {
log.warn("OccupySpot: 车位不存在, spotId: {}", spotId);
// throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "车位不存在");
return false; // 与新 updateSpotStatus 行为一致
}
// 校验车位是否可被占用 (例如必须是AVAILABLE状态)
if (!ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
log.warn("尝试占用一个非可用车位: spotId={}, currentStatus={}", spotId, spot.getStatus());
// throw new BusinessException(ErrorCode.OPERATION_ERROR, "车位当前状态不可占用");
return false; // 状态不符,占用失败
}
if (!spot.getRobotAssignable()) {
log.warn("尝试占用一个不可指派机器人的车位: spotId={}", spotId);
// throw new BusinessException(ErrorCode.OPERATION_ERROR, "车位不可指派机器人");
return false; // 不可指派,占用失败
}
// return updateParkingSpotStatus(spotId, targetStatus, sessionId); // 旧调用
return this.updateSpotStatus(spot.getSpotUid(), targetStatus, sessionId); // 新调用
}
@Override
@Transactional
public boolean releaseSpot(Long spotId) {
ParkingSpot spot = this.getById(spotId);
if (spot == null) {
log.warn("尝试释放车位失败,车位不存在: {}", spotId);
return false;
}
// 只有非AVAILABLE的车位才需要被释放回AVAILABLE
// 新的 updateSpotStatus 内部会检查是否真的需要更新,所以这里的检查可以简化或移除
// if (ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
// log.info("车位 {} (UID: {}) 本身已是可用状态,无需释放。", spotId, spot.getSpotUid());
// return true;
// }
// return updateParkingSpotStatus(spotId, ParkingSpotStatusEnum.AVAILABLE, null); // 旧调用
return this.updateSpotStatus(spot.getSpotUid(), ParkingSpotStatusEnum.AVAILABLE, null); // 新调用
}
@Override
@Transactional
public boolean updateSpotStatus(String spotUID, ParkingSpotStatusEnum newStatus, Long currentSessionId) {
if (StringUtils.isBlank(spotUID) || newStatus == null) {
log.warn("更新车位状态失败spotUID 或 newStatus 为空。spotUID: {}, newStatus: {}", spotUID, newStatus);
return false;
}
ParkingSpot parkingSpot = this.findBySpotUid(spotUID);
if (parkingSpot == null) {
log.warn("更新车位状态失败:未找到 UID 为 {} 的车位。", spotUID);
return false;
}
ParkingSpot spotToUpdate = new ParkingSpot();
spotToUpdate.setId(parkingSpot.getId());
boolean changed = false;
// Compare enum with String value from entity
if (newStatus != null && !newStatus.getValue().equals(parkingSpot.getStatus())) {
spotToUpdate.setStatus(newStatus.getValue()); // Set String value to entity
changed = true;
}
if (currentSessionId == null && parkingSpot.getCurrentSessionId() != null) {
spotToUpdate.setCurrentSessionId(null);
changed = true;
} else if (currentSessionId != null && !currentSessionId.equals(parkingSpot.getCurrentSessionId())) {
spotToUpdate.setCurrentSessionId(currentSessionId);
changed = true;
}
if (!changed) {
log.info("车位 {} 的状态与数据库一致 (状态: {}, 会话ID: {}),无需更新。",
spotUID, newStatus, currentSessionId == null ? "null" : currentSessionId);
return true; // Data is consistent, operation considered successful
}
// updateTime will be auto-filled
boolean success = this.updateById(spotToUpdate);
if (success) {
log.info("成功更新车位 {} 的状态。新状态: {}, 当前会话ID: {}",
spotUID, newStatus, currentSessionId == null ? "null" : currentSessionId);
} else {
log.error("更新车位 {} 的状态失败 (数据库操作失败)。", spotUID);
}
return success;
}
}

View File

@@ -295,38 +295,24 @@ public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask
@Override
@Transactional
public boolean markTaskAsFailed(Long taskId, Date ackTime, String errorCode, String errorMessage) {
if (taskId == null) {
log.error("Cannot mark task as failed: taskId is null.");
return false;
}
public boolean markTaskAsFailed(Long taskId, String errorMessage, Date failedTime) {
RobotTask task = this.getById(taskId);
if (task == null) {
log.warn("Cannot mark task as failed: Task with ID {} not found.", taskId);
log.warn("尝试标记任务 {} 为失败失败:任务不存在", taskId);
return false;
}
// Allow transition from any non-terminal state (PENDING, SENT, PROCESSING) to FAILED
if (task.getStatus() == RobotTaskStatusEnum.COMPLETED || task.getStatus() == RobotTaskStatusEnum.FAILED || task.getStatus() == RobotTaskStatusEnum.TIMED_OUT) {
log.warn("Cannot mark task {} as FAILED. Task is already in a terminal state: {}.", taskId, task.getStatus());
// 只能标记非终态的任务
if (RobotTaskStatusEnum.isFinalStatus(task.getStatus())) {
log.warn("任务 {} 当前状态为 {} (终态),无法标记为失败", taskId, task.getStatus());
return false;
}
RobotTask updateTask = new RobotTask();
updateTask.setId(taskId);
updateTask.setStatus(RobotTaskStatusEnum.FAILED);
if (ackTime != null) {
updateTask.setAckTime(ackTime);
}
// Construct a comprehensive error message if both are provided
String finalErrorMessage = (errorCode != null ? "Code [" + errorCode + "] " : "") + (errorMessage != null ? errorMessage : "");
updateTask.setErrorMessage(finalErrorMessage.isEmpty() ? null : finalErrorMessage);
boolean updated = this.updateById(updateTask);
task.setStatus(RobotTaskStatusEnum.FAILED);
task.setErrorMessage(errorMessage);
task.setAckTime(failedTime); // Using ackTime field to store the failure time
task.setUpdateTime(new Date());
boolean updated = this.updateById(task);
if (updated) {
log.info("Marked RobotTask with ID: {} as FAILED. Error: {}", taskId, finalErrorMessage);
} else {
log.error("Failed to mark RobotTask with ID: {} as FAILED.", taskId);
log.info("机器人任务 {} 已标记为失败. 原因: {}", taskId, errorMessage);
}
return updated;
}

View File

@@ -168,56 +168,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
return safetyUser;
}
@Override
@Transactional
public boolean deductBalance(Long userId, BigDecimal amount) {
if (userId == null || userId <= 0 || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误");
}
boolean updateResult = this.update()
.setSql("balance = balance - " + amount.doubleValue())
.eq("id", userId)
.ge("balance", amount)
.update();
if (!updateResult) {
User user = this.getById(userId);
if (user == null || user.getIsDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
} else if (user.getBalance().compareTo(amount) < 0) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "余额不足");
} else {
log.warn("Deduct balance failed due to concurrent update for userId: {}", userId);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "扣款失败,请重试");
}
}
log.info("Deducted {} from balance for user {}", amount, userId);
return true;
}
@Override
@Transactional
public boolean addBalance(Long userId, BigDecimal amount) {
if (userId == null || userId <= 0 || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误");
}
boolean updateResult = this.update()
.setSql("balance = balance + " + amount.doubleValue())
.eq("id", userId)
.update();
if (!updateResult) {
User user = this.getById(userId);
if (user == null || user.getIsDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
}
log.error("Add balance failed unexpectedly for userId: {}", userId);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "充值失败,请稍后重试");
}
log.info("Added {} to balance for user {}", amount, userId);
return true;
}
@Override
public List<User> listUsers() {
List<User> userList = this.list(new QueryWrapper<User>().eq("isDeleted", 0));
@@ -389,4 +339,60 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
);
return true;
}
@Override
@Transactional
public boolean decreaseBalance(Long userId, BigDecimal amountToDecrease) {
if (userId == null || amountToDecrease == null || amountToDecrease.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID或扣款金额无效");
}
User user = this.getById(userId);
if (user == null) {
// 即使SQL会因为用户不存在而不更新这里提前检查可以提供更明确的错误信息
log.warn("尝试为不存在的用户ID {} 扣款 {} 失败。", userId, amountToDecrease);
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在,无法扣款");
}
// 数据库层面的原子操作将处理余额是否足够
// The amountChange passed to mapper is negative for decrease
int updatedRows = baseMapper.updateUserBalance(userId, amountToDecrease.negate());
if (updatedRows > 0) {
log.info("用户 {} 余额扣款 {} 成功 (通过原子操作)。", userId, amountToDecrease);
// 获取更新后的余额用于日志记录是可选的,且可能引入额外查询。为简单起见,此处省略。
return true;
} else {
// updatedRows == 0 表示扣款未成功主要原因是余额不足由SQL的WHERE子句保证或用户ID无效可能性较低因已预查
log.warn("用户 {} 余额扣款 {} 失败。原子操作更新行数为0通常表示余额不足或用户不存在。", userId, amountToDecrease);
// 可以选择再次查询用户以确认具体原因但对于调用方来说false已足够
return false;
}
}
@Override
@Transactional
public boolean increaseBalance(Long userId, BigDecimal amountToIncrease) {
if (userId == null || amountToIncrease == null || amountToIncrease.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID或增加金额无效");
}
// Ensure user exists before attempting to update
User user = this.getById(userId);
if (user == null) {
log.warn("尝试为不存在的用户 {} 增加余额 {} 失败。", userId, amountToIncrease);
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在,无法增加余额");
}
int updatedRows = baseMapper.updateUserBalance(userId, amountToIncrease);
if (updatedRows > 0) {
log.info("用户 {} 余额增加 {} 成功。当前余额: {}", userId, amountToIncrease, user.getBalance().add(amountToIncrease));
return true;
} else {
// This case should ideally not happen if user existence is checked before, unless user is deleted concurrently.
log.warn("用户 {} 余额增加 {} 失败更新数据库记录数为0。", userId, amountToIncrease);
return false;
}
}
}

View File

@@ -23,4 +23,13 @@
userPassword,createTime,updateTime,
isDelete
</sql>
<update id="updateUserBalance">
UPDATE user
SET balance = balance + #{amountChange}
WHERE id = #{userId}
<if test="amountChange.signum() == -1">
AND balance >= #{amountChange.abs()}
</if>
</update>
</mapper>