# 智能充电桩管理系统 (Charging Pile Management System)

## 1. 项目简介

本项目是一个基于 Spring Boot (后端) 和 Next.js (前端) 构建的智能充电桩管理与计费系统。旨在为用户提供便捷的充电服务，并为管理员提供高效的充电桩、用户及计费管理功能。支持通过 MQTT 协议与充电桩硬件（单片机）进行实时通信和控制。

### 核心功能
*   **用户端**:
    *   用户注册与登录
    *   查看账户余额、充电记录
    *   扫码/输入ID启动充电
    *   停止充电
    *   激活码兑换充值
*   **管理端**:
    *   用户管理（查询、添加、编辑、删除）
    *   充电桩/车位管理（状态监控、添加、编辑、删除）
    *   充电会话管理（查询、监控）
    *   激活码管理（生成、查询、删除）
    *   MQTT通信日志查看（查询、审计所有MQTT消息）
    *   系统概览与统计
*   **硬件交互 (通过 MQTT)**:
    *   充电桩状态上报 (空闲、连接中、充电中、故障等)
    *   远程启动/停止充电指令下发
    *   心跳维持

## 2. 技术栈

*   **后端**: Spring Boot, Spring Security, MyBatis Plus, MySQL, Mosquitto (MQTT Broker), Spring AOP (用于日志)
*   **前端**: Next.js, React, TypeScript, Ant Design, Axios
*   **数据库**: MySQL
*   **通信协议**: HTTP/HTTPS, MQTT

## 3. 项目部署与运行

### 3.1. 环境准备
*   Java JDK 1.8 或更高版本
*   Maven 3.x
*   Node.js 16.x 或更高版本
*   Yarn 或 npm
*   MySQL 5.7 或更高版本
*   MQTT Broker (例如 Mosquitto) 已安装并运行 (默认端口 1883)

### 3.2. 后端服务 (springboot-init-main)
1.  **数据库配置**:
    *   创建一个数据库 (例如 `charging_system_db`)。
    *   修改 `springboot-init-main/src/main/resources/application.yml` (或对应环境的配置文件) 中的数据库连接信息:
    ```yaml
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: your_db_username
        password: your_db_password
    ```
2.  **MQTT Broker 配置**:
    *   确保 MQTT Broker 正在运行。
    *   在 `application.yml` 中配置 MQTT 连接参数 (如果需要修改默认值或添加认证):
    ```yaml
    mqtt:
      broker-url: tcp://localhost:1883 # 替换为您的 MQTT Broker 地址
      client-id-publisher: backend-publisher-default
      client-id-subscriber: backend-subscriber-default
      default-topic: "charging/default/topic" # 示例，具体业务主题见单片机对接部分
      # username: your_mqtt_username # 如果 Broker 需要认证
      # password: your_mqtt_password # 如果 Broker 需要认证
    ```
3.  **启动后端服务**:
    *   在 `springboot-init-main` 目录下，使用 Maven 运行:
      ```bash
      mvn spring-boot:run
      ```
    *   或者打包成 jar 文件后运行:
      ```bash
      mvn clean package
      java -jar target/springboot-init-main-0.0.1-SNAPSHOT.jar # 注意替换为实际生成的jar包名
      ```
    *   **配置文件外部化**: `application.yml` 在打包时已配置为外部化，并会复制到 `target/config/application.yml`。在生产环境部署时，可以将此文件放置于 JAR 包同级目录的 `config` 文件夹下 (例如: `your_app_dir/config/application.yml`)，Spring Boot 将自动加载。或者，您也可以通过 `--spring.config.location` 启动参数来指定外部配置文件的具体路径，例如：`java -jar your_app.jar --spring.config.location=file:/path/to/your/external/application.yml`。
    *   服务默认运行在 `http://localhost:7529` (或您在 `application.yml` 中配置的端口)。

### 3.3. 前端应用 (charging_web_app)
1.  **安装依赖**:
    在 `charging_web_app` 目录下运行:
    ```bash
    yarn install
    # 或者
    # npm install
    ```
