diff --git a/LogBook.md b/LogBook.md
index 0b3ce38..900a111 100644
--- a/LogBook.md
+++ b/LogBook.md
@@ -46,4 +46,64 @@
- 更正了 `springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttServiceImpl.java` 的实现,确保 `sendCommand` 方法的逻辑完整和正确。
- 实现 `TaskTimeoutHandler.java`,使用 `@Scheduled` 定时调用 `RobotTaskService.findAndMarkTimedOutTasks` 处理任务超时。
- 在 `MyApplication.java` 中添加 `@EnableScheduling` 以启用定时任务。
-- 在 `application.yml` 中添加了 `mqtt.task.timeoutSeconds` 和 `mqtt.task.timeoutCheckRateMs` 配置项。
\ No newline at end of file
+- 在 `application.yml` 中添加了 `mqtt.task.timeoutSeconds` 和 `mqtt.task.timeoutCheckRateMs` 配置项。
+
+---
+
+**第二阶段 (MQTT 集成) 已于 YYYY-MM-DD 完成。**
+
+所有核心功能点,包括MQTT连接、消息发布/订阅、RobotTask状态跟踪和基础超时处理已实现。
+依赖于 ChargingSession 的超时后联动处理已明确推至第三阶段。
+
+---
+
+## YYYY-MM-DD (请替换为当前日期) - 第三阶段后端开发
+
+- **核心业务实体与服务实现**:
+ - 创建了枚举类: `RobotStatusEnum`, `ParkingSpotStatusEnum`, `ChargingSessionStatusEnum`, `PaymentStatusEnum`.
+ - 创建了数据库实体: `ChargingRobot`, `ParkingSpot`, `ChargingSession`.
+ - 创建了对应的Mapper接口: `ChargingRobotMapper`, `ParkingSpotMapper`, `ChargingSessionMapper`.
+ - 创建了Service接口: `ChargingRobotService`, `ParkingSpotService`, `ChargingSessionService`.
+ - 创建了Service实现类: `ChargingRobotServiceImpl`, `ParkingSpotServiceImpl`, `ChargingSessionServiceImpl`.
+ - `ChargingSessionServiceImpl` 中实现了充电请求、机器人分配、状态流转 (到达、开始/结束充电)、费用计算、支付、取消、超时处理等核心逻辑。
+- **API 控制器实现**:
+ - 创建了 `ChargingRobotAdminController` 用于管理员管理充电机器人 (CRUD, 列表查询, 状态类型)。
+ - 创建了 `ParkingSpotAdminController` 用于管理员管理车位 (CRUD, 列表查询, 状态类型)。
+ - 创建了 `ChargingSessionController` 用于用户发起充电请求、查询历史会话、支付、取消会话。
+ - 创建了相关的DTOs (如 `ChargingRobotAddRequest`, `ParkingSpotQueryRequest`, `ChargingRequest`, `PaymentRequest`) 和 VO (`ChargingSessionVO`).
+- **MQTT与任务处理联动**:
+ - 更新了 `MqttMessageHandlerImpl`,使其在收到机器人状态ACK后,能调用 `ChargingSessionService` 更新相关充电会话的状态。
+ - 更新了 `TaskTimeoutHandler`,使其在检测到与会话关联的任务超时后,能调用 `ChargingSessionService` 处理会话超时逻辑。
+ - 在 `ChargingSessionService` 中补充了 `getQueryWrapper` 方法用于支持分页和条件查询。
+
+- **主要实现功能点**:
+ - 管理员可以增删改查充电机器人和车位。
+ - 用户可以请求在特定车位充电。
+ - 系统能够尝试分配空闲机器人,并向其发送移动指令 (通过MQTT,并记录RobotTask)。
+ - 系统能够根据机器人通过MQTT反馈的状态(到达、开始充电、结束充电)更新充电会话的生命周期。
+ - 充电结束后,系统能计算费用,并允许用户支付。
+ - 用户可以在特定阶段取消充电会话。
+ - 机器人任务超时会影响关联的充电会话状态。
+
+- **补充后端功能 (根据阶段计划调整)**:
+ - 在 `ChargingSessionAdminController.java` 中添加了管理员分页查询所有充电会话的接口 (`POST /admin/session/list/page`)。
+ - 在 `ChargingSessionController.java` 中添加了用户"优雅停止充电"的接口 (`POST /session/stop`)。
+ - 此接口会向机器人发送 `STOP_CHARGE` 指令,并通过 `ChargingSessionServiceImpl.stopChargingByUser` 方法创建相应的 `RobotTask`。
+ - 会话的最终完成和计费依赖 `MqttMessageHandlerImpl` 收到机器人对 `STOP_CHARGE` 指令的成功ACK后,调用 `chargingSessionService.handleChargingEnd` 处理。
+
+- **下一步**:
+ - 进行详细的单元测试和集成测试。
+ - 完善错误处理、日志记录和边界条件。
+ - 更新API文档。
+ - **开始第三阶段前端开发**。
+ - 根据 `stage_3_core_charging_logic.md` 检查业务流程覆盖情况。
+
+## 后端开发 - 第三阶段核心充电逻辑完成
+
+* **状态**: 后端核心业务逻辑、服务实现、MQTT集成及主要API Controller已完成开发并通过多轮编译错误修复。
+ * 充电全流程 (请求、分配、移动、到达、开始、结束、计费、支付、取消、超时) 已实现。
+ * 机器人和车位的状态管理服务已实现并集成到主流程。
+ * 用户余额扣减实现原子性操作。
+ * MQTT消息处理机制已建立,可处理任务ACK和常规状态上报。
+* **决策**: 经过讨论,充电过程中的实时时长更新 (`currentChargingDurationSeconds`) 功能在本阶段不实现,最终计费依赖充电结束时上报的 `totalDurationSeconds`。
+* **后续**: 后端已为前端开发提供基础。建议在前端大规模开发前,后端进行核心API的冒烟测试,并完善API文档(如使用Swagger)。
\ No newline at end of file
diff --git a/springboot-init-main/doc/development_stages/stage_3_core_charging_logic.md b/springboot-init-main/doc/development_stages/stage_3_core_charging_logic.md
index 641d8e2..11c60a2 100644
--- a/springboot-init-main/doc/development_stages/stage_3_core_charging_logic.md
+++ b/springboot-init-main/doc/development_stages/stage_3_core_charging_logic.md
@@ -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`。
diff --git a/springboot-init-main/pom.xml b/springboot-init-main/pom.xml
index 5392d40..cc09b81 100644
--- a/springboot-init-main/pom.xml
+++ b/springboot-init-main/pom.xml
@@ -97,6 +97,11 @@
org.eclipse.paho.client.mqttv3
1.2.5
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
diff --git a/springboot-init-main/sql/mqtt_power.sql b/springboot-init-main/sql/mqtt_power.sql
new file mode 100644
index 0000000..155b659
--- /dev/null
+++ b/springboot-init-main/sql/mqtt_power.sql
@@ -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;
diff --git a/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingRobotAdminController.java b/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingRobotAdminController.java
new file mode 100644
index 0000000..99af0f4
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingRobotAdminController.java
@@ -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 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 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 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 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> listChargingRobotsByPage(@RequestBody ChargingRobotQueryRequest queryRequest, HttpServletRequest request) {
+ if (queryRequest == null) {
+ throw new BusinessException(ErrorCode.PARAMS_ERROR);
+ }
+ long current = queryRequest.getCurrent();
+ long size = queryRequest.getPageSize();
+ Page page = new Page<>(current, size);
+ QueryWrapper 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> getRobotStatusTypes() {
+ List statusTypes = Arrays.stream(RobotStatusEnum.values())
+ .map(RobotStatusEnum::getValue)
+ .collect(Collectors.toList());
+ return ResultUtils.success(statusTypes);
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingSessionAdminController.java b/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingSessionAdminController.java
new file mode 100644
index 0000000..7d32c23
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingSessionAdminController.java
@@ -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> 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 sessionPage = chargingSessionService.page(new Page<>(current, size),
+ chargingSessionService.getQueryWrapper(queryRequest));
+
+ Page sessionVOPage = new Page<>(sessionPage.getCurrent(), sessionPage.getSize(), sessionPage.getTotal());
+
+ // 转换VO并填充用户信息
+ List 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: 管理员可能需要的其他接口,例如:
+ // - 管理员手动取消某个会话 (不同于用户取消)
+ // - 查看特定会话的详细日志或关联任务
+ // - 重试某个失败的支付等
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingSessionController.java b/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingSessionController.java
new file mode 100644
index 0000000..2431a5a
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/controller/ChargingSessionController.java
@@ -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 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> 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 sessionPage = chargingSessionService.page(new Page<>(current, size),
+ chargingSessionService.getQueryWrapper(queryRequest));
+
+ Page sessionVOPage = new Page<>(sessionPage.getCurrent(), sessionPage.getSize(), sessionPage.getTotal());
+ List voList = sessionPage.getRecords().stream().map(ChargingSessionVO::objToVo).collect(Collectors.toList());
+ sessionVOPage.setRecords(voList);
+
+ return ResultUtils.success(sessionVOPage);
+ }
+
+ @ApiOperation("用户获取单个充电会话详情")
+ @GetMapping("/get")
+ @AuthCheck
+ public BaseResponse 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 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 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 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);
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/controller/ParkingSpotAdminController.java b/springboot-init-main/src/main/java/com/yupi/project/controller/ParkingSpotAdminController.java
new file mode 100644
index 0000000..cece047
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/controller/ParkingSpotAdminController.java
@@ -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 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 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 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 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> listParkingSpotsByPage(@RequestBody ParkingSpotQueryRequest queryRequest, HttpServletRequest request) {
+ if (queryRequest == null) {
+ throw new BusinessException(ErrorCode.PARAMS_ERROR);
+ }
+ long current = queryRequest.getCurrent();
+ long size = queryRequest.getPageSize();
+ Page page = new Page<>(current, size);
+ QueryWrapper 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> getParkingSpotStatusTypes() {
+ List statusTypes = Arrays.stream(ParkingSpotStatusEnum.values())
+ .map(ParkingSpotStatusEnum::getValue)
+ .collect(Collectors.toList());
+ return ResultUtils.success(statusTypes);
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/exception/ThrowUtils.java b/springboot-init-main/src/main/java/com/yupi/project/exception/ThrowUtils.java
new file mode 100644
index 0000000..4292f98
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/exception/ThrowUtils.java
@@ -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() {}
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/mapper/ChargingRobotMapper.java b/springboot-init-main/src/main/java/com/yupi/project/mapper/ChargingRobotMapper.java
new file mode 100644
index 0000000..f7789c7
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/mapper/ChargingRobotMapper.java
@@ -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 {
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/mapper/ChargingSessionMapper.java b/springboot-init-main/src/main/java/com/yupi/project/mapper/ChargingSessionMapper.java
new file mode 100644
index 0000000..21d00b2
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/mapper/ChargingSessionMapper.java
@@ -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 {
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/mapper/ParkingSpotMapper.java b/springboot-init-main/src/main/java/com/yupi/project/mapper/ParkingSpotMapper.java
new file mode 100644
index 0000000..d1c1934
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/mapper/ParkingSpotMapper.java
@@ -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 {
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/mapper/UserMapper.java b/springboot-init-main/src/main/java/com/yupi/project/mapper/UserMapper.java
index 4efdd78..9433118 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/mapper/UserMapper.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/mapper/UserMapper.java
@@ -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 {
+ /**
+ * 更新用户余额
+ * @param userId 用户ID
+ * @param amountChange 要变更的金额 (元), 正数表示增加,负数表示减少
+ * @return 影响的行数
+ */
+ int updateUserBalance(@Param("userId") Long userId, @Param("amountChange") BigDecimal amountChange);
}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotAddRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotAddRequest.java
new file mode 100644
index 0000000..0749207
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotAddRequest.java
@@ -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;
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotQueryRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotQueryRequest.java
new file mode 100644
index 0000000..e355671
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotQueryRequest.java
@@ -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;
+
+ // 可以根据需要添加其他查询条件,如电量范围等
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotUpdateRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotUpdateRequest.java
new file mode 100644
index 0000000..31df61f
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_robot/ChargingRobotUpdateRequest.java
@@ -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 不建议直接通过此接口修改,应由业务流程驱动
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/ChargingRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/ChargingRequest.java
new file mode 100644
index 0000000..31df4b3
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/ChargingRequest.java
@@ -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; // 偏好的机器人类型 (如果支持多种)
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/ChargingSessionQueryRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/ChargingSessionQueryRequest.java
new file mode 100644
index 0000000..dca7adb
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/ChargingSessionQueryRequest.java
@@ -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 orStatusList;
+
+ /**
+ * 支付状态 (精确查询, 来自 PaymentStatusEnum 的 value)
+ */
+ private String paymentStatus;
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/PaymentRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/PaymentRequest.java
new file mode 100644
index 0000000..87b5ad2
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/charging_session/PaymentRequest.java
@@ -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;
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java
index ea6ba42..dd3f005 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java
@@ -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)
}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotAddRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotAddRequest.java
new file mode 100644
index 0000000..727cc5d
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotAddRequest.java
@@ -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;
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotQueryRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotQueryRequest.java
new file mode 100644
index 0000000..bde326b
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotQueryRequest.java
@@ -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;
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotUpdateRequest.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotUpdateRequest.java
new file mode 100644
index 0000000..de9f048
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/parking_spot/ParkingSpotUpdateRequest.java
@@ -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 不建议直接通过此接口修改,应由业务流程驱动
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/entity/ChargingRobot.java b/springboot-init-main/src/main/java/com/yupi/project/model/entity/ChargingRobot.java
new file mode 100644
index 0000000..35a6d32
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/entity/ChargingRobot.java
@@ -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;
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/entity/ChargingSession.java b/springboot-init-main/src/main/java/com/yupi/project/model/entity/ChargingSession.java
new file mode 100644
index 0000000..bf7b198
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/entity/ChargingSession.java
@@ -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;
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/entity/ParkingSpot.java b/springboot-init-main/src/main/java/com/yupi/project/model/entity/ParkingSpot.java
new file mode 100644
index 0000000..0dee532
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/entity/ParkingSpot.java
@@ -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;
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/entity/RobotTask.java b/springboot-init-main/src/main/java/com/yupi/project/model/entity/RobotTask.java
index c51509f..107a65e 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/model/entity/RobotTask.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/entity/RobotTask.java
@@ -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;
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/enums/ChargingSessionStatusEnum.java b/springboot-init-main/src/main/java/com/yupi/project/model/enums/ChargingSessionStatusEnum.java
new file mode 100644
index 0000000..c00fb41
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/enums/ChargingSessionStatusEnum.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java b/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java
index 430a960..8f8db35 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java
@@ -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;
}
}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/enums/ParkingSpotStatusEnum.java b/springboot-init-main/src/main/java/com/yupi/project/model/enums/ParkingSpotStatusEnum.java
new file mode 100644
index 0000000..63274d8
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/enums/ParkingSpotStatusEnum.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/enums/PaymentStatusEnum.java b/springboot-init-main/src/main/java/com/yupi/project/model/enums/PaymentStatusEnum.java
new file mode 100644
index 0000000..18ec441
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/enums/PaymentStatusEnum.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/enums/RobotStatusEnum.java b/springboot-init-main/src/main/java/com/yupi/project/model/enums/RobotStatusEnum.java
new file mode 100644
index 0000000..67eacde
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/enums/RobotStatusEnum.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/enums/RobotTaskStatusEnum.java b/springboot-init-main/src/main/java/com/yupi/project/model/enums/RobotTaskStatusEnum.java
index ef67f6b..74f7edc 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/model/enums/RobotTaskStatusEnum.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/enums/RobotTaskStatusEnum.java
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/vo/ChargingSessionVO.java b/springboot-init-main/src/main/java/com/yupi/project/model/vo/ChargingSessionVO.java
new file mode 100644
index 0000000..d318e7b
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/vo/ChargingSessionVO.java
@@ -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 也有类似的转换方法
+ }
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/vo/UserVO.java b/springboot-init-main/src/main/java/com/yupi/project/model/vo/UserVO.java
new file mode 100644
index 0000000..a87ed40
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/model/vo/UserVO.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/MqttMessageHandler.java b/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/MqttMessageHandler.java
new file mode 100644
index 0000000..91e5b87
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/MqttMessageHandler.java
@@ -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);
+
+ // 可以根据需要添加其他类型的消息处理方法
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/MqttMessageHandlerImpl.java b/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/MqttMessageHandlerImpl.java
new file mode 100644
index 0000000..7846b3f
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/MqttMessageHandlerImpl.java
@@ -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 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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/TaskTimeoutHandler.java b/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/TaskTimeoutHandler.java
new file mode 100644
index 0000000..a64d00b
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/mqtt/handler/TaskTimeoutHandler.java
@@ -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 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());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/ChargingRobotService.java b/springboot-init-main/src/main/java/com/yupi/project/service/ChargingRobotService.java
new file mode 100644
index 0000000..5ffb614
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/ChargingRobotService.java
@@ -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 {
+
+ /**
+ * 注册一个新的充电机器人
+ *
+ * @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 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);
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/ChargingSessionService.java b/springboot-init-main/src/main/java/com/yupi/project/service/ChargingSessionService.java
new file mode 100644
index 0000000..dcc7988
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/ChargingSessionService.java
@@ -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 {
+
+ /**
+ * 用户请求充电
+ *
+ * @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 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 getQueryWrapper(ChargingSessionQueryRequest queryRequest);
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/ParkingSpotService.java b/springboot-init-main/src/main/java/com/yupi/project/service/ParkingSpotService.java
new file mode 100644
index 0000000..85e9d3d
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/ParkingSpotService.java
@@ -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 {
+
+ /**
+ * 添加新的车位信息
+ *
+ * @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 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);
+
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/RobotTaskService.java b/springboot-init-main/src/main/java/com/yupi/project/service/RobotTaskService.java
index 300b805..f7e1b4b 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/service/RobotTaskService.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/RobotTaskService.java
@@ -92,14 +92,13 @@ public interface RobotTaskService extends IService {
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);
}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/UserService.java b/springboot-init-main/src/main/java/com/yupi/project/service/UserService.java
index e8cd041..2dd4017 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/service/UserService.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/UserService.java
@@ -57,20 +57,22 @@ public interface UserService extends IService {
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);
/**
* 获取用户列表 (仅管理员)
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingRobotServiceImpl.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingRobotServiceImpl.java
new file mode 100644
index 0000000..9b12d45
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingRobotServiceImpl.java
@@ -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
+ 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 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 findRobotsByStatus(RobotStatusEnum status) {
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("status", status.getValue());
+ return this.list(queryWrapper);
+ }
+
+ @Override
+ @Transactional // 确保原子性
+ public ChargingRobot assignIdleRobot() {
+ // 简单策略:查找第一个空闲的机器人并尝试锁定
+ // 后续可优化:考虑负载均衡、机器人位置、电量等因素
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("status", RobotStatusEnum.IDLE.getValue())
+ .orderByAsc("update_time") // 尝试获取最近更新为空闲的,或根据其他策略排序
+ .last("FOR UPDATE"); // 行级锁,防止并发问题
+
+ List 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
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingSessionServiceImpl.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingSessionServiceImpl.java
new file mode 100644
index 0000000..058df91
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingSessionServiceImpl.java
@@ -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
+ 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 findUserSessions(Long userId) {
+ QueryWrapper 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 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 getQueryWrapper(ChargingSessionQueryRequest queryRequest) {
+ if (queryRequest == null) {
+ throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
+ }
+ QueryWrapper 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 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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java
index 8b9b0b5..39dc596 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java
@@ -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);
}
}
}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/ParkingSpotServiceImpl.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ParkingSpotServiceImpl.java
new file mode 100644
index 0000000..b933f15
--- /dev/null
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ParkingSpotServiceImpl.java
@@ -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
+ 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 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 findAvailableAndAssignableSpots() {
+ QueryWrapper 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;
+ }
+}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java
index ebf8c7c..a133ac2 100644
--- a/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java
+++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java
@@ -295,38 +295,24 @@ public class RobotTaskServiceImpl extends ServiceImpl
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 listUsers() {
List userList = this.list(new QueryWrapper().eq("isDeleted", 0));
@@ -389,4 +339,60 @@ public class UserServiceImpl extends ServiceImpl
);
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;
+ }
+ }
}
\ No newline at end of file
diff --git a/springboot-init-main/src/main/resources/mapper/UserMapper.xml b/springboot-init-main/src/main/resources/mapper/UserMapper.xml
index f9e87c8..e54cd5d 100644
--- a/springboot-init-main/src/main/resources/mapper/UserMapper.xml
+++ b/springboot-init-main/src/main/resources/mapper/UserMapper.xml
@@ -23,4 +23,13 @@
userPassword,createTime,updateTime,
isDelete
+
+
+ UPDATE user
+ SET balance = balance + #{amountChange}
+ WHERE id = #{userId}
+
+ AND balance >= #{amountChange.abs()}
+
+