用户
This commit is contained in:
233
springboot-init-main/doc/development_plan.md
Normal file
233
springboot-init-main/doc/development_plan.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# MQTT 智能充电机器人系统 - 开发方案
|
||||
|
||||
## 1. 引言
|
||||
|
||||
本文档旨在为 "MQTT 智能充电机器人系统" 提供详细的开发方案。该方案基于已确认的需求文档 (`requirements.md`) 和技术选型,旨在指导后端平台的开发工作。项目目标是构建一个稳定、可扩展的后端服务,用于管理用户、调度充电机器人、处理充电业务逻辑,并通过 MQTT 与机器人硬件进行通信。
|
||||
|
||||
## 2. 系统架构概览
|
||||
|
||||
系统采用典型的 **前后端分离 + 物联网 (IoT)** 架构:
|
||||
|
||||
* **后端平台 (本项目核心)**: 基于 Spring Boot,负责业务逻辑处理、数据持久化、用户认证授权、与 MQTT Broker 交互。
|
||||
* **MQTT Broker**: 外部公共服务(如 EMQX),作为消息中间件,负责平台与机器人之间的消息传递。**需要用户提供具体的连接信息和认证凭据。**
|
||||
* **充电机器人 (硬件)**: 基于 ESP32-CAM,负责执行指令、采集状态、通过 MQTT 与 Broker 通信。**本项目不涉及硬件的具体开发。**
|
||||
* **前端应用 (可选)**: Web 或 App,通过调用后端 API 与系统交互。**本项目不包含前端开发。**
|
||||
|
||||
**关键技术栈:**
|
||||
|
||||
* **后端**: Spring Boot 2.7.0, Java 1.8
|
||||
* **数据访问**: MyBatis-Plus, MySQL
|
||||
* **缓存**: Redis (用于 Session 管理等)
|
||||
* **消息队列**: MQTT (使用 Eclipse Paho Java Client)
|
||||
* **API 文档**: Knife4j
|
||||
|
||||
## 3. 开发阶段与模块划分
|
||||
|
||||
我们将开发过程划分为以下几个主要阶段/模块:
|
||||
|
||||
**阶段一:基础架构与用户管理**
|
||||
|
||||
1. **环境配置**: 确认 JDK, Maven, MySQL, Redis 环境。
|
||||
2. **数据库初始化**:
|
||||
* 根据 `requirements.md` 中的设计,创建数据库 `mqtt_charging_system` (或使用现有 `my_db` 并调整)。
|
||||
* 编写并执行 SQL DDL 脚本,创建 `user`, `charging_robot`, `parking_spot`, `charging_session`, `activation_code` 表(详细见第 4 节)。
|
||||
3. **用户模块实现**:
|
||||
* 创建 `User` 实体类 (`model/entity/User.java`),包含余额、角色等字段。
|
||||
* 创建 `UserMapper` 接口 (`mapper/UserMapper.java`)。
|
||||
* 创建 `UserService` 接口及实现 (`service/UserService.java`, `service/impl/UserServiceImpl.java`),包含用户注册、查询、余额更新等方法。
|
||||
* 实现密码存储:使用 Spring Security 的 `BCryptPasswordEncoder` 对密码进行加密。在 `config` 包下配置 `PasswordEncoder` Bean。
|
||||
* 创建 `UserController` (`controller/UserController.java`),提供登录接口。
|
||||
4. **认证与授权**:
|
||||
* 实现登录接口 `/api/user/login`。
|
||||
* 登录成功后,使用 Spring Session (已配置为 Redis 存储) 存储用户会话信息。
|
||||
* 后续请求通过 Session 进行用户身份验证。
|
||||
* 实现简单的基于角色的访问控制:在需要权限的 Controller 方法或 Service 方法上,通过检查当前登录用户的 `role` 字段进行判断。可以创建一个简单的 AOP 切面或直接在方法内判断。
|
||||
|
||||
**阶段二:MQTT 集成**
|
||||
|
||||
1. **配置 MQTT 连接**:
|
||||
* 在 `application.yml` 中添加 MQTT Broker 的 `url`, `username`, `password`, 以及用于命令和状态的 `commandTopic`, `statusTopic` 基础路径。**这些值需要用户提供。**
|
||||
* 创建 `MqttProperties.java` (`config/properties/MqttProperties.java`) 类,使用 `@ConfigurationProperties` 读取配置。
|
||||
2. **实现 MQTT 客户端**:
|
||||
* 创建 `MqttConfig.java` (`config/MqttConfig.java`)。
|
||||
* 配置 `MqttConnectOptions` Bean,设置用户名、密码、自动重连、清理会话等。
|
||||
* 配置 `MqttClient` Bean,并在应用启动后连接到 Broker。
|
||||
* **重点**: 实现 `MqttCallbackExtended` 接口,处理连接成功 (`connectComplete`)、连接丢失 (`connectionLost`)、消息到达 (`messageArrived`) 事件。
|
||||
* 在 `connectComplete` 中,订阅状态 Topic (`robot/status/+`)。`+` 是通配符,用于接收所有机器人的状态。
|
||||
* 在 `connectionLost` 中,记录日志并依赖 Paho 客户端的自动重连机制。
|
||||
* 在 `messageArrived` 中,将接收到的消息(JSON 字符串)传递给专门的消息处理服务。
|
||||
3. **实现消息发布与处理**:
|
||||
* 创建 `RobotTaskService.java` (`service/RobotTaskService.java`) 用于管理 `robot_task` 表的 CRUD 和状态检查。
|
||||
* 创建 `MqttService.java` (`service/MqttService.java`)。
|
||||
* 提供 `sendCommand(String robotId, String commandType, String payloadJson, Long sessionId)` 方法:
|
||||
* **调用 `RobotTaskService` 检查 `robotId` 是否有状态为 `SENT` 的任务。如果有,则不允许发送,直接返回错误或特定状态。**
|
||||
* 如果允许发送,**调用 `RobotTaskService` 在 `robot_task` 表创建 `PENDING` 状态的任务记录。**
|
||||
* 构造实际的 MQTT Topic (`robot/command/{robotId}`)
|
||||
* 调用 Paho 客户端的 `publish` 方法发送 MQTT 消息。
|
||||
* **发送成功后,立即调用 `RobotTaskService` 将对应任务状态更新为 `SENT` 并记录 `sent_time`。**
|
||||
* 创建 `MqttMessageHandler.java` (`service/MqttMessageHandler.java` 或类似名称)。
|
||||
* 提供 `handleStatusUpdate(String topic, String payload)` 方法,由 `MqttCallback` 的 `messageArrived` 调用。
|
||||
* 在此方法中:
|
||||
* 解析 `topic` 获取 `robotId`。
|
||||
* 使用 Gson (或 Jackson) 将 `payload` (JSON) 解析为 `RobotStatusDTO`。
|
||||
* **根据收到的状态,调用 `RobotTaskService` 查找并更新对应的 `SENT` 任务状态为 `ACKNOWLEDGED_SUCCESS` 或 `ACKNOWLEDGED_FAILURE`,记录 `ack_time`。**
|
||||
* **在任务状态更新成功后**,再根据 DTO 中的 `status` 字段,调用相应的业务逻辑(如 `ChargingService` 更新机器人状态、处理充电完成事件等)。
|
||||
|
||||
**阶段三:核心充电业务逻辑**
|
||||
|
||||
1. **机器人与车位管理**:
|
||||
* 创建 `ChargingRobot` 和 `ParkingSpot` 实体、Mapper、Service、Controller。
|
||||
* 提供基础的 CRUD 接口供管理员使用(可选,根据 `requirements.md`)。
|
||||
* `ChargingRobotService` 需要包含更新机器人状态(如 `idle`, `moving`, `charging`, `error`)和位置的方法。
|
||||
2. **充电会话管理**:
|
||||
* 创建 `ChargingSession` 实体、Mapper、Service。
|
||||
3. **充电流程实现**:
|
||||
* 创建 `ChargingController.java` (`controller/ChargingController.java`)。
|
||||
* 实现 `/api/charging/request` 接口:
|
||||
* 接收用户请求(包含 `spotId`)。
|
||||
* 验证用户登录状态和余额。
|
||||
* 调用 `ChargingRobotService` 查找可用机器人。
|
||||
* **调用 `MqttService.sendCommand` 发送 `move_to_spot` 指令 (该方法内部会检查任务表并创建任务记录)。如果返回错误(机器人忙),则告知用户。**
|
||||
* (可选)创建或更新 `ChargingSession` 记录为 `pending` 或 `robot_moving` 状态。
|
||||
* 在 `MqttMessageHandler` 中处理状态更新:
|
||||
* **首先更新 `robot_task` 表状态。**
|
||||
* `arrived_at_spot` (确认 `move_to_spot` 成功后): 更新机器人DB状态,**调用 `MqttService.sendCommand` 发送 `start_charge` 指令。**
|
||||
* `charging` (确认 `start_charge` 可能隐含的响应,或只是状态更新): 记录开始时间,更新 `ChargingSession`。
|
||||
* `charge_complete` (确认充电结束,可能是主动上报或响应 `stop_charge`): 记录结束时间、时长,**触发计费**,更新用户余额,更新 `ChargingSession`。
|
||||
* `error`: 记录错误,更新机器人和 `ChargingSession` 状态。
|
||||
* 实现 `/api/charging/stop` 接口(用户手动停止):
|
||||
* **调用 `MqttService.sendCommand` 发送 `stop_charge` 指令。**
|
||||
* 后续处理逻辑依赖于 `charge_complete` 或 `error` 消息的处理。
|
||||
4. **计费逻辑**:
|
||||
* 在 `UserService` 或单独的 `BillingService` 中实现计费方法。
|
||||
* 根据 `ChargingSession` 的总时长和预设的单价计算费用。
|
||||
* 调用 `UserService` 更新用户余额 (注意并发安全,可使用数据库行锁或乐观锁)。
|
||||
|
||||
**阶段四:激活码与完善**
|
||||
|
||||
1. **激活码模块**:
|
||||
* 创建 `ActivationCode` 实体、Mapper、Service。
|
||||
* 实现激活码生成逻辑(管理员功能,可在 Service 中提供)。
|
||||
* 创建 `ActivationCodeController.java` (`controller/ActivationCodeController.java`)。
|
||||
* 实现 `/api/codes/redeem` 接口:接收激活码,验证有效性,调用 `UserService` 更新用户余额,将激活码标记为已使用。
|
||||
2. **API 文档**:
|
||||
* 在所有 Controller 和 DTO 上添加 Knife4j (Swagger) 注解 (`@Api`, `@ApiOperation`, `@ApiModelProperty` 等)。
|
||||
3. **测试**:
|
||||
* 编写单元测试 (JUnit) 覆盖 Service 层核心逻辑。
|
||||
* 进行接口测试(使用 Postman 或类似工具)。
|
||||
* **难点**: MQTT 交互的测试,可能需要 Mock `MqttClient` 或搭建本地 MQTT Broker 进行集成测试。
|
||||
4. **错误处理与日志**:
|
||||
* 完善全局异常处理 (`GlobalExceptionHandler.java`)。
|
||||
* 在关键业务点添加详细日志(使用 SLF4j)。
|
||||
* **新增:实现任务超时处理逻辑。**
|
||||
* 创建 `TaskTimeoutHandler.java` (`service/TaskTimeoutHandler.java` 或类似名称)。
|
||||
* 使用 `@Scheduled` 注解创建一个定时任务,定期执行。
|
||||
* 在定时任务中,调用 `RobotTaskService` 查找状态为 `SENT` 且超时的任务。
|
||||
* 对于超时的任务,更新其状态为 `TIMED_OUT`,记录错误信息。
|
||||
* 根据业务需要,可能需要同步更新关联的 `charging_robot` 和 `charging_session` 的状态。
|
||||
|
||||
## 4. 数据库 Schema (初步 DDL)
|
||||
|
||||
```sql
|
||||
-- 用户表
|
||||
CREATE TABLE `user` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`username` VARCHAR(255) NOT NULL UNIQUE COMMENT '用户名',
|
||||
`password` VARCHAR(255) NOT NULL COMMENT '密码 (加密存储)',
|
||||
`role` VARCHAR(50) NOT NULL DEFAULT 'user' COMMENT '角色 (user/admin)',
|
||||
`balance` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 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 TABLE `charging_robot` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`robot_id` VARCHAR(100) NOT NULL UNIQUE COMMENT '机器人唯一标识符 (对应MQTT clientId)',
|
||||
`status` VARCHAR(50) DEFAULT 'idle' COMMENT '状态 (idle, moving, charging, error, offline)',
|
||||
`location` VARCHAR(255) COMMENT '当前位置描述 (如 base, near_P001, P001)',
|
||||
`battery_level` INT 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 '逻辑删除标志'
|
||||
) COMMENT='充电机器人表';
|
||||
|
||||
-- 车位表
|
||||
CREATE TABLE `parking_spot` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`spot_id` VARCHAR(100) NOT NULL UNIQUE COMMENT '车位唯一标识符',
|
||||
`location_desc` VARCHAR(255) COMMENT '位置描述',
|
||||
`status` VARCHAR(50) DEFAULT 'available' COMMENT '状态 (available, occupied, maintenance)',
|
||||
`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 '逻辑删除标志'
|
||||
) COMMENT='车位表';
|
||||
|
||||
-- 充电记录表
|
||||
CREATE TABLE `charging_session` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`robot_id` VARCHAR(100) NOT NULL COMMENT '机器人ID',
|
||||
`spot_id` VARCHAR(100) NOT NULL COMMENT '车位ID',
|
||||
`start_time` DATETIME COMMENT '充电开始时间',
|
||||
`end_time` DATETIME COMMENT '充电结束时间',
|
||||
`duration_seconds` INT COMMENT '总充电时长 (秒)',
|
||||
`cost` DECIMAL(10, 2) COMMENT '本次消费金额',
|
||||
`status` VARCHAR(50) COMMENT '会话状态 (pending, robot_moving, charging, completed, error, cancelled)',
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间'
|
||||
) COMMENT='充电记录表';
|
||||
|
||||
-- 激活码表
|
||||
CREATE TABLE `activation_code` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`code` VARCHAR(100) NOT NULL UNIQUE COMMENT '激活码字符串',
|
||||
`value` DECIMAL(10, 2) NOT NULL COMMENT '充值金额',
|
||||
`is_used` TINYINT(1) DEFAULT 0 COMMENT '是否已使用 (0:未使用, 1:已使用)',
|
||||
`user_id` BIGINT COMMENT '使用者ID (使用后)',
|
||||
`use_time` DATETIME COMMENT '使用时间',
|
||||
`expire_time` DATETIME COMMENT '过期时间 (可选)',
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) COMMENT='激活码表';
|
||||
|
||||
```
|
||||
*注:以上 DDL 仅为初步设计,字段类型、长度、索引等可能需要根据实际情况调整。*
|
||||
|
||||
## 5. API 设计 (高层规划)
|
||||
|
||||
* **认证**:
|
||||
* `POST /api/user/login`
|
||||
* `POST /api/user/logout` (可选, 使 Session 失效)
|
||||
* **用户**:
|
||||
* `GET /api/user/current` (获取当前用户信息,包括余额)
|
||||
* `GET /api/users` (管理员 - 获取用户列表)
|
||||
* **充电**:
|
||||
* `POST /api/charging/request` (用户 - 发起充电请求,参数: `spotId`)
|
||||
* `POST /api/charging/stop` (用户 - 请求停止当前充电)
|
||||
* `GET /api/charging/history` (用户 - 查看自己的充电记录)
|
||||
* `GET /api/charging/sessions` (管理员 - 查看所有充电记录)
|
||||
* **激活码**:
|
||||
* `POST /api/codes/redeem` (用户 - 使用激活码充值,参数: `code`)
|
||||
* `POST /api/codes` (管理员 - 生成激活码,可选)
|
||||
* **管理 (可选)**:
|
||||
* `GET /api/robots`
|
||||
* `POST /api/robots`
|
||||
* `GET /api/spots`
|
||||
* `POST /api/spots`
|
||||
|
||||
## 6. MQTT 消息契约
|
||||
|
||||
再次强调,`requirements.md` 中定义的 MQTT Topic 和 Payload 结构为 **关键接口**。必须与硬件开发团队 **最终确认并严格遵守** 此约定。任何变动都需要双方同步更新。
|
||||
|
||||
## 7. 后续步骤
|
||||
|
||||
1. **等待用户提供**:
|
||||
* MQTT Broker 的详细连接信息 (URL, Port)。
|
||||
* 用于指令 (`robot/command/{clientId}`) 和状态 (`robot/status/{clientId}`) Topic 的用户名和密码。
|
||||
* 与硬件团队确认最终的 MQTT Topic 和 Payload 结构。
|
||||
2. **开始开发**: 在获取 MQTT 信息后,可以并行开始:
|
||||
* **阶段一**: 数据库初始化、用户模块开发、**`robot_task` 相关基础 Service 开发**。
|
||||
* **阶段二**: MQTT 配置、基础连接、订阅实现、**集成 `robot_task` 检查与更新逻辑**、**任务超时处理实现**。
|
||||
3. 按照开发阶段逐步推进。
|
||||
@@ -0,0 +1,152 @@
|
||||
# 开发阶段一:基础架构与用户管理
|
||||
|
||||
## 1. 目标
|
||||
|
||||
* 搭建项目基础运行环境。
|
||||
* 初始化数据库及核心用户表。
|
||||
* 实现用户实体、数据访问、业务逻辑和基础 API。
|
||||
* 实现安全的密码存储和用户登录认证机制 (基于 Session)。
|
||||
* 构建移动端 App 的基础框架和登录/注册页面。
|
||||
* 实现基于角色的基础页面导航。
|
||||
|
||||
## 2. 后端开发详解
|
||||
|
||||
1. **环境配置确认**:
|
||||
* 确保本地开发环境已安装并配置好 JDK 1.8, Maven, MySQL (5.7+), Redis。
|
||||
* 确认 Maven `settings.xml` 配置正确(如果使用私服)。
|
||||
* 在 IDE (如 IntelliJ IDEA) 中正确导入项目,确保依赖下载无误。
|
||||
2. **数据库初始化**:
|
||||
* 连接 MySQL 数据库。
|
||||
* 创建数据库,例如 `mqtt_charging_system` (字符集推荐 `utf8mb4`)。
|
||||
* **创建 `user` 表**: 执行 `development_plan.md` 中定义的 `user` 表 DDL 语句。
|
||||
```sql
|
||||
CREATE TABLE `user` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`username` VARCHAR(255) NOT NULL UNIQUE COMMENT '用户名',
|
||||
`password` VARCHAR(255) NOT NULL COMMENT '密码 (加密存储)',
|
||||
`role` VARCHAR(50) NOT NULL DEFAULT 'user' COMMENT '角色 (user/admin)',
|
||||
`balance` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 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='用户表';
|
||||
```
|
||||
* **配置数据源**: 检查并确认 `application.yml` 中的 `spring.datasource` 配置正确连接到创建的数据库。
|
||||
3. **用户模块实现**:
|
||||
* **Entity**: 创建 `src/main/java/com/yupi/project/model/entity/User.java`。使用 Lombok (`@Data`) 简化代码,使用 MyBatis-Plus 注解 (`@TableName`, `@TableId`, `@TableField`) 映射数据库字段,特别是驼峰与下划线。
|
||||
* **Mapper**: 创建 `src/main/java/com/yupi/project/mapper/UserMapper.java` 接口,继承 `BaseMapper<User>`。确保 `MyApplication.java` 中的 `@MapperScan` 包含此路径。
|
||||
* **Service**:
|
||||
* 创建 `src/main/java/com/yupi/project/service/UserService.java` 接口,继承 `IService<User>`,并定义业务方法,如 `userLogin(String username, String password)`, `userRegister(String username, String password, String checkPassword)`, `getCurrentUser(HttpServletRequest request)` 等。
|
||||
* 创建 `src/main/java/com/yupi/project/service/impl/UserServiceImpl.java` 实现类,注入 `UserMapper`。
|
||||
* 在注册逻辑中,添加参数校验(用户名、密码长度,两次密码是否一致),检查用户名是否已存在。
|
||||
* 在登录逻辑中,查询用户,校验密码。
|
||||
* **密码加密**:
|
||||
* 创建 `src/main/java/com/yupi/project/config/SecurityConfig.java` (或类似名称)。
|
||||
* 在该配置类中声明一个 `PasswordEncoder` Bean:
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
```
|
||||
* 在 `UserServiceImpl` 中注入 `PasswordEncoder`,在注册时对密码进行加密 (`passwordEncoder.encode(rawPassword)`),在登录时进行比对 (`passwordEncoder.matches(rawPassword, encodedPassword)`)。
|
||||
* **Controller**:
|
||||
* 创建 `src/main/java/com/yupi/project/controller/UserController.java`。
|
||||
* 注入 `UserService`。
|
||||
* 实现 `POST /api/user/login` 接口,调用 `UserService.userLogin`,登录成功后将用户信息存入 Session (Spring Session 会自动处理)。定义清晰的请求体 DTO (`UserLoginRequest`) 和响应体 (`BaseResponse<User>`)。
|
||||
* 实现 `POST /api/user/register` 接口 (可选,或仅限管理员创建),调用 `UserService.userRegister`。定义请求体 DTO (`UserRegisterRequest`)。
|
||||
* 实现 `GET /api/user/current` 接口,用于前端获取当前登录用户的信息(从 Session 中获取)。
|
||||
* 实现 `POST /api/user/logout` 接口,用于用户登出(使 Session 失效)。
|
||||
* **常量与异常**: 在 `constant` 包定义用户会话 Key。在 `exception` 包定义业务异常(如 `BusinessException`),并在 Service 层适当抛出(如用户名已存在、密码错误),由 `GlobalExceptionHandler` 统一处理。
|
||||
4. **认证与授权 (Session)**:
|
||||
* 确认 `pom.xml` 中包含 `spring-session-data-redis` 和 `spring-boot-starter-data-redis` 依赖。
|
||||
* 确认 `application.yml` 中 Redis 配置正确。
|
||||
* Spring Boot 会自动配置 Spring Session 使用 Redis。登录成功后,将用户信息(脱敏后,如移除密码)存入 `HttpSession`。
|
||||
* `session.setAttribute(UserConstant.USER_LOGIN_STATE, safeUser);`
|
||||
* 在需要登录才能访问的接口方法中,从 `HttpServletRequest` 获取 `HttpSession`,再获取用户信息。可以封装一个获取当前用户的方法在 `UserService` 中。
|
||||
* **基础角色检查**: 在需要区分管理员和普通用户的接口方法内部(或 Service 方法内部),获取当前登录用户的 `role` 字段进行判断。
|
||||
```java
|
||||
User currentUser = userService.getCurrentUser(request);
|
||||
if (!"admin".equals(currentUser.getRole())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
// ... admin logic ...
|
||||
```
|
||||
(更高级的权限控制可以使用 Spring Security 或 AOP,本阶段可先用简单方式)。
|
||||
|
||||
## 3. 前端 (手机版网页) 开发详解
|
||||
|
||||
* **技术选型**: **Next.js (基于 React)**。使用 Tailwind CSS 进行样式设计,确保响应式布局,优先适配移动端浏览器。
|
||||
* **目标**: 实现 Web App 基础框架、登录页、注册页(如果需要)、用户主页(区分角色)。
|
||||
* **UI 风格**: (与之前一致,但实现方式变为HTML/CSS)
|
||||
* **主色调**: 科技蓝 (#0A7AFF 或类似) 与白色 (#FFFFFF)。
|
||||
* **辅助色**: 浅灰 (#F0F0F0)、深灰 (#808080)。
|
||||
* **字体**: 无衬线字体,如系统默认字体,或引入网络字体如 Inter, Noto Sans SC。
|
||||
* **图标**: 简洁、线条化的科技风格图标 (可使用 SVG图标库如 Heroicons, Feather Icons, 或 Iconfont CSS)。
|
||||
* **布局**: 简洁、留白充足,控件圆角化。使用Flexbox和Grid进行响应式布局。
|
||||
* **动效**: 轻微、平滑的CSS过渡和动画,增强科技感。
|
||||
* **整体**: 保持所有页面风格统一。
|
||||
|
||||
* **项目结构 (Next.js App Router)**:
|
||||
* `charging_web_app/src/app/`:页面路由和布局。
|
||||
* `layout.tsx`: 全局根布局 (包含 `AuthProvider`)。
|
||||
* `login/page.tsx`: 登录页面。
|
||||
* `register/page.tsx`: 注册页面。
|
||||
* `(authenticated)/dashboard/page.tsx`: 普通用户主页 (示例路径,使用路由组进行权限控制)。
|
||||
* `(authenticated)/admin/dashboard/page.tsx`: 管理员主页 (示例路径)。
|
||||
* `charging_web_app/src/components/`: 可复用UI组件。
|
||||
* `charging_web_app/src/contexts/`: React Context (如 `AuthContext.tsx`)。
|
||||
* `charging_web_app/src/services/`: API服务 (如 `api.ts`)。
|
||||
* `charging_web_app/src/hooks/`: 自定义React Hooks。
|
||||
* `charging_web_app/src/lib/` (或 `utils`): 工具函数。
|
||||
* `charging_web_app/public/`: 静态资源。
|
||||
* `tailwind.config.ts`, `postcss.config.js`: Tailwind CSS 配置。
|
||||
|
||||
* **页面设计与流程 (Web)**:
|
||||
1. **登录页 (`src/app/login/page.tsx`)**:
|
||||
* **UI**: 蓝色顶部Banner或背景元素,白色内容区域。App Logo (可选)、用户名输入框 (`<input type="text">`)、密码输入框 (`<input type="password">`)、"登录"按钮 (`<button>`)(蓝色填充)、"注册"链接 (`<Link href="/register">`)。输入框和按钮使用Tailwind CSS进行样式化,具有圆角和科技感边框。
|
||||
* **交互**: 输入校验(非空)。点击登录按钮,显示加载指示器(如旋转SVG图标或文字提示)。
|
||||
* **API 调用**: 调用后端 `POST /api/user/login` (通过 `AuthContext` 中的 `login` 方法)。
|
||||
* **响应处理**:
|
||||
* 成功:`AuthContext` 更新用户状态。页面导航由 `AuthContext` 或页面逻辑处理 (例如,使用 `useRouter().push('/dashboard')`)。会话主要由后端的HttpOnly Cookie管理。
|
||||
* 失败:隐藏加载指示器,显示错误提示(如页面内红色文本)。
|
||||
2. **注册页 (`src/app/register/page.tsx`)**: (如果开放注册)
|
||||
* **UI**: 与登录页风格一致。包含用户名、密码、确认密码输入框,"注册"按钮。
|
||||
* **交互**: 输入校验(非空、密码一致性、格式要求等)。点击注册按钮。
|
||||
* **API 调用**: 调用后端 `POST /api/user/register` (通过 `AuthContext` 中的 `register` 方法)。
|
||||
* **响应处理**: 成功后可提示注册成功并跳转到登录页;失败则提示错误。
|
||||
3. **用户主页 (根据角色不同,示例路径 `src/app/(authenticated)/dashboard/page.tsx` 或 `src/app/(authenticated)/admin/dashboard/page.tsx`)**:
|
||||
* **UI**:
|
||||
* **普通用户**: 响应式导航栏(移动端可以是汉堡菜单或底部Tab模拟)。页面内容可包含欢迎信息、账户余额等。整体蓝白风格,卡片式布局。
|
||||
* **管理员**: 响应式导航栏(侧边栏或顶部菜单)。列表和数据展示为主,使用表格或卡片。
|
||||
* **功能**: 根据角色展示不同功能入口。
|
||||
* **API 调用**: 进入时 `AuthContext` 会尝试通过 `checkAuth` 获取最新用户信息。
|
||||
4. **导航与权限控制**:
|
||||
* 使用Next.js App Router进行页面路由。
|
||||
* 在根布局 `src/app/layout.tsx` 中包裹 `AuthProvider`。
|
||||
* 可以使用路由组 `(authenticated)` 结合中间件 ( `middleware.ts` ) 或在布局/页面组件中通过 `useAuth()` 检查认证状态和用户角色,实现页面级权限控制和未登录重定向。
|
||||
|
||||
* **状态管理**: 主要使用 `AuthContext` 管理用户登录状态和用户信息。对于其他全局状态,可按需引入 Zustand 或 Valtio 等轻量级状态管理库。
|
||||
* **网络请求**: 已封装 `src/services/api.ts` (Axios实例),处理 API Base URL、请求头(如 `withCredentials` 以支持Session Cookie)、加载状态、错误处理。
|
||||
* **Next.js代理**: 为了避免CORS问题和隐藏后端API实际地址,可以在 `next.config.mjs` 中配置rewrites代理,将前端的 `/api/*` 请求转发到后端Spring Boot服务的地址 (例如 `http://localhost:7529/api/*`)。
|
||||
|
||||
## 4. 本阶段交付物
|
||||
|
||||
* 可运行的后端 Spring Boot 项目。
|
||||
* 包含 `user` 表的数据库。
|
||||
* 实现用户注册(可选)、登录、获取当前用户信息、登出功能的后端 API。
|
||||
* 密码加密存储。
|
||||
* 基于 Redis 的 Session 认证机制。
|
||||
* **基础的Next.js手机版网页前端框架 (`charging_web_app`)。**
|
||||
* **实现登录、注册(可选)页面及功能的手机版网页。**
|
||||
* **根据用户角色导航到不同主页框架的手机版网页 (基础实现)。**
|
||||
* 简要的接口测试报告(如 Postman 截图,或浏览器开发者工具网络请求截图)。
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
* 密码绝不能明文存储或传输。
|
||||
* API 响应中避免返回用户密码等敏感信息。
|
||||
* 做好基础的参数校验。
|
||||
* 前后端接口地址、请求/响应格式需约定一致。
|
||||
* **确保Next.js项目正确配置了对后端API的代理,或在生产环境中通过Nginx等进行反向代理。**
|
||||
* **Tailwind CSS的正确配置和使用,以实现响应式和移动优先的UI。**
|
||||
@@ -0,0 +1,156 @@
|
||||
# 开发阶段二:MQTT 集成
|
||||
|
||||
## 1. 目标
|
||||
|
||||
* 在后端配置并建立与 MQTT Broker 的稳定连接。
|
||||
* 实现 MQTT 消息的订阅 (接收机器人状态) 和发布 (发送控制指令)。
|
||||
* 集成 `robot_task` 表逻辑,确保命令按顺序发送并跟踪状态。
|
||||
* 实现任务超时处理机制。
|
||||
* (前端暂无重大变更,主要为后端集成工作)。
|
||||
|
||||
## 2. 先决条件
|
||||
|
||||
* **MQTT Broker 信息**:
|
||||
* **地址**: 本项目将使用公共 MQTT Broker: `broker.emqx.io`
|
||||
* **TCP 端口**: `1883`
|
||||
* **连接认证**: 此公共 Broker 通常允许匿名连接,即连接时不需要提供用户名和密码。
|
||||
* **Topic 与 Payload 约定**: 必须与硬件团队最终确认 MQTT Topic 结构和 JSON Payload 格式。
|
||||
* **Topic 唯一性**: 由于使用公共 Broker,Topic 必须包含项目唯一标识前缀 (例如: `yupi_mqtt_power_project/`),以避免与其他用户冲突。最终结构如: `[项目唯一前缀]/robot/command/{clientId}` 和 `[项目唯一前缀]/robot/status/{clientId}`。
|
||||
* **应用层鉴权**: 鉴于公共 Broker 的特性,强烈建议在消息 Payload层面实现应用层鉴权机制,以确保消息的合法性和安全性。例如,机器人上报状态时携带特定令牌,后端发送指令时也包含可供机器人验证的令牌或签名。
|
||||
|
||||
## 3. 后端开发详解
|
||||
|
||||
1. **数据库初始化**:
|
||||
* **创建 `robot_task` 表**: 执行 `development_plan.md` 中定义的 `robot_task` 表 DDL 语句。
|
||||
```sql
|
||||
CREATE TABLE `robot_task` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`robot_id` VARCHAR(100) NOT NULL COMMENT '机器人ID',
|
||||
`command_type` VARCHAR(50) NOT NULL COMMENT '命令类型 (MOVE_TO_SPOT, START_CHARGE, STOP_CHARGE, QUERY_STATUS)',
|
||||
`command_payload` TEXT COMMENT '命令参数 (JSON格式)',
|
||||
`status` VARCHAR(50) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态 (PENDING, SENT, ACKNOWLEDGED_SUCCESS, ACKNOWLEDGED_FAILURE, TIMED_OUT)',
|
||||
`sent_time` DATETIME COMMENT '命令发送时间',
|
||||
`ack_time` DATETIME COMMENT '命令确认时间',
|
||||
`related_session_id` BIGINT COMMENT '关联的充电会话ID (可选)',
|
||||
`error_message` TEXT COMMENT '失败或超时的错误信息',
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
INDEX `idx_robot_status` (`robot_id`, `status`) COMMENT '机器人和状态索引,便于查询'
|
||||
) COMMENT='机器人指令任务表';
|
||||
```
|
||||
2. **配置 MQTT 连接 (`application.yml`)**:
|
||||
* 添加 MQTT 相关配置项 (根据用户提供的信息填写):
|
||||
```yaml
|
||||
mqtt:
|
||||
broker-url: tcp://broker.emqx.io:1883 # 公共 Broker 地址
|
||||
username: # 通常为空,此公共 Broker 无需连接认证
|
||||
password: # 通常为空
|
||||
client-id-prefix: backend-yupi-mqtt-power- # 项目唯一客户端ID前缀
|
||||
default-qos: 1
|
||||
connection-timeout: 30
|
||||
keep-alive-interval: 60
|
||||
command-topic-base: yupi_mqtt_power_project/robot/command # 包含项目唯一前缀的指令Topic基础路径
|
||||
status-topic-base: yupi_mqtt_power_project/robot/status # 包含项目唯一前缀的状态Topic基础路径
|
||||
```
|
||||
* 创建 `src/main/java/com/yupi/project/config/properties/MqttProperties.java` 类,使用 `@ConfigurationProperties(prefix = "mqtt")` 和 Lombok (`@Data`) 来映射这些配置。
|
||||
3. **实现 MQTT 客户端 (`MqttConfig.java`)**:
|
||||
* 创建 `src/main/java/com/yupi/project/config/MqttConfig.java`。
|
||||
* **注入 `MqttProperties`**。
|
||||
* **创建 `MqttConnectOptions` Bean**:
|
||||
* 设置 `userName`, `password` (如果配置了)。
|
||||
* 设置 `isAutomaticReconnect(true)`。
|
||||
* 设置 `isCleanSession(true)` (或根据需要设置为 `false` 以接收离线消息)。
|
||||
* 设置 `connectionTimeout`, `keepAliveInterval`。
|
||||
* **创建 `MqttClient` Bean**:
|
||||
* 生成唯一的 `clientId` (例如: `properties.getClientIdPrefix() + UUID.randomUUID().toString()`)。
|
||||
* 创建 `MqttClient` 实例 (`new MqttClient(properties.getBrokerUrl(), clientId, new MemoryPersistence())`)。
|
||||
* **创建 `MqttCallbackExtended` Bean (核心)**:
|
||||
* 创建一个类实现 `MqttCallbackExtended` (例如, `MqttCallbackHandler`)。
|
||||
* **注入 `MqttMessageHandler` 服务** (将在下一步创建)。
|
||||
* 实现 `connectComplete(boolean reconnect, String serverURI)` 方法:
|
||||
* 在此方法中订阅状态主题: `client.subscribe(properties.getStatusTopicBase() + "/+", properties.getDefaultQos());` (`+` 是单层通配符)。记录订阅成功日志。
|
||||
* 如果是重连 (`reconnect == true`),可能需要执行一些状态同步逻辑(例如查询所有机器人的状态)。
|
||||
* 实现 `connectionLost(Throwable cause)` 方法:
|
||||
* 记录连接丢失的错误日志。
|
||||
* Paho 客户端的 `automaticReconnect` 会尝试重连。
|
||||
* 实现 `messageArrived(String topic, MqttMessage message)` 方法:
|
||||
* 获取 `payload` (`new String(message.getPayload())`)。
|
||||
* **应用层鉴权**: 在此步骤或 `mqttMessageHandler.handleStatusUpdate` 内部,应校验 `payload` 中携带的机器人身份标识或令牌,确保消息来源可靠。
|
||||
* 调用 `mqttMessageHandler.handleStatusUpdate(topic, payload)` 处理消息。
|
||||
* **注意**: 此方法应快速完成,避免阻塞回调线程。复杂的处理应异步进行或在 `handleStatusUpdate` 内部处理。
|
||||
* 实现 `deliveryComplete(IMqttDeliveryToken token)` 方法 (可选): 消息发布完成后的回调,通常用于 QoS 1 或 2 的确认。
|
||||
* **连接 MQTT Broker**:
|
||||
* 为 `MqttClient` Bean 设置初始化方法 (`initMethod="connectBroker"`) 或使用 `@PostConstruct`。
|
||||
* 在连接方法中:
|
||||
* 设置 `MqttCallback` (`client.setCallback(mqttCallbackHandlerBean)`)。
|
||||
* 调用 `client.connect(mqttConnectOptionsBean)`。
|
||||
* 添加适当的异常处理 (如 `MqttException`)。
|
||||
4. **实现 `RobotTask` 管理 (`RobotTaskService`)**:
|
||||
* 创建 `src/main/java/com/yupi/project/model/entity/RobotTask.java`。
|
||||
* 创建 `src/main/java/com/yupi/project/mapper/RobotTaskMapper.java`。
|
||||
* 创建 `src/main/java/com/yupi/project/service/RobotTaskService.java` 接口,定义方法如:
|
||||
* `hasPendingSentTask(String robotId)`: 检查是否有 `SENT` 状态的任务。
|
||||
* `createTask(String robotId, String commandType, String payloadJson, Long sessionId)`: 创建 `PENDING` 任务。
|
||||
* `markTaskAsSent(Long taskId)`: 更新任务状态为 `SENT`。
|
||||
* `findSentTask(String robotId, String expectedResponseType)`: 根据机器人响应查找对应的 `SENT` 任务 (需要逻辑判断响应类型对应哪个命令)。
|
||||
* `markTaskAsAcknowledged(Long taskId, boolean success, String errorMessage)`: 更新任务状态为 `ACKNOWLEDGED_SUCCESS` 或 `ACKNOWLEDGED_FAILURE`。
|
||||
* `findAndMarkTimedOutTasks(int timeoutSeconds)`: 查找并标记超时任务。
|
||||
* 创建 `src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java` 实现类。
|
||||
5. **实现消息发布 (`MqttService`)**:
|
||||
* 创建 `src/main/java/com/yupi/project/service/MqttService.java` 接口,定义 `sendCommand(...)` 方法。
|
||||
* 创建 `src/main/java/com/yupi/project/service/impl/MqttServiceImpl.java` 实现类。
|
||||
* 注入 `MqttClient`, `MqttProperties`, `RobotTaskService`。
|
||||
* 实现 `sendCommand(...)` 方法:
|
||||
* 调用 `robotTaskService.hasPendingSentTask(robotId)` 检查。
|
||||
* 调用 `robotTaskService.createTask(...)` 创建任务。
|
||||
* 构造 Topic (`properties.getCommandTopicBase() + "/" + robotId`)。
|
||||
* **应用层鉴权**: 构造 `payloadJson` 时,应包含可供机器人验证的令牌或签名,确保指令的合法性。
|
||||
* 调用 `mqttClient.publish(topic, payloadJson.getBytes(), properties.getDefaultQos(), false)` 发送。
|
||||
* 发送成功后,调用 `robotTaskService.markTaskAsSent(taskId)`。
|
||||
* 添加 `MqttException` 处理。
|
||||
6. **实现消息处理 (`MqttMessageHandler`)**:
|
||||
* 创建 `src/main/java/com/yupi/project/service/MqttMessageHandler.java` (接口和实现)。
|
||||
* 注入 `RobotTaskService`, `Gson` (或其他 JSON 库), 以及后续阶段需要的 Service (如 `ChargingService`, `ChargingRobotService`)。
|
||||
* 实现 `handleStatusUpdate(String topic, String payload)` 方法:
|
||||
* 解析 `topic` 提取 `robotId`。
|
||||
* 解析 `payload` (JSON) 为 `RobotStatusDTO` (需要创建此 DTO)。
|
||||
* **核心**: 根据 `RobotStatusDTO` 的内容判断这是对哪个命令的响应 (或者是一个主动状态上报)。调用 `robotTaskService.findSentTask(...)` 查找对应的 `SENT` 任务。
|
||||
* 如果找到任务,调用 `robotTaskService.markTaskAsAcknowledged(...)` 更新任务状态。
|
||||
* **只有在任务确认后**,才根据 `RobotStatusDTO.status` 调用后续业务逻辑 Service 的方法 (例如: `chargingService.handleRobotArrival(robotId, spotId)`, `chargingService.handleChargeCompletion(robotId, duration)`)。
|
||||
* 处理 JSON 解析异常、任务未找到等情况。
|
||||
7. **实现任务超时处理 (`TaskTimeoutHandler`)**:
|
||||
* 创建 `src/main/java/com/yupi/project/schedule/TaskTimeoutHandler.java` (或放在 service 包)。
|
||||
* 注入 `RobotTaskService`, 以及可能需要更新状态的 `ChargingRobotService`, `ChargingSessionService`。
|
||||
* 添加 `@Component` 和 `@EnableScheduling` (在主启动类或配置类上)。
|
||||
* 创建一个方法,使用 `@Scheduled(fixedRate = 60000)` (例如每分钟执行一次)。
|
||||
* 在该方法中:
|
||||
* 调用 `robotTaskService.findAndMarkTimedOutTasks(timeoutSeconds)` (例如超时设为 120 秒)。
|
||||
* 获取超时的任务列表。
|
||||
* 对每个超时任务,根据业务逻辑更新关联的 `charging_robot` (如设为 `offline` 或 `error`) 和 `charging_session` (如设为 `error`) 的状态。
|
||||
* 添加日志记录。
|
||||
|
||||
## 4. 前端 (移动端 App) 开发详解
|
||||
|
||||
* **本阶段前端无核心功能变更。**
|
||||
* 可以考虑在界面上预留位置,用于未来展示机器人的连接状态或基本信息,但这依赖于后端是否通过 API 提供这些聚合信息,或者前端是否也直接订阅 MQTT (通常不推荐前端直接连 Broker)。
|
||||
* 主要工作是等待后端完成 MQTT 集成,为下一阶段的核心业务功能提供基础。
|
||||
|
||||
## 5. 本阶段交付物
|
||||
|
||||
* 包含 `robot_task` 表的数据库。
|
||||
* 配置完成并能在启动时自动连接 MQTT Broker 的后端服务。
|
||||
* 实现 MQTT 状态消息订阅和命令消息发布的功能。
|
||||
* 集成 `robot_task` 表的命令发送检查和状态跟踪逻辑。
|
||||
* 实现任务超时检查和处理的定时任务。
|
||||
* 必要的单元测试 (例如 `RobotTaskService` 的逻辑)。
|
||||
* 描述 MQTT 配置和运行状态的简要文档或日志。
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
* MQTT 连接信息 (特别是密码,如果未来使用私有 Broker) 应妥善保管,避免硬编码在代码中,使用配置文件管理。
|
||||
* **Topic 设计与唯一性**: 在公共 Broker 环境下,Topic 设计必须包含项目唯一标识前缀,以防止命名冲突。所有参与方(后端、机器人)都必须使用此约定。
|
||||
* **Payload 格式**: 必须严格遵守与硬件端的约定。
|
||||
* **应用层鉴权**: 由于使用公共 Broker,强烈建议在消息 Payload 中实现双向的应用层鉴权机制。后端需验证机器人状态的来源,机器人需验证指令的来源。
|
||||
* `MqttCallback` 中的 `messageArrived` 必须快速返回,避免阻塞。
|
||||
* 超时时间的设置需要根据实际网络情况和机器人响应时间进行调整。
|
||||
* 健壮的错误处理对于 MQTT 集成至关重要。
|
||||
@@ -0,0 +1,142 @@
|
||||
# 开发阶段三:核心充电业务逻辑
|
||||
|
||||
## 1. 目标
|
||||
|
||||
* 实现充电机器人和车位的基础管理功能 (如果需要管理员界面)。
|
||||
* 实现完整的充电请求、机器人调度、充电开始/结束、状态跟踪和计费流程。
|
||||
* 将业务逻辑与 MQTT 消息处理紧密结合。
|
||||
* 在移动端实现发起充电、查看充电状态和历史记录的核心用户功能。
|
||||
* 在移动端为管理员提供基础的监控界面(可选)。
|
||||
|
||||
## 2. 后端开发详解
|
||||
|
||||
1. **数据库初始化**:
|
||||
* **创建 `charging_robot`, `parking_spot`, `charging_session` 表**: 执行 `development_plan.md` 中定义的相应 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='充电记录表';
|
||||
```
|
||||
2. **机器人与车位管理 (可选,主要面向管理员)**:
|
||||
* **Entity**: 创建 `ChargingRobot.java`, `ParkingSpot.java`。
|
||||
* **Mapper**: 创建 `ChargingRobotMapper.java`, `ParkingSpotMapper.java`。
|
||||
* **Service**: 创建 `ChargingRobotService.java`, `ParkingSpotService.java` 接口和实现类。
|
||||
* `ChargingRobotService` 需包含方法:`findAvailableRobot()`, `updateRobotStatus(String robotId, String status, String location, Integer batteryLevel)`。
|
||||
* `ParkingSpotService` 需包含方法:`updateSpotStatus(String spotId, String status)`。
|
||||
* **Controller (可选)**: 创建 `ChargingRobotController.java`, `ParkingSpotController.java`,提供 CRUD 接口 (`/api/robots`, `/api/spots`),并进行管理员角色检查。
|
||||
3. **充电会话管理**:
|
||||
* **Entity**: 创建 `ChargingSession.java`。
|
||||
* **Mapper**: 创建 `ChargingSessionMapper.java`。
|
||||
* **Service**: 创建 `ChargingSessionService.java` 接口和实现类。包含创建、更新状态、记录时间/费用、查询用户历史记录等方法。
|
||||
4. **充电流程实现**:
|
||||
* **创建 `ChargingService` (核心业务编排)**: 创建接口 `ChargingService.java` 和实现 `ChargingServiceImpl.java`。
|
||||
* 注入 `UserService`, `ChargingRobotService`, `ParkingSpotService`, `ChargingSessionService`, `MqttService`, `RobotTaskService`。
|
||||
* 定义核心方法,如:
|
||||
* `requestCharging(String userId, String spotId)`: 处理用户充电请求。
|
||||
* `handleRobotArrival(String robotId, String spotId)`: 处理机器人到达事件。
|
||||
* `handleChargingStart(String robotId, String spotId)`: 处理充电开始事件。
|
||||
* `handleChargeUpdate(String robotId, int durationSeconds)`: 处理充电中状态更新 (可选)。
|
||||
* `handleChargeCompletion(String robotId, String spotId, int totalDurationSeconds)`: 处理充电完成事件。
|
||||
* `handleRobotError(String robotId, String errorCode, String message)`: 处理机器人错误。
|
||||
* `stopChargingByUser(String userId)`: 处理用户停止请求。
|
||||
* **修改 `MqttMessageHandler`**:
|
||||
* 在其 `handleStatusUpdate` 方法中,当 `robot_task` 确认成功后,根据收到的机器人状态 (`RobotStatusDTO.status`),调用 `ChargingService` 中对应的 `handleXxx` 方法。
|
||||
* 例如:收到 `arrived_at_spot` 状态 -> 调用 `chargingService.handleRobotArrival(...)`。
|
||||
* 例如:收到 `charge_complete` 状态 -> 调用 `chargingService.handleChargeCompletion(...)`。
|
||||
* **实现 `ChargingServiceImpl` 核心逻辑**:
|
||||
* `requestCharging`:
|
||||
1. 检查用户余额。
|
||||
2. 查找可用机器人 (`chargingRobotService.findAvailableRobot()`)。
|
||||
3. 查找车位信息。
|
||||
4. 创建 `ChargingSession` 记录 (状态 `PENDING`)。
|
||||
5. 调用 `mqttService.sendCommand(robotId, "move_to_spot", payload, sessionId)` 发送移动指令。
|
||||
6. 如果发送成功,更新 `ChargingSession` 状态为 `ROBOT_MOVING`,更新机器人/车位DB状态。
|
||||
* `handleRobotArrival`:
|
||||
1. 更新机器人DB状态。
|
||||
2. 调用 `mqttService.sendCommand(robotId, "start_charge", null, sessionId)` 发送开始充电指令。
|
||||
* `handleChargingStart`:
|
||||
1. 更新机器人DB状态。
|
||||
2. 更新 `ChargingSession` 状态为 `CHARGING`,记录 `start_time`。
|
||||
* `handleChargeCompletion`:
|
||||
1. 更新机器人DB状态为 `IDLE`。
|
||||
2. 更新车位DB状态为 `AVAILABLE`。
|
||||
3. 记录 `end_time`, `totalDurationSeconds` 到 `ChargingSession`。
|
||||
4. **调用计费逻辑** (见下一点)。
|
||||
5. 更新 `ChargingSession` 状态为 `COMPLETED`。
|
||||
* `handleRobotError`: 更新相关实体的状态为 `ERROR`,记录错误信息。
|
||||
* `stopChargingByUser`: 查找用户当前的 `CHARGING` 会话,获取 `robotId`,调用 `mqttService.sendCommand(robotId, "stop_charge", null, sessionId)`。后续计费在收到 `charge_complete` 时处理。
|
||||
* **创建 `ChargingController`**:
|
||||
* 注入 `ChargingService`。
|
||||
* 实现 `POST /api/charging/request` 接口: 调用 `chargingService.requestCharging(...)`。
|
||||
* 实现 `POST /api/charging/stop` 接口: 调用 `chargingService.stopChargingByUser(...)`。
|
||||
* 实现 `GET /api/charging/history` 接口: 调用 `chargingSessionService.getUserHistory(...)`。
|
||||
* 实现 `GET /api/charging/sessions` 接口 (管理员): 调用 `chargingSessionService.getAllSessions(...)`。
|
||||
5. **计费逻辑**:
|
||||
* 在 `ChargingServiceImpl.handleChargeCompletion` 中实现。
|
||||
* 定义计费单价 (元/秒 或 元/小时,需转换)。可以配置在 `application.yml` 中或作为常量。
|
||||
* 计算费用: `cost = totalDurationSeconds * pricePerSecond`。
|
||||
* 调用 `userService.deductBalance(userId, cost)`。 **此方法需要处理并发**,例如使用 SQL `UPDATE user SET balance = balance - ? WHERE id = ? AND balance >= ?` 来保证原子性扣款。
|
||||
* 将计算出的 `cost` 记录到 `ChargingSession`。
|
||||
|
||||
## 3. 前端 (移动端 App) 开发详解
|
||||
|
||||
* **目标**: 实现用户发起充电、查看状态、停止充电、查看历史的核心交互。
|
||||
|
||||
* **页面设计与流程**:
|
||||
1. **首页/充电页 (`ChargeScreen.js`)**: (普通用户 Tab 之一)
|
||||
* **UI**:
|
||||
* 顶部显示用户当前余额 (`GET /api/user/current`)。
|
||||
* 地图或列表展示附近可用的充电车位 (`GET /api/spots?status=available`,需要后端支持此查询)。车位用蓝色空闲图标表示。
|
||||
* (可选)显示可用机器人的数量或状态 (`GET /api/robots?status=idle`)。
|
||||
* 选择车位后,弹窗或按钮显示 "发起充电"。
|
||||
* **交互**: 用户点击选择一个可用车位,然后点击 "发起充电" 按钮。
|
||||
* **API 调用**: 点击按钮后,调用 `POST /api/charging/request`,参数为 `{ spotId: selectedSpotId }`。
|
||||
* **响应处理**: 成功则提示 "机器人正在前往...",并导航到充电状态页;失败(如机器人忙、余额不足)则提示用户。
|
||||
2. **充电状态页 (`ChargingStatusScreen.js`)**:
|
||||
* **UI**:
|
||||
* 醒目位置显示当前充电状态(如 "机器人移动中", "正在充电", "充电完成", "错误"),可以使用不同颜色和图标(蓝色移动、绿色充电、灰色完成、红色错误)。
|
||||
* 显示目标车位 ID。
|
||||
* 显示使用的机器人 ID。
|
||||
* 如果是 "正在充电" 状态,显示已充电时长(需要后端通过 WebSocket 或轮询 API 提供实时数据,本阶段可先只显示开始时间或总状态)。
|
||||
* 显示 "停止充电" 按钮。
|
||||
* 科技感的进度条或动画效果展示充电过程。
|
||||
* **交互**: 用户可以点击 "停止充电" 按钮。
|
||||
* **API 调用**:
|
||||
* 进入页面时,需要获取当前正在进行的充电会话信息(后端需要提供接口,如 `GET /api/charging/current`)。
|
||||
* (难点)实时状态更新:
|
||||
* **方案一 (轮询)**: 前端定时 (如 5-10 秒) 调用 `GET /api/charging/current` 刷新状态和时长。
|
||||
* **方案二 (WebSocket)**: 后端在状态变更时通过 WebSocket 推送消息给前端。这需要额外集成 WebSocket。**本方案优先考虑轮询**。
|
||||
* 点击 "停止充电" 时,调用 `POST /api/charging/stop`。
|
||||
* **响应处理**: "停止充电" 请求成功后,等待状态更新为 "充电完成" 或 "错误"。
|
||||
3. **充电历史页 (`HistoryScreen.js`)**: (普通用户 Tab 之一, "我的" 页面内)
|
||||
* **UI**: 列表展示用户的充电记录。每条记录显示车位 ID、机器人 ID、开始/结束时间、时长、费用。按时间倒序排列。蓝白卡片风格。
|
||||
* **交互**: 下拉刷新,上拉加载更多。
|
||||
* **API 调用**: 进入页面时调用 `GET /api/charging/history` (支持分页参数)。
|
||||
4. **管理员监控页 (`AdminMonitorScreen.js`)**: (管理员 Tab 之一)
|
||||
* **UI**:
|
||||
* 列表展示所有机器人状态 (`GET /api/robots`)。
|
||||
* 列表展示所有车位状态 (`GET /api/spots`)。
|
||||
* 列表展示所有进行中/最近的充电会话 (`GET /api/charging/sessions`)。
|
||||
* 数据使用不同颜色区分状态 (如 idle/available-蓝色, moving/charging/occupied-橙色/绿色, error/maintenance-红色)。
|
||||
* **交互**: 实时刷新数据(同样可通过轮询或 WebSocket)。
|
||||
|
||||
## 4. 本阶段交付物
|
||||
|
||||
* 包含 `charging_robot`, `parking_spot`, `charging_session` 表的数据库。
|
||||
* 实现充电请求处理、机器人状态更新、充电会话管理、计费逻辑的后端服务。
|
||||
* 充电相关核心业务 API (`/api/charging/*`)。
|
||||
* (可选) 机器人和车位管理的 CRUD API。
|
||||
* 移动端 App 实现选择车位、发起充电、查看充电状态(轮询)、停止充电、查看历史记录的功能。
|
||||
* (可选) 移动端 App 实现管理员监控界面。
|
||||
* 覆盖核心业务逻辑的单元测试。
|
||||
* 接口测试报告。
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
* 计费逻辑和余额扣减必须保证数据一致性和准确性,考虑并发问题。
|
||||
* 状态转换逻辑要清晰、完备,覆盖所有正常和异常情况。
|
||||
* 前后端对于充电状态的定义和展示需要统一。
|
||||
* 实时状态更新的实现方式(轮询/WebSocket)需要权衡复杂度和实时性要求。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 开发阶段四:激活码与完善
|
||||
|
||||
## 1. 目标
|
||||
|
||||
* 实现激活码生成、管理和兑换充值功能。
|
||||
* 完善 API 文档。
|
||||
* 增强系统测试覆盖率。
|
||||
* 优化错误处理和日志记录。
|
||||
* 对移动端 App 进行 UI 细节打磨和体验优化。
|
||||
|
||||
## 2. 后端开发详解
|
||||
|
||||
1. **数据库初始化**:
|
||||
* **创建 `activation_code` 表**: 执行 `development_plan.md` 中定义的 `activation_code` 表 DDL 语句。
|
||||
```sql
|
||||
CREATE TABLE `activation_code` (...) COMMENT='激活码表';
|
||||
```
|
||||
2. **激活码模块实现**:
|
||||
* **Entity**: 创建 `ActivationCode.java`。
|
||||
* **Mapper**: 创建 `ActivationCodeMapper.java`。
|
||||
* **Service**: 创建 `ActivationCodeService.java` 接口和实现类。
|
||||
* `generateCodes(int count, BigDecimal value, Date expireTime)`: (管理员) 批量生成激活码。生成逻辑可以使用 UUID 或自定义规则,确保唯一性。将生成的 Code 和 Value 存入数据库。
|
||||
* `redeemCode(String userId, String code)`: (用户) 兑换激活码。
|
||||
1. 查询 `code` 是否存在、是否未使用 (`is_used = 0`)、是否过期 (如果 `expire_time` 不为空)。
|
||||
2. 如果有效,获取其 `value`。
|
||||
3. 调用 `userService.addBalance(userId, value)` 增加用户余额 (注意并发)。
|
||||
4. 将激活码标记为已使用 (`is_used = 1`), 记录 `user_id`, `use_time`。
|
||||
5. 返回成功或失败信息。
|
||||
* (可选) `listCodes(Page page, ActivationCodeQuery query)`: (管理员) 查询激活码列表。
|
||||
* **Controller**: 创建 `ActivationCodeController.java`。
|
||||
* 实现 `POST /api/codes/redeem` 接口: 调用 `activationCodeService.redeemCode(...)`。
|
||||
* (可选) 实现 `POST /api/codes` 接口 (管理员): 调用 `activationCodeService.generateCodes(...)`。
|
||||
* (可选) 实现 `GET /api/codes` 接口 (管理员): 调用 `activationCodeService.listCodes(...)`。
|
||||
3. **API 文档完善 (Knife4j/Swagger)**:
|
||||
* 检查所有 Controller 类和方法,确保添加了 `@Api`, `@ApiOperation` 注解,描述清晰。
|
||||
* 检查所有请求 DTO 和响应 VO (视图对象),确保添加了 `@ApiModel`, `@ApiModelProperty` 注解,描述字段含义和是否必需。
|
||||
* 启动项目,访问 Knife4j 文档地址 (通常是 `/doc.html`),检查文档是否完整、准确。
|
||||
4. **测试增强**:
|
||||
* **单元测试**: 使用 JUnit 和 Mockito (或 PowerMock) 对 Service 层的核心业务逻辑进行测试,特别是计费、余额变更、激活码兑换、任务状态处理等。
|
||||
* **集成测试 (可选但推荐)**:
|
||||
* 使用 Spring Boot Test (`@SpringBootTest`) 测试 Service 层与数据库的交互。
|
||||
* **MQTT 集成测试**: 搭建本地 MQTT Broker (如 Docker 版 EMQX 或 Mosquitto),编写测试用例模拟机器人发送状态消息,验证 `MqttMessageHandler` 和后续业务逻辑是否正确处理。
|
||||
* **接口测试**: 使用 Postman 或 Apifox 等工具,对所有 API 进行测试,覆盖正常和异常场景。
|
||||
5. **错误处理与日志优化**:
|
||||
* **全局异常处理**: 检查 `GlobalExceptionHandler.java`,确保捕获了常见的业务异常 (`BusinessException`) 和系统异常,并返回统一格式的错误响应给前端。
|
||||
* **日志**: 使用 SLF4j + Logback (Spring Boot 默认)。
|
||||
* 在关键路径 (如用户登录、发起充电、收到 MQTT 消息、计费、余额变更、任务超时) 添加 `info` 级别的日志。
|
||||
* 在异常捕获处添加 `error` 级别的日志,包含堆栈信息。
|
||||
* 考虑异步记录日志 (如果性能要求高)。
|
||||
* 配置 `logback-spring.xml`,区分不同环境的日志级别和输出目的地(控制台、文件)。
|
||||
|
||||
## 3. 前端 (移动端 App) 开发详解
|
||||
|
||||
* **目标**: 实现激活码兑换功能,优化用户体验和界面细节。
|
||||
|
||||
* **页面设计与流程**:
|
||||
1. **激活码兑换页 (`RedeemCodeScreen.js`)**: (普通用户,通常在 "我的" 页面内入口)
|
||||
* **UI**: 简洁风格。一个输入框用于输入激活码,一个 "兑换" 按钮 (蓝色)。可以加一些提示文字说明。
|
||||
* **交互**: 输入激活码,点击兑换按钮,显示加载状态。
|
||||
* **API 调用**: 调用 `POST /api/codes/redeem`,参数 `{ code: enteredCode }`。
|
||||
* **响应处理**: 成功则提示 "兑换成功,增加余额 X 元",并更新本地用户余额显示;失败则提示错误原因(如无效码、已使用、已过期)。
|
||||
2. **用户中心/我的页面 (`ProfileScreen.js`)**:
|
||||
* **UI**: 优化布局,清晰展示用户名、角色、**账户余额**。
|
||||
* 提供 "充电历史"、"激活码兑换" 等功能的入口。
|
||||
* 提供 "退出登录" 按钮。
|
||||
* 整体风格保持蓝白科技感。
|
||||
3. **UI 细节打磨**:
|
||||
* 检查所有页面的 UI 元素对齐、间距、颜色、字体是否符合设计规范。
|
||||
* 优化加载状态的显示(如骨架屏、更平滑的加载动画)。
|
||||
* 优化错误提示方式,使其更友好、清晰。
|
||||
* 确保在不同尺寸和分辨率的手机屏幕上显示良好(响应式布局)。
|
||||
* 增加必要的过渡动画,提升操作流畅感和科技感。
|
||||
4. **管理员激活码管理页 (`AdminCodeManagementScreen.js`)**: (管理员)
|
||||
* **UI**:
|
||||
* 提供生成激活码的功能入口(输入数量、金额、过期时间)。
|
||||
* 列表展示已生成的激活码,包含码字、金额、状态(未使用/已使用/已过期)、使用者、使用时间等信息。
|
||||
* 支持搜索、筛选、分页。
|
||||
* **API 调用**: 调用 `/api/codes` (POST 生成, GET 查询)。
|
||||
|
||||
* **代码优化**:
|
||||
* 检查代码结构,封装可复用组件。
|
||||
* 处理潜在的内存泄漏。
|
||||
* 优化性能,特别是列表渲染。
|
||||
|
||||
## 4. 本阶段交付物
|
||||
|
||||
* 包含 `activation_code` 表的数据库。
|
||||
* 实现激活码生成、查询、兑换功能的后端服务及 API。
|
||||
* 完善的 Knife4j API 文档。
|
||||
* 更高覆盖率的单元测试和集成测试报告。
|
||||
* 优化后的错误处理和日志配置。
|
||||
* 移动端 App 实现激活码兑换功能。
|
||||
* 移动端 App 整体 UI/UX 优化版本。
|
||||
* (可选) 移动端 App 实现管理员激活码管理界面。
|
||||
* 最终的接口测试报告。
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
* 激活码生成要保证唯一性。
|
||||
* 激活码兑换和余额增加操作要保证原子性。
|
||||
* API 文档应与实际代码保持同步。
|
||||
* 测试应尽可能覆盖边界条件和异常情况。
|
||||
37
springboot-init-main/doc/kaiti.md
Normal file
37
springboot-init-main/doc/kaiti.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## 项目目标与核心技术
|
||||
|
||||
本项目的主要设计目标是打造一款能够 **自主导航、避障、充电和远程控制** 的智能机器人。
|
||||
|
||||
* **核心控制器**: ESP32-CAM,提供计算与无线通信能力。
|
||||
* **通信方式**: 通过 Wi-Fi 连接 MQTT 服务器(提及使用 **EMQX**),实现与云端(即我们的后端平台)的实时数据交互。
|
||||
* **导航与避障**: 配备红外循迹模块和超声波避障模块,实现自主导航和避障。
|
||||
* **运动控制**: 舵机控制系统用于调整运动方向与停车位置。
|
||||
* **充电控制**: 继电器控制充电桩(或充电接口)的开关。
|
||||
* **报警系统**: 蜂鸣器用于状态提示或报警。
|
||||
|
||||
## 远程控制与监控
|
||||
|
||||
* 机器人通过 MQTT 协议将实时状态和数据上传到云端。
|
||||
* 用户可以通过 **手机APP或网页**(由我们的后端平台支持)随时查看并远程控制机器人的运行。
|
||||
|
||||
## 技术挑战 (硬件/嵌入式侧重点)
|
||||
|
||||
* 智能导航与避障算法实现 (利用红外与超声波)。
|
||||
* 充电与停车的精准对接。
|
||||
* MQTT 通信的稳定性和安全性 (硬件端)。
|
||||
* 声光报警的准确性。
|
||||
|
||||
## 技术路线总结
|
||||
|
||||
* **系统定位**: 基于 MQTT 协议的停车场自助充电机器人。
|
||||
* **核心硬件**: ESP32-CAM。
|
||||
* **关键技术**: 红外循迹、超声波避障、舵机控制、MQTT 通信。
|
||||
* **核心功能**: 自主导航、定点停车、自助充电、远程监控。
|
||||
* **价值**: 提升停车场管理效率和用户体验,实现智能化与自动化。
|
||||
|
||||
## 设计关键内容
|
||||
|
||||
* **硬件集成**: ESP32-CAM 与各传感器、执行器的集成。
|
||||
* **控制算法**: 导航、避障、停靠、充电逻辑 (嵌入式实现)。
|
||||
* **数据传输与管理**: 通过 Wi-Fi 和 MQTT 与远程控制端(后端平台)进行数据交互。
|
||||
* **安全与提醒**: 通过 MQTT 上报状态,蜂鸣器进行本地报警。
|
||||
124
springboot-init-main/doc/requirements.md
Normal file
124
springboot-init-main/doc/requirements.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# MQTT 智能充电桩系统 - 需求文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
本项目旨在开发一个基于 Spring Boot 的 **移动式** 智能充电管理系统。系统作为后端平台,通过 MQTT 协议与 **基于 ESP32-CAM 的充电机器人** 硬件设备进行通信,实现远程控制、状态监控以及 **调度机器人到指定车位进行充电**。同时,系统提供用户管理、充值计费等功能。**平台软件不负责机器人底层的导航、避障、循迹等具体算法实现**,仅发送高级指令并接收处理后的状态。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
### 2.1 用户管理与认证
|
||||
|
||||
* **用户角色**: 系统需支持两种用户角色:
|
||||
* 管理员:拥有系统所有管理权限。
|
||||
* 普通用户:可进行充电、充值等操作。
|
||||
* **用户登录**: 提供安全的用户名密码登录机制。
|
||||
* 区分管理员和普通用户登录接口或逻辑。
|
||||
* **用户信息管理** (管理员权限):
|
||||
* 查看用户列表。
|
||||
* (可选)添加、编辑、删除用户信息。
|
||||
|
||||
### 2.2 MQTT 集成与设备控制
|
||||
|
||||
* **MQTT 连接**:
|
||||
* 连接至 **指定的公共 MQTT 服务器 (如 EMQX,需提供具体地址和端口)**。
|
||||
* 支持为特定的 MQTT 主题(Topic)设置用户名和密码进行认证 **(需提供)**。
|
||||
* 配置用于指令下发和状态上报的 Topic。
|
||||
* **消息格式**:
|
||||
* 与充电机器人通过 JSON 字符串格式进行消息交互。
|
||||
* **设备控制** (平台侧重点):
|
||||
* 向充电机器人发送 **高级控制指令**(见 MQTT Topic 规划)。
|
||||
* 平台 **不处理** 原始传感器数据(如红外、超声波读数)。
|
||||
* **状态接收** (平台侧重点):
|
||||
* 接收充电机器人上报的 **综合状态信息**(见 MQTT Topic 规划)。
|
||||
|
||||
### 2.3 智能充电核心业务
|
||||
|
||||
* **充电机器人管理** (管理员权限):
|
||||
* (可选)添加、查看、管理充电机器人设备信息(如:设备ID、当前状态、位置)。
|
||||
* **车位管理** (管理员权限):
|
||||
* (可选)管理可用的充电车位信息(如:车位ID、位置描述)。
|
||||
* **激活码充值**:
|
||||
* 普通用户可以使用激活码为自己的账户充值。
|
||||
* 系统需验证激活码的有效性,并将对应金额或时长添加到用户账户。
|
||||
* 需要设计激活码生成和管理机制(管理员)。
|
||||
* **充电计费**:
|
||||
* 根据实际充电时长进行计费。
|
||||
* 需要明确计费单价(例如:元/小时)。
|
||||
* 充电结束后,从用户账户扣除相应费用。
|
||||
* 用户账户余额不足时,应有相应处理(如:无法启动充电、自动停止充电)。
|
||||
* **充电流程** (平台侧重点):
|
||||
1. 用户登录系统。
|
||||
2. 用户 **选择目标充电车位** `{parkingSpotId}`。
|
||||
3. 用户发起充电请求。
|
||||
4. 系统检查用户余额。
|
||||
5. 系统选择一个可用的充电机器人 `{robotId}` (如果系统管理多个机器人)。
|
||||
6. 系统通过 MQTT 发送 **移动指令 (`move_to_spot`)** 给机器人 `{robotId}`,目标为车位 `{parkingSpotId}`。
|
||||
7. 机器人移动到位后,通过 MQTT 上报 `arrived_at_spot` 状态。
|
||||
8. 系统通过 MQTT 发送 **启动充电指令 (`start_charge`)** 给机器人。
|
||||
9. 机器人开始充电并通过 MQTT 上报 `charging` 状态(及充电时长)。
|
||||
10. 用户发起停止充电请求 或 机器人上报 `charge_complete` / `error` 状态。
|
||||
11. 系统通过 MQTT 发送 **停止充电指令 (`stop_charge`)** 给机器人。
|
||||
12. 系统根据 MQTT 上报的 **总充电时长** 计算费用并扣费。
|
||||
13. 记录充电日志。
|
||||
|
||||
### 2.4 充电记录
|
||||
|
||||
* 记录用户的充电历史,包括:用户、**使用的机器人**、**充电车位**、开始时间、结束时间、充电时长、消费金额。
|
||||
* 普通用户可查看自己的充电记录。
|
||||
* 管理员可查看所有充电记录。
|
||||
|
||||
## 3. 非功能需求
|
||||
|
||||
* **可用性**: 系统应稳定可靠,提供持续服务。
|
||||
* **安全性**: 用户密码需加密存储;MQTT 通道需进行安全认证;防止未授权访问。
|
||||
* **可维护性**: 代码结构清晰,遵循高内聚低耦合原则,易于维护和扩展。
|
||||
* **性能**: 系统应能及时响应用户请求和处理 MQTT 消息。
|
||||
|
||||
## 4. 技术选型(初步)
|
||||
|
||||
* 后端框架: Spring Boot
|
||||
* 持久层: MyBatis-Plus
|
||||
* 数据库: MySQL
|
||||
* 缓存: Redis
|
||||
* **MQTT Broker**: 外部公共服务 (如 EMQX, **需用户提供实例信息**)
|
||||
* MQTT 客户端库: Eclipse Paho MQTT Client
|
||||
* API 文档: Knife4j
|
||||
* 数据交换格式: JSON
|
||||
* **机器人控制器 (参考)**: ESP32-CAM
|
||||
|
||||
## 5. MQTT 通道(Topic)规划 (需要用户确认并提供细节)
|
||||
|
||||
* **基础 Topic 结构**: (建议)
|
||||
* 指令下发: `robot/command/{clientId}`
|
||||
* 状态上报: `robot/status/{clientId}`
|
||||
* *注:`{clientId}` 通常是认证时使用的客户端ID,可以等同于机器人ID `{robotId}`,需要与硬件端约定。*
|
||||
|
||||
* **指令下发 Topic**: `robot/command/{clientId}` (平台 -> 机器人)
|
||||
* **认证**: **需要** (用户名/密码由用户提供)
|
||||
* **Payload (JSON 示例)**:
|
||||
* 移动到车位: `{"action": "move_to_spot", "spotId": "P001"}`
|
||||
* 开始充电: `{"action": "start_charge"}`
|
||||
* 停止充电: `{"action": "stop_charge"}`
|
||||
* 查询状态: `{"action": "query_status"}` (可选)
|
||||
|
||||
* **状态上报 Topic**: `robot/status/{clientId}` (机器人 -> 平台)
|
||||
* **认证**: **需要** (用户名/密码由用户提供)
|
||||
* **Payload (JSON 示例)**:
|
||||
* 空闲状态: `{"robotId": "R001", "status": "idle", "location": "base", "battery": 95}`
|
||||
* 移动中: `{"robotId": "R001", "status": "moving", "targetSpot": "P001", "battery": 90}`
|
||||
* 到达车位: `{"robotId": "R001", "status": "arrived_at_spot", "spotId": "P001", "battery": 88}`
|
||||
* 充电中: `{"robotId": "R001", "status": "charging", "spotId": "P001", "durationSeconds": 120, "battery": 92}`
|
||||
* 充电完成: `{"robotId": "R001", "status": "charge_complete", "spotId": "P001", "totalDurationSeconds": 3600, "battery": 100}`
|
||||
* 错误状态: `{"robotId": "R001", "status": "error", "errorCode": "NAV_BLOCKED", "message": "Navigation path blocked", "location": "near_P002"}`
|
||||
* 通用状态响应 (对 query_status): `{"robotId": "R001", "status": "idle", "location": "base", "battery": 95}`
|
||||
|
||||
* **重要提示**: 上述 Topic 结构和 Payload 内容为 **建议**,最终需要与硬件(ESP32)开发侧 **详细约定** 并 **统一**。平台将根据约定的格式进行开发。
|
||||
|
||||
## 6. 数据库设计 (已精简)
|
||||
|
||||
* **用户表 (`user`)**: 包含用户基本信息、**密码**、**角色 (如: admin, user)**、**账户余额**。
|
||||
* 充电机器人表 (`charging_robot`)
|
||||
* 车位表 (`parking_spot`)
|
||||
* 充电记录表 (`charging_session`)
|
||||
* 激活码表 (`activation_code`)
|
||||
* **机器人任务表 (`robot_task`)**: 用于跟踪发送给机器人的指令及其状态 (pending, sent, acknowledged_success, acknowledged_failure, timed_out),防止重复发送指令给未响应的机器人。
|
||||
316
springboot-init-main/mvnw
vendored
Normal file
316
springboot-init-main/mvnw
vendored
Normal file
@@ -0,0 +1,316 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven Start Up Batch script
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`\\unset -f command; \\command -v java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
echo "${basedir}"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
else
|
||||
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $jarUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl -o "$wrapperJarPath" "$jarUrl" -f
|
||||
else
|
||||
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
||||
fi
|
||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
fi
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
188
springboot-init-main/mvnw.cmd
vendored
Normal file
188
springboot-init-main/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven Start Up Batch script
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
||||
119
springboot-init-main/pom.xml
Normal file
119
springboot-init-main/pom.xml
Normal file
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.yupi</groupId>
|
||||
<artifactId>mqtt-charging-system</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>mqtt-charging-system</name>
|
||||
<description>MQTT 智能充电桩系统</description>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>2.2.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.9.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
<!-- https://doc.xiaominfo.com/knife4j/documentation/get_start.html-->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/junit/junit -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- MQTT Client -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.5</version> <!-- 使用稳定版本 -->
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.yupi.project;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.yupi.project.mapper")
|
||||
public class MyApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MyApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yupi.project.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 权限校验
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AuthCheck {
|
||||
|
||||
/**
|
||||
* 有任何一个角色
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] anyRole() default "";
|
||||
|
||||
/**
|
||||
* 必须有某个角色
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String mustRole() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.yupi.project.aop;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import com.yupi.project.annotation.AuthCheck;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import com.yupi.project.model.enums.UserRoleEnum;
|
||||
import com.yupi.project.service.UserService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* 权限校验 AOP
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class AuthInterceptor {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 执行拦截
|
||||
*
|
||||
* @param joinPoint
|
||||
* @param authCheck
|
||||
* @return
|
||||
*/
|
||||
@Around("@annotation(authCheck)")
|
||||
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
|
||||
List<String> anyRole = Arrays.stream(authCheck.anyRole()).filter(StringUtils::isNotBlank).collect(Collectors.toList());
|
||||
String mustRole = authCheck.mustRole();
|
||||
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
|
||||
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
// 当前登录用户
|
||||
User user = userService.getCurrentUser(request);
|
||||
// 拥有任意权限即通过
|
||||
if (CollectionUtils.isNotEmpty(anyRole)) {
|
||||
String userRole = user.getRole();
|
||||
if (!anyRole.contains(userRole)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
}
|
||||
// 必须有所有权限才通过
|
||||
if (StringUtils.isNotBlank(mustRole)) {
|
||||
UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
|
||||
if (mustUserRoleEnum == null) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
String userRole = user.getRole();
|
||||
if (UserRoleEnum.ADMIN.equals(mustUserRoleEnum)) {
|
||||
if (!mustRole.equals(userRole)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 通过权限校验,放行
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.yupi.project.aop;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 请求响应日志 AOP
|
||||
*
|
||||
* @author yupi
|
||||
**/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
public class LogInterceptor {
|
||||
|
||||
/**
|
||||
* 执行拦截
|
||||
*/
|
||||
@Around("execution(* com.yupi.project.controller.*.*(..))")
|
||||
public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
|
||||
// 计时
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
// 获取请求路径
|
||||
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
|
||||
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
// 生成请求唯一 id
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
String url = httpServletRequest.getRequestURI();
|
||||
// 获取请求参数
|
||||
Object[] args = point.getArgs();
|
||||
String reqParam = "[" + StringUtils.join(args, ", ") + "]";
|
||||
// 输出请求日志
|
||||
log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
|
||||
httpServletRequest.getRemoteHost(), reqParam);
|
||||
// 执行原方法
|
||||
Object result = point.proceed();
|
||||
// 输出响应日志
|
||||
stopWatch.stop();
|
||||
long totalTimeMillis = stopWatch.getTotalTimeMillis();
|
||||
log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.yupi.project.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 通用返回类
|
||||
*
|
||||
* @param <T>
|
||||
* @author yupi
|
||||
*/
|
||||
@Data
|
||||
public class BaseResponse<T> implements Serializable {
|
||||
|
||||
private int code;
|
||||
|
||||
private T data;
|
||||
|
||||
private String message;
|
||||
|
||||
public BaseResponse(int code, T data, String message) {
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BaseResponse(int code, T data) {
|
||||
this(code, data, "");
|
||||
}
|
||||
|
||||
public BaseResponse(ErrorCode errorCode) {
|
||||
this(errorCode.getCode(), null, errorCode.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yupi.project.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 删除请求
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
@Data
|
||||
public class DeleteRequest implements Serializable {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.yupi.project.common;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
public enum ErrorCode {
|
||||
|
||||
SUCCESS(0, "ok"),
|
||||
PARAMS_ERROR(40000, "请求参数错误"),
|
||||
NOT_LOGIN_ERROR(40100, "未登录"),
|
||||
NO_AUTH_ERROR(40101, "无权限"),
|
||||
NOT_FOUND_ERROR(40400, "请求数据不存在"),
|
||||
FORBIDDEN_ERROR(40300, "禁止访问"),
|
||||
SYSTEM_ERROR(50000, "系统内部异常"),
|
||||
OPERATION_ERROR(50001, "操作失败");
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
private final int code;
|
||||
|
||||
/**
|
||||
* 信息
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
ErrorCode(int code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yupi.project.common;
|
||||
|
||||
import com.yupi.project.constant.CommonConstant;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分页请求
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
@Data
|
||||
public class PageRequest {
|
||||
|
||||
/**
|
||||
* 当前页号
|
||||
*/
|
||||
private long current = 1;
|
||||
|
||||
/**
|
||||
* 页面大小
|
||||
*/
|
||||
private long pageSize = 10;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private String sortField;
|
||||
|
||||
/**
|
||||
* 排序顺序(默认升序)
|
||||
*/
|
||||
private String sortOrder = CommonConstant.SORT_ORDER_ASC;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.yupi.project.common;
|
||||
|
||||
/**
|
||||
* 返回工具类
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
public class ResultUtils {
|
||||
|
||||
/**
|
||||
* 成功
|
||||
*
|
||||
* @param data
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
public static <T> BaseResponse<T> success(T data) {
|
||||
return new BaseResponse<>(0, data, "ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败
|
||||
*
|
||||
* @param errorCode
|
||||
* @return
|
||||
*/
|
||||
public static BaseResponse error(ErrorCode errorCode) {
|
||||
return new BaseResponse<>(errorCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败
|
||||
*
|
||||
* @param code
|
||||
* @param message
|
||||
* @return
|
||||
*/
|
||||
public static BaseResponse error(int code, String message) {
|
||||
return new BaseResponse(code, null, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败
|
||||
*
|
||||
* @param errorCode
|
||||
* @return
|
||||
*/
|
||||
public static BaseResponse error(ErrorCode errorCode, String message) {
|
||||
return new BaseResponse(errorCode.getCode(), null, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.yupi.project.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
/**
|
||||
* Knife4j 接口文档配置
|
||||
* https://doc.xiaominfo.com/knife4j/documentation/get_start.html
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
@Configuration
|
||||
@EnableSwagger2
|
||||
@Profile("dev")
|
||||
public class Knife4jConfig {
|
||||
|
||||
@Bean
|
||||
public Docket defaultApi2() {
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
.apiInfo(new ApiInfoBuilder()
|
||||
.title("project-backend")
|
||||
.description("project-backend")
|
||||
.version("1.0")
|
||||
.build())
|
||||
.select()
|
||||
// 指定 Controller 扫描包路径
|
||||
.apis(RequestHandlerSelectors.basePackage("com.yupi.project.controller"))
|
||||
.paths(PathSelectors.any())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.yupi.project.config;
|
||||
|
||||
import com.yupi.project.config.properties.MqttProperties;
|
||||
import com.yupi.project.mqtt.MqttCallbackHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class MqttConfig {
|
||||
|
||||
private final MqttProperties mqttProperties;
|
||||
private final MqttCallbackHandler mqttCallbackHandler;
|
||||
|
||||
@Autowired // Autowire the MqttClient bean defined below
|
||||
private MqttClient mqttClient;
|
||||
|
||||
@Bean
|
||||
public MqttConnectOptions mqttConnectOptions() {
|
||||
MqttConnectOptions options = new MqttConnectOptions();
|
||||
if (StringUtils.hasText(mqttProperties.getUsername())) {
|
||||
options.setUserName(mqttProperties.getUsername());
|
||||
}
|
||||
if (StringUtils.hasText(mqttProperties.getPassword())) {
|
||||
options.setPassword(mqttProperties.getPassword().toCharArray());
|
||||
}
|
||||
options.setAutomaticReconnect(true); // Enable automatic reconnect
|
||||
options.setCleanSession(true); // Start with a clean session
|
||||
options.setConnectionTimeout(mqttProperties.getConnectionTimeout());
|
||||
options.setKeepAliveInterval(mqttProperties.getKeepAliveInterval());
|
||||
// options.setWill(...) // Configure Last Will and Testament if needed
|
||||
return options;
|
||||
}
|
||||
|
||||
@Bean // Bean method name will be the bean name by default: "mqttClientBean"
|
||||
public MqttClient mqttClientBean(MqttConnectOptions mqttConnectOptions) throws MqttException {
|
||||
String clientId = mqttProperties.getClientIdPrefix() + UUID.randomUUID().toString().replace("-", "");
|
||||
MqttClient client = new MqttClient(mqttProperties.getBrokerUrl(), clientId, new MemoryPersistence());
|
||||
client.setCallback(mqttCallbackHandler);
|
||||
// Pass the client instance to the handler so it can subscribe on connectComplete
|
||||
mqttCallbackHandler.setMqttClient(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void connect() {
|
||||
try {
|
||||
// Use the autowired mqttClient field
|
||||
if (this.mqttClient != null && !this.mqttClient.isConnected()) {
|
||||
log.info("Attempting to connect to MQTT broker: {} with client ID: {}", mqttProperties.getBrokerUrl(), this.mqttClient.getClientId());
|
||||
this.mqttClient.connect(mqttConnectOptions()); // mqttConnectOptions() provides the bean
|
||||
// Subscription logic is now in MqttCallbackHandler.connectComplete
|
||||
} else if (this.mqttClient == null) {
|
||||
log.error("MqttClient (autowired) is null, cannot connect.");
|
||||
}
|
||||
} catch (MqttException e) {
|
||||
log.error("Error connecting to MQTT broker: ", e);
|
||||
// Consider retry logic or application failure based on requirements
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void disconnect() {
|
||||
try {
|
||||
// Use the autowired mqttClient field
|
||||
if (this.mqttClient != null && this.mqttClient.isConnected()) {
|
||||
log.info("Disconnecting from MQTT broker: {}", this.mqttClient.getServerURI());
|
||||
this.mqttClient.disconnect();
|
||||
log.info("Successfully disconnected from MQTT broker.");
|
||||
}
|
||||
if (this.mqttClient != null) {
|
||||
this.mqttClient.close();
|
||||
}
|
||||
} catch (MqttException e) {
|
||||
log.error("Error disconnecting from MQTT broker: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.yupi.project.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 配置
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan("com.yupi.project.mapper")
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 拦截器配置
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.yupi.project.config;
|
||||
|
||||
import com.yupi.project.model.enums.UserRoleEnum;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 安全相关配置 (集成CORS)
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity // 启用Spring Security的Web安全支持
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
// 使用 BCrypt 进行密码加密
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 集成CORS配置
|
||||
.cors()
|
||||
.and()
|
||||
// 明确禁用CSRF保护
|
||||
.csrf().disable()
|
||||
.authorizeRequests(
|
||||
// 允许对登录和注册接口的匿名访问 (路径不带 /api 前缀)
|
||||
authorize -> authorize
|
||||
.antMatchers("/user/login", "/user/register", "/error").permitAll()
|
||||
.antMatchers("/user/current").authenticated()
|
||||
// 使用 hasAuthority 确保匹配 UserRoleEnum.ADMIN.getValue() 即 "admin"
|
||||
.antMatchers("/user/list").hasAuthority(UserRoleEnum.ADMIN.getValue())
|
||||
.antMatchers(HttpMethod.DELETE, "/user/delete/**").hasAuthority(UserRoleEnum.ADMIN.getValue())
|
||||
.antMatchers(HttpMethod.POST, "/user/admin/add").hasAuthority(UserRoleEnum.ADMIN.getValue())
|
||||
.antMatchers(HttpMethod.PUT, "/user/admin/update").hasAuthority(UserRoleEnum.ADMIN.getValue())
|
||||
// 其他所有请求都需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
// 禁用默认的表单登录页面
|
||||
.formLogin().disable()
|
||||
// 禁用HTTP Basic认证
|
||||
.httpBasic().disable();
|
||||
}
|
||||
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
// TODO: 在生产环境请显式指定允许的源,而不是 "*"
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList("*")); // 允许所有源模式
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("*")); // 允许所有头
|
||||
configuration.setAllowCredentials(true); // 允许发送 Cookie
|
||||
configuration.setMaxAge(3600L); // 预检请求的有效期
|
||||
// configuration.setExposedHeaders(Arrays.asList("YourCustomHeader")); // 如果需要暴露自定义头
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration); // 对所有路径应用配置
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yupi.project.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "mqtt")
|
||||
public class MqttProperties {
|
||||
|
||||
private String brokerUrl;
|
||||
private String username;
|
||||
private String password;
|
||||
private String clientIdPrefix;
|
||||
private int defaultQos;
|
||||
private int connectionTimeout;
|
||||
private int keepAliveInterval;
|
||||
private String commandTopicBase;
|
||||
private String statusTopicBase;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yupi.project.constant;
|
||||
|
||||
/**
|
||||
* 通用常量
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
public interface CommonConstant {
|
||||
|
||||
/**
|
||||
* 升序
|
||||
*/
|
||||
String SORT_ORDER_ASC = "ascend";
|
||||
|
||||
/**
|
||||
* 降序
|
||||
*/
|
||||
String SORT_ORDER_DESC = " descend";
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yupi.project.constant;
|
||||
|
||||
/**
|
||||
* 用户常量
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
public interface UserConstant {
|
||||
|
||||
/**
|
||||
* 用户登录态键
|
||||
*/
|
||||
String USER_LOGIN_STATE = "userLoginState";
|
||||
|
||||
/**
|
||||
* 系统用户 id(虚拟用户)
|
||||
*/
|
||||
long SYSTEM_USER_ID = 0;
|
||||
|
||||
// region 权限
|
||||
|
||||
/**
|
||||
* 默认角色
|
||||
*/
|
||||
String DEFAULT_ROLE = "user";
|
||||
|
||||
/**
|
||||
* 管理员角色
|
||||
*/
|
||||
String ADMIN_ROLE = "admin";
|
||||
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.yupi.project.controller;
|
||||
|
||||
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.model.dto.user.UserLoginRequest;
|
||||
import com.yupi.project.model.dto.user.UserRegisterRequest;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import com.yupi.project.model.enums.UserRoleEnum;
|
||||
import com.yupi.project.service.UserService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
// --- 登录注册相关 --- //
|
||||
|
||||
@PostMapping("/register")
|
||||
public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
|
||||
if (userRegisterRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
String username = userRegisterRequest.getUsername();
|
||||
String password = userRegisterRequest.getPassword();
|
||||
String checkPassword = userRegisterRequest.getCheckPassword();
|
||||
if (StringUtils.isAnyBlank(username, password, checkPassword)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
|
||||
}
|
||||
long result = userService.userRegister(username, password, checkPassword);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
|
||||
if (userLoginRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
String username = userLoginRequest.getUsername();
|
||||
String password = userLoginRequest.getPassword();
|
||||
if (StringUtils.isAnyBlank(username, password)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
|
||||
}
|
||||
User user = userService.userLogin(username, password, request);
|
||||
return ResultUtils.success(user);
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
boolean result = userService.userLogout(request);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/current")
|
||||
public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
|
||||
User currentUser = userService.getCurrentUser(request);
|
||||
// getCurrentUser 内部已处理未登录情况,并返回脱敏用户
|
||||
return ResultUtils.success(currentUser);
|
||||
}
|
||||
|
||||
// --- 管理员功能 --- //
|
||||
|
||||
@GetMapping("/list")
|
||||
public BaseResponse<List<User>> listUsers(HttpServletRequest request) {
|
||||
// 权限校验 (在 SecurityConfig 中配置,这里可以简化或移除,但保留显式检查作为双重保险)
|
||||
User currentUser = userService.getCurrentUser(request); // 获取当前用户以检查角色
|
||||
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
|
||||
}
|
||||
List<User> userList = userService.listUsers(); // 调用新的 service 方法,它已经处理了脱敏
|
||||
return ResultUtils.success(userList);
|
||||
}
|
||||
|
||||
// 新增管理员删除用户接口
|
||||
@DeleteMapping("/delete/{userId}")
|
||||
public BaseResponse<Boolean> adminDeleteUser(@PathVariable Long userId, HttpServletRequest request) {
|
||||
// 权限校验 (虽然 SecurityConfig 会拦截,但 Controller 层再校验一次更安全)
|
||||
User currentUser = userService.getCurrentUser(request);
|
||||
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
|
||||
}
|
||||
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID参数错误");
|
||||
}
|
||||
|
||||
// 防止管理员误删自己 (可选逻辑)
|
||||
if (currentUser.getId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "不能删除当前登录的管理员账户");
|
||||
}
|
||||
|
||||
boolean result = userService.adminDeleteUser(userId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
// 新增管理员添加用户接口
|
||||
@PostMapping("/admin/add")
|
||||
public BaseResponse<Long> adminAddUser(@RequestBody com.yupi.project.model.dto.user.UserAdminAddRequest addRequest, HttpServletRequest request) {
|
||||
// 权限校验
|
||||
User currentUser = userService.getCurrentUser(request);
|
||||
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
|
||||
}
|
||||
|
||||
if (addRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
|
||||
}
|
||||
|
||||
long newUserId = userService.adminAddUser(addRequest);
|
||||
return ResultUtils.success(newUserId);
|
||||
}
|
||||
|
||||
// 新增管理员更新用户接口
|
||||
@PutMapping("/admin/update")
|
||||
public BaseResponse<Boolean> adminUpdateUser(@RequestBody com.yupi.project.model.dto.user.UserAdminUpdateRequest updateRequest, HttpServletRequest request) {
|
||||
// 权限校验
|
||||
User currentUser = userService.getCurrentUser(request);
|
||||
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
|
||||
}
|
||||
|
||||
if (updateRequest == null || updateRequest.getId() == null || updateRequest.getId() <=0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数错误,必须提供用户ID");
|
||||
}
|
||||
|
||||
// 防止管理员通过此接口修改自己的角色把自己改成普通用户,或修改关键的ADMIN账户 (可选)
|
||||
// 如果要更新的用户是当前登录的管理员,并且尝试修改角色为非admin,则拒绝
|
||||
if (currentUser.getId().equals(updateRequest.getId()) &&
|
||||
StringUtils.isNotBlank(updateRequest.getRole()) &&
|
||||
!UserRoleEnum.ADMIN.getValue().equals(updateRequest.getRole())) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "管理员不能将自己的角色修改为非管理员");
|
||||
}
|
||||
|
||||
boolean result = userService.adminUpdateUser(updateRequest);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yupi.project.exception;
|
||||
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
|
||||
/**
|
||||
* 自定义异常类
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final int code;
|
||||
|
||||
public BusinessException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yupi.project.exception;
|
||||
|
||||
import com.yupi.project.common.BaseResponse;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.common.ResultUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
|
||||
log.error("businessException: " + e.getMessage(), e);
|
||||
return ResultUtils.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
|
||||
log.error("runtimeException", e);
|
||||
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.yupi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yupi.project.model.entity.User;
|
||||
|
||||
/**
|
||||
* @description 针对表【user(用户表)】的数据库操作Mapper
|
||||
* @createDate 2023-11-24 10:00:00
|
||||
* @Entity com.yupi.project.model.entity.User
|
||||
*/
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.yupi.project.model.dto.user;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 管理员添加用户请求体
|
||||
*/
|
||||
@Data
|
||||
public class UserAdminAddRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 用户角色 (例如 "user", "admin")
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 初始余额 (可选)
|
||||
*/
|
||||
private BigDecimal balance;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yupi.project.model.dto.user;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 管理员更新用户请求体
|
||||
*/
|
||||
@Data
|
||||
public class UserAdminUpdateRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID (必须)
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户名 (可选,如果提供则尝试更新)
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 新密码 (可选,如果提供则重置密码)
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 用户角色 (可选,如果提供则尝试更新)
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 余额 (可选,如果提供则尝试更新)
|
||||
*/
|
||||
private BigDecimal balance;
|
||||
|
||||
// isDeleted 不应由管理员直接通过此接口修改,应通过删除接口
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.yupi.project.model.dto.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户登录请求体
|
||||
*/
|
||||
@Data
|
||||
public class UserLoginRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 3191241716373120793L;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.yupi.project.model.dto.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户注册请求体
|
||||
*/
|
||||
@Data
|
||||
public class UserRegisterRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 3191241716373120793L;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private String checkPassword;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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(value ="user")
|
||||
@Data
|
||||
public class User implements Serializable {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码 (加密存储)
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 角色 (user/admin)
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 账户余额
|
||||
*/
|
||||
private BigDecimal balance;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 逻辑删除标志 (0:未删, 1:已删)
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.yupi.project.model.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
/**
|
||||
* 用户角色枚举
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
public enum UserRoleEnum {
|
||||
|
||||
USER("user", "用户"),
|
||||
ADMIN("admin", "管理员"),
|
||||
BAN("ban", "被封号");
|
||||
|
||||
private final String value;
|
||||
|
||||
private final String text;
|
||||
|
||||
UserRoleEnum(String value, String text) {
|
||||
this.value = value;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static List<String> getValues() {
|
||||
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 value 获取枚举
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static UserRoleEnum getEnumByValue(String value) {
|
||||
if (ObjectUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
for (UserRoleEnum anEnum : UserRoleEnum.values()) {
|
||||
if (anEnum.value.equals(value)) {
|
||||
return anEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.yupi.project.mqtt;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
|
||||
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||
import org.springframework.stereotype.Component;
|
||||
// import com.yupi.project.service.MqttMessageHandler; // Will be uncommented and used later
|
||||
import com.yupi.project.config.properties.MqttProperties;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MqttCallbackHandler implements MqttCallbackExtended {
|
||||
|
||||
// private final MqttMessageHandler mqttMessageHandler; // Will be uncommented and used later
|
||||
private final MqttProperties mqttProperties;
|
||||
private MqttClient mqttClient; // Setter needed or passed in constructor/method
|
||||
|
||||
public void setMqttClient(MqttClient mqttClient) {
|
||||
this.mqttClient = mqttClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectComplete(boolean reconnect, String serverURI) {
|
||||
log.info("MQTT connection {} to broker: {}", reconnect ? "re-established" : "established", serverURI);
|
||||
// Subscribe to the status topic upon connection/reconnection
|
||||
try {
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
String statusTopic = mqttProperties.getStatusTopicBase() + "/+"; // Subscribe to all robot statuses
|
||||
mqttClient.subscribe(statusTopic, mqttProperties.getDefaultQos());
|
||||
log.info("Subscribed to MQTT topic: {}", statusTopic);
|
||||
} else {
|
||||
log.warn("MQTT client not available or not connected, cannot subscribe to topic.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error subscribing to MQTT topic after connection complete: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionLost(Throwable cause) {
|
||||
log.error("MQTT connection lost!", cause);
|
||||
// Automatic reconnect is handled by MqttConnectOptions
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageArrived(String topic, MqttMessage message) throws Exception {
|
||||
String payload = new String(message.getPayload());
|
||||
log.debug("MQTT message arrived. Topic: {}, QoS: {}, Payload:\n{}", topic, message.getQos(), payload);
|
||||
|
||||
// TODO: Implement application-level authentication/validation of the message payload here or in MqttMessageHandler
|
||||
|
||||
// try {
|
||||
// mqttMessageHandler.handleStatusUpdate(topic, payload); // Will be uncommented and used later
|
||||
// } catch (Exception e) {
|
||||
// log.error("Error handling MQTT message for topic {}: ", topic, e);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliveryComplete(IMqttDeliveryToken token) {
|
||||
// This callback is invoked when a message published by this client has been successfully delivered
|
||||
// Used for QoS 1 and 2. For QoS 0, it's called after the message has been handed to the network.
|
||||
try {
|
||||
if (token.isComplete() && token.getMessage() != null) {
|
||||
log.trace("MQTT message delivery complete. Message ID: {}, Payload: {}", token.getMessageId(), new String(token.getMessage().getPayload()));
|
||||
} else if (token.isComplete()){
|
||||
log.trace("MQTT message delivery complete. Message ID: {}", token.getMessageId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error in MQTT deliveryComplete callback: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.yupi.project.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description 针对表【user(用户表)】的数据库操作Service
|
||||
* @createDate 2023-11-24 10:05:00
|
||||
*/
|
||||
public interface UserService extends IService<User> {
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @param checkPassword 校验密码
|
||||
* @return 新用户 id
|
||||
*/
|
||||
long userRegister(String username, String password, String checkPassword);
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @param request HttpServletRequest 用于操作 session
|
||||
* @return 脱敏后的用户信息
|
||||
*/
|
||||
User userLogin(String username, String password, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前登录用户
|
||||
*
|
||||
* @param request HttpServletRequest
|
||||
* @return 当前登录用户,未登录则返回 null
|
||||
*/
|
||||
User getCurrentUser(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 用户注销
|
||||
*
|
||||
* @param request HttpServletRequest
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean userLogout(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 获取脱敏的用户信息
|
||||
*
|
||||
* @param originUser 原始用户信息
|
||||
* @return 脱敏后的用户信息
|
||||
*/
|
||||
User getSafetyUser(User originUser);
|
||||
|
||||
/**
|
||||
* 扣减用户余额 (需要保证线程安全和数据一致性)
|
||||
* @param userId 用户ID
|
||||
* @param amount 扣减金额 (正数)
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean deductBalance(Long userId, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 增加用户余额 (需要保证线程安全和数据一致性)
|
||||
* @param userId 用户ID
|
||||
* @param amount 增加金额 (正数)
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean addBalance(Long userId, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 获取用户列表 (仅管理员)
|
||||
*
|
||||
* @return 脱敏后的用户列表
|
||||
*/
|
||||
List<User> listUsers();
|
||||
|
||||
/**
|
||||
* 管理员删除用户 (逻辑删除)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean adminDeleteUser(Long userId);
|
||||
|
||||
/**
|
||||
* 管理员添加新用户
|
||||
*
|
||||
* @param addRequest 包含新用户信息的请求体
|
||||
* @return 新用户的ID
|
||||
*/
|
||||
long adminAddUser(com.yupi.project.model.dto.user.UserAdminAddRequest addRequest);
|
||||
|
||||
/**
|
||||
* 管理员更新用户信息
|
||||
*
|
||||
* @param updateRequest 包含待更新用户信息的请求体
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
boolean adminUpdateUser(com.yupi.project.model.dto.user.UserAdminUpdateRequest updateRequest);
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
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.constant.UserConstant;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.mapper.UserMapper;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import com.yupi.project.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
implements UserService {
|
||||
|
||||
@Resource
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Resource
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
// 用户名校验正则:允许字母、数字、下划线,长度4到16位
|
||||
private static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]{4,16}$";
|
||||
// 密码校验正则:至少包含字母和数字,长度至少6位
|
||||
private static final String PASSWORD_PATTERN = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,}$";
|
||||
|
||||
@Override
|
||||
public long userRegister(String username, String password, String checkPassword) {
|
||||
// 1. 参数校验
|
||||
if (StringUtils.isAnyBlank(username, password, checkPassword)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
|
||||
}
|
||||
if (!Pattern.matches(USERNAME_PATTERN, username)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名不符合规范(4-16位字母、数字、下划线)");
|
||||
}
|
||||
if (password.length() < 6) { // 使用更宽松的校验,正则校验留给前端
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码长度不能小于6位");
|
||||
}
|
||||
if (!password.equals(checkPassword)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
|
||||
}
|
||||
|
||||
synchronized (username.intern()) {
|
||||
// 2. 检查用户名是否已存在
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", username);
|
||||
long count = this.count(queryWrapper);
|
||||
if (count > 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名已存在");
|
||||
}
|
||||
|
||||
// 3. 密码加密
|
||||
String encodedPassword = passwordEncoder.encode(password);
|
||||
|
||||
// 4. 创建用户
|
||||
User userToSave = new User();
|
||||
userToSave.setUsername(username);
|
||||
userToSave.setPassword(encodedPassword);
|
||||
userToSave.setRole(UserConstant.DEFAULT_ROLE); // 默认角色 'user'
|
||||
userToSave.setBalance(BigDecimal.ZERO); // 初始余额为0
|
||||
boolean saveResult = this.save(userToSave);
|
||||
if (!saveResult) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
|
||||
}
|
||||
return userToSave.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public User userLogin(String username, String password, HttpServletRequest request) {
|
||||
// 1. 参数校验
|
||||
if (StringUtils.isAnyBlank(username, password)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名或密码不能为空");
|
||||
}
|
||||
if (!Pattern.matches(USERNAME_PATTERN, username)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名格式错误");
|
||||
}
|
||||
|
||||
// 2. 查询用户
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", username);
|
||||
User user = this.getOne(queryWrapper);
|
||||
|
||||
// 3. 校验密码 和 用户存在性
|
||||
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名或密码错误");
|
||||
}
|
||||
|
||||
// 4. 用户脱敏
|
||||
User safetyUser = getSafetyUser(user);
|
||||
|
||||
// 5. 创建 Authentication 对象并设置到 SecurityContext
|
||||
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(user.getRole())) { // 确保角色不为空
|
||||
authorities.add(new SimpleGrantedAuthority(user.getRole()));
|
||||
}
|
||||
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(safetyUser, null, authorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return safetyUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getCurrentUser(HttpServletRequest request) {
|
||||
// 优先从 SecurityContextHolder 获取认证信息
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated() && !(authentication.getPrincipal() instanceof String && authentication.getPrincipal().equals("anonymousUser"))) {
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof User) {
|
||||
return (User) principal; // principal 已经是 safetyUser
|
||||
} else if (principal instanceof org.springframework.security.core.userdetails.User) {
|
||||
// 如果 principal 是 Spring Security 的 User (不太可能在这里,因为我们设置的是 safetyUser)
|
||||
// 需要转换或重新查询
|
||||
// For now, assume it's our User object based on login logic
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 SecurityContextHolder 中没有,尝试从 session (旧逻辑,作为后备或移除)
|
||||
Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
|
||||
if (userObj instanceof User) {
|
||||
// 最好在这里也验证一下数据库中的用户状态,或者确保session中的信息足够可信
|
||||
return (User) userObj;
|
||||
}
|
||||
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean userLogout(HttpServletRequest request) {
|
||||
SecurityContextHolder.clearContext(); // 清除安全上下文
|
||||
if (request.getSession(false) != null) { // 获取session但不创建新的
|
||||
request.getSession(false).invalidate(); // 使session无效
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getSafetyUser(User originUser) {
|
||||
if (originUser == null) {
|
||||
return null;
|
||||
}
|
||||
User safetyUser = new User();
|
||||
BeanUtils.copyProperties(originUser, safetyUser, "password");
|
||||
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));
|
||||
return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 新增管理员删除用户实现
|
||||
@Override
|
||||
public boolean adminDeleteUser(Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID参数错误");
|
||||
}
|
||||
User user = this.getById(userId);
|
||||
if (user == null || user.getIsDeleted() == 1) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在或已被删除");
|
||||
}
|
||||
boolean result = this.removeById(userId);
|
||||
if (!result) {
|
||||
log.warn("Attempted to delete user that might already be deleted or does not exist, userId: {}", userId);
|
||||
User checkUser = this.getById(userId);
|
||||
if(checkUser == null || checkUser.getIsDeleted() == 1){
|
||||
return true;
|
||||
}
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除用户失败,数据库操作异常");
|
||||
}
|
||||
log.info("User with id: {} logically deleted by admin.", userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 新增管理员添加用户实现
|
||||
@Override
|
||||
@Transactional // 确保操作的原子性
|
||||
public long adminAddUser(com.yupi.project.model.dto.user.UserAdminAddRequest addRequest) {
|
||||
String username = addRequest.getUsername();
|
||||
String password = addRequest.getPassword();
|
||||
String role = addRequest.getRole();
|
||||
BigDecimal balance = addRequest.getBalance();
|
||||
|
||||
// 1. 参数校验
|
||||
if (StringUtils.isAnyBlank(username, password, role)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名、密码和角色不能为空");
|
||||
}
|
||||
if (!Pattern.matches(USERNAME_PATTERN, username)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名不符合规范(4-16位字母、数字、下划线)");
|
||||
}
|
||||
if (password.length() < 6) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码长度不能小于6位");
|
||||
}
|
||||
// 校验角色是否合法 (例如:必须是 UserRoleEnum 中的值)
|
||||
try {
|
||||
com.yupi.project.model.enums.UserRoleEnum.valueOf(role.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的用户角色");
|
||||
}
|
||||
if (balance != null && balance.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "初始余额不能为负数");
|
||||
}
|
||||
|
||||
synchronized (username.intern()) {
|
||||
// 2. 检查用户名是否已存在
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", username);
|
||||
if (this.count(queryWrapper) > 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名已存在");
|
||||
}
|
||||
|
||||
// 3. 密码加密
|
||||
String encodedPassword = passwordEncoder.encode(password);
|
||||
|
||||
// 4. 创建用户
|
||||
User newUser = new User();
|
||||
newUser.setUsername(username);
|
||||
newUser.setPassword(encodedPassword);
|
||||
newUser.setRole(role); // 直接使用管理员指定的角色
|
||||
newUser.setBalance(balance != null ? balance : BigDecimal.ZERO); // 如果未提供余额,默认为0
|
||||
|
||||
boolean saveResult = this.save(newUser);
|
||||
if (!saveResult) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "添加用户失败,数据库错误");
|
||||
}
|
||||
log.info("Admin added new user: {}, ID: {}, Role: {}", username, newUser.getId(), role);
|
||||
return newUser.getId();
|
||||
}
|
||||
}
|
||||
|
||||
// 新增管理员更新用户实现
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean adminUpdateUser(com.yupi.project.model.dto.user.UserAdminUpdateRequest updateRequest) {
|
||||
Long userId = updateRequest.getId();
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID不能为空");
|
||||
}
|
||||
|
||||
User existingUser = this.getById(userId);
|
||||
if (existingUser == null || existingUser.getIsDeleted() == 1) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在或已被删除");
|
||||
}
|
||||
|
||||
boolean needsUpdate = false;
|
||||
User userToUpdate = new User();
|
||||
userToUpdate.setId(userId);
|
||||
|
||||
// 更新用户名 (如果提供)
|
||||
if (StringUtils.isNotBlank(updateRequest.getUsername())) {
|
||||
String newUsername = updateRequest.getUsername();
|
||||
if (!Pattern.matches(USERNAME_PATTERN, newUsername)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "新用户名不符合规范");
|
||||
}
|
||||
// 检查新用户名是否与现有其他用户冲突
|
||||
if (!existingUser.getUsername().equals(newUsername)) {
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", newUsername).ne("id", userId);
|
||||
if (this.count(queryWrapper) > 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "新用户名已被其他用户使用");
|
||||
}
|
||||
userToUpdate.setUsername(newUsername);
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新密码 (如果提供)
|
||||
if (StringUtils.isNotBlank(updateRequest.getPassword())) {
|
||||
String newPassword = updateRequest.getPassword();
|
||||
if (newPassword.length() < 6) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "新密码长度不能小于6位");
|
||||
}
|
||||
userToUpdate.setPassword(passwordEncoder.encode(newPassword));
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
// 更新角色 (如果提供)
|
||||
if (StringUtils.isNotBlank(updateRequest.getRole())) {
|
||||
String newRole = updateRequest.getRole();
|
||||
try {
|
||||
com.yupi.project.model.enums.UserRoleEnum.valueOf(newRole.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的新用户角色");
|
||||
}
|
||||
userToUpdate.setRole(newRole);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
// 更新余额 (如果提供)
|
||||
if (updateRequest.getBalance() != null) {
|
||||
BigDecimal newBalance = updateRequest.getBalance();
|
||||
if (newBalance.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "余额不能为负数");
|
||||
}
|
||||
userToUpdate.setBalance(newBalance);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (!needsUpdate) {
|
||||
log.info("No fields to update for user ID: {}", userId);
|
||||
return true; // 没有字段需要更新,也视为成功
|
||||
}
|
||||
|
||||
boolean updateResult = this.updateById(userToUpdate);
|
||||
if (!updateResult) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新用户信息失败,数据库操作异常");
|
||||
}
|
||||
log.info("User ID: {} updated by admin. Fields updated: username={}, role={}, balance={}, password_changed={}",
|
||||
userId,
|
||||
userToUpdate.getUsername() != null,
|
||||
userToUpdate.getRole() != null,
|
||||
userToUpdate.getBalance() != null,
|
||||
userToUpdate.getPassword() != null
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/my_db
|
||||
username: root
|
||||
password: 123456
|
||||
62
springboot-init-main/src/main/resources/application.yml
Normal file
62
springboot-init-main/src/main/resources/application.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
spring:
|
||||
application:
|
||||
name: mqtt-charging-system
|
||||
# DataSource Config
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://yuyun-us1.stormrain.cn:3306/mqtt_power?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: mysql_a4MQ4P
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER
|
||||
# session 失效时间(秒)
|
||||
session:
|
||||
timeout: 86400
|
||||
server:
|
||||
port: 7529
|
||||
servlet:
|
||||
context-path: /api
|
||||
session:
|
||||
timeout: 86400 # 设置session的过期时间,单位为秒,这里设置为1天
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
|
||||
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
|
||||
# Logging configuration
|
||||
logging:
|
||||
level:
|
||||
# Set root logger level (e.g., INFO, WARN, ERROR, DEBUG)
|
||||
root: INFO
|
||||
# Set specific package levels
|
||||
com.yupi.project: DEBUG # Example: Set your project's base package to DEBUG
|
||||
org.springframework.web: INFO # Set Spring Web logging level
|
||||
org.springframework.security: DEBUG # Enable Spring Security DEBUG logging
|
||||
org.mybatis: INFO # Set MyBatis logging level
|
||||
# ... other specific loggers
|
||||
#file:
|
||||
#name: logs/application.log # Log file name
|
||||
#path: ./logs # Log file path
|
||||
#pattern:
|
||||
#console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
#file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# ===================================================================
|
||||
# MQTT Configurations
|
||||
# ===================================================================
|
||||
mqtt:
|
||||
broker-url: tcp://broker.emqx.io:1883
|
||||
username: # Public broker, no credentials specified for connection
|
||||
password: # Public broker, no credentials specified for connection
|
||||
client-id-prefix: backend-yupi-mqtt-power- # Unique client ID prefix for our project
|
||||
default-qos: 1 # Default Quality of Service (0, 1, 2)
|
||||
connection-timeout: 30 # Connection timeout in seconds
|
||||
keep-alive-interval: 60 # Keep alive interval in seconds
|
||||
command-topic-base: yupi_mqtt_power_project/robot/command # Prefixed base topic for sending commands
|
||||
status-topic-base: yupi_mqtt_power_project/robot/status # Prefixed base topic for receiving status
|
||||
1
springboot-init-main/src/main/resources/banner.txt
Normal file
1
springboot-init-main/src/main/resources/banner.txt
Normal file
@@ -0,0 +1 @@
|
||||
我的项目 by 程序员鱼皮 https://github.com/liyupi
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.yupi.project.mapper.UserMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.yupi.project.model.entity.User">
|
||||
<id property="id" column="id" jdbcType="BIGINT"/>
|
||||
<result property="userName" column="userName" jdbcType="VARCHAR"/>
|
||||
<result property="userAccount" column="userAccount" jdbcType="VARCHAR"/>
|
||||
<result property="userAvatar" column="userAvatar" jdbcType="VARCHAR"/>
|
||||
<result property="gender" column="gender" jdbcType="TINYINT"/>
|
||||
<result property="userRole" column="userRole" jdbcType="VARCHAR"/>
|
||||
<result property="userPassword" column="userPassword" jdbcType="VARCHAR"/>
|
||||
<result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
|
||||
<result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
|
||||
<result property="isDelete" column="isDelete" jdbcType="TINYINT"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,userName,userAccount,
|
||||
userAvatar,gender,userRole,
|
||||
userPassword,createTime,updateTime,
|
||||
isDelete
|
||||
</sql>
|
||||
</mapper>
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "mqtt",
|
||||
"type": "com.yupi.project.config.properties.MqttProperties",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
}
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"name": "mqtt.broker-url",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.client-id-prefix",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.command-topic-base",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.connection-timeout",
|
||||
"type": "java.lang.Integer",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.default-qos",
|
||||
"type": "java.lang.Integer",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.keep-alive-interval",
|
||||
"type": "java.lang.Integer",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.password",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.status-topic-base",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.username",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
}
|
||||
],
|
||||
"hints": []
|
||||
}
|
||||
6
springboot-init-main/target/classes/application-prod.yml
Normal file
6
springboot-init-main/target/classes/application-prod.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/my_db
|
||||
username: root
|
||||
password: 123456
|
||||
62
springboot-init-main/target/classes/application.yml
Normal file
62
springboot-init-main/target/classes/application.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
spring:
|
||||
application:
|
||||
name: mqtt-charging-system
|
||||
# DataSource Config
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://yuyun-us1.stormrain.cn:3306/mqtt_power?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: mysql_a4MQ4P
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER
|
||||
# session 失效时间(秒)
|
||||
session:
|
||||
timeout: 86400
|
||||
server:
|
||||
port: 7529
|
||||
servlet:
|
||||
context-path: /api
|
||||
session:
|
||||
timeout: 86400 # 设置session的过期时间,单位为秒,这里设置为1天
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
|
||||
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
|
||||
# Logging configuration
|
||||
logging:
|
||||
level:
|
||||
# Set root logger level (e.g., INFO, WARN, ERROR, DEBUG)
|
||||
root: INFO
|
||||
# Set specific package levels
|
||||
com.yupi.project: DEBUG # Example: Set your project's base package to DEBUG
|
||||
org.springframework.web: INFO # Set Spring Web logging level
|
||||
org.springframework.security: DEBUG # Enable Spring Security DEBUG logging
|
||||
org.mybatis: INFO # Set MyBatis logging level
|
||||
# ... other specific loggers
|
||||
#file:
|
||||
#name: logs/application.log # Log file name
|
||||
#path: ./logs # Log file path
|
||||
#pattern:
|
||||
#console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
#file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# ===================================================================
|
||||
# MQTT Configurations
|
||||
# ===================================================================
|
||||
mqtt:
|
||||
broker-url: tcp://broker.emqx.io:1883
|
||||
username: # Public broker, no credentials specified for connection
|
||||
password: # Public broker, no credentials specified for connection
|
||||
client-id-prefix: backend-yupi-mqtt-power- # Unique client ID prefix for our project
|
||||
default-qos: 1 # Default Quality of Service (0, 1, 2)
|
||||
connection-timeout: 30 # Connection timeout in seconds
|
||||
keep-alive-interval: 60 # Keep alive interval in seconds
|
||||
command-topic-base: yupi_mqtt_power_project/robot/command # Prefixed base topic for sending commands
|
||||
status-topic-base: yupi_mqtt_power_project/robot/status # Prefixed base topic for receiving status
|
||||
1
springboot-init-main/target/classes/banner.txt
Normal file
1
springboot-init-main/target/classes/banner.txt
Normal file
@@ -0,0 +1 @@
|
||||
我的项目 by 程序员鱼皮 https://github.com/liyupi
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
26
springboot-init-main/target/classes/mapper/UserMapper.xml
Normal file
26
springboot-init-main/target/classes/mapper/UserMapper.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.yupi.project.mapper.UserMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.yupi.project.model.entity.User">
|
||||
<id property="id" column="id" jdbcType="BIGINT"/>
|
||||
<result property="userName" column="userName" jdbcType="VARCHAR"/>
|
||||
<result property="userAccount" column="userAccount" jdbcType="VARCHAR"/>
|
||||
<result property="userAvatar" column="userAvatar" jdbcType="VARCHAR"/>
|
||||
<result property="gender" column="gender" jdbcType="TINYINT"/>
|
||||
<result property="userRole" column="userRole" jdbcType="VARCHAR"/>
|
||||
<result property="userPassword" column="userPassword" jdbcType="VARCHAR"/>
|
||||
<result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
|
||||
<result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
|
||||
<result property="isDelete" column="isDelete" jdbcType="TINYINT"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,userName,userAccount,
|
||||
userAvatar,gender,userRole,
|
||||
userPassword,createTime,updateTime,
|
||||
isDelete
|
||||
</sql>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user