2.  **API 代理配置**:
    前端应用通过 Next.js 的代理将 `/api` 请求转发到后端服务。此配置在 `next.config.js` 中：
    ```javascript
    // next.config.js (示例)
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      reactStrictMode: true,
      async rewrites() {
        return [
          {
            source: '/api/:path*',
            destination: 'http://localhost:7529/api/:path*', // 后端服务地址
          },
        ];
      },
    };
    module.exports = nextConfig;
    ```
    确保 `destination` 指向您后端服务的正确地址和端口。
3.  **启动前端开发服务器**:
    ```bash
    yarn dev
    # 或者
    # npm run dev
    ```
    前端应用默认运行在 `http://localhost:3000`。

4.  **构建生产版本**:
    ```bash
    yarn build
    yarn start
    # 或者
    # npm run build
    # npm run start
    ```

## 4. 单片机 (充电桩硬件) 对接指南

智能充电桩硬件（在本例中为基于ESP32的移动机器人）需通过 MQTT 协议与本系统的后端服务进行通信。
**注意**: 本项目提供了一个新的、功能更完整的ESP32固件项目，位于 `esp32_robot_firmware/` 文件夹下。本文档后续的对接指南主要基于此新固件的逻辑。旧的 `mqtt_esp32_client.ino` 主要作为简易模拟器，其功能已被新固件覆盖和扩展。

### 4.1. MQTT 连接配置
*   **Broker 地址**: 单片机需配置连接到与后端服务相同的 MQTT Broker (例如 `tcp://your_mqtt_broker_address:1883`)。
*   **Client ID**: 每个充电桩应使用唯一的 Client ID。对于新固件，这在 `esp32_robot_firmware/config.h` 中通过 `DEVICE_UID` 配置，必须与后端系统中注册的机器人ID一致。
*   **认证**: 如果 MQTT Broker 配置了用户名/密码认证，单片机连接时也需提供 (在 `config.h` 中配置)。

### 4.2. MQTT 主题 (Topics) 约定

根据后端服务的实际MQTT通信实现，主题约定如下：

#### 4.2.1. 上行消息 (单片机 -> 后端)

所有类型的上行消息，包括设备状态更新、心跳以及对后端指令的执行回执 (ACK)，都统一发送到以下主题：

*   **统一上行主题格式**: `yupi_mqtt_power_project/robot/status/{spotUid}`
    *   `{spotUid}`: 充电桩/车位的唯一标识符 (例如 `ESP32_SPOT_001`，即 `DEVICE_UID`)。
*   **消息格式 (Payload)**: JSON。
    消息体结构应与后端 `com.yupi.project.model.dto.mqtt.RobotStatusMessage` 类对应。通过消息体内的字段来区分具体的消息含义。

    **示例 - 常规状态更新 (如移动中、充电中、空闲等) 或心跳包 (Heartbeat):**
    ```json
    {
      "robotUid": "ESP32_SPOT_001",
      "actualRobotStatus": "CHARGING", // 设备当前状态, 如 IDLE, MOVING, CHARGING, COMPLETED, FAULTED
      // 以下为可选的详细状态，根据实际需求和后端处理逻辑添加
      "voltage": 0, // 充电时相关 (示例值，实际由固件模拟或测量)
      "current": 0,   // 充电时相关 (示例值)
      "power": 0,    // kW, 充电时相关 (示例值)
      "energyConsumed": 0, // kWh, 本次充电已消耗电量 (示例值)
      "batteryLevel": 85, // 机器人当前电量百分比
      "currentLocation": "SPOT_ID_001", // 机器人当前所在或最后到达的位置ID
      "chargeDurationSeconds": 120, // 如果在充电状态，已充电时长（秒）
      "errorCode": 0,   // 0表示无错误，其他值表示特定错误类型
      "message": "Status normal", // 附加信息，例如错误描述
      "activeTaskId": "session_xyz123" // 如果设备当前正在执行某个充电会话，上报其会话ID。后端主要通过ACK中的task_id来驱动任务和会话状态变更，此字段更多用于状态同步或辅助信息。
    }
    ```

    **示例 - 对后端指令的ACK (成功, 以MOVE_TO_SPOT为例):**
    机器人成功到达目标车位并开始充电后，发送此ACK。
    **注意：后端期望的ACK格式包含 `command_ack` (指令的中文描述), `task_id` (数字类型), 和 `success` (布尔类型)。**
    ```json
    {
      "robotUid": "ESP32_SPOT_001",
      "command_ack": "移动到指定点", 
      "task_id": 789,               
      "success": true,              
      "message": "Move to spot completed, charging started.",
      "actualRobotStatus": "CHARGING", 
      "spotId": "SPOT_UID_001" 
    }
    ```

    **示例 - 对后端指令的ACK (失败):**
    ```json
    {
      "robotUid": "ESP32_SPOT_001",
      "command_ack": "移动到指定点", 
      "task_id": 790,               
      "success": false,             
      "message": "Failed to reach spot, obstacle detected.",
      "actualRobotStatus": "FAULTED" 
    }
    ```

