第三阶段核心业务开发完成
This commit is contained in:
60
LogBook.md
60
LogBook.md
@@ -47,3 +47,63 @@
|
||||
- 实现 `TaskTimeoutHandler.java`,使用 `@Scheduled` 定时调用 `RobotTaskService.findAndMarkTimedOutTasks` 处理任务超时。
|
||||
- 在 `MyApplication.java` 中添加 `@EnableScheduling` 以启用定时任务。
|
||||
- 在 `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)。
|
||||
@@ -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`。
|
||||
|
||||
@@ -97,6 +97,11 @@
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.5</version> <!-- 使用稳定版本 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>4.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
126
springboot-init-main/sql/mqtt_power.sql
Normal file
126
springboot-init-main/sql/mqtt_power.sql
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Navicat Premium Data Transfer
|
||||
|
||||
Source Server : yuyun
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 50744
|
||||
Source Host : yuyun-us1.stormrain.cn:3306
|
||||
Source Schema : mqtt_power
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 50744
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 17/05/2025 21:14:59
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for charging_robot
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `charging_robot`;
|
||||
CREATE TABLE `charging_robot` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`robot_uid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '机器人唯一标识符 (例如,硬件序列号)',
|
||||
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'IDLE' COMMENT '机器人状态 (IDLE, MOVING, CHARGING, ERROR, MAINTENANCE, OFFLINE)',
|
||||
`current_location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '当前位置描述 (例如,坐标或区域ID)',
|
||||
`battery_level` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '电池电量百分比 (0-100)',
|
||||
`current_task_id` bigint(20) NULL DEFAULT NULL COMMENT '当前执行的任务ID (关联 robot_task.id)',
|
||||
`last_heartbeat_time` datetime NULL DEFAULT NULL COMMENT '最后心跳时间',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
|
||||
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
|
||||
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `robot_uid`(`robot_uid`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '充电机器人表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for charging_session
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `charging_session`;
|
||||
CREATE TABLE `charging_session` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID,充电会话唯一标识',
|
||||
`user_id` bigint(20) NOT NULL COMMENT '用户ID (关联 user.id)',
|
||||
`robot_id` bigint(20) NULL DEFAULT NULL COMMENT '服务的机器人ID (关联 charging_robot.id)',
|
||||
`robot_uid_snapshot` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '服务机器人的UID快照 (冗余字段,方便查询历史)',
|
||||
`spot_id` bigint(20) NOT NULL COMMENT '车位ID (关联 parking_spot.id)',
|
||||
`spot_uid_snapshot` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '车位UID快照 (冗余字段)',
|
||||
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'REQUESTED' COMMENT '充电会话状态 (REQUESTED, ROBOT_ASSIGNED, ROBOT_MOVING, CHARGING_STARTED, CHARGING_IN_PROGRESS, COMPLETED, CANCELLED_BY_USER, CANCELLED_BY_SYSTEM, ERROR, ROBOT_TASK_TIMEOUT)',
|
||||
`request_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '用户请求充电时间',
|
||||
`robot_assigned_time` datetime NULL DEFAULT NULL COMMENT '机器人分配时间',
|
||||
`robot_arrival_time` datetime NULL DEFAULT NULL COMMENT '机器人到达车位时间',
|
||||
`charge_start_time` datetime NULL DEFAULT NULL COMMENT '充电开始时间',
|
||||
`charge_end_time` datetime NULL DEFAULT NULL COMMENT '充电结束时间',
|
||||
`total_duration_seconds` int(11) NULL DEFAULT NULL COMMENT '总充电时长 (秒)',
|
||||
`energy_consumed_kwh` decimal(10, 3) NULL DEFAULT NULL COMMENT '消耗电量 (kWh)',
|
||||
`cost` decimal(10, 2) NULL DEFAULT NULL COMMENT '本次充电费用 (元)',
|
||||
`payment_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT 'PENDING' COMMENT '支付状态 (PENDING, PAID, FAILED)',
|
||||
`error_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '错误码 (如果会话出错)',
|
||||
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL COMMENT '错误信息',
|
||||
`related_robot_task_id` bigint(20) NULL DEFAULT NULL COMMENT '启动本次会话或关键步骤的 RobotTask ID',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
|
||||
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '充电记录表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for parking_spot
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `parking_spot`;
|
||||
CREATE TABLE `parking_spot` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`spot_uid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '车位唯一标识符 (例如,车位号)',
|
||||
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'AVAILABLE' COMMENT '车位状态 (AVAILABLE, OCCUPIED_BY_CAR, RESERVED, CHARGING, MAINTENANCE, UNAVAILABLE)',
|
||||
`location_description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '位置描述 (例如,楼层、区域)',
|
||||
`robot_assignable` tinyint(1) NULL DEFAULT 1 COMMENT '此车位是否可指派机器人前往',
|
||||
`current_session_id` bigint(20) NULL DEFAULT NULL COMMENT '当前占用此车位的充电会话ID (关联 charging_session.id)',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
|
||||
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `spot_uid`(`spot_uid`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '车位表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for robot_task
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `robot_task`;
|
||||
CREATE TABLE `robot_task` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`robot_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '机器人ID',
|
||||
`command_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '命令类型 (MOVE_TO_SPOT, START_CHARGE, STOP_CHARGE, QUERY_STATUS)',
|
||||
`command_payload` text CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL COMMENT '命令参数 (JSON格式)',
|
||||
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'PENDING' COMMENT '任务状态 (PENDING, SENT, ACKNOWLEDGED_SUCCESS, ACKNOWLEDGED_FAILURE, TIMED_OUT)',
|
||||
`sent_time` datetime NULL DEFAULT NULL COMMENT '命令发送时间',
|
||||
`ack_time` datetime NULL DEFAULT NULL COMMENT '命令确认时间',
|
||||
`related_session_id` bigint(20) NULL DEFAULT NULL COMMENT '关联的充电会话ID (可选)',
|
||||
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL COMMENT '失败或超时的错误信息',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`is_delete` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_robot_status`(`robot_id`, `status`) USING BTREE COMMENT '机器人和状态索引,便于查询'
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '机器人指令任务表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for user
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
CREATE TABLE `user` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '用户名',
|
||||
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '密码 (加密存储)',
|
||||
`role` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL DEFAULT 'user' COMMENT '角色 (user/admin)',
|
||||
`balance` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '账户余额',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标志 (0:未删, 1:已删)',
|
||||
`isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除(0-未删, 1-已删)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `username`(`username`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.yupi.project.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.yupi.project.annotation.AuthCheck;
|
||||
import com.yupi.project.common.BaseResponse;
|
||||
import com.yupi.project.common.DeleteRequest;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.common.ResultUtils;
|
||||
import com.yupi.project.constant.UserConstant;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.exception.ThrowUtils;
|
||||
import com.yupi.project.model.dto.charging_robot.ChargingRobotAddRequest;
|
||||
import com.yupi.project.model.dto.charging_robot.ChargingRobotQueryRequest;
|
||||
import com.yupi.project.model.dto.charging_robot.ChargingRobotUpdateRequest;
|
||||
import com.yupi.project.model.entity.ChargingRobot;
|
||||
import com.yupi.project.model.enums.RobotStatusEnum;
|
||||
import com.yupi.project.service.ChargingRobotService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 充电机器人管理接口 (管理员权限)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/robot")
|
||||
@Slf4j
|
||||
@Api(tags = "充电机器人管理接口 (管理员)")
|
||||
public class ChargingRobotAdminController {
|
||||
|
||||
@Resource
|
||||
private ChargingRobotService chargingRobotService;
|
||||
|
||||
@ApiOperation("添加充电机器人")
|
||||
@PostMapping("/add")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Long> addChargingRobot(@RequestBody ChargingRobotAddRequest addRequest, HttpServletRequest request) {
|
||||
if (addRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
// 参数校验
|
||||
if (StringUtils.isBlank(addRequest.getRobotUid())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "机器人UID不能为空");
|
||||
}
|
||||
if (addRequest.getStatus() == null || RobotStatusEnum.getEnumByValue(addRequest.getStatus()) == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的机器人状态");
|
||||
}
|
||||
|
||||
ChargingRobot robot = chargingRobotService.registerRobot(addRequest.getRobotUid(), RobotStatusEnum.getEnumByValue(addRequest.getStatus()));
|
||||
if (robot == null || robot.getId() == null) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加机器人失败");
|
||||
}
|
||||
// 可以选择性地更新其他字段,如初始电量等
|
||||
if (addRequest.getBatteryLevel() != null) {
|
||||
chargingRobotService.updateBatteryLevel(robot.getId(), addRequest.getBatteryLevel());
|
||||
}
|
||||
if (StringUtils.isNotBlank(addRequest.getCurrentLocation())) {
|
||||
ChargingRobot updateRobot = new ChargingRobot();
|
||||
updateRobot.setId(robot.getId());
|
||||
updateRobot.setCurrentLocation(addRequest.getCurrentLocation());
|
||||
chargingRobotService.updateById(updateRobot);
|
||||
}
|
||||
|
||||
return ResultUtils.success(robot.getId());
|
||||
}
|
||||
|
||||
@ApiOperation("删除充电机器人")
|
||||
@PostMapping("/delete")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Boolean> deleteChargingRobot(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
|
||||
if (deleteRequest == null || deleteRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
boolean b = chargingRobotService.removeById(deleteRequest.getId());
|
||||
return ResultUtils.success(b);
|
||||
}
|
||||
|
||||
@ApiOperation("更新充电机器人信息")
|
||||
@PostMapping("/update")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Boolean> updateChargingRobot(@RequestBody ChargingRobotUpdateRequest updateRequest, HttpServletRequest request) {
|
||||
if (updateRequest == null || updateRequest.getId() == null || updateRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
ChargingRobot chargingRobot = new ChargingRobot();
|
||||
BeanUtils.copyProperties(updateRequest, chargingRobot);
|
||||
|
||||
if (StringUtils.isNotBlank(updateRequest.getStatus()) && RobotStatusEnum.getEnumByValue(updateRequest.getStatus()) == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的机器人状态值");
|
||||
}
|
||||
if (updateRequest.getBatteryLevel() != null && (updateRequest.getBatteryLevel() < 0 || updateRequest.getBatteryLevel() > 100)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的电池电量值");
|
||||
}
|
||||
|
||||
boolean result = chargingRobotService.updateById(chargingRobot);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
@ApiOperation("根据ID获取充电机器人信息")
|
||||
@GetMapping("/get")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<ChargingRobot> getChargingRobotById(long id, HttpServletRequest request) {
|
||||
if (id <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
ChargingRobot chargingRobot = chargingRobotService.getById(id);
|
||||
ThrowUtils.throwIf(chargingRobot == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
return ResultUtils.success(chargingRobot);
|
||||
}
|
||||
|
||||
@ApiOperation("分页获取充电机器人列表")
|
||||
@PostMapping("/list/page")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Page<ChargingRobot>> listChargingRobotsByPage(@RequestBody ChargingRobotQueryRequest queryRequest, HttpServletRequest request) {
|
||||
if (queryRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
long current = queryRequest.getCurrent();
|
||||
long size = queryRequest.getPageSize();
|
||||
Page<ChargingRobot> page = new Page<>(current, size);
|
||||
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
|
||||
if (StringUtils.isNotBlank(queryRequest.getRobotUid())) {
|
||||
queryWrapper.like("robot_uid", queryRequest.getRobotUid());
|
||||
}
|
||||
if (StringUtils.isNotBlank(queryRequest.getStatus())) {
|
||||
queryWrapper.eq("status", queryRequest.getStatus());
|
||||
}
|
||||
if (StringUtils.isNotBlank(queryRequest.getSortField())) {
|
||||
queryWrapper.orderBy(true, queryRequest.getSortOrder().equals("ascend"), queryRequest.getSortField());
|
||||
}
|
||||
chargingRobotService.page(page, queryWrapper);
|
||||
return ResultUtils.success(page);
|
||||
}
|
||||
|
||||
@ApiOperation("获取所有机器人状态类型")
|
||||
@GetMapping("/status/types")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<List<String>> getRobotStatusTypes() {
|
||||
List<String> statusTypes = Arrays.stream(RobotStatusEnum.values())
|
||||
.map(RobotStatusEnum::getValue)
|
||||
.collect(Collectors.toList());
|
||||
return ResultUtils.success(statusTypes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.yupi.project.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.yupi.project.annotation.AuthCheck;
|
||||
import com.yupi.project.common.BaseResponse;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.common.ResultUtils;
|
||||
import com.yupi.project.constant.UserConstant;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
|
||||
import com.yupi.project.model.entity.ChargingSession;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import com.yupi.project.model.vo.ChargingSessionVO;
|
||||
import com.yupi.project.service.ChargingSessionService;
|
||||
import com.yupi.project.service.UserService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 充电会话管理接口 (管理员权限)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/session")
|
||||
@Slf4j
|
||||
@Api(tags = "充电会话管理接口 (管理员)")
|
||||
public class ChargingSessionAdminController {
|
||||
|
||||
@Resource
|
||||
private ChargingSessionService chargingSessionService;
|
||||
|
||||
@Resource
|
||||
private UserService userService; // 可能需要用于填充用户信息
|
||||
|
||||
@ApiOperation("管理员分页获取所有充电会话列表")
|
||||
@PostMapping("/list/page")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Page<ChargingSessionVO>> listAllChargingSessionsByPage(@RequestBody ChargingSessionQueryRequest queryRequest, HttpServletRequest request) {
|
||||
if (queryRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
// 管理员查询,不强制设置userId
|
||||
long current = queryRequest.getCurrent();
|
||||
long size = queryRequest.getPageSize();
|
||||
|
||||
Page<ChargingSession> sessionPage = chargingSessionService.page(new Page<>(current, size),
|
||||
chargingSessionService.getQueryWrapper(queryRequest));
|
||||
|
||||
Page<ChargingSessionVO> sessionVOPage = new Page<>(sessionPage.getCurrent(), sessionPage.getSize(), sessionPage.getTotal());
|
||||
|
||||
// 转换VO并填充用户信息
|
||||
List<ChargingSessionVO> voList = sessionPage.getRecords().stream().map(session -> {
|
||||
ChargingSessionVO vo = ChargingSessionVO.objToVo(session);
|
||||
if (session.getUserId() != null) {
|
||||
User user = userService.getById(session.getUserId());
|
||||
if (user != null) {
|
||||
vo.setUser(user); // UserVO 转换会在 setUser 方法内进行
|
||||
}
|
||||
}
|
||||
// TODO: 填充机器人和车位快照对应的实体信息 (如果VO中需要)
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
sessionVOPage.setRecords(voList);
|
||||
|
||||
return ResultUtils.success(sessionVOPage);
|
||||
}
|
||||
|
||||
// TODO: 管理员可能需要的其他接口,例如:
|
||||
// - 管理员手动取消某个会话 (不同于用户取消)
|
||||
// - 查看特定会话的详细日志或关联任务
|
||||
// - 重试某个失败的支付等
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.yupi.project.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.yupi.project.annotation.AuthCheck;
|
||||
import com.yupi.project.common.BaseResponse;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.common.ResultUtils;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.exception.ThrowUtils;
|
||||
import com.yupi.project.model.dto.charging_session.ChargingRequest;
|
||||
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
|
||||
import com.yupi.project.model.dto.charging_session.PaymentRequest;
|
||||
import com.yupi.project.model.entity.ChargingSession;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import com.yupi.project.model.enums.ChargingSessionStatusEnum;
|
||||
import com.yupi.project.model.vo.ChargingSessionVO;
|
||||
import com.yupi.project.service.ChargingSessionService;
|
||||
import com.yupi.project.service.UserService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 充电会话接口 (用户权限)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/session")
|
||||
@Slf4j
|
||||
@Api(tags = "充电会话接口 (用户)")
|
||||
public class ChargingSessionController {
|
||||
|
||||
@Resource
|
||||
private ChargingSessionService chargingSessionService;
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@ApiOperation("用户请求充电")
|
||||
@PostMapping("/request")
|
||||
@AuthCheck // 需要登录
|
||||
public BaseResponse<Long> requestCharging(@RequestBody ChargingRequest chargingRequest, HttpServletRequest request) {
|
||||
if (chargingRequest == null || chargingRequest.getSpotId() == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数错误,必须指定车位ID");
|
||||
}
|
||||
User loginUser = userService.getCurrentUser(request);
|
||||
ChargingSession session = chargingSessionService.requestCharging(loginUser, chargingRequest);
|
||||
return ResultUtils.success(session.getId());
|
||||
}
|
||||
|
||||
@ApiOperation("用户获取自己的充电会话列表 (分页)")
|
||||
@PostMapping("/my/list/page")
|
||||
@AuthCheck // 需要登录
|
||||
public BaseResponse<Page<ChargingSessionVO>> listMyChargingSessionsByPage(@RequestBody ChargingSessionQueryRequest queryRequest, HttpServletRequest request) {
|
||||
if (queryRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getCurrentUser(request);
|
||||
queryRequest.setUserId(loginUser.getId()); // 强制查询当前用户的
|
||||
|
||||
long current = queryRequest.getCurrent();
|
||||
long size = queryRequest.getPageSize();
|
||||
|
||||
// 限制爬虫
|
||||
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||
Page<ChargingSession> sessionPage = chargingSessionService.page(new Page<>(current, size),
|
||||
chargingSessionService.getQueryWrapper(queryRequest));
|
||||
|
||||
Page<ChargingSessionVO> sessionVOPage = new Page<>(sessionPage.getCurrent(), sessionPage.getSize(), sessionPage.getTotal());
|
||||
List<ChargingSessionVO> voList = sessionPage.getRecords().stream().map(ChargingSessionVO::objToVo).collect(Collectors.toList());
|
||||
sessionVOPage.setRecords(voList);
|
||||
|
||||
return ResultUtils.success(sessionVOPage);
|
||||
}
|
||||
|
||||
@ApiOperation("用户获取单个充电会话详情")
|
||||
@GetMapping("/get")
|
||||
@AuthCheck
|
||||
public BaseResponse<ChargingSessionVO> getChargingSessionById(long id, HttpServletRequest request) {
|
||||
if (id <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getCurrentUser(request);
|
||||
ChargingSession session = chargingSessionService.getById(id);
|
||||
if (session == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
|
||||
}
|
||||
// 权限校验,只能看自己的
|
||||
if (!session.getUserId().equals(loginUser.getId())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
return ResultUtils.success(ChargingSessionVO.objToVo(session));
|
||||
}
|
||||
|
||||
@ApiOperation("用户取消充电会话")
|
||||
@PostMapping("/cancel")
|
||||
@AuthCheck
|
||||
public BaseResponse<Boolean> cancelChargingSession(@RequestBody PaymentRequest cancelRequest, HttpServletRequest request) { // 复用PaymentRequest只取sessionId
|
||||
if (cancelRequest == null || cancelRequest.getSessionId() == null || cancelRequest.getSessionId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getCurrentUser(request);
|
||||
boolean result = chargingSessionService.cancelChargingSession(cancelRequest.getSessionId(), loginUser.getId(), "用户主动取消", ChargingSessionStatusEnum.CANCELLED_BY_USER);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@ApiOperation("用户请求停止当前充电")
|
||||
@PostMapping("/stop")
|
||||
@AuthCheck
|
||||
public BaseResponse<Boolean> stopCharging(@RequestBody PaymentRequest stopRequest, HttpServletRequest request) { // 复用PaymentRequest只取sessionId
|
||||
if (stopRequest == null || stopRequest.getSessionId() == null || stopRequest.getSessionId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getCurrentUser(request);
|
||||
boolean result = chargingSessionService.stopChargingByUser(stopRequest.getSessionId(), loginUser.getId());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@ApiOperation("用户支付充电费用")
|
||||
@PostMapping("/pay")
|
||||
@AuthCheck
|
||||
public BaseResponse<Boolean> payChargingSession(@RequestBody PaymentRequest paymentRequest, HttpServletRequest request) {
|
||||
if (paymentRequest == null || paymentRequest.getSessionId() == null || paymentRequest.getSessionId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getCurrentUser(request);
|
||||
boolean result = chargingSessionService.processPayment(paymentRequest.getSessionId(), loginUser.getId());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.yupi.project.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.yupi.project.annotation.AuthCheck;
|
||||
import com.yupi.project.common.BaseResponse;
|
||||
import com.yupi.project.common.DeleteRequest;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.common.ResultUtils;
|
||||
import com.yupi.project.constant.UserConstant;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.exception.ThrowUtils;
|
||||
import com.yupi.project.model.dto.parking_spot.ParkingSpotAddRequest;
|
||||
import com.yupi.project.model.dto.parking_spot.ParkingSpotQueryRequest;
|
||||
import com.yupi.project.model.dto.parking_spot.ParkingSpotUpdateRequest;
|
||||
import com.yupi.project.model.entity.ParkingSpot;
|
||||
import com.yupi.project.model.enums.ParkingSpotStatusEnum;
|
||||
import com.yupi.project.service.ParkingSpotService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 车位管理接口 (管理员权限)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/spot")
|
||||
@Slf4j
|
||||
@Api(tags = "车位管理接口 (管理员)")
|
||||
public class ParkingSpotAdminController {
|
||||
|
||||
@Resource
|
||||
private ParkingSpotService parkingSpotService;
|
||||
|
||||
@ApiOperation("添加车位")
|
||||
@PostMapping("/add")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Long> addParkingSpot(@RequestBody ParkingSpotAddRequest addRequest, HttpServletRequest request) {
|
||||
if (addRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
if (StringUtils.isBlank(addRequest.getSpotUid())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "车位UID不能为空");
|
||||
}
|
||||
// robotAssignable 默认为true,如果 DTO 中是 Boolean,则 null 会被处理
|
||||
|
||||
ParkingSpot spot = parkingSpotService.addParkingSpot(
|
||||
addRequest.getSpotUid(),
|
||||
addRequest.getLocationDescription(),
|
||||
addRequest.getRobotAssignable() == null ? true : addRequest.getRobotAssignable()
|
||||
);
|
||||
|
||||
if (spot == null || spot.getId() == null) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加车位失败");
|
||||
}
|
||||
return ResultUtils.success(spot.getId());
|
||||
}
|
||||
|
||||
@ApiOperation("删除车位")
|
||||
@PostMapping("/delete")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Boolean> deleteParkingSpot(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
|
||||
if (deleteRequest == null || deleteRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
// 检查车位是否正在使用等逻辑可以在Service层实现,这里直接删除
|
||||
boolean b = parkingSpotService.removeById(deleteRequest.getId());
|
||||
return ResultUtils.success(b);
|
||||
}
|
||||
|
||||
@ApiOperation("更新车位信息")
|
||||
@PostMapping("/update")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Boolean> updateParkingSpot(@RequestBody ParkingSpotUpdateRequest updateRequest, HttpServletRequest request) {
|
||||
if (updateRequest == null || updateRequest.getId() == null || updateRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
ParkingSpot parkingSpot = new ParkingSpot();
|
||||
BeanUtils.copyProperties(updateRequest, parkingSpot);
|
||||
|
||||
if (StringUtils.isNotBlank(updateRequest.getStatus()) && ParkingSpotStatusEnum.getEnumByValue(updateRequest.getStatus()) == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的车位状态值");
|
||||
}
|
||||
|
||||
boolean result = parkingSpotService.updateById(parkingSpot);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
@ApiOperation("根据ID获取车位信息")
|
||||
@GetMapping("/get")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<ParkingSpot> getParkingSpotById(long id, HttpServletRequest request) {
|
||||
if (id <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
ParkingSpot parkingSpot = parkingSpotService.getById(id);
|
||||
ThrowUtils.throwIf(parkingSpot == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
return ResultUtils.success(parkingSpot);
|
||||
}
|
||||
|
||||
@ApiOperation("分页获取车位列表")
|
||||
@PostMapping("/list/page")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Page<ParkingSpot>> listParkingSpotsByPage(@RequestBody ParkingSpotQueryRequest queryRequest, HttpServletRequest request) {
|
||||
if (queryRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
long current = queryRequest.getCurrent();
|
||||
long size = queryRequest.getPageSize();
|
||||
Page<ParkingSpot> page = new Page<>(current, size);
|
||||
QueryWrapper<ParkingSpot> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
if (StringUtils.isNotBlank(queryRequest.getSpotUid())) {
|
||||
queryWrapper.like("spot_uid", queryRequest.getSpotUid());
|
||||
}
|
||||
if (StringUtils.isNotBlank(queryRequest.getStatus())) {
|
||||
queryWrapper.eq("status", queryRequest.getStatus());
|
||||
}
|
||||
if (queryRequest.getRobotAssignable() != null) {
|
||||
queryWrapper.eq("robot_assignable", queryRequest.getRobotAssignable());
|
||||
}
|
||||
if (StringUtils.isNotBlank(queryRequest.getSortField())) {
|
||||
queryWrapper.orderBy(true, queryRequest.getSortOrder().equals("ascend"), queryRequest.getSortField());
|
||||
}
|
||||
|
||||
parkingSpotService.page(page, queryWrapper);
|
||||
return ResultUtils.success(page);
|
||||
}
|
||||
|
||||
@ApiOperation("获取所有车位状态类型")
|
||||
@GetMapping("/status/types")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<List<String>> getParkingSpotStatusTypes() {
|
||||
List<String> statusTypes = Arrays.stream(ParkingSpotStatusEnum.values())
|
||||
.map(ParkingSpotStatusEnum::getValue)
|
||||
.collect(Collectors.toList());
|
||||
return ResultUtils.success(statusTypes);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.yupi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yupi.project.model.entity.ChargingRobot;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【charging_robot(充电机器人表)】的数据库操作Mapper
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
* @Entity com.yupi.project.model.entity.ChargingRobot
|
||||
*/
|
||||
public interface ChargingRobotMapper extends BaseMapper<ChargingRobot> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.yupi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yupi.project.model.entity.ChargingSession;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【charging_session(充电记录表)】的数据库操作Mapper
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
* @Entity com.yupi.project.model.entity.ChargingSession
|
||||
*/
|
||||
public interface ChargingSessionMapper extends BaseMapper<ChargingSession> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.yupi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yupi.project.model.entity.ParkingSpot;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【parking_spot(车位表)】的数据库操作Mapper
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
* @Entity com.yupi.project.model.entity.ParkingSpot
|
||||
*/
|
||||
public interface ParkingSpotMapper extends BaseMapper<ParkingSpot> {
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.yupi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* @description 针对表【user(用户表)】的数据库操作Mapper
|
||||
@@ -10,4 +12,11 @@ import com.yupi.project.model.entity.User;
|
||||
*/
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
||||
/**
|
||||
* 更新用户余额
|
||||
* @param userId 用户ID
|
||||
* @param amountChange 要变更的金额 (元), 正数表示增加,负数表示减少
|
||||
* @return 影响的行数
|
||||
*/
|
||||
int updateUserBalance(@Param("userId") Long userId, @Param("amountChange") BigDecimal amountChange);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
// 可以根据需要添加其他查询条件,如电量范围等
|
||||
}
|
||||
@@ -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 不建议直接通过此接口修改,应由业务流程驱动
|
||||
}
|
||||
@@ -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; // 偏好的机器人类型 (如果支持多种)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.yupi.project.model.dto.charging_session;
|
||||
|
||||
import com.yupi.project.common.PageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 充电会话查询请求
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class ChargingSessionQueryRequest extends PageRequest implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID (管理员接口可能需要)
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 机器人ID
|
||||
*/
|
||||
private Long robotId;
|
||||
|
||||
/**
|
||||
* 车位ID
|
||||
*/
|
||||
private Long spotId;
|
||||
|
||||
/**
|
||||
* 机器人UID快照 (模糊查询)
|
||||
*/
|
||||
private String robotUidSnapshot;
|
||||
|
||||
/**
|
||||
* 车位UID快照 (模糊查询)
|
||||
*/
|
||||
private String spotUidSnapshot;
|
||||
|
||||
/**
|
||||
* 会话状态 (精确查询, 来自 ChargingSessionStatusEnum 的 value)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 多个会话状态 (用于查询)
|
||||
*/
|
||||
private List<String> orStatusList;
|
||||
|
||||
/**
|
||||
* 支付状态 (精确查询, 来自 PaymentStatusEnum 的 value)
|
||||
*/
|
||||
private String paymentStatus;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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 不建议直接通过此接口修改,应由业务流程驱动
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 也有类似的转换方法
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
// 可以根据需要添加其他类型的消息处理方法
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.yupi.project.mqtt.handler;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yupi.project.model.entity.RobotTask;
|
||||
import com.yupi.project.model.enums.CommandTypeEnum;
|
||||
import com.yupi.project.model.enums.RobotTaskStatusEnum;
|
||||
import com.yupi.project.model.enums.ChargingSessionStatusEnum;
|
||||
import com.yupi.project.service.ChargingSessionService;
|
||||
import com.yupi.project.service.RobotTaskService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MqttMessageHandlerImpl implements MqttMessageHandler {
|
||||
|
||||
@Resource
|
||||
private RobotTaskService robotTaskService;
|
||||
|
||||
@Resource
|
||||
private ChargingSessionService chargingSessionService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public void handleStatusUpdate(String topic, String payload) {
|
||||
log.info("收到MQTT状态消息 - Topic: {}, Payload: {}", topic, payload);
|
||||
try {
|
||||
// 1. 解析Payload
|
||||
Map<String, Object> payloadMap = objectMapper.readValue(payload, Map.class);
|
||||
String robotUid = extractRobotUidFromTopic(topic);
|
||||
String commandTypeStr = (String) payloadMap.get("command_ack");
|
||||
CommandTypeEnum commandType = CommandTypeEnum.fromAck(commandTypeStr);
|
||||
Long originalTaskId = ((Number) payloadMap.get("task_id")).longValue();
|
||||
boolean success = (Boolean) payloadMap.getOrDefault("success", false);
|
||||
String message = (String) payloadMap.getOrDefault("message", "");
|
||||
|
||||
if (robotUid == null || commandType == null || originalTaskId == null) {
|
||||
log.error("MQTT状态消息解析失败:robotUid, commandType,或taskId为空. Topic: {}, Payload: {}", topic, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 更新RobotTask状态
|
||||
RobotTask task = robotTaskService.getById(originalTaskId);
|
||||
if (task == null) {
|
||||
log.warn("未找到与MQTT消息关联的机器人任务: taskId={}, robotUid={}", originalTaskId, robotUid);
|
||||
return;
|
||||
}
|
||||
if (!RobotTaskStatusEnum.SENT.getValue().equals(task.getStatus())){
|
||||
log.warn("任务 {} 状态为 {}, 非SENT状态,不再处理ACK消息。", originalTaskId, task.getStatus());
|
||||
return;
|
||||
}
|
||||
robotTaskService.markTaskAsAcknowledged(originalTaskId, success, success ? null : message, new Date());
|
||||
log.info("机器人任务 {} (robotUid: {}) 已确认为: {}, 消息: '{}'", originalTaskId, robotUid, success ? "成功" : "失败", message);
|
||||
|
||||
// 3. 如果任务成功,则根据任务类型更新 ChargingSession
|
||||
if (success) {
|
||||
Long sessionId = task.getRelatedSessionId();
|
||||
if (sessionId == null) {
|
||||
log.warn("机器人任务 {} 成功,但未关联充电会话ID,不处理会话状态更新。", originalTaskId);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (commandType) {
|
||||
case MOVE_TO_SPOT:
|
||||
log.info("机器人 {} 到达车位指令已确认 (任务ID: {}), 更新会话 {} 状态。", robotUid, originalTaskId, sessionId);
|
||||
chargingSessionService.handleRobotArrival(sessionId, originalTaskId);
|
||||
break;
|
||||
case START_CHARGE:
|
||||
log.info("机器人 {} 开始充电指令已确认 (任务ID: {}), 更新会话 {} 状态。", robotUid, originalTaskId, sessionId);
|
||||
chargingSessionService.handleChargingStart(sessionId, originalTaskId);
|
||||
break;
|
||||
case STOP_CHARGE:
|
||||
BigDecimal energyConsumed = payloadMap.containsKey("energy_kwh") ? new BigDecimal(payloadMap.get("energy_kwh").toString()) : BigDecimal.ZERO;
|
||||
int durationSeconds = payloadMap.containsKey("duration_s") ? ((Number) payloadMap.get("duration_s")).intValue() : 0;
|
||||
log.info("机器人 {} 停止充电指令已确认 (任务ID: {}), 更新会话 {} 状态。电量: {}kWh, 时长: {}s",
|
||||
robotUid, originalTaskId, sessionId, energyConsumed, durationSeconds);
|
||||
chargingSessionService.handleChargingEnd(sessionId, originalTaskId, energyConsumed, durationSeconds);
|
||||
break;
|
||||
default:
|
||||
log.info("收到机器人任务 {} (类型:{}) 的ACK,但此类型不直接更新充电会话。", originalTaskId, commandType);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Long sessionId = task.getRelatedSessionId();
|
||||
if (sessionId != null) {
|
||||
log.warn("机器人任务 {} (类型:{}) 失败,将关联的会话 {} 标记为错误。错误: {}", originalTaskId, commandType, sessionId, message);
|
||||
chargingSessionService.cancelChargingSession(sessionId, null,
|
||||
"机器人任务失败: " + commandType + " - " + message,
|
||||
ChargingSessionStatusEnum.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理MQTT状态消息时发生错误 - Topic: {}, Payload: {}", topic, payload, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractRobotUidFromTopic(String topic) {
|
||||
String[] parts = topic.split("/");
|
||||
if (parts.length > 0) {
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import com.yupi.project.model.entity.RobotTask;
|
||||
import com.yupi.project.service.ChargingSessionService;
|
||||
import com.yupi.project.service.RobotTaskService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TaskTimeoutHandler {
|
||||
|
||||
@Resource
|
||||
private RobotTaskService robotTaskService;
|
||||
|
||||
@Resource
|
||||
private ChargingSessionService chargingSessionService;
|
||||
|
||||
@Value("${mqtt.task.timeoutSeconds:300}")
|
||||
private int taskTimeoutSeconds;
|
||||
|
||||
@Scheduled(fixedRateString = "${mqtt.task.timeoutCheckRateMs:60000}")
|
||||
public void checkForTimedOutTasks() {
|
||||
log.debug("开始检查超时的机器人任务 (超时阈值: {}s)", taskTimeoutSeconds);
|
||||
List<RobotTask> timedOutTasks = robotTaskService.findAndMarkTimedOutTasks(taskTimeoutSeconds);
|
||||
|
||||
if (timedOutTasks.isEmpty()) {
|
||||
log.debug("没有发现超时的机器人任务。");
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn("发现 {} 个超时的机器人任务。", timedOutTasks.size());
|
||||
for (RobotTask task : timedOutTasks) {
|
||||
log.warn("任务ID: {} (RobotUID: {}, Command: {}, Status: {}, SentTime: {}) 已超时。",
|
||||
task.getId(), task.getRobotId(), task.getCommandType(), task.getStatus(), task.getSentTime());
|
||||
|
||||
if (task.getRelatedSessionId() != null) {
|
||||
log.info("任务 {} 超时与充电会话 {} 相关联,将通知会话服务处理。", task.getId(), task.getRelatedSessionId());
|
||||
try {
|
||||
chargingSessionService.handleSessionTaskTimeout(task.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("调用 chargingSessionService.handleSessionTaskTimeout 失败 for task ID {}: ", task.getId(), e);
|
||||
}
|
||||
} else {
|
||||
log.warn("任务 {} 超时,但未关联任何充电会话。", task.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.yupi.project.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.yupi.project.model.entity.ChargingRobot;
|
||||
import com.yupi.project.model.enums.RobotStatusEnum;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【charging_robot(充电机器人表)】的数据库操作Service
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
*/
|
||||
public interface ChargingRobotService extends IService<ChargingRobot> {
|
||||
|
||||
/**
|
||||
* 注册一个新的充电机器人
|
||||
*
|
||||
* @param robotUid 机器人唯一ID
|
||||
* @param initialStatus 初始状态
|
||||
* @return 创建的机器人实体,如果已存在则返回null或抛出异常
|
||||
*/
|
||||
ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus);
|
||||
|
||||
/**
|
||||
* 根据机器人UID查找机器人
|
||||
*
|
||||
* @param robotUid 机器人唯一ID
|
||||
* @return 机器人实体,未找到则返回null
|
||||
*/
|
||||
ChargingRobot findByRobotUid(String robotUid);
|
||||
|
||||
/**
|
||||
* 更新机器人电量
|
||||
*
|
||||
* @param robotId 机器人DB主键ID
|
||||
* @param batteryLevel 电量百分比
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
boolean updateBatteryLevel(Long robotId, Integer batteryLevel);
|
||||
|
||||
/**
|
||||
* 更新机器人心跳时间
|
||||
*
|
||||
* @param robotId 机器人DB主键ID
|
||||
* @param heartbeatTime 心跳时间
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
boolean updateHeartbeatTime(Long robotId, Date heartbeatTime);
|
||||
|
||||
/**
|
||||
* 查找指定状态的机器人列表
|
||||
*
|
||||
* @param status 机器人状态
|
||||
* @return 机器人列表
|
||||
*/
|
||||
List<ChargingRobot> findRobotsByStatus(RobotStatusEnum status);
|
||||
|
||||
/**
|
||||
* 分配一个空闲的机器人用于执行任务
|
||||
* (此方法需要考虑并发和选择策略)
|
||||
*
|
||||
* @return 分配到的机器人实体,若无空闲则返回null
|
||||
*/
|
||||
ChargingRobot assignIdleRobot();
|
||||
|
||||
/**
|
||||
* 释放机器人(例如任务完成或取消后)
|
||||
*
|
||||
* @param robotId 机器人ID
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean releaseRobot(Long robotId);
|
||||
|
||||
/**
|
||||
* 更新机器人的实时状态。
|
||||
* 此方法用于处理机器人通过MQTT等方式上报的常规状态信息(如心跳)。
|
||||
*
|
||||
* @param robotUID 机器人唯一标识符
|
||||
* @param status 机器人上报的当前状态 (枚举)
|
||||
* @param location 机器人上报的当前位置 (可选)
|
||||
* @param batteryLevel 机器人上报的电池电量 (可选, 0-100)
|
||||
* @param currentTaskId 机器人当前正在执行的任务ID (可选, 如果上报了)
|
||||
* @param lastHeartbeatTime 收到此状态消息的时间,可视为最后心跳时间
|
||||
* @return 更新是否成功
|
||||
*/
|
||||
boolean updateRobotStatus(String robotUID, RobotStatusEnum status, String location, Integer batteryLevel, Long currentTaskId, Date lastHeartbeatTime);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.yupi.project.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.yupi.project.model.dto.charging_session.ChargingRequest;
|
||||
import com.yupi.project.model.entity.ChargingSession;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import com.yupi.project.model.enums.ChargingSessionStatusEnum;
|
||||
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【charging_session(充电记录表)】的数据库操作Service
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
*/
|
||||
public interface ChargingSessionService extends IService<ChargingSession> {
|
||||
|
||||
/**
|
||||
* 用户请求充电
|
||||
*
|
||||
* @param currentUser 当前登录用户
|
||||
* @param chargingRequest 充电请求参数 (如 车位ID等)
|
||||
* @return 创建的充电会话实体
|
||||
*/
|
||||
ChargingSession requestCharging(User currentUser, ChargingRequest chargingRequest);
|
||||
|
||||
/**
|
||||
* 系统处理充电请求,分配机器人
|
||||
* (通常在 requestCharging 内部被调用,或由异步任务触发)
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @return 更新后的充电会话实体,或null如果无法分配
|
||||
*/
|
||||
ChargingSession assignRobotToSession(Long sessionId);
|
||||
|
||||
/**
|
||||
* 更新充电会话状态 (机器人已到达)
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @param robotTaskId 关联的机器人任务ID (例如,机器人移动任务)
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean handleRobotArrival(Long sessionId, Long robotTaskId);
|
||||
|
||||
/**
|
||||
* 更新充电会话状态 (充电开始)
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @param robotTaskId 关联的机器人任务ID (例如,开始充电任务)
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean handleChargingStart(Long sessionId, Long robotTaskId);
|
||||
|
||||
/**
|
||||
* 更新充电会话状态 (充电结束)
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @param robotTaskId 关联的机器人任务ID (例如,结束充电任务)
|
||||
* @param energyConsumedKwh 消耗电量
|
||||
* @param durationSeconds 充电时长
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean handleChargingEnd(Long sessionId, Long robotTaskId, BigDecimal energyConsumedKwh, int durationSeconds);
|
||||
|
||||
/**
|
||||
* 计算费用并更新会话 (在充电结束后调用)
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean calculateCostAndFinalizeSession(Long sessionId);
|
||||
|
||||
/**
|
||||
* 用户支付充电费用
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @param userId 用户ID (用于校验)
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean processPayment(Long sessionId, Long userId);
|
||||
|
||||
/**
|
||||
* 取消充电会话
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @param userId (可选) 如果是用户发起的取消,则为用户ID
|
||||
* @param reason 取消原因/发起方
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean cancelChargingSession(Long sessionId, Long userId, String reason, ChargingSessionStatusEnum cancelStatus);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找其充电记录
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 充电会话列表
|
||||
*/
|
||||
List<ChargingSession> findUserSessions(Long userId);
|
||||
|
||||
/**
|
||||
* 根据机器人任务ID查找关联的充电会话
|
||||
* @param robotTaskId 机器人任务ID
|
||||
* @return 充电会话
|
||||
*/
|
||||
ChargingSession findSessionByRobotTaskId(Long robotTaskId);
|
||||
|
||||
/**
|
||||
* 处理充电会话相关的机器人任务超时
|
||||
* @param robotTaskId 超时的机器人任务ID
|
||||
*/
|
||||
void handleSessionTaskTimeout(Long robotTaskId);
|
||||
|
||||
/**
|
||||
* 用户请求停止当前正在进行的充电
|
||||
* (会向机器人发送停止指令,等待机器人确认后再完成计费)
|
||||
*
|
||||
* @param sessionId 充电会话ID
|
||||
* @param userId 用户ID (用于校验)
|
||||
* @return 是否成功发起停止请求 (不代表已完成)
|
||||
*/
|
||||
boolean stopChargingByUser(Long sessionId, Long userId);
|
||||
|
||||
/**
|
||||
* 获取查询包装类
|
||||
*
|
||||
* @param queryRequest
|
||||
* @return
|
||||
*/
|
||||
QueryWrapper<ChargingSession> getQueryWrapper(ChargingSessionQueryRequest queryRequest);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.yupi.project.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.yupi.project.model.entity.ParkingSpot;
|
||||
import com.yupi.project.model.enums.ParkingSpotStatusEnum;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【parking_spot(车位表)】的数据库操作Service
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
*/
|
||||
public interface ParkingSpotService extends IService<ParkingSpot> {
|
||||
|
||||
/**
|
||||
* 添加新的车位信息
|
||||
*
|
||||
* @param spotUid 车位唯一ID
|
||||
* @param locationDescription 位置描述
|
||||
* @param robotAssignable 是否可指派机器人
|
||||
* @return 创建的车位实体
|
||||
*/
|
||||
ParkingSpot addParkingSpot(String spotUid, String locationDescription, boolean robotAssignable);
|
||||
|
||||
/**
|
||||
* 根据车位UID查找车位
|
||||
*
|
||||
* @param spotUid 车位唯一ID
|
||||
* @return 车位实体,未找到则返回null
|
||||
*/
|
||||
ParkingSpot findBySpotUid(String spotUid);
|
||||
|
||||
/**
|
||||
* 更新车位状态
|
||||
*
|
||||
* @param spotId 车位DB主键ID
|
||||
* @param newStatus 新状态
|
||||
* @param currentSessionId (可选) 当前关联的充电会话ID
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
boolean updateParkingSpotStatus(Long spotId, ParkingSpotStatusEnum newStatus, Long currentSessionId);
|
||||
|
||||
/**
|
||||
* 查找所有可用的、可指派机器人的车位
|
||||
*
|
||||
* @return 可用车位列表
|
||||
*/
|
||||
List<ParkingSpot> findAvailableAndAssignableSpots();
|
||||
|
||||
/**
|
||||
* 占用一个车位(例如,用户预订或开始充电会话时)
|
||||
*
|
||||
* @param spotId 车位ID
|
||||
* @param sessionId 关联的充电会话ID
|
||||
* @param targetStatus 目标状态 (如 OCCUPIED_BY_CAR, RESERVED, CHARGING)
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean occupySpot(Long spotId, Long sessionId, ParkingSpotStatusEnum targetStatus);
|
||||
|
||||
/**
|
||||
* 释放一个车位 (例如,充电结束或取消预订)
|
||||
*
|
||||
* @param spotId 车位ID
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean releaseSpot(Long spotId);
|
||||
|
||||
/**
|
||||
* 更新车位状态及当前关联的充电会话ID。
|
||||
*
|
||||
* @param spotUID 车位唯一标识符
|
||||
* @param newStatus 新的车位状态
|
||||
* @param currentSessionId 当前占用此车位的充电会话ID (如果车位变为空闲,则应传入 null)
|
||||
* @return 更新是否成功
|
||||
*/
|
||||
boolean updateSpotStatus(String spotUID, ParkingSpotStatusEnum newStatus, Long currentSessionId);
|
||||
|
||||
}
|
||||
@@ -92,14 +92,13 @@ public interface RobotTaskService extends IService<RobotTask> {
|
||||
boolean markTaskAsCompleted(Long taskId, Date ackTime, String message);
|
||||
|
||||
/**
|
||||
* 将任务标记为失败。
|
||||
* 标记任务为失败
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @param ackTime 失败确认时间 (可选)
|
||||
* @param errorCode 错误码 (可选)
|
||||
* @param errorMessage 错误信息 (可选)
|
||||
* @return 是否成功标记
|
||||
* @param taskId 任务ID
|
||||
* @param errorMessage 错误信息
|
||||
* @param failedTime 失败时间
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markTaskAsFailed(Long taskId, Date ackTime, String errorCode, String errorMessage);
|
||||
boolean markTaskAsFailed(Long taskId, String errorMessage, Date failedTime);
|
||||
|
||||
}
|
||||
@@ -57,20 +57,22 @@ public interface UserService extends IService<User> {
|
||||
User getSafetyUser(User originUser);
|
||||
|
||||
/**
|
||||
* 扣减用户余额 (需要保证线程安全和数据一致性)
|
||||
* 扣减用户余额
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param amount 扣减金额 (正数)
|
||||
* @return 操作是否成功
|
||||
* @param amount 要扣减的金额 (注意:这里假设amount是正数)
|
||||
* @return 操作是否成功 (例如,余额不足则失败)
|
||||
*/
|
||||
boolean deductBalance(Long userId, BigDecimal amount);
|
||||
boolean decreaseBalance(Long userId, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 增加用户余额 (需要保证线程安全和数据一致性)
|
||||
* 增加用户余额
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param amount 增加金额 (正数)
|
||||
* @param amount 要增加的金额
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean addBalance(Long userId, BigDecimal amount);
|
||||
boolean increaseBalance(Long userId, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 获取用户列表 (仅管理员)
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.yupi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.mapper.ChargingRobotMapper;
|
||||
import com.yupi.project.model.entity.ChargingRobot;
|
||||
import com.yupi.project.model.enums.RobotStatusEnum;
|
||||
import com.yupi.project.service.ChargingRobotService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【charging_robot(充电机器人表)】的数据库操作Service实现
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, ChargingRobot>
|
||||
implements ChargingRobotService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus) {
|
||||
if (findByRobotUid(robotUid) != null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "机器人UID已存在: " + robotUid);
|
||||
}
|
||||
ChargingRobot robot = new ChargingRobot();
|
||||
robot.setRobotUid(robotUid);
|
||||
robot.setStatus(initialStatus.getValue());
|
||||
robot.setCreateTime(new Date());
|
||||
robot.setUpdateTime(new Date());
|
||||
boolean saved = this.save(robot);
|
||||
if (!saved) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "机器人注册失败");
|
||||
}
|
||||
log.info("机器人注册成功: {}", robot);
|
||||
return robot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChargingRobot findByRobotUid(String robotUid) {
|
||||
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("robot_uid", robotUid);
|
||||
return this.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateRobotStatus(String robotUID, RobotStatusEnum status, String location,
|
||||
Integer batteryLevel, Long currentTaskId, Date lastHeartbeatTime) {
|
||||
if (StringUtils.isBlank(robotUID) || status == null) {
|
||||
log.warn("更新机器人状态失败:robotUID 或 status 为空。robotUID: {}, status: {}", robotUID, status);
|
||||
return false;
|
||||
}
|
||||
|
||||
ChargingRobot robot = this.findByRobotUid(robotUID);
|
||||
if (robot == null) {
|
||||
log.warn("更新机器人状态失败:未找到 UID 为 {} 的机器人。", robotUID);
|
||||
return false;
|
||||
}
|
||||
|
||||
ChargingRobot robotToUpdate = new ChargingRobot();
|
||||
robotToUpdate.setId(robot.getId());
|
||||
boolean changed = false;
|
||||
|
||||
if (status != null && !status.getValue().equals(robot.getStatus())) {
|
||||
robotToUpdate.setStatus(status.getValue());
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (location != null && !location.equals(robot.getCurrentLocation())) {
|
||||
robotToUpdate.setCurrentLocation(location);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (batteryLevel != null && !batteryLevel.equals(robot.getBatteryLevel())) {
|
||||
robotToUpdate.setBatteryLevel(batteryLevel);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (currentTaskId != null && !currentTaskId.equals(robot.getCurrentTaskId())) {
|
||||
robotToUpdate.setCurrentTaskId(currentTaskId);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (lastHeartbeatTime != null && (robot.getLastHeartbeatTime() == null || !lastHeartbeatTime.equals(robot.getLastHeartbeatTime()))){
|
||||
robotToUpdate.setLastHeartbeatTime(lastHeartbeatTime);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
log.info("机器人 {} 的状态与数据库一致 (状态: {}, 位置: {}, 电量: {}, 任务ID: {}, 心跳: {}),无需更新。",
|
||||
robotUID, status, location, batteryLevel, currentTaskId, lastHeartbeatTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean success = this.updateById(robotToUpdate);
|
||||
if (success) {
|
||||
log.info("成功更新机器人 {} 的状态。新状态: {}, 位置: {}, 电量: {}, 当前任务ID: {}, 心跳: {}",
|
||||
robotUID, status, location, batteryLevel, currentTaskId, lastHeartbeatTime);
|
||||
} else {
|
||||
log.error("更新机器人 {} 的状态失败 (数据库操作失败)。", robotUID);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateBatteryLevel(Long robotId, Integer batteryLevel) {
|
||||
ChargingRobot robot = this.getById(robotId);
|
||||
if (robot == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "机器人不存在");
|
||||
}
|
||||
if (batteryLevel < 0 || batteryLevel > 100) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的电池电量值");
|
||||
}
|
||||
robot.setBatteryLevel(batteryLevel);
|
||||
robot.setUpdateTime(new Date());
|
||||
return this.updateById(robot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateHeartbeatTime(Long robotId, Date heartbeatTime) {
|
||||
ChargingRobot robot = this.getById(robotId);
|
||||
if (robot == null) {
|
||||
// 心跳通常来自已注册机器人,如果找不到可以只记录日志不抛异常,或根据策略决定
|
||||
log.warn("尝试更新心跳失败,机器人不存在: {}", robotId);
|
||||
return false;
|
||||
}
|
||||
robot.setLastHeartbeatTime(heartbeatTime);
|
||||
robot.setUpdateTime(new Date()); // 可选,心跳更新是否算作整体记录更新
|
||||
return this.updateById(robot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChargingRobot> findRobotsByStatus(RobotStatusEnum status) {
|
||||
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("status", status.getValue());
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional // 确保原子性
|
||||
public ChargingRobot assignIdleRobot() {
|
||||
// 简单策略:查找第一个空闲的机器人并尝试锁定
|
||||
// 后续可优化:考虑负载均衡、机器人位置、电量等因素
|
||||
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("status", RobotStatusEnum.IDLE.getValue())
|
||||
.orderByAsc("update_time") // 尝试获取最近更新为空闲的,或根据其他策略排序
|
||||
.last("FOR UPDATE"); // 行级锁,防止并发问题
|
||||
|
||||
List<ChargingRobot> idleRobots = this.list(queryWrapper);
|
||||
if (idleRobots.isEmpty()) {
|
||||
log.warn("没有可用的空闲机器人");
|
||||
return null;
|
||||
}
|
||||
|
||||
ChargingRobot assignedRobot = idleRobots.get(0); // 取第一个
|
||||
// 更新机器人状态为MOVING或ASSIGNED,并关联任务(如果此时有任务ID)
|
||||
// 这里暂时只更新为MOVING,具体任务ID关联由调用方处理
|
||||
boolean success = updateRobotStatus(assignedRobot.getRobotUid(), RobotStatusEnum.MOVING, null, null, null, null);
|
||||
if (success) {
|
||||
log.info("成功分配机器人: {} (ID: {})", assignedRobot.getRobotUid(), assignedRobot.getId());
|
||||
return assignedRobot;
|
||||
} else {
|
||||
log.error("分配机器人 {} 失败,未能更新其状态", assignedRobot.getRobotUid());
|
||||
// 可能需要重试或选择其他机器人
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "分配机器人失败,无法更新状态");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean releaseRobot(Long robotId) {
|
||||
ChargingRobot robot = getById(robotId);
|
||||
if (robot == null) {
|
||||
log.warn("尝试释放机器人失败,机器人不存在: {}", robotId);
|
||||
return false;
|
||||
}
|
||||
// 将机器人状态设置为空闲,并清除当前任务ID
|
||||
return updateRobotStatus(robot.getRobotUid(), RobotStatusEnum.IDLE, null, null, null, null); // 第二个参数传null以清除任务ID
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
package com.yupi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.mapper.ChargingSessionMapper;
|
||||
import com.yupi.project.model.dto.charging_session.ChargingRequest;
|
||||
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
|
||||
import com.yupi.project.model.entity.*;
|
||||
import com.yupi.project.model.enums.*;
|
||||
import com.yupi.project.service.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【charging_session(充电记录表)】的数据库操作Service实现
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMapper, ChargingSession>
|
||||
implements ChargingSessionService {
|
||||
|
||||
@Resource
|
||||
private ParkingSpotService parkingSpotService;
|
||||
|
||||
@Resource
|
||||
private ChargingRobotService chargingRobotService;
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private RobotTaskService robotTaskService; // 用于创建和关联机器人任务
|
||||
|
||||
@Resource
|
||||
@Lazy // 避免循环依赖 MqttService -> ChargingSessionService -> MqttService (通过TaskTimeoutHandler)
|
||||
private MqttService mqttService; // 用于发送指令给机器人
|
||||
|
||||
// 计费相关配置 (后续可以移到配置类或数据库)
|
||||
@Value("${charging.price.perHour:10.0}") // 每小时10元
|
||||
private BigDecimal pricePerHour;
|
||||
|
||||
@Value("${charging.minChargeAmount:0.5}") // 最低消费0.5元
|
||||
private BigDecimal minChargeAmount;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ChargingSession requestCharging(User currentUser, ChargingRequest chargingRequest) {
|
||||
if (currentUser == null || chargingRequest == null || chargingRequest.getSpotId() == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数错误");
|
||||
}
|
||||
|
||||
// 1. 校验用户账户状态,例如余额是否充足(如果需要预付费或押金)
|
||||
// (此处简化,暂不校验余额)
|
||||
|
||||
// 2. 校验车位是否存在且可用
|
||||
ParkingSpot spot = parkingSpotService.getById(chargingRequest.getSpotId());
|
||||
if (spot == null || !spot.getRobotAssignable() || !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "车位不可用或不存在");
|
||||
}
|
||||
|
||||
// 3. 创建充电会话记录
|
||||
ChargingSession session = new ChargingSession();
|
||||
session.setUserId(currentUser.getId());
|
||||
session.setSpotId(spot.getId());
|
||||
session.setSpotUidSnapshot(spot.getSpotUid());
|
||||
session.setStatus(ChargingSessionStatusEnum.REQUESTED.getValue());
|
||||
session.setRequestTime(new Date());
|
||||
session.setPaymentStatus(PaymentStatusEnum.PENDING.getValue()); // 初始为待支付
|
||||
session.setCreateTime(new Date());
|
||||
session.setUpdateTime(new Date());
|
||||
|
||||
boolean saved = this.save(session);
|
||||
if (!saved) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建充电会话失败");
|
||||
}
|
||||
log.info("用户 {} 请求在车位 {} (ID:{}) 开始充电会话 (ID:{})", currentUser.getId(), spot.getSpotUid(), spot.getId(), session.getId());
|
||||
|
||||
// 4. 尝试分配机器人 (可以同步或异步)
|
||||
// 此处为简化,采用同步调用。实际项目中可能需要异步处理并通知用户分配结果。
|
||||
ChargingSession updatedSession = assignRobotToSession(session.getId());
|
||||
if (updatedSession == null) {
|
||||
// 如果没有可用机器人,标记会话为系统取消
|
||||
this.cancelChargingSession(session.getId(), null, "无可用机器人", ChargingSessionStatusEnum.CANCELLED_BY_SYSTEM);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "无可用机器人,充电请求失败");
|
||||
}
|
||||
|
||||
return updatedSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ChargingSession assignRobotToSession(Long sessionId) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null || !ChargingSessionStatusEnum.REQUESTED.getValue().equals(session.getStatus())) {
|
||||
log.warn("分配机器人失败,会话不存在或状态不正确: sessionId={}", sessionId);
|
||||
return null; // 或抛出异常
|
||||
}
|
||||
|
||||
// 1. 查找并分配一个空闲机器人
|
||||
ChargingRobot assignedRobot = chargingRobotService.assignIdleRobot(); // assignIdleRobot 内部已将机器人状态改为MOVING
|
||||
if (assignedRobot == null) {
|
||||
log.warn("分配机器人失败,没有空闲机器人可供会话 {} 使用", sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 更新车位状态为"已预订"或"机器人前往中"
|
||||
boolean spotOccupied = parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.RESERVED, session.getId());
|
||||
if (!spotOccupied) {
|
||||
log.error("分配机器人后,占用车位 {} (UID: {}) 失败 for session {}", session.getSpotId(), session.getSpotUidSnapshot(), sessionId);
|
||||
// 需要回滚机器人状态
|
||||
chargingRobotService.releaseRobot(assignedRobot.getId());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "占用车位失败");
|
||||
}
|
||||
|
||||
// 3. 创建机器人任务:移动到指定车位
|
||||
// Payload 需要根据与硬件的约定来构建
|
||||
String moveToSpotPayload = String.format("{\"command\":\"MOVE\", \"target_spot_uid\":\"%s\"}", session.getSpotUidSnapshot());
|
||||
RobotTask moveTask = robotTaskService.createTask(assignedRobot.getRobotUid(), CommandTypeEnum.MOVE_TO_SPOT, moveToSpotPayload, sessionId);
|
||||
if (moveTask == null) {
|
||||
log.error("为会话 {} 创建机器人移动任务失败", sessionId);
|
||||
chargingRobotService.releaseRobot(assignedRobot.getId());
|
||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建机器人移动任务失败");
|
||||
}
|
||||
|
||||
// 4. 更新充电会话信息
|
||||
session.setRobotId(assignedRobot.getId());
|
||||
session.setRobotUidSnapshot(assignedRobot.getRobotUid());
|
||||
session.setStatus(ChargingSessionStatusEnum.ROBOT_ASSIGNED.getValue());
|
||||
session.setRobotAssignedTime(new Date());
|
||||
session.setRelatedRobotTaskId(moveTask.getId()); // 关联当前任务
|
||||
session.setUpdateTime(new Date());
|
||||
this.updateById(session);
|
||||
|
||||
// 5. 发送MQTT指令给机器人
|
||||
try {
|
||||
mqttService.sendCommand(assignedRobot.getRobotUid(), CommandTypeEnum.MOVE_TO_SPOT, moveToSpotPayload, moveTask.getId());
|
||||
robotTaskService.markTaskAsSent(moveTask.getId(), new Date());
|
||||
log.info("已向机器人 {} 发送移动指令 for session {}, 任务ID: {}", assignedRobot.getRobotUid(), sessionId, moveTask.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("发送MQTT移动指令失败 for session {}, 任务ID: {}: ", sessionId, moveTask.getId(), e);
|
||||
// 此处可能需要更复杂的错误处理,如重试、标记任务失败等
|
||||
// 为了简化,如果发送失败,也回滚,并标记会话错误
|
||||
robotTaskService.markTaskAsFailed(moveTask.getId(), "MQTT发送失败: " + e.getMessage(), new Date());
|
||||
chargingRobotService.releaseRobot(assignedRobot.getId());
|
||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
|
||||
session.setStatus(ChargingSessionStatusEnum.ERROR.getValue());
|
||||
session.setErrorMessage("分配机器人并发送指令失败");
|
||||
this.updateById(session);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "发送MQTT指令失败");
|
||||
}
|
||||
|
||||
log.info("成功为会话 {} 分配机器人 {} (任务ID: {})", sessionId, assignedRobot.getRobotUid(), moveTask.getId());
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean handleRobotArrival(Long sessionId, Long robotTaskId) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null) {
|
||||
log.warn("处理机器人到达事件失败,会话不存在: sessionId={}", sessionId);
|
||||
return false;
|
||||
}
|
||||
// 校验会话状态和关联的任务ID
|
||||
if (!ChargingSessionStatusEnum.ROBOT_ASSIGNED.getValue().equals(session.getStatus()) ||
|
||||
!robotTaskId.equals(session.getRelatedRobotTaskId())) {
|
||||
log.warn("处理机器人到达事件失败,会话状态 ({}) 或任务ID ({}) 不匹配 (期望任务ID: {}) for session {}",
|
||||
session.getStatus(), robotTaskId, session.getRelatedRobotTaskId(), sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
session.setStatus(ChargingSessionStatusEnum.ROBOT_ARRIVED.getValue());
|
||||
session.setRobotArrivalTime(new Date());
|
||||
session.setUpdateTime(new Date());
|
||||
// 接下来可以创建"开始充电"的机器人任务,或等待用户指令/自动开始
|
||||
// 此处简化,认为到达后即可准备开始充电,相关任务由MQTT回调触发创建
|
||||
boolean updated = this.updateById(session);
|
||||
if(updated) {
|
||||
log.info("机器人已到达,会话 {} 状态更新为 ROBOT_ARRIVED. 关联任务ID: {}", sessionId, robotTaskId);
|
||||
// 可选:更新机器人状态为IDLE(在车位旁等待)或 CHARGING_READY
|
||||
chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.IDLE, null, null, null, new Date());
|
||||
// 可选:更新车位状态为 CHARGING (如果机器人到达即代表车位被用于充电)
|
||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.CHARGING, session.getId());
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean handleChargingStart(Long sessionId, Long robotTaskId) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null) {
|
||||
log.warn("处理充电开始事件失败,会话不存在: sessionId={}", sessionId);
|
||||
return false;
|
||||
}
|
||||
// 校验状态,应该在 ROBOT_ARRIVED 之后
|
||||
if (!ChargingSessionStatusEnum.ROBOT_ARRIVED.getValue().equals(session.getStatus()) ||
|
||||
!robotTaskId.equals(session.getRelatedRobotTaskId())) {
|
||||
log.warn("处理充电开始事件失败,会话状态 ({}) 或任务ID ({}) 不匹配 (期望任务ID: {}) for session {}",
|
||||
session.getStatus(), robotTaskId, session.getRelatedRobotTaskId(), sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
session.setStatus(ChargingSessionStatusEnum.CHARGING_STARTED.getValue());
|
||||
session.setChargeStartTime(new Date());
|
||||
session.setUpdateTime(new Date());
|
||||
boolean updated = this.updateById(session);
|
||||
if (updated) {
|
||||
log.info("充电开始,会话 {} 状态更新为 CHARGING_STARTED. 关联任务ID: {}", sessionId, robotTaskId);
|
||||
// 更新机器人状态为 CHARGING
|
||||
chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.CHARGING, null, null, robotTaskId, new Date());
|
||||
// 确保车位状态为 CHARGING
|
||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.CHARGING, session.getId());
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean handleChargingEnd(Long sessionId, Long robotTaskId, BigDecimal energyConsumedKwh, int durationSeconds) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null) {
|
||||
log.warn("处理充电结束事件失败,会话不存在: sessionId={}", sessionId);
|
||||
return false;
|
||||
}
|
||||
// 校验状态
|
||||
if (!ChargingSessionStatusEnum.CHARGING_STARTED.getValue().equals(session.getStatus()) ||
|
||||
!robotTaskId.equals(session.getRelatedRobotTaskId())) {
|
||||
log.warn("处理充电结束事件失败,会话状态 ({}) 或任务ID ({}) 不匹配 (期望任务ID: {}) for session {}",
|
||||
session.getStatus(), robotTaskId, session.getRelatedRobotTaskId(), sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
session.setStatus(ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue());
|
||||
session.setChargeEndTime(new Date());
|
||||
session.setEnergyConsumedKwh(energyConsumedKwh);
|
||||
session.setTotalDurationSeconds(durationSeconds);
|
||||
session.setUpdateTime(new Date());
|
||||
boolean updated = this.updateById(session);
|
||||
|
||||
if (updated) {
|
||||
log.info("充电结束,会话 {} 状态更新为 CHARGING_COMPLETED. 电量:{} kWh, 时长:{}s. 关联任务ID: {}",
|
||||
sessionId, energyConsumedKwh, durationSeconds, robotTaskId);
|
||||
// 释放机器人和车位
|
||||
chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.IDLE, null, null, null, new Date());
|
||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null); // 车位变为空闲
|
||||
|
||||
// 执行计费和最终化会话逻辑
|
||||
this.calculateCostAndFinalizeSession(sessionId);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean calculateCostAndFinalizeSession(Long sessionId) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null || session.getChargeEndTime() == null || session.getChargeStartTime() == null) {
|
||||
log.error("计算费用失败,会话 {} 不存在或充电时间不完整", sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
long durationMillis = session.getChargeEndTime().getTime() - session.getChargeStartTime().getTime();
|
||||
double hours = (double) durationMillis / TimeUnit.HOURS.toMillis(1);
|
||||
|
||||
// 费用 = 时长(小时) * 每小时单价
|
||||
BigDecimal cost = pricePerHour.multiply(BigDecimal.valueOf(hours)).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
// 应用最低消费
|
||||
if (cost.compareTo(minChargeAmount) < 0) {
|
||||
cost = minChargeAmount;
|
||||
}
|
||||
session.setCost(cost);
|
||||
session.setStatus(ChargingSessionStatusEnum.PAYMENT_PENDING.getValue()); // 更新为待支付
|
||||
session.setPaymentStatus(PaymentStatusEnum.PENDING.getValue());
|
||||
session.setUpdateTime(new Date());
|
||||
|
||||
boolean updated = this.updateById(session);
|
||||
if(updated){
|
||||
log.info("会话 {} 费用计算完成: {}元. 状态更新为 PAYMENT_PENDING", sessionId, cost);
|
||||
// 此处可以触发通知用户支付
|
||||
}
|
||||
|
||||
// 在计费完成后,如果会话已标记为COMPLETED,但尚未支付,并且车位状态不是AVAILABLE,则将其设置为空闲
|
||||
// 这主要覆盖了 handleChargingEnd 中可能未完全释放车位的情况(例如,如果计费失败)
|
||||
// 或者如果 calculateCostAndFinalizeSession 是独立调用的。
|
||||
String sessionStatus = session.getStatus();
|
||||
String paymentStatus = session.getPaymentStatus();
|
||||
|
||||
if (ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue().equals(sessionStatus) ||
|
||||
PaymentStatusEnum.PAID.getValue().equals(paymentStatus) ||
|
||||
PaymentStatusEnum.FAILED.getValue().equals(paymentStatus)) {
|
||||
|
||||
ParkingSpot spot = parkingSpotService.getById(session.getSpotId());
|
||||
if (spot != null && !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
|
||||
log.info("在 finalizeSession for session {} 时,车位 {} (UID: {}) 状态为 {},将其更新为 AVAILABLE。",
|
||||
sessionId, spot.getId(), spot.getSpotUid(), spot.getStatus());
|
||||
parkingSpotService.updateSpotStatus(spot.getSpotUid(), ParkingSpotStatusEnum.AVAILABLE, null);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean processPayment(Long sessionId, Long userId) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "充电会话不存在");
|
||||
}
|
||||
if (!session.getUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权操作他人充电记录");
|
||||
}
|
||||
if (!ChargingSessionStatusEnum.PAYMENT_PENDING.getValue().equals(session.getStatus()) ||
|
||||
!PaymentStatusEnum.PENDING.getValue().equals(session.getPaymentStatus())) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "会话状态非待支付,无法处理支付");
|
||||
}
|
||||
|
||||
BigDecimal cost = session.getCost();
|
||||
if (cost == null || cost.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
log.info("会话 {} 费用为0或无效,标记为已支付 (无需支付)", sessionId);
|
||||
session.setPaymentStatus(PaymentStatusEnum.PAID.getValue());
|
||||
session.setStatus(ChargingSessionStatusEnum.PAID.getValue()); // 主状态也更新为已支付
|
||||
session.setUpdateTime(new Date());
|
||||
return this.updateById(session);
|
||||
}
|
||||
|
||||
// 模拟扣款
|
||||
boolean paymentSuccess = userService.decreaseBalance(userId, cost);
|
||||
if (!paymentSuccess) {
|
||||
log.warn("用户 {} 为会话 {} 支付 {}元 失败,余额不足或操作失败", userId, sessionId, cost);
|
||||
session.setPaymentStatus(PaymentStatusEnum.FAILED.getValue());
|
||||
// 可以考虑是否将会话状态改为ERROR或保持PAYMENT_PENDING
|
||||
this.updateById(session);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "支付失败,余额不足或系统错误");
|
||||
}
|
||||
|
||||
session.setPaymentStatus(PaymentStatusEnum.PAID.getValue());
|
||||
session.setStatus(ChargingSessionStatusEnum.PAID.getValue()); // 主状态也更新为已支付
|
||||
session.setUpdateTime(new Date());
|
||||
boolean updated = this.updateById(session);
|
||||
if (updated) {
|
||||
log.info("用户 {} 为会话 {} 成功支付 {}元", userId, sessionId, cost);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean cancelChargingSession(Long sessionId, Long userId, String reason, ChargingSessionStatusEnum newStatus) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null) {
|
||||
log.warn("尝试取消会话 {} 失败: 会话不存在", sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否可以取消 (例如,不能取消已完成或已支付的会话)
|
||||
ChargingSessionStatusEnum currentStatus = ChargingSessionStatusEnum.getEnumByValue(session.getStatus());
|
||||
if (currentStatus == ChargingSessionStatusEnum.CHARGING_COMPLETED ||
|
||||
currentStatus == ChargingSessionStatusEnum.PAYMENT_PENDING ||
|
||||
currentStatus == ChargingSessionStatusEnum.PAID ||
|
||||
currentStatus == ChargingSessionStatusEnum.CANCELLED_BY_SYSTEM ||
|
||||
currentStatus == ChargingSessionStatusEnum.CANCELLED_BY_USER) {
|
||||
log.warn("会话 {} 当前状态 {} 不可取消", sessionId, currentStatus);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "当前状态不可取消");
|
||||
}
|
||||
|
||||
if (userId != null && !session.getUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权取消他人会话");
|
||||
}
|
||||
|
||||
session.setStatus(newStatus.getValue());
|
||||
session.setErrorMessage("会话取消: " + reason);
|
||||
session.setChargeEndTime(new Date()); // 标记一个结束时间
|
||||
session.setUpdateTime(new Date());
|
||||
|
||||
boolean updated = this.updateById(session);
|
||||
if (updated) {
|
||||
log.info("充电会话 {} 已被取消,原因: {}. 操作者ID(若有): {}", sessionId, reason, userId);
|
||||
// 如果机器人已分配或在执行任务,需要处理机器人和车位状态
|
||||
if (session.getRobotId() != null) {
|
||||
chargingRobotService.releaseRobot(session.getRobotId());
|
||||
// 如果有关联的MQTT任务,可能需要发送取消指令或标记任务为取消
|
||||
if (session.getRelatedRobotTaskId() != null) {
|
||||
RobotTask task = robotTaskService.getById(session.getRelatedRobotTaskId());
|
||||
if (task != null &&
|
||||
(task.getStatus() == RobotTaskStatusEnum.PENDING || task.getStatus() == RobotTaskStatusEnum.SENT || task.getStatus() == RobotTaskStatusEnum.PROCESSING)) {
|
||||
robotTaskService.markTaskAsFailed(task.getId(), "充电会话被取消: " + reason, new Date());
|
||||
log.info("因会话 {} 取消,关联的机器人任务 {} 已标记为失败。", sessionId, task.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 释放车位 (如果它被此会话占用)
|
||||
ParkingSpot spot = parkingSpotService.findBySpotUid(session.getSpotUidSnapshot());
|
||||
if (spot != null && sessionId.equals(spot.getCurrentSessionId()) && !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
|
||||
log.info("取消会话 {} 时,释放车位 {} (UID: {})。", sessionId, spot.getId(), spot.getSpotUid());
|
||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChargingSession> findUserSessions(Long userId) {
|
||||
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId);
|
||||
queryWrapper.orderByDesc("request_time");
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChargingSession findSessionByRobotTaskId(Long robotTaskId) {
|
||||
if (robotTaskId == null) return null;
|
||||
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("related_robot_task_id", robotTaskId);
|
||||
// 通常一个任务只关联一个会话的特定阶段
|
||||
return this.getOne(queryWrapper, false); // false: 不抛异常如果找到多个
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void handleSessionTaskTimeout(Long robotTaskId) {
|
||||
log.warn("处理机器人任务超时,任务ID: {}", robotTaskId);
|
||||
RobotTask task = robotTaskService.getById(robotTaskId);
|
||||
if (task == null) {
|
||||
log.error("任务超时处理失败:任务 {} 未找到", robotTaskId);
|
||||
return;
|
||||
}
|
||||
|
||||
ChargingSession session = findSessionByRobotTaskId(robotTaskId);
|
||||
if (session == null) {
|
||||
log.warn("任务 {} 超时,但未找到关联的充电会话。可能任务与会话解耦或已被处理。", robotTaskId);
|
||||
// 如果任务不直接关联会话,或者会话已结束,可能不需要进一步操作。
|
||||
// 但如果机器人仍占用,需要释放
|
||||
ChargingRobot robot = chargingRobotService.findByRobotUid(task.getRobotId()); // task.getRobotId()是robotUid
|
||||
if(robot != null && !RobotStatusEnum.IDLE.getValue().equals(robot.getStatus())){
|
||||
log.info("超时任务 {} 的机器人 {} 将被释放。", robotTaskId, robot.getRobotUid());
|
||||
chargingRobotService.releaseRobot(robot.getId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("任务 {} (类型: {}) 超时,关联会话ID: {}", robotTaskId, task.getCommandType(), session.getId());
|
||||
// 根据当前会话状态和任务类型决定如何处理
|
||||
// 例如:如果是移动任务超时,可能意味着机器人未能到达
|
||||
// 如果是充电任务超时,可能意味着充电异常
|
||||
// 此处统一将会话标记为错误,并释放资源
|
||||
session.setStatus(ChargingSessionStatusEnum.ERROR.getValue());
|
||||
session.setErrorMessage("机器人任务超时 (TaskID: " + robotTaskId + ", Type: " + task.getCommandType() + ")");
|
||||
session.setChargeEndTime(new Date()); // 标记一个结束时间
|
||||
session.setUpdateTime(new Date());
|
||||
this.updateById(session);
|
||||
|
||||
if (session.getRobotId() != null) {
|
||||
chargingRobotService.releaseRobot(session.getRobotId());
|
||||
}
|
||||
if (session.getSpotId() != null) {
|
||||
parkingSpotService.releaseSpot(session.getSpotId());
|
||||
}
|
||||
log.warn("会话 {} 因任务 {} 超时而被标记为错误并释放了资源。", session.getId(), robotTaskId);
|
||||
|
||||
// 如果会话被标记为错误或取消,并且之前占用了车位,则释放车位
|
||||
if (session != null && session.getSpotUidSnapshot() != null &&
|
||||
(ChargingSessionStatusEnum.ERROR.getValue().equals(session.getStatus()) ||
|
||||
ChargingSessionStatusEnum.CANCELLED_BY_SYSTEM.getValue().equals(session.getStatus()) ||
|
||||
ChargingSessionStatusEnum.CANCELLED_BY_USER.getValue().equals(session.getStatus()))) {
|
||||
|
||||
ParkingSpot spot = parkingSpotService.findBySpotUid(session.getSpotUidSnapshot());
|
||||
if (spot != null && session.getId().equals(spot.getCurrentSessionId()) && !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
|
||||
log.info("任务 {} 超时导致会话 {} 结束,释放车位 {} (UID: {})。", robotTaskId, session.getId(), spot.getId(), spot.getSpotUid());
|
||||
parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryWrapper<ChargingSession> getQueryWrapper(ChargingSessionQueryRequest queryRequest) {
|
||||
if (queryRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
|
||||
}
|
||||
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
|
||||
Long id = queryRequest.getId();
|
||||
Long userId = queryRequest.getUserId();
|
||||
Long robotId = queryRequest.getRobotId();
|
||||
Long spotId = queryRequest.getSpotId();
|
||||
String robotUidSnapshot = queryRequest.getRobotUidSnapshot();
|
||||
String spotUidSnapshot = queryRequest.getSpotUidSnapshot();
|
||||
String status = queryRequest.getStatus();
|
||||
List<String> orStatusList = queryRequest.getOrStatusList();
|
||||
String paymentStatus = queryRequest.getPaymentStatus();
|
||||
String sortField = queryRequest.getSortField();
|
||||
String sortOrder = queryRequest.getSortOrder();
|
||||
|
||||
queryWrapper.eq(id != null && id > 0, "id", id);
|
||||
queryWrapper.eq(userId != null && userId > 0, "user_id", userId);
|
||||
queryWrapper.eq(robotId != null && robotId > 0, "robot_id", robotId);
|
||||
queryWrapper.eq(spotId != null && spotId > 0, "spot_id", spotId);
|
||||
queryWrapper.like(StringUtils.isNotBlank(robotUidSnapshot), "robot_uid_snapshot", robotUidSnapshot);
|
||||
queryWrapper.like(StringUtils.isNotBlank(spotUidSnapshot), "spot_uid_snapshot", spotUidSnapshot);
|
||||
queryWrapper.eq(StringUtils.isNotBlank(status), "status", status);
|
||||
queryWrapper.eq(StringUtils.isNotBlank(paymentStatus), "payment_status", paymentStatus);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(orStatusList)) {
|
||||
queryWrapper.and(qw -> {
|
||||
for (String orStatus : orStatusList) {
|
||||
qw.or().eq("status", orStatus);
|
||||
}
|
||||
});
|
||||
}
|
||||
queryWrapper.orderBy(StringUtils.isNotBlank(sortField),
|
||||
sortOrder.equals(com.yupi.project.constant.CommonConstant.SORT_ORDER_ASC), sortField);
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean stopChargingByUser(Long sessionId, Long userId) {
|
||||
ChargingSession session = this.getById(sessionId);
|
||||
if (session == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "充电会话不存在");
|
||||
}
|
||||
if (!session.getUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权操作他人充电记录");
|
||||
}
|
||||
|
||||
if (!ChargingSessionStatusEnum.CHARGING_STARTED.getValue().equals(session.getStatus())) {
|
||||
log.warn("用户 {} 尝试停止一个非充电中状态的会话 {} (当前状态: {})", userId, sessionId, session.getStatus());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "当前会话并非正在充电状态,无法停止");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(session.getRobotUidSnapshot())) {
|
||||
log.error("会话 {} 缺少机器人UID快照,无法发送停止指令", sessionId);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "会话数据异常,缺少机器人UID快照");
|
||||
}
|
||||
|
||||
String stopChargePayload = String.format("{\"command\":\"STOP_CHARGE\", \"session_id\":%d}", sessionId);
|
||||
RobotTask stopTask = robotTaskService.createTask(session.getRobotUidSnapshot(), CommandTypeEnum.STOP_CHARGE, stopChargePayload, sessionId);
|
||||
if (stopTask == null) {
|
||||
log.error("为会话 {} 创建机器人停止充电任务失败", sessionId);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "创建机器人停止任务失败");
|
||||
}
|
||||
|
||||
session.setRelatedRobotTaskId(stopTask.getId());
|
||||
session.setUpdateTime(new Date());
|
||||
this.updateById(session);
|
||||
|
||||
boolean mqttSent = false;
|
||||
try {
|
||||
mqttSent = mqttService.sendCommand(session.getRobotUidSnapshot(), CommandTypeEnum.STOP_CHARGE, stopChargePayload, stopTask.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("发送MQTT停止充电指令失败 for session {}, 任务ID: {}: {}", sessionId, stopTask.getId(), e.getMessage(), e);
|
||||
robotTaskService.markTaskAsFailed(stopTask.getId(), "MQTT发送失败: " + e.getMessage(), new Date());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "发送MQTT停止指令失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (!mqttSent) {
|
||||
log.error("发送MQTT停止充电指令失败 for session {}, 任务ID: {}", sessionId, stopTask.getId());
|
||||
robotTaskService.markTaskAsFailed(stopTask.getId(), "MQTT发送失败", new Date());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "发送MQTT停止指令失败");
|
||||
}
|
||||
|
||||
log.info("用户 {} 已请求停止充电会话 {}。等待机器人确认。", userId, sessionId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
|
||||
RobotStatusEnum robotStatus = RobotStatusEnum.getEnumByValue(statusMessage.getActualRobotStatus());
|
||||
if (robotStatus == null) {
|
||||
log.warn("Received unknown status value '{}' from robot {}. Message: {}",
|
||||
statusMessage.getActualRobotStatus(), actualRobotUIDToUse, statusMessage);
|
||||
}
|
||||
|
||||
String location = statusMessage.getLocation();
|
||||
Integer batteryLevel = statusMessage.getBatteryLevel();
|
||||
Long currentRobotTask = statusMessage.getActiveTaskId();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.yupi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.mapper.ParkingSpotMapper;
|
||||
import com.yupi.project.model.entity.ParkingSpot;
|
||||
import com.yupi.project.model.enums.ParkingSpotStatusEnum;
|
||||
import com.yupi.project.service.ParkingSpotService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Yupi
|
||||
* @description 针对表【parking_spot(车位表)】的数据库操作Service实现
|
||||
* @createDate 2023-12-03 10:00:00
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ParkingSpotServiceImpl extends ServiceImpl<ParkingSpotMapper, ParkingSpot>
|
||||
implements ParkingSpotService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ParkingSpot addParkingSpot(String spotUid, String locationDescription, boolean robotAssignable) {
|
||||
if (findBySpotUid(spotUid) != null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "车位UID已存在: " + spotUid);
|
||||
}
|
||||
ParkingSpot spot = new ParkingSpot();
|
||||
spot.setSpotUid(spotUid);
|
||||
spot.setLocationDescription(locationDescription);
|
||||
spot.setRobotAssignable(robotAssignable);
|
||||
spot.setStatus(ParkingSpotStatusEnum.AVAILABLE.getValue()); // 默认可用
|
||||
spot.setCreateTime(new Date());
|
||||
spot.setUpdateTime(new Date());
|
||||
boolean saved = this.save(spot);
|
||||
if (!saved) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加车位失败");
|
||||
}
|
||||
log.info("车位添加成功: {}", spot);
|
||||
return spot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParkingSpot findBySpotUid(String spotUid) {
|
||||
QueryWrapper<ParkingSpot> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("spot_uid", spotUid);
|
||||
return this.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateParkingSpotStatus(Long spotId, ParkingSpotStatusEnum newStatus, Long currentSessionId) {
|
||||
ParkingSpot spot = this.getById(spotId);
|
||||
if (spot == null) {
|
||||
log.warn("旧版 updateParkingSpotStatus: 车位不存在, spotId: {}", spotId);
|
||||
// throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "车位不存在");
|
||||
// 为了与新方法的行为保持一致(新方法如果UID找不到会返回false),这里也返回false
|
||||
return false;
|
||||
}
|
||||
// 调用新的基于UID的方法
|
||||
return this.updateSpotStatus(spot.getSpotUid(), newStatus, currentSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParkingSpot> findAvailableAndAssignableSpots() {
|
||||
QueryWrapper<ParkingSpot> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("status", ParkingSpotStatusEnum.AVAILABLE.getValue());
|
||||
queryWrapper.eq("robot_assignable", true);
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean occupySpot(Long spotId, Long sessionId, ParkingSpotStatusEnum targetStatus) {
|
||||
ParkingSpot spot = this.getById(spotId);
|
||||
if (spot == null) {
|
||||
log.warn("OccupySpot: 车位不存在, spotId: {}", spotId);
|
||||
// throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "车位不存在");
|
||||
return false; // 与新 updateSpotStatus 行为一致
|
||||
}
|
||||
// 校验车位是否可被占用 (例如必须是AVAILABLE状态)
|
||||
if (!ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
|
||||
log.warn("尝试占用一个非可用车位: spotId={}, currentStatus={}", spotId, spot.getStatus());
|
||||
// throw new BusinessException(ErrorCode.OPERATION_ERROR, "车位当前状态不可占用");
|
||||
return false; // 状态不符,占用失败
|
||||
}
|
||||
if (!spot.getRobotAssignable()) {
|
||||
log.warn("尝试占用一个不可指派机器人的车位: spotId={}", spotId);
|
||||
// throw new BusinessException(ErrorCode.OPERATION_ERROR, "车位不可指派机器人");
|
||||
return false; // 不可指派,占用失败
|
||||
}
|
||||
|
||||
// return updateParkingSpotStatus(spotId, targetStatus, sessionId); // 旧调用
|
||||
return this.updateSpotStatus(spot.getSpotUid(), targetStatus, sessionId); // 新调用
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean releaseSpot(Long spotId) {
|
||||
ParkingSpot spot = this.getById(spotId);
|
||||
if (spot == null) {
|
||||
log.warn("尝试释放车位失败,车位不存在: {}", spotId);
|
||||
return false;
|
||||
}
|
||||
// 只有非AVAILABLE的车位才需要被释放回AVAILABLE
|
||||
// 新的 updateSpotStatus 内部会检查是否真的需要更新,所以这里的检查可以简化或移除
|
||||
// if (ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
|
||||
// log.info("车位 {} (UID: {}) 本身已是可用状态,无需释放。", spotId, spot.getSpotUid());
|
||||
// return true;
|
||||
// }
|
||||
// return updateParkingSpotStatus(spotId, ParkingSpotStatusEnum.AVAILABLE, null); // 旧调用
|
||||
return this.updateSpotStatus(spot.getSpotUid(), ParkingSpotStatusEnum.AVAILABLE, null); // 新调用
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateSpotStatus(String spotUID, ParkingSpotStatusEnum newStatus, Long currentSessionId) {
|
||||
if (StringUtils.isBlank(spotUID) || newStatus == null) {
|
||||
log.warn("更新车位状态失败:spotUID 或 newStatus 为空。spotUID: {}, newStatus: {}", spotUID, newStatus);
|
||||
return false;
|
||||
}
|
||||
|
||||
ParkingSpot parkingSpot = this.findBySpotUid(spotUID);
|
||||
if (parkingSpot == null) {
|
||||
log.warn("更新车位状态失败:未找到 UID 为 {} 的车位。", spotUID);
|
||||
return false;
|
||||
}
|
||||
|
||||
ParkingSpot spotToUpdate = new ParkingSpot();
|
||||
spotToUpdate.setId(parkingSpot.getId());
|
||||
boolean changed = false;
|
||||
|
||||
// Compare enum with String value from entity
|
||||
if (newStatus != null && !newStatus.getValue().equals(parkingSpot.getStatus())) {
|
||||
spotToUpdate.setStatus(newStatus.getValue()); // Set String value to entity
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (currentSessionId == null && parkingSpot.getCurrentSessionId() != null) {
|
||||
spotToUpdate.setCurrentSessionId(null);
|
||||
changed = true;
|
||||
} else if (currentSessionId != null && !currentSessionId.equals(parkingSpot.getCurrentSessionId())) {
|
||||
spotToUpdate.setCurrentSessionId(currentSessionId);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
log.info("车位 {} 的状态与数据库一致 (状态: {}, 会话ID: {}),无需更新。",
|
||||
spotUID, newStatus, currentSessionId == null ? "null" : currentSessionId);
|
||||
return true; // Data is consistent, operation considered successful
|
||||
}
|
||||
|
||||
// updateTime will be auto-filled
|
||||
boolean success = this.updateById(spotToUpdate);
|
||||
if (success) {
|
||||
log.info("成功更新车位 {} 的状态。新状态: {}, 当前会话ID: {}",
|
||||
spotUID, newStatus, currentSessionId == null ? "null" : currentSessionId);
|
||||
} else {
|
||||
log.error("更新车位 {} 的状态失败 (数据库操作失败)。", spotUID);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
@@ -295,38 +295,24 @@ public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean markTaskAsFailed(Long taskId, Date ackTime, String errorCode, String errorMessage) {
|
||||
if (taskId == null) {
|
||||
log.error("Cannot mark task as failed: taskId is null.");
|
||||
return false;
|
||||
}
|
||||
public boolean markTaskAsFailed(Long taskId, String errorMessage, Date failedTime) {
|
||||
RobotTask task = this.getById(taskId);
|
||||
if (task == null) {
|
||||
log.warn("Cannot mark task as failed: Task with ID {} not found.", taskId);
|
||||
log.warn("尝试标记任务 {} 为失败失败:任务不存在", taskId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow transition from any non-terminal state (PENDING, SENT, PROCESSING) to FAILED
|
||||
if (task.getStatus() == RobotTaskStatusEnum.COMPLETED || task.getStatus() == RobotTaskStatusEnum.FAILED || task.getStatus() == RobotTaskStatusEnum.TIMED_OUT) {
|
||||
log.warn("Cannot mark task {} as FAILED. Task is already in a terminal state: {}.", taskId, task.getStatus());
|
||||
// 只能标记非终态的任务
|
||||
if (RobotTaskStatusEnum.isFinalStatus(task.getStatus())) {
|
||||
log.warn("任务 {} 当前状态为 {} (终态),无法标记为失败", taskId, task.getStatus());
|
||||
return false;
|
||||
}
|
||||
|
||||
RobotTask updateTask = new RobotTask();
|
||||
updateTask.setId(taskId);
|
||||
updateTask.setStatus(RobotTaskStatusEnum.FAILED);
|
||||
if (ackTime != null) {
|
||||
updateTask.setAckTime(ackTime);
|
||||
}
|
||||
// Construct a comprehensive error message if both are provided
|
||||
String finalErrorMessage = (errorCode != null ? "Code [" + errorCode + "] " : "") + (errorMessage != null ? errorMessage : "");
|
||||
updateTask.setErrorMessage(finalErrorMessage.isEmpty() ? null : finalErrorMessage);
|
||||
|
||||
boolean updated = this.updateById(updateTask);
|
||||
task.setStatus(RobotTaskStatusEnum.FAILED);
|
||||
task.setErrorMessage(errorMessage);
|
||||
task.setAckTime(failedTime); // Using ackTime field to store the failure time
|
||||
task.setUpdateTime(new Date());
|
||||
boolean updated = this.updateById(task);
|
||||
if (updated) {
|
||||
log.info("Marked RobotTask with ID: {} as FAILED. Error: {}", taskId, finalErrorMessage);
|
||||
} else {
|
||||
log.error("Failed to mark RobotTask with ID: {} as FAILED.", taskId);
|
||||
log.info("机器人任务 {} 已标记为失败. 原因: {}", taskId, errorMessage);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -168,56 +168,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
return safetyUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deductBalance(Long userId, BigDecimal amount) {
|
||||
if (userId == null || userId <= 0 || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误");
|
||||
}
|
||||
boolean updateResult = this.update()
|
||||
.setSql("balance = balance - " + amount.doubleValue())
|
||||
.eq("id", userId)
|
||||
.ge("balance", amount)
|
||||
.update();
|
||||
|
||||
if (!updateResult) {
|
||||
User user = this.getById(userId);
|
||||
if (user == null || user.getIsDeleted() == 1) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
|
||||
} else if (user.getBalance().compareTo(amount) < 0) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "余额不足");
|
||||
} else {
|
||||
log.warn("Deduct balance failed due to concurrent update for userId: {}", userId);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "扣款失败,请重试");
|
||||
}
|
||||
}
|
||||
log.info("Deducted {} from balance for user {}", amount, userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean addBalance(Long userId, BigDecimal amount) {
|
||||
if (userId == null || userId <= 0 || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误");
|
||||
}
|
||||
boolean updateResult = this.update()
|
||||
.setSql("balance = balance + " + amount.doubleValue())
|
||||
.eq("id", userId)
|
||||
.update();
|
||||
|
||||
if (!updateResult) {
|
||||
User user = this.getById(userId);
|
||||
if (user == null || user.getIsDeleted() == 1) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
|
||||
}
|
||||
log.error("Add balance failed unexpectedly for userId: {}", userId);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "充值失败,请稍后重试");
|
||||
}
|
||||
log.info("Added {} to balance for user {}", amount, userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> listUsers() {
|
||||
List<User> userList = this.list(new QueryWrapper<User>().eq("isDeleted", 0));
|
||||
@@ -389,4 +339,60 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean decreaseBalance(Long userId, BigDecimal amountToDecrease) {
|
||||
if (userId == null || amountToDecrease == null || amountToDecrease.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID或扣款金额无效");
|
||||
}
|
||||
|
||||
User user = this.getById(userId);
|
||||
if (user == null) {
|
||||
// 即使SQL会因为用户不存在而不更新,这里提前检查可以提供更明确的错误信息
|
||||
log.warn("尝试为不存在的用户ID {} 扣款 {} 失败。", userId, amountToDecrease);
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在,无法扣款");
|
||||
}
|
||||
|
||||
// 数据库层面的原子操作将处理余额是否足够
|
||||
// The amountChange passed to mapper is negative for decrease
|
||||
int updatedRows = baseMapper.updateUserBalance(userId, amountToDecrease.negate());
|
||||
|
||||
if (updatedRows > 0) {
|
||||
log.info("用户 {} 余额扣款 {} 成功 (通过原子操作)。", userId, amountToDecrease);
|
||||
// 获取更新后的余额用于日志记录是可选的,且可能引入额外查询。为简单起见,此处省略。
|
||||
return true;
|
||||
} else {
|
||||
// updatedRows == 0 表示扣款未成功,主要原因是余额不足(由SQL的WHERE子句保证)或用户ID无效(可能性较低,因已预查)
|
||||
log.warn("用户 {} 余额扣款 {} 失败。原子操作更新行数为0,通常表示余额不足或用户不存在。", userId, amountToDecrease);
|
||||
// 可以选择再次查询用户以确认具体原因,但对于调用方来说,false已足够
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean increaseBalance(Long userId, BigDecimal amountToIncrease) {
|
||||
if (userId == null || amountToIncrease == null || amountToIncrease.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID或增加金额无效");
|
||||
}
|
||||
|
||||
// Ensure user exists before attempting to update
|
||||
User user = this.getById(userId);
|
||||
if (user == null) {
|
||||
log.warn("尝试为不存在的用户 {} 增加余额 {} 失败。", userId, amountToIncrease);
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在,无法增加余额");
|
||||
}
|
||||
|
||||
int updatedRows = baseMapper.updateUserBalance(userId, amountToIncrease);
|
||||
|
||||
if (updatedRows > 0) {
|
||||
log.info("用户 {} 余额增加 {} 成功。当前余额: {}", userId, amountToIncrease, user.getBalance().add(amountToIncrease));
|
||||
return true;
|
||||
} else {
|
||||
// This case should ideally not happen if user existence is checked before, unless user is deleted concurrently.
|
||||
log.warn("用户 {} 余额增加 {} 失败,更新数据库记录数为0。", userId, amountToIncrease);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,4 +23,13 @@
|
||||
userPassword,createTime,updateTime,
|
||||
isDelete
|
||||
</sql>
|
||||
|
||||
<update id="updateUserBalance">
|
||||
UPDATE user
|
||||
SET balance = balance + #{amountChange}
|
||||
WHERE id = #{userId}
|
||||
<if test="amountChange.signum() == -1">
|
||||
AND balance >= #{amountChange.abs()}
|
||||
</if>
|
||||
</update>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user