#### 4.2.2. 下行指令 (后端 -> 单片机)

后端服务向特定单片机下发指令时，使用以下主题格式。单片机应订阅其自身的这个专属指令主题。

*   **统一下行指令主题格式**: `yupi_mqtt_power_project/robot/command/{spotUid}`
    *   `{spotUid}`: 目标充电桩的唯一标识符。
*   **消息格式 (Payload)**: JSON。
    JSON消息体内部包含了具体的指令类型和所需参数。后端 `MqttServiceImpl.sendCommand` 方法会自动向 payload 中注入 `taskId`。

    **示例 - `MOVE_TO_SPOT` 指令 (后端实际发送格式):**
    ```json
    {
      "command": "MOVE", 
      "targetSpotUid": "SPOT_UID_001",
      "sessionId": "session_xyz123", // 充电会话ID，由后端业务逻辑注入
      "taskId": 123 // 机器人任务ID，由MqttServiceImpl自动注入
    }
    ```
    *说明：单片机固件已兼容处理 `command` 字段为 `MOVE` 的指令，并将其映射为 `MOVE_TO_SPOT` 行为。*

    **示例 - `STOP_CHARGE` 指令:**
    ```json
    {
      "command": "STOP_CHARGE",
      "sessionId": "session_xyz123", // 充电会话ID
      "taskId": 456 // 机器人任务ID，由MqttServiceImpl自动注入
    }
    ```

    **示例 - `START_CHARGE` 指令 (目前后端不再主动发送，仅作兼容性说明):**
    ```json
    {
      "command": "START_CHARGE",
      "sessionId": "session_xyz123", // 充电会话ID
      "taskId": 789 // 机器人任务ID，由MqttServiceImpl自动注入
    }
    ```

### 4.3. 单片机主要指令处理逻辑

#### 4.3.1. `MOVE_TO_SPOT` 指令处理

*   **指令接收**: 单片机收到 `command` 为 `MOVE` 的指令。
*   **执行流程**: 
    1.  解析 `targetSpotUid`，开始向目标车位移动。
    2.  将自身 `actualRobotStatus` 更新为 `MOVING`，并周期性上报状态。
    3.  成功到达目标车位后，根据“到达即充电”的业务逻辑，机器人**立即**将自身 `actualRobotStatus` 更新为 `CHARGING`。
    4.  向后端发送 `MOVE_TO_SPOT` 任务的 ACK 消息，其中 `success` 为 `true`，`actualRobotStatus` 为 `CHARGING`。
*   **后端响应**: 后端收到 ACK 后，将 `RobotTask` 标记为 `COMPLETED`，并调用 `ChargingSessionServiceImpl.handleRobotArrival()` 将充电会话状态更新为 `CHARGING_STARTED`，记录充电开始时间。

#### 4.3.2. `STOP_CHARGE` 指令处理

*   **指令接收**: 单片机收到 `command` 为 `STOP_CHARGE` 的指令。
*   **执行流程**: 
    1.  停止充电。
    2.  将自身 `actualRobotStatus` 更新为 `IDLE` 或 `AVAILABLE`。
    3.  向后端发送 `STOP_CHARGE` 任务的 ACK 消息，其中 `success` 为 `true`。
*   **后端响应**: 后端收到 ACK 后，将 `RobotTask` 标记为 `COMPLETED`，并调用 `ChargingSessionServiceImpl.handleChargingEnd()` 处理充电结束、费用计算和会话终结。

#### 4.3.3. `START_CHARGE` 指令处理

*   **指令接收**: 单片机收到 `command` 为 `START_CHARGE` 的指令。
*   **执行流程**: 
    1.  开始充电。
    2.  将自身 `actualRobotStatus` 更新为 `CHARGING`。
    3.  向后端发送 `START_CHARGE` 任务的 ACK 消息，其中 `success` 为 `true`。
*   **后端响应**: 后端收到 ACK 后，将 `RobotTask` 标记为 `COMPLETED`，并调用 `ChargingSessionServiceImpl.handleChargingStart()` 处理充电开始。
*   **重要说明**: **在当前“到达即充电”的业务逻辑下，后端不再主动向机器人发送 `START_CHARGE` MQTT 指令。机器人完成 `MOVE_TO_SPOT` 任务并到达车位后，会立即自动开始充电并上报 `CHARGING` 状态。此指令的保留主要是为了兼容性或未来可能的业务扩展。**

### 4.3. 单片机开发关键逻辑 (基于 `esp32_robot_firmware`)
1.  **配置 (`config.h`)**: 仔细配置WiFi凭据、MQTT Broker、`DEVICE_UID`以及所有硬件引脚。
2.  **MQTT 初始化与重连机制** (由 `mqtt_handler.cpp` 处理)。
3.  **订阅指令主题** (由 `mqtt_handler.cpp` 处理)。
4.  **JSON 指令消息解析** (在 `esp32_robot_firmware.ino` 的 `handle_mqtt_command` 中处理, `mqtt_handler.cpp`的`callback`负责通用解析并调用回调)。
    *   需能正确解析后端下发的指令，如 `MOVE` (实际) 或 `MOVE_TO_SPOT` (枚举定义)。
5.  **移动控制** (由 `motor_control.cpp` 和 `navigation.cpp` 实现)。
6.  **充电启停控制** (由 `relay_control.cpp` 实现，停止充电时操作继电器)。
7.  **状态监测与实时上报** (主要在 `esp32_robot_firmware.ino` 的 `loop()` 函数和 `handle_mqtt_command` 中管理和触发):
    *   收到 `MOVE` (或 `MOVE_TO_SPOT`) 指令后，`current_robot_status` 变为 `MOVING`，`target_spot_for_move` 被设置，并立即上报状态。
    *   `loop()` 函数中的导航逻辑 (`navigation_move_to_spot()`) 执行移动。
    *   成功到达目标车位后 (`navigation_move_to_spot()` 返回成功):
        *   `current_robot_status` 变为 `CHARGING`。
        *   `robot_current_location` 更新为到达的车位ID。
        *   调用 `relay_start_charging()`。
        *   向上行主题发送 `MOVE` 指令的成功ACK。**此ACK需符合后端期望格式**: 包含 `robotUid`, `command_ack` (值为"移动到指定点"), `task_id` (来自原指令, 类型为数字), `success` (true), `message`, `actualRobotStatus` ("CHARGING"), 和 `spotId`。
        *   立即上报一次完整的 `CHARGING` 状态。
    *   导航失败后 (`navigation_move_to_spot()` 返回失败):
        *   `current_robot_status` 变为 `FAULTED`。
        *   向上行主题发送 `MOVE` 指令的失败ACK。**此ACK需符合后端期望格式**: 包含 `command_ack` (值为"移动到指定点"), `task_id`, `success` (false), `message` (错误信息), `actualRobotStatus` ("FAULTED")。
        *   立即上报一次完整的 `FAULTED` 状态。
    *   收到 `STOP_CHARGE` 后:
        *   调用 `relay_stop_charging()`。
        *   `current_robot_status` 变为 `COMPLETED` (或 `IDLE`，根据固件逻辑)。
        *   向上行主题发送 `STOP_CHARGE` 的成功ACK。**此ACK需符合后端期望格式**: 包含 `command_ack` (值为"停止充电"), `task_id`, `success` (true), `message`, `actualRobotStatus` ("COMPLETED")。
        *   立即上报一次完整的状态。
8.  **心跳包定时发送** (由 `esp32_robot_firmware.ino` 的 `loop()` 函数处理)。
9.  **故障检测与上报** (部分在导航逻辑中处理，状态上报时包含错误码)。
10. **安全性**: 确保 `DEVICE_UID` 唯一性；推荐 MQTT 通信使用 TLS/SSL 加密 (当前固件未直接集成，需额外配置)。
11. **电池电量模拟与上报** (由 `esp32_robot_firmware.ino` 的 `simulate_battery_update()` 和状态上报逻辑处理)。

### 4.4. 示例流程：用户启动充电 (已更新为到达即充逻辑)
1.  用户在前端 App 请求在车位 `SPOT001` 进行充电。
2.  后端服务验证，创建充电会话 `sessionId=789`，状态为 `REQUESTED`。
3.  后端分配机器人 (例如 `DEVICE_UID=ESP32_BOT_001`)，创建 `MOVE_TO_SPOT` 任务 (例如 `taskId=task_move_123`)，并将机器人DB状态更新为 `ASSIGNED` (或 `MOVING`，取决于后端状态流转设计)，会话状态更新为 `ROBOT_ASSIGNED`。
4.  后端向 MQTT 主题 `yupi_mqtt_power_project/robot/command/ESP32_BOT_001` 发布 `MOVE_TO_SPOT` 指令，payload 包含 `taskId=task_move_123` 和 `target_spot_uid="SPOT001"`。
5.  单片机 `ESP32_BOT_001` 接收指令：
    a.  将其内部状态 `current_robot_status` 更新为 `MOVING`。
    b.  向上行主题 `yupi_mqtt_power_project/robot/status/ESP32_BOT_001` 上报状态 `MOVING`。
    c.  执行导航到 `SPOT001`。
    d.  移动到达后，将其内部状态 `current_robot_status` 更新为 `CHARGING`，`robot_current_location` 更新为 `SPOT001`，并启动充电 (闭合继电器)。
    e.  向上行主题发送 ACK 消息，确认 `taskId=task_move_123` 完成，消息中 `actualRobotStatus` 为 `CHARGING`，并包含 `spotId="SPOT001"`。
    f.  (通常ACK消息已包含最新状态，但为确保) 再次向上行主题上报完整的 `CHARGING` 状态，包含 `sessionId` 和 `chargeDurationSeconds` (初始为0)。
6.  后端接收到 `MOVE_TO_SPOT` 任务 ( `taskId=task_move_123`) 的 ACK (其中机器人状态为 `CHARGING`):
    a.  将 `RobotTask` ( `taskId=task_move_123`) 标记为 `COMPLETED`。
    b.  将机器人 `ESP32_BOT_001` 在数据库中的状态更新为 `CHARGING`。
    c.  将充电会话 `sessionId=789` 的状态更新为 `CHARGING_STARTED`，并记录充电开始时间。
7.  前端界面同步更新，显示充电已开始。

## 5. 项目结构 (简要)

```
. 项目根目录
├── charging_web_app/         # 前端 Next.js 应用
│   ├── src/
│   ├── public/
│   └── package.json
├── esp32_robot_firmware/     # ESP32 机器人固件项目 (推荐使用)
│   ├── esp32_robot_firmware.ino  # 主程序文件
│   ├── config.h                # 配置文件 (WiFi, MQTT, 引脚等)
│   ├── mqtt_handler.h/.cpp     # MQTT 通信处理
│   ├── motor_control.h/.cpp    # 电机与舵机控制
│   ├── sensor_handler.h/.cpp   # 传感器数据读取
│   ├── navigation.h/.cpp       # 导航与循迹逻辑
│   ├── relay_control.h/.cpp    # 继电器控制 (模拟充电)
│   └── NEXT_STEPS.md           # 后续开发与测试步骤指南
├── springboot-init-main/     # 后端 Spring Boot 应用
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/yupi/project/
│   │   │   └── resources/
│   │   └── test/
│   ├── doc/                      # 项目文档 (如数据库DDL, 阶段计划等)
│   └── pom.xml
├── mqtt_esp32_client.ino     # 旧的、简易的ESP32 MQTT模拟器 (不推荐新开发使用)
└── README.md                 # 本文件
```

## 6. ESP32 固件 (`esp32_robot_firmware`) 使用入门

1.  **环境准备**:
    *   安装 Arduino IDE 或 PlatformIO IDE。
    *   在IDE中安装 ESP32 开发板支持。
    *   根据需要安装相关库 (如 `PubSubClient` for MQTT, `ESP32Servo` for Servo control等，大部分已包含在ESP32核心库中或作为标准库提供)。
2.  **打开项目**:
    *   使用IDE打开 `esp32_robot_firmware/esp32_robot_firmware.ino`。IDE通常会自动加载文件夹内所有 `.h` 和 `.cpp` 文件。
3.  **配置 `config.h`**:
    *   **非常重要**: 打开 `config.h` 文件。
    *   填写您的 `WIFI_SSID` 和 `WIFI_PASSWORD`。
    *   配置 `MQTT_BROKER_HOST`, `MQTT_BROKER_PORT`, 以及可选的 `MQTT_USERNAME`, `MQTT_PASSWORD`。
    *   **设置 `DEVICE_UID`**: 此ID必须与您在后端系统中为该机器人注册的ID完全一致。
    *   **核对并修改所有硬件引脚定义** (`MOTOR_*_PIN`, `SERVO_PIN`, `ULTRASONIC_*_PIN`, `TCRT5000_*_PIN`, `RELAY_PIN`) 以匹配您的实际硬件连接。
    *   调整其他业务参数如 `SIDE_ULTRASONIC_SPOT_THRESHOLD_CM`, `NUM_CHARGING_SPOTS` 等。
4.  **编译与烧录**:
    *   选择正确的ESP32开发板型号和端口。
    *   编译并上传固件到您的ESP32设备。
5.  **监控与调试**:
    *   打开串口监视器 (波特率在 `config.h` 中 `SERIAL_BAUD_RATE` 定义，默认为 115200)。
    *   观察串口输出，检查WiFi连接、MQTT连接状态、传感器读数、指令接收和状态上报等。
    *   参考 `esp32_robot_firmware/NEXT_STEPS.md` 文件进行详细的模块化测试和导航逻辑调试。

**重要提示**: `navigation.cpp` 中的循迹逻辑 (`navigation_follow_line_once`) 是一个非常基础的实现，**您极有可能需要根据您的具体机器人平台、传感器和环境对其进行大幅修改和参数调优，甚至采用更高级的控制算法（如PID）才能获得满意的循迹效果。** `NEXT_STEPS.md` 提供了更详细的指导。

## 7. 后续工作与展望
*   完善各模块的单元测试和集成测试。
*   进一步优化 API 文档和错误处理机制。
*   根据实际运营需求，增加更细致的统计报表功能。
*   MQTT通信日志模块：
    *   (可选) 进一步优化Payload的展示，例如对JSON格式的Payload进行美化或提供折叠/展开功能。
    *   (可选) 根据实际需求，考虑日志数据的定期归档或清理策略的实现。
*   考虑引入更高级的 MQTT 特性，如QoS、遗嘱消息等。
*   前端 UI/UX 持续打磨，提升用户体验。