mqtt消息记录开发完成
This commit is contained in:
208
LogBook.md
208
LogBook.md
@@ -1,3 +1,33 @@
|
|||||||
|
## 2024-08-08 (MQTT通信日志功能 - 开发完成)
|
||||||
|
|
||||||
|
- **概述**: MQTT通信日志功能模块开发完成。该模块旨在记录系统与MQTT代理之间的所有通信消息,并提供前端界面供管理员查询和审计。
|
||||||
|
|
||||||
|
- **后端**:
|
||||||
|
- 实现了独立的MQTT客户端用于监听所有相关主题的上行消息。
|
||||||
|
- 通过AOP切面记录了业务系统主动发送的下行消息。
|
||||||
|
- 消息数据异步存入 `mqtt_communication_log` 表,确保不阻塞主业务线程。
|
||||||
|
- 提供了分页查询API (`/api/admin/mqtt-log/list/page`),支持多种条件筛选和排序。
|
||||||
|
- 关键配置项(如日志开关、订阅主题、客户端ID前缀、异步线程池参数)均可通过 `application.yml` 进行配置。
|
||||||
|
- 修复了开发过程中的Bean注入冲突和API路径错误。
|
||||||
|
|
||||||
|
- **前端 (`charging_web_app`)**:
|
||||||
|
- 创建了MQTT日志查看页面 (`/admin/mqtt-logs`)。
|
||||||
|
- 实现了基于Ant Design的查询表单和数据表格,支持分页、条件查询和排序。
|
||||||
|
- 表格中的关键字段(如方向、Payload格式、是否保留)已进行本地化中文显示。
|
||||||
|
- 解决了API调用404错误和数据无法在表格中正确显示的问题。
|
||||||
|
- 在管理员导航栏中添加了页面入口。
|
||||||
|
|
||||||
|
- **核心功能确认**: `payload` 字段记录的是MQTT消息队列中的原始发送/接收内容。
|
||||||
|
|
||||||
|
- **当前状态**: 功能已可基本使用。后端日志记录和API服务正常,前端页面可展示和查询日志数据。
|
||||||
|
|
||||||
|
- **后续待办**:
|
||||||
|
- 处理前端控制台中剩余的Ant Design Modal `destroyOnClose` 废弃警告 (位于 `DashboardPage`)。
|
||||||
|
- 调研并处理Ant Design v5与React 19的兼容性警告。
|
||||||
|
- 进行更全面的功能测试,包括各种边界条件和长时间运行的稳定性。
|
||||||
|
- (可选) 进一步优化Payload的展示,例如对JSON格式的Payload进行美化或提供折叠/展开功能。
|
||||||
|
- (可选) 根据实际需求,考虑日志数据的定期归档或清理策略的实现。
|
||||||
|
|
||||||
## 2024-07-29 (用户激活码功能 - 后端主体)
|
## 2024-07-29 (用户激活码功能 - 后端主体)
|
||||||
|
|
||||||
- **核心业务逻辑实现 (Service & Impl)**:
|
- **核心业务逻辑实现 (Service & Impl)**:
|
||||||
@@ -31,3 +61,181 @@
|
|||||||
- 编写单元测试和集成测试。
|
- 编写单元测试和集成测试。
|
||||||
- 前端页面对接和开发。
|
- 前端页面对接和开发。
|
||||||
- 根据实际测试反馈进一步完善错误处理和日志。
|
- 根据实际测试反馈进一步完善错误处理和日志。
|
||||||
|
|
||||||
|
## 2024-08-05 (MQTT通信日志系统 - 开发方案)
|
||||||
|
|
||||||
|
- **开发方案文档创建**:
|
||||||
|
- 在 `springboot-init-main/doc/` 目录下创建了 `mqtt_communication_log_plan.md` 文件,详细描述了MQTT通信日志系统的设计与实现方案。
|
||||||
|
- 该系统旨在完整记录单片机与服务器之间的所有MQTT消息通信,用于通信可视化、故障诊断、性能分析和安全审计。
|
||||||
|
|
||||||
|
- **系统定位与设计原则**:
|
||||||
|
- 纯日志记录系统,不干扰正常业务流程,不消费消息队列中的数据
|
||||||
|
- 采用"监听者模式",通过独立的MQTT客户端进行旁路监听
|
||||||
|
- 异步记录消息,确保不影响主业务性能
|
||||||
|
|
||||||
|
- **核心架构设计**:
|
||||||
|
- 利用已有的 `mqtt_communication_log` 数据表存储日志数据
|
||||||
|
- 设计了 `MqttCommunicationLogger`、`MqttLogService` 和 `MqttLogController` 三个核心组件
|
||||||
|
- 使用AOP技术拦截发送消息,记录下行消息内容
|
||||||
|
- 通过独立MQTT客户端订阅同样的主题,记录上行消息内容
|
||||||
|
|
||||||
|
- **技术实现细节**:
|
||||||
|
- 设计了消息监听、记录、解析和关联的具体实现方法
|
||||||
|
- 提供了完整的配置项和部署说明
|
||||||
|
- 包含数据库维护策略,避免日志数据膨胀
|
||||||
|
|
||||||
|
- **风险评估与解决方案**:
|
||||||
|
- 识别了性能影响、存储膨胀、消息完整性等技术风险
|
||||||
|
- 提出了敏感信息处理和数据一致性等业务风险的解决方案
|
||||||
|
|
||||||
|
- **项目计划**:
|
||||||
|
- 制定了包括设计、开发、测试、部署在内的完整计划
|
||||||
|
- 总体开发周期预计为11个工作日
|
||||||
|
|
||||||
|
- **下一步**:
|
||||||
|
- 开始实施开发方案,首先进行后端核心功能实现
|
||||||
|
- 准备测试数据和测试用例
|
||||||
|
- 开发管理员日志查询界面
|
||||||
|
|
||||||
|
## 2024-08-06 (MQTT通信日志系统 - 后端核心功能实现)
|
||||||
|
|
||||||
|
- **配置与属性类**:
|
||||||
|
- 在 `application.yml` 中添加了 `mqtt.logger` 相关配置,包括启用开关、客户端ID前缀、订阅主题、异步线程池参数和日志保留策略。
|
||||||
|
- 创建了 `MqttLoggerProperties.java` 用于映射这些配置。
|
||||||
|
|
||||||
|
- **数据模型与Mapper**:
|
||||||
|
- 创建了 `MqttCommunicationLog.java` 实体类,对应数据库表 `mqtt_communication_log`。
|
||||||
|
- 创建了 `MqttCommunicationLogMapper.java` MyBatis Plus接口。
|
||||||
|
|
||||||
|
- **核心服务层**:
|
||||||
|
- 创建了 `MqttCommunicationLogService.java` 接口,定义了异步记录上行和下行消息的方法。
|
||||||
|
- 创建了 `MqttCommunicationLogServiceImpl.java` 实现类,包含:
|
||||||
|
- `asyncLogMessage`: 异步记录完整的MQTT消息,包括方向、客户端ID、主题、Payload、QoS等,并进行初步的Payload格式判断(TEXT/JSON)。
|
||||||
|
- `asyncLogDownstreamMessage`: 异步记录出站消息的简化版本。
|
||||||
|
- 使用 `@Async("mqttLoggerThreadPoolTaskExecutor")` 指定自定义线程池执行异步任务。
|
||||||
|
|
||||||
|
- **异步配置**:
|
||||||
|
- 创建了 `MqttLoggerAsyncConfigurer.java`,配置了名为 `mqttLoggerThreadPoolTaskExecutor` 的专用线程池,用于异步写入日志,避免阻塞主线程。
|
||||||
|
|
||||||
|
- **MQTT日志专用客户端**:
|
||||||
|
- 创建了 `MqttLoggerClientConfig.java`,用于配置和创建专用于日志记录的MQTT客户端 (`mqttLoggerClient`)。此客户端懒加载,并使用与主业务不同的客户端ID。
|
||||||
|
- 创建了 `MqttLoggerCallbackHandler.java`,作为日志客户端的回调处理器:
|
||||||
|
- 在 `connectComplete` 中,根据配置订阅 `mqtt.logger.topics` 中指定的主题,用于监听上行消息。
|
||||||
|
- 在 `messageArrived` 中,调用 `logService.asyncLogMessage` 记录接收到的上行消息。
|
||||||
|
- 创建了 `MqttLoggerConnectionManager.java`,负责在应用启动时连接日志客户端,并在应用关闭时断开和关闭客户端。
|
||||||
|
|
||||||
|
- **出站消息记录 (AOP)**:
|
||||||
|
- 创建了 `MqttPublishLogAspect.java` 切面,通过 `@Around` 注解拦截 `MqttServiceImpl.sendCommand` 方法。
|
||||||
|
- 在目标方法执行后,调用 `logService.asyncLogDownstreamMessage` 记录出站(DOWNSTREAM)消息的详情。
|
||||||
|
- 为了能关联到具体的业务任务,在 `RobotTaskService` 接口及其实现类 `RobotTaskServiceImpl` 中添加了 `findLatestTaskByRobotIdAndSessionId` 方法,以便AOP切面可以根据 `robotId` 和 `sessionId` 查询关联的 `taskId`。
|
||||||
|
|
||||||
|
- **管理API接口**:
|
||||||
|
- 创建了 `MqttCommunicationLogController.java`,提供了 `/api/admin/mqtt-log/list/page` 接口,允许管理员分页查询MQTT通信日志。
|
||||||
|
- 创建了 `MqttLogQueryRequest.java` DTO,用于封装查询条件,包括消息ID、方向、客户端ID、主题、Payload内容、QoS、时间范围、关联会话ID和任务ID等。
|
||||||
|
- 控制器方法使用 `@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)` 进行了权限控制。
|
||||||
|
|
||||||
|
- **问题修复**:
|
||||||
|
- 修复了 `MqttLoggerConnectionManager.java` 中 `disconnectFromMqtt` 方法内误用 `mqttLoggerClient.isOpen()`(此方法不存在)导致的编译错误。修改为直接调用 `mqttLoggerClient.close()`。
|
||||||
|
- 解决了 `MqttPublishLogAspect` 中因存在两个 `MqttClient` 类型的Bean (`mqttClientBean` 和 `mqttLoggerClient`) 而导致的 `NoUniqueBeanDefinitionException` 启动错误。通过在 `MqttPublishLogAspect` 的构造函数中为 `MqttClient` 参数添加 `@Qualifier("mqttClientBean")` 注解,明确指定注入主业务MQTT客户端。
|
||||||
|
- 解决了 `MqttConnectionManager` 中类似的 `NoUniqueBeanDefinitionException` 启动错误。通过在其构造函数中为 `MqttClient` 参数添加 `@Qualifier("mqttClientBean")` 注解,明确指定注入主业务MQTT客户端。
|
||||||
|
|
||||||
|
- **状态**: 后端核心功能已开发完成,具备了独立的MQTT客户端监听上行消息和通过AOP记录下行消息的能力,并将日志异步存入数据库。同时提供了管理员查询日志的API接口。相关启动错误已修复。
|
||||||
|
|
||||||
|
- **下一步**:
|
||||||
|
- 前端日志查看与管理界面的开发。
|
||||||
|
- 对日志记录的准确性和完整性进行全面测试。
|
||||||
|
- 根据测试结果进行优化和调整。
|
||||||
|
|
||||||
|
## 2024-08-07 (MQTT通信日志系统 - 前端页面与导航)
|
||||||
|
|
||||||
|
- **页面创建与组件**:
|
||||||
|
- 在 `charging_web_app/src/app/admin/mqtt-logs/` 目录下创建了 `page.tsx`,作为MQTT通信日志的查看页面。
|
||||||
|
- 使用 Ant Design 组件 (`Card`, `Form`, `Input`, `Select`, `DatePicker`, `Button`, `Table`, `Tag`, `Tooltip`, `message`) 构建了页面结构。
|
||||||
|
- 包含一个查询表单,其字段对应后端的 `MqttLogQueryRequest`。
|
||||||
|
- 包含一个数据表格,列定义对应后端的 `MqttCommunicationLog` 实体,并对部分字段(如日期、方向、Payload)进行了格式化显示。
|
||||||
|
- 实现了基本的查询、重置、分页和客户端排序逻辑的骨架。
|
||||||
|
|
||||||
|
- **类型定义**:
|
||||||
|
- 创建了 `charging_web_app/src/types/log.types.ts` 文件,定义了前端所需的 `MqttCommunicationLog`、`MqttLogQueryRequest` 和通用分页响应 `PageResponse<T>` 接口。
|
||||||
|
|
||||||
|
- **API服务**:
|
||||||
|
- 创建了 `charging_web_app/src/services/axiosInstance.ts`,提供了一个基础的全局 Axios 实例配置。
|
||||||
|
- 创建了 `charging_web_app/src/services/logService.ts`,其中包含 `fetchMqttLogs`异步函数,用于调用后端 `/api/admin/mqtt-log/list/page` 接口获取日志数据。
|
||||||
|
- 解决了 `logService.ts` 中因 `axiosInstance` 相对路径导入问题导致的TypeScript编译错误,改为使用 `@/services/axiosInstance` 别名路径。
|
||||||
|
|
||||||
|
- **页面逻辑集成**:
|
||||||
|
- `mqtt-logs/page.tsx` 导入了类型定义和API服务函数。
|
||||||
|
- 实现了 `useEffect` Hook 在页面加载时获取初始日志数据。
|
||||||
|
- 完善了表单提交 (`handleSearch`)、表单重置 (`handleReset`) 和表格变化 (`handleTableChange`) 的处理函数,使其能够正确调用API获取和展示数据。
|
||||||
|
- 使用 Ant Design `message` 组件在API请求失败时给出用户提示。
|
||||||
|
|
||||||
|
- **导航链接添加**:
|
||||||
|
- 在 `charging_web_app/src/app/(authenticated)/layout.tsx` 文件中,为管理员角色的用户在头部导航栏添加了一个"MQTT日志"链接,指向 `/admin/mqtt-logs` 页面。
|
||||||
|
- 使用了 Next.js 的 `Link` 组件以优化导航体验。
|
||||||
|
|
||||||
|
- **状态**: MQTT通信日志查看页面的前端基本框架已完成,包括UI界面、API对接和导航入口。可以进行初步的功能测试。
|
||||||
|
|
||||||
|
- **下一步**:
|
||||||
|
- 全面测试前端页面的查询、分页、排序、重置等功能。
|
||||||
|
- 确保与后端API的交互无误,数据能正确展示和更新。
|
||||||
|
- 根据测试反馈和视觉需求,进一步优化页面样式和用户交互体验。
|
||||||
|
- 考虑在Payload列增加更完善的JSON格式化显示或代码高亮功能(如果需要)。
|
||||||
|
- 检查并确保所有代码符合项目规范和最佳实践。
|
||||||
|
|
||||||
|
## 2024-08-08 (MQTT通信日志系统 - 前端表格列本地化)
|
||||||
|
|
||||||
|
- **需求**: 将MQTT日志表格中的 `direction`, `payloadFormat`, `isRetained` 等字段值转换为中文显示,提升可读性。
|
||||||
|
- **解决方案**:
|
||||||
|
- 修改 `charging_web_app/src/app/admin/mqtt-logs/page.tsx` 文件中 `columns` 的定义。
|
||||||
|
- 为 `direction` 列的 `render` 函数增加逻辑:`UPSTREAM` -> `上行` (蓝色标签), `DOWNSTREAM` -> `下行` (绿色标签)。
|
||||||
|
- 为 `payloadFormat` 列的 `render` 函数增加逻辑:`TEXT` -> `文本`, `JSON` -> `JSON`。
|
||||||
|
- `isRetained` 列的 `render` 函数已能正确处理布尔值到 `是`/`否` 的转换。
|
||||||
|
- **状态**: 表格相关列的显示已本地化。
|
||||||
|
- **下一步**:
|
||||||
|
- 用户验证本地化显示效果。
|
||||||
|
- 处理Ant Design Modal `destroyOnClose` 废弃警告和 React 19 兼容性警告。
|
||||||
|
|
||||||
|
## 2024-08-08 (MQTT通信日志系统 - 前端数据显示修复)
|
||||||
|
|
||||||
|
- **问题定位**:
|
||||||
|
- 前端API调用成功并获取到数据,但Ant Design Table显示"No data"。
|
||||||
|
- 原因在于前端从API响应中提取数据的方式与实际的 `BaseResponse<PageResponse<T>>` 嵌套结构不匹配。
|
||||||
|
- `fetchMqttLogs` 函数返回的类型与其实际获取的数据结构存在偏差,导致在页面组件中访问 `response.data` 时类型不匹配,进而无法正确提取 `records` 和 `total`。
|
||||||
|
|
||||||
|
- **解决方案**:
|
||||||
|
1. **修改 `charging_web_app/src/services/logService.ts`**:
|
||||||
|
- 添加了 `BaseResponse<T>` 接口定义以匹配后端通用响应结构。
|
||||||
|
- 更新了 `fetchMqttLogs` 函数的返回类型为 `Promise<BaseResponse<PageResponse<MqttCommunicationLog>>>`。
|
||||||
|
- 更新了 `axiosInstance.post` 的泛型参数为 `BaseResponse<PageResponse<MqttCommunicationLog>>`。
|
||||||
|
- 确保 `fetchMqttLogs` 返回的是从Axios响应中获取的 `response.data`,这个 `data` 实际上是后端的 `BaseResponse` 对象。
|
||||||
|
2. **修改 `charging_web_app/src/app/admin/mqtt-logs/page.tsx`**:
|
||||||
|
- 在 `loadLogs` 函数中,当接收到 `fetchMqttLogs` 的结果 (`response`) 时,数据提取逻辑调整为从 `response.data` (即 `PageResponse` 对象) 中获取 `records`、`total` 等分页信息。
|
||||||
|
- 例如: `const pageData = response.data; setLogs(pageData.records);`
|
||||||
|
|
||||||
|
- **状态**: 已调整前端服务层和页面组件的数据处理逻辑,以正确解析后端返回的嵌套响应结构。预计数据能够正确显示在表格中。
|
||||||
|
|
||||||
|
- **下一步**:
|
||||||
|
- 用户验证数据是否已在前端表格中正确显示。
|
||||||
|
- 检查分页、查询、排序等功能是否均正常工作。
|
||||||
|
- 继续处理前端控制台剩余的警告。
|
||||||
|
|
||||||
|
## 2024-08-08 (MQTT通信日志系统 - 404错误修复)
|
||||||
|
|
||||||
|
- **问题定位**:
|
||||||
|
- 前端请求MQTT日志列表API (`/api/admin/mqtt-log/list/page`) 时出现 404 错误。
|
||||||
|
- 后端 `application.yml` 中配置了 `server.servlet.context-path: /api`。
|
||||||
|
- 后端 `MqttCommunicationLogController` 的 `@RequestMapping` 错写为 `"/api/admin/mqtt-log"`。
|
||||||
|
- 导致实际的API路径变为 `/api` (context-path) + `/api/admin/mqtt-log` (controller mapping) = `/api/api/admin/mqtt-log/list/page`。
|
||||||
|
- 而前端请求路径为 `/api/admin/mqtt-log/list/page`,因此不匹配。
|
||||||
|
|
||||||
|
- **解决方案**:
|
||||||
|
- 修改 `springboot-init-main/src/main/java/com/yupi/project/controller/MqttCommunicationLogController.java`。
|
||||||
|
- 将 `@RequestMapping("/api/admin/mqtt-log")` 修改为 `@RequestMapping("/admin/mqtt-log")`。
|
||||||
|
- 这样,结合 `context-path`,正确的API路径为 `/api/admin/mqtt-log/list/page`,与前端请求一致。
|
||||||
|
|
||||||
|
- **状态**: 已修改后端Controller的路径,预计能解决404问题。等待用户重启后端服务并验证。
|
||||||
|
|
||||||
|
- **下一步**:
|
||||||
|
- 用户验证404错误是否已解决。
|
||||||
|
- 继续处理前端控制台的其他警告(`Link` `legacyBehavior` 已处理,Ant Design Modal `destroyOnClose` 和 React 19 兼容性问题)。
|
||||||
|
- 检查并确保所有代码符合项目规范和最佳实践。
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
// import Link from 'next/link'; // Temporarily remove Link to test a plain anchor
|
import Link from 'next/link';
|
||||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||||
|
|
||||||
export default function AuthenticatedLayout({
|
export default function AuthenticatedLayout({
|
||||||
@@ -11,18 +11,16 @@ export default function AuthenticatedLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { isAuthenticated, isLoading, user, logout } = useAuth(); // 获取 user 和 logout
|
const { isAuthenticated, isLoading, user, logout } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only redirect if loading is complete and user is not authenticated
|
|
||||||
if (!isLoading && !isAuthenticated) {
|
if (!isLoading && !isAuthenticated) {
|
||||||
console.log('AuthenticatedLayout: User not authenticated, redirecting to login.');
|
console.log('AuthenticatedLayout: User not authenticated, redirecting to login.');
|
||||||
router.replace('/login');
|
router.replace('/login');
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, isLoading, router]);
|
}, [isAuthenticated, isLoading, router]);
|
||||||
|
|
||||||
// If still loading, show spinner
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
||||||
@@ -31,9 +29,7 @@ export default function AuthenticatedLayout({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If loading is complete but user is not authenticated,
|
if (!isAuthenticated || !user) {
|
||||||
// useEffect will handle redirection. Render null or spinner to avoid flashing content.
|
|
||||||
if (!isAuthenticated || !user) { // Also check if user object is available
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
@@ -41,16 +37,24 @@ export default function AuthenticatedLayout({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If authenticated, render children
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logout();
|
||||||
|
router.push('/login');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注销失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
<div className="min-h-screen flex flex-col bg-gray-50">
|
||||||
<header className="bg-white shadow-md w-full sticky top-0 z-50">
|
<header className="bg-white shadow-md w-full sticky top-0 z-50">
|
||||||
<nav className="container mx-auto px-6 py-3 flex justify-between items-center">
|
<nav className="container mx-auto px-6 py-3 flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
{/* Temporarily replaced Link with a plain anchor tag for testing */}
|
<Link href={user.role === 'admin' ? '/admin/dashboard' : '/dashboard'}
|
||||||
<a href={user.role === 'admin' ? '/admin/dashboard' : '/dashboard'} className="text-xl font-bold text-blue-700 hover:text-blue-900 transition-colors">
|
className="text-xl font-bold text-blue-700 hover:text-blue-900 transition-colors">
|
||||||
充电机器人管理系统
|
充电机器人管理系统
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
{user && (
|
{user && (
|
||||||
@@ -58,16 +62,14 @@ export default function AuthenticatedLayout({
|
|||||||
欢迎, <span className="font-medium">{user.username}</span> ({user.role === 'admin' ? '管理员' : '用户'})
|
欢迎, <span className="font-medium">{user.username}</span> ({user.role === 'admin' ? '管理员' : '用户'})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<Link href="/admin/mqtt-logs"
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-800 hover:underline transition-colors">
|
||||||
|
MQTT日志
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={handleLogout}
|
||||||
try {
|
|
||||||
await logout();
|
|
||||||
router.push('/login');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('注销失败:', error);
|
|
||||||
// 可以选择性地通知用户注销失败
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded-md text-sm font-semibold transition duration-150 ease-in-out shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
|
className="bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded-md text-sm font-semibold transition duration-150 ease-in-out shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
|
||||||
>
|
>
|
||||||
注销
|
注销
|
||||||
|
|||||||
297
charging_web_app/src/app/admin/mqtt-logs/page.tsx
Normal file
297
charging_web_app/src/app/admin/mqtt-logs/page.tsx
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
message, // Import Ant Design message for notifications
|
||||||
|
} from 'antd';
|
||||||
|
import type { TableProps } from 'antd';
|
||||||
|
import { SearchOutlined, RedoOutlined } from '@ant-design/icons';
|
||||||
|
import { MqttLogQueryRequest, MqttCommunicationLog, PageResponse } from '@/types/log.types';
|
||||||
|
import { fetchMqttLogs } from '@/services/logService';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
|
||||||
|
const MqttLogsPage: React.FC = () => {
|
||||||
|
const [form] = Form.useForm<MqttLogQueryRequest>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [logs, setLogs] = useState<MqttCommunicationLog[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to fetch logs
|
||||||
|
const loadLogs = async (params: MqttLogQueryRequest) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// fetchMqttLogs is expected to return the structure that includes { records, total, current, size }
|
||||||
|
// If fetchMqttLogs returns BaseResponse<PageResponse<...>>, then we need to access response.data
|
||||||
|
const response = await fetchMqttLogs(params);
|
||||||
|
|
||||||
|
// Assuming fetchMqttLogs returns BaseResponse<PageResponse<MqttCommunicationLog>> as per backend
|
||||||
|
// and logService.ts might be updated to return response.data.data or similar,
|
||||||
|
// OR, if logService.ts returns the raw BaseResponse.data (which itself is PageResponse)
|
||||||
|
// For now, let's assume 'response' here IS PageResponse as suggested by previous type hints
|
||||||
|
// If the actual response from fetchMqttLogs is { code, data: { records, total, ... }, message },
|
||||||
|
// then we need:
|
||||||
|
// const pageData = response.data; // Access the 'data' property of BaseResponse
|
||||||
|
// setLogs(pageData.records || []);
|
||||||
|
// setTotal(pageData.total || 0);
|
||||||
|
// setPagination(prev => ({ ...prev, current: pageData.current || 1, pageSize: pageData.size || 10 }));
|
||||||
|
|
||||||
|
// Based on the screenshot, fetchMqttLogs returns an object that has a 'data' property which contains records, total etc.
|
||||||
|
// So, the PageResponse<MqttCommunicationLog> is inside response.data
|
||||||
|
if (response && response.data) { // Check if response and response.data exist
|
||||||
|
const pageData: PageResponse<MqttCommunicationLog> = response.data;
|
||||||
|
setLogs(pageData.records || []);
|
||||||
|
setTotal(pageData.total || 0);
|
||||||
|
setPagination(prev => ({ ...prev, current: pageData.current || 1, pageSize: pageData.size || 10 }));
|
||||||
|
} else {
|
||||||
|
// Handle cases where response or response.data might be undefined, though API should return it
|
||||||
|
setLogs([]);
|
||||||
|
setTotal(0);
|
||||||
|
message.error('获取到的数据格式不正确!');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch MQTT logs:', error);
|
||||||
|
message.error('获取MQTT日志失败!');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
useEffect(() => {
|
||||||
|
loadLogs({ current: pagination.current, pageSize: pagination.pageSize });
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
// Initial load only on mount, pagination changes will trigger via handleTableChange -> form.submit() -> handleSearch
|
||||||
|
|
||||||
|
const handleSearch = async (values: MqttLogQueryRequest) => {
|
||||||
|
const queryParams: MqttLogQueryRequest = {
|
||||||
|
...values,
|
||||||
|
current: 1, // Reset to first page for new search
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
};
|
||||||
|
if (values.dateRange && values.dateRange[0] && values.dateRange[1]) {
|
||||||
|
queryParams.startTime = values.dateRange[0].toISOString();
|
||||||
|
queryParams.endTime = values.dateRange[1].toISOString();
|
||||||
|
}
|
||||||
|
delete queryParams.dateRange; // Remove AntD specific field
|
||||||
|
loadLogs(queryParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
form.resetFields();
|
||||||
|
setPagination({ current: 1, pageSize: 10 });
|
||||||
|
loadLogs({ current: 1, pageSize: 10 }); // Load with default pagination after reset
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableChange: TableProps<MqttCommunicationLog>['onChange'] = (
|
||||||
|
paginationConfig,
|
||||||
|
filters,
|
||||||
|
sorter
|
||||||
|
) => {
|
||||||
|
const newCurrent = paginationConfig.current || 1;
|
||||||
|
const newPageSize = paginationConfig.pageSize || 10;
|
||||||
|
|
||||||
|
const params: MqttLogQueryRequest = {
|
||||||
|
...form.getFieldsValue(), // Get current form values
|
||||||
|
current: newCurrent,
|
||||||
|
pageSize: newPageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sorter && (sorter as any).field) {
|
||||||
|
params.sortField = (sorter as any).field as string;
|
||||||
|
params.sortOrder = (sorter as any).order === 'ascend' ? 'ascend' : 'descend';
|
||||||
|
} else {
|
||||||
|
delete params.sortField;
|
||||||
|
delete params.sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRangeValue = form.getFieldValue('dateRange');
|
||||||
|
if (dateRangeValue && dateRangeValue[0] && dateRangeValue[1]) {
|
||||||
|
params.startTime = dateRangeValue[0].toISOString();
|
||||||
|
params.endTime = dateRangeValue[1].toISOString();
|
||||||
|
}
|
||||||
|
delete params.dateRange;
|
||||||
|
|
||||||
|
setPagination({current: newCurrent, pageSize: newPageSize}); // Update local pagination state first
|
||||||
|
loadLogs(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: TableProps<MqttCommunicationLog>['columns'] = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', key: 'id', sorter: true, width: 80 },
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'logTimestamp',
|
||||||
|
key: 'logTimestamp',
|
||||||
|
sorter: true,
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => text ? new Date(text).toLocaleString() : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '方向',
|
||||||
|
dataIndex: 'direction',
|
||||||
|
key: 'direction',
|
||||||
|
sorter: true,
|
||||||
|
width: 120,
|
||||||
|
render: (direction: string) => {
|
||||||
|
if (direction === 'UPSTREAM') return <Tag color='blue'>上行</Tag>;
|
||||||
|
if (direction === 'DOWNSTREAM') return <Tag color='green'>下行</Tag>;
|
||||||
|
return direction || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '消息ID', dataIndex: 'messageId', key: 'messageId', sorter: true, width: 150 },
|
||||||
|
{ title: '客户端ID', dataIndex: 'clientId', key: 'clientId', sorter: true, width: 200, ellipsis: true },
|
||||||
|
{ title: '主题', dataIndex: 'topic', key: 'topic', sorter: true, ellipsis: true },
|
||||||
|
{
|
||||||
|
title: 'Payload',
|
||||||
|
dataIndex: 'payload',
|
||||||
|
key: 'payload',
|
||||||
|
ellipsis: true,
|
||||||
|
width: 250,
|
||||||
|
render: (text: string) => (
|
||||||
|
text ? (
|
||||||
|
<Tooltip title={<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{text}</pre>}>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0, maxHeight: '60px', overflowY: 'auto' }}>
|
||||||
|
{text.length > 100 ? `${text.substring(0, 100)}...` : text}
|
||||||
|
</pre>
|
||||||
|
</Tooltip>
|
||||||
|
) : '-'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '格式',
|
||||||
|
dataIndex: 'payloadFormat',
|
||||||
|
key: 'payloadFormat',
|
||||||
|
width: 100,
|
||||||
|
render: (format?: string) => {
|
||||||
|
if (format === 'TEXT') return '文本';
|
||||||
|
if (format === 'JSON') return 'JSON';
|
||||||
|
return format || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ title: 'QoS', dataIndex: 'qos', key: 'qos', sorter: true, width: 80 },
|
||||||
|
{
|
||||||
|
title: '保留',
|
||||||
|
dataIndex: 'isRetained',
|
||||||
|
key: 'isRetained',
|
||||||
|
render: (retained?: boolean) => (typeof retained === 'boolean' ? (retained ? '是' : '否') : '-'),
|
||||||
|
width: 80
|
||||||
|
},
|
||||||
|
{ title: '会话ID', dataIndex: 'relatedSessionId', key: 'relatedSessionId', sorter: true, width: 100 },
|
||||||
|
{ title: '任务ID', dataIndex: 'relatedTaskId', key: 'relatedTaskId', sorter: true, width: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Title level={4}>MQTT 通信日志</Title>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleSearch} initialValues={{qos: undefined /* Ensure placeholder shows */}}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="messageId" label="消息ID">
|
||||||
|
<Input placeholder="请输入消息ID" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="direction" label="方向">
|
||||||
|
<Select placeholder="选择方向" allowClear>
|
||||||
|
<Select.Option value="UPSTREAM">UPSTREAM</Select.Option>
|
||||||
|
<Select.Option value="DOWNSTREAM">DOWNSTREAM</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="clientId" label="客户端ID">
|
||||||
|
<Input placeholder="请输入客户端ID" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="topic" label="主题">
|
||||||
|
<Input placeholder="请输入主题" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="payloadContains" label="Payload包含">
|
||||||
|
<Input placeholder="请输入Payload包含的内容" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="qos" label="QoS">
|
||||||
|
<Select placeholder="选择QoS" allowClear>
|
||||||
|
<Select.Option value={0}>0</Select.Option>
|
||||||
|
<Select.Option value={1}>1</Select.Option>
|
||||||
|
<Select.Option value={2}>2</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="relatedSessionId" label="关联会话ID">
|
||||||
|
<Input type="number" placeholder="请输入关联会话ID" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="relatedTaskId" label="关联任务ID">
|
||||||
|
<Input type="number" placeholder="请输入关联任务ID" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Form.Item name="dateRange" label="日期范围">
|
||||||
|
<RangePicker showTime format="YYYY-MM-DD HH:mm:ss" style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col span={24} style={{ textAlign: 'right' }}>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" icon={<SearchOutlined />} loading={loading}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button icon={<RedoOutlined />} onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
style={{ marginTop: 20 }}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={logs}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={{
|
||||||
|
...pagination,
|
||||||
|
total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (totalLogs) => `总计 ${totalLogs} 条`,
|
||||||
|
}}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
scroll={{ x: 1500 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MqttLogsPage;
|
||||||
48
charging_web_app/src/services/axiosInstance.ts
Normal file
48
charging_web_app/src/services/axiosInstance.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
// baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '/api', // Default to /api if not set for Next.js proxy
|
||||||
|
// Since your Spring Boot backend is at /api, and Next.js dev server can proxy,
|
||||||
|
// for client-side requests, you might not need a full baseURL if using relative paths correctly,
|
||||||
|
// or ensure your Next.js proxy is set up for /api paths.
|
||||||
|
// If Spring Boot is running on a different port during dev (e.g. 7529) and not proxied,
|
||||||
|
// you'd need something like: baseURL: 'http://localhost:7529/api'
|
||||||
|
// However, the backend controller is already mapped to /api/admin/mqtt-log, so calling /api/admin/... should work if proxied.
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional: Add interceptors for request/response, e.g., for auth tokens
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// Example: Add auth token if available
|
||||||
|
// if (typeof window !== 'undefined') {
|
||||||
|
// const token = localStorage.getItem('token');
|
||||||
|
// if (token) {
|
||||||
|
// config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// Any status code that lie within the range of 2xx cause this function to trigger
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Any status codes that falls outside the range of 2xx cause this function to trigger
|
||||||
|
// You can add global error handling here, e.g., for 401 Unauthorized redirect to login
|
||||||
|
// if (error.response && error.response.status === 401) {
|
||||||
|
// // redirect to login page
|
||||||
|
// }
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default axiosInstance;
|
||||||
65
charging_web_app/src/services/logService.ts
Normal file
65
charging_web_app/src/services/logService.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import axiosInstance from '@/services/axiosInstance'; // Try using alias path
|
||||||
|
import { MqttLogQueryRequest, MqttCommunicationLog, PageResponse } from '@/types/log.types';
|
||||||
|
|
||||||
|
// Define a generic BaseResponse type to match the backend structure
|
||||||
|
export interface BaseResponse<T> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch MQTT communication logs from the backend.
|
||||||
|
* @param params - The query parameters for fetching logs.
|
||||||
|
* @returns A promise that resolves to a BaseResponse containing a page of MQTT communication logs.
|
||||||
|
*/
|
||||||
|
export const fetchMqttLogs = async (
|
||||||
|
params: MqttLogQueryRequest
|
||||||
|
): Promise<BaseResponse<PageResponse<MqttCommunicationLog>>> => {
|
||||||
|
try {
|
||||||
|
// Clean up dateRange before sending to backend
|
||||||
|
const backendParams = { ...params };
|
||||||
|
if (backendParams.dateRange) {
|
||||||
|
delete backendParams.dateRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axiosInstance.post<BaseResponse<PageResponse<MqttCommunicationLog>>>(
|
||||||
|
'/api/admin/mqtt-log/list/page',
|
||||||
|
backendParams
|
||||||
|
);
|
||||||
|
return response.data; // Axios wraps the actual server response in its own 'data' property
|
||||||
|
// So, response.data here IS the BaseResponse from the backend.
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching MQTT logs:', error);
|
||||||
|
// Consider throwing a more specific error or handling it as per your app's error strategy
|
||||||
|
// For now, rethrow to be caught by the caller
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// You might need to create axiosInstance.ts if it doesn't exist
|
||||||
|
// Example for axiosInstance.ts:
|
||||||
|
// import axios from 'axios';
|
||||||
|
//
|
||||||
|
// const axiosInstance = axios.create({
|
||||||
|
// baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '', // Your API base URL
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // Optional: Add interceptors for request/response, e.g., for auth tokens
|
||||||
|
// axiosInstance.interceptors.request.use(
|
||||||
|
// (config) => {
|
||||||
|
// // const token = localStorage.getItem('token'); // Or however you store your token
|
||||||
|
// // if (token) {
|
||||||
|
// // config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
// // }
|
||||||
|
// return config;
|
||||||
|
// },
|
||||||
|
// (error) => {
|
||||||
|
// return Promise.reject(error);
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// export default axiosInstance;
|
||||||
43
charging_web_app/src/types/log.types.ts
Normal file
43
charging_web_app/src/types/log.types.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export interface MqttCommunicationLog {
|
||||||
|
id: number;
|
||||||
|
logTimestamp: string;
|
||||||
|
direction: 'UPSTREAM' | 'DOWNSTREAM';
|
||||||
|
messageId?: string;
|
||||||
|
clientId?: string;
|
||||||
|
topic: string;
|
||||||
|
payload: string;
|
||||||
|
payloadFormat?: 'TEXT' | 'JSON' | 'BINARY';
|
||||||
|
qos?: number;
|
||||||
|
isRetained?: boolean;
|
||||||
|
backendProcessingStatus?: string;
|
||||||
|
backendProcessingInfo?: string;
|
||||||
|
relatedSessionId?: number;
|
||||||
|
relatedTaskId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MqttLogQueryRequest {
|
||||||
|
messageId?: string;
|
||||||
|
direction?: 'UPSTREAM' | 'DOWNSTREAM';
|
||||||
|
clientId?: string;
|
||||||
|
topic?: string;
|
||||||
|
payloadContains?: string;
|
||||||
|
qos?: number;
|
||||||
|
startTime?: string; // ISO string for backend
|
||||||
|
endTime?: string; // ISO string for backend
|
||||||
|
relatedSessionId?: number;
|
||||||
|
relatedTaskId?: number;
|
||||||
|
current?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sortField?: string;
|
||||||
|
sortOrder?: 'ascend' | 'descend';
|
||||||
|
// For AntD RangePicker in Form, converted to startTime/endTime before API call
|
||||||
|
dateRange?: [any, any];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageResponse<T> {
|
||||||
|
records: T[];
|
||||||
|
total: number;
|
||||||
|
current: number;
|
||||||
|
size: number;
|
||||||
|
// Add other pagination fields if your backend returns them
|
||||||
|
}
|
||||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mqtt_power",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"dependencies": {
|
|
||||||
"react-icons": "^5.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react": {
|
|
||||||
"version": "19.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/react/-/react-19.1.0.tgz",
|
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-icons": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/react-icons/-/react-icons-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"react-icons": "^5.5.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
283
springboot-init-main/doc/mqtt_communication_log_plan.md
Normal file
283
springboot-init-main/doc/mqtt_communication_log_plan.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# MQTT通信日志系统开发方案
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 目标与意义
|
||||||
|
|
||||||
|
开发MQTT通信日志系统,用于完整记录单片机与服务器系统之间的所有MQTT消息通信。该系统具有以下价值:
|
||||||
|
|
||||||
|
- **通信可视化**:直观展示单片机与服务器之间的通信细节和消息内容
|
||||||
|
- **故障诊断**:为消息处理失败、设备离线等异常场景提供完整的调试信息
|
||||||
|
- **性能分析**:通过日志记录消息量、响应时间等指标,评估系统性能
|
||||||
|
- **安全审计**:记录所有通信活动,便于安全审计和问题追踪
|
||||||
|
- **业务分析**:通过通信数据分析设备使用模式和业务流程
|
||||||
|
|
||||||
|
### 1.2 系统定位
|
||||||
|
|
||||||
|
该系统为纯日志记录系统,**不干扰**正常的业务流程:
|
||||||
|
|
||||||
|
- 不消费消息队列中的数据
|
||||||
|
- 不修改消息内容或状态
|
||||||
|
- 仅作为"旁路监听者"记录通信过程
|
||||||
|
- 以非阻塞方式运行,不影响主业务性能
|
||||||
|
|
||||||
|
## 2. 技术架构设计
|
||||||
|
|
||||||
|
### 2.1 总体架构
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
系统采用"监听者模式",不直接参与消息处理流程:
|
||||||
|
|
||||||
|
- **MQTT消息监听层**:订阅与主业务相同的Topic,但只读取不处理
|
||||||
|
- **日志记录层**:异步将消息内容写入数据库
|
||||||
|
- **查询展示层**:提供日志查询、过滤、导出功能的管理界面
|
||||||
|
|
||||||
|
### 2.2 数据库结构
|
||||||
|
|
||||||
|
已有数据表 `mqtt_communication_log` 包含了足够的字段设计:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `mqtt_communication_log` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||||
|
`message_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '消息的唯一标识',
|
||||||
|
`direction` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息方向: UPSTREAM (设备->服务器), DOWNSTREAM (服务器->设备)',
|
||||||
|
`client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '相关的客户端ID',
|
||||||
|
`topic` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'MQTT 主题',
|
||||||
|
`payload_format` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'TEXT' COMMENT 'Payload 格式',
|
||||||
|
`payload` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '消息原文',
|
||||||
|
`qos` tinyint(4) NULL DEFAULT NULL COMMENT '消息QoS级别',
|
||||||
|
`is_retained` tinyint(1) NULL DEFAULT NULL COMMENT '是否为保留消息',
|
||||||
|
`log_timestamp` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '日志记录时间戳',
|
||||||
|
`backend_processing_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '后端处理状态',
|
||||||
|
`backend_processing_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '后端处理附加信息',
|
||||||
|
`related_session_id` bigint(20) NULL DEFAULT NULL COMMENT '关联的充电会话ID',
|
||||||
|
`related_task_id` bigint(20) NULL DEFAULT NULL COMMENT '关联的机器人任务ID',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `idx_log_timestamp`(`log_timestamp`) USING BTREE,
|
||||||
|
INDEX `idx_topic`(`topic`(255)) USING BTREE,
|
||||||
|
INDEX `idx_client_id`(`client_id`) USING BTREE,
|
||||||
|
INDEX `idx_related_session_id`(`related_session_id`) USING BTREE,
|
||||||
|
INDEX `idx_related_task_id`(`related_task_id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MQTT通信日志表'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 核心组件设计
|
||||||
|
|
||||||
|
#### 2.3.1 MQTT日志监听器
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class MqttCommunicationLogger {
|
||||||
|
// 依赖注入日志服务和MQTT客户端
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.2 日志记录服务
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class MqttLogService {
|
||||||
|
// 异步记录消息到数据库
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.3 日志查询Controller
|
||||||
|
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/mqtt-logs")
|
||||||
|
public class MqttLogController {
|
||||||
|
// 提供日志查询API
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 实现步骤
|
||||||
|
|
||||||
|
### 3.1 后端实现
|
||||||
|
|
||||||
|
1. **创建数据访问层**
|
||||||
|
- 实现 `MqttCommunicationLogMapper` 接口
|
||||||
|
- 实现 `MqttCommunicationLogService` 接口及其实现类
|
||||||
|
|
||||||
|
2. **实现消息监听与记录**
|
||||||
|
- 创建独立的 MQTT 客户端用于日志记录
|
||||||
|
- 配置该客户端订阅所有业务相关的主题
|
||||||
|
- 实现监听回调,异步记录收到的消息
|
||||||
|
- 为发送的消息添加拦截记录逻辑
|
||||||
|
|
||||||
|
3. **实现关联解析**
|
||||||
|
- 解析消息内容,提取关联的会话ID、任务ID
|
||||||
|
- 根据消息主题和内容推断消息类型和处理状态
|
||||||
|
|
||||||
|
4. **实现管理API**
|
||||||
|
- 创建日志查询Controller
|
||||||
|
- 实现分页查询、条件过滤、时间范围查询
|
||||||
|
- 实现日志导出功能
|
||||||
|
|
||||||
|
### 3.2 前端实现
|
||||||
|
|
||||||
|
1. **日志查询页面**
|
||||||
|
- 创建管理员专用的日志查询页面
|
||||||
|
- 实现条件筛选表单(时间范围、客户端ID、主题等)
|
||||||
|
- 实现分页表格展示日志记录
|
||||||
|
|
||||||
|
2. **日志详情展示**
|
||||||
|
- 实现日志详情模态框
|
||||||
|
- 格式化JSON消息内容,便于阅读
|
||||||
|
- 显示相关的业务信息(会话、任务)链接
|
||||||
|
|
||||||
|
3. **日志分析功能**
|
||||||
|
- 实现简单的统计分析(消息量、成功率等)
|
||||||
|
- 提供日志导出功能
|
||||||
|
|
||||||
|
## 4. 技术实现细节
|
||||||
|
|
||||||
|
### 4.1 MQTT日志客户端配置
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Configuration
|
||||||
|
public class MqttLoggerConfig {
|
||||||
|
@Bean(name = "mqttLoggerClient")
|
||||||
|
public MqttClient mqttLoggerClient() {
|
||||||
|
// 配置单独的MQTT客户端用于日志记录
|
||||||
|
// 与主业务使用相同的broker但不同的clientId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 消息记录实现
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class MqttLogSubscriber implements MqttCallback {
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void messageArrived(String topic, MqttMessage message) {
|
||||||
|
// 1. 解析消息,提取元数据
|
||||||
|
// 2. 构建日志记录对象
|
||||||
|
// 3. 异步保存到数据库
|
||||||
|
// 注意:完全不干扰消息的正常传递
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 发送消息记录
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class MqttPublishAspect {
|
||||||
|
|
||||||
|
@Around("execution(* com.yupi.project.mqtt.MqttService.publish*(..)) || execution(* com.yupi.project.mqtt.MqttService.sendCommand*(..))")
|
||||||
|
public Object logMqttPublish(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
// 前置:记录发送前信息
|
||||||
|
Object result = joinPoint.proceed(); // 执行原方法
|
||||||
|
// 后置:记录发送后信息
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 业务关联解析
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class MqttPayloadParser {
|
||||||
|
|
||||||
|
public MqttLogBusinessInfo parsePayload(String topic, String payload) {
|
||||||
|
// 根据主题和内容解析出业务相关信息
|
||||||
|
// 例如:会话ID、任务ID、指令类型等
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 部署与配置
|
||||||
|
|
||||||
|
### 5.1 配置项
|
||||||
|
|
||||||
|
在 `application.yml` 中添加MQTT日志系统的配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mqtt:
|
||||||
|
logger:
|
||||||
|
enabled: true
|
||||||
|
client-id-prefix: logger-
|
||||||
|
topics:
|
||||||
|
- charging/spot/+/status
|
||||||
|
- charging/spot/+/command/#
|
||||||
|
- charging/spot/+/heartbeat
|
||||||
|
async:
|
||||||
|
core-pool-size: 2
|
||||||
|
max-pool-size: 5
|
||||||
|
queue-capacity: 500
|
||||||
|
retention:
|
||||||
|
days: 30 # 日志保留天数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 数据库维护
|
||||||
|
|
||||||
|
- 设计日志清理策略,避免数据过度膨胀
|
||||||
|
- 实现定时任务,清理超过保留期的日志数据
|
||||||
|
- 考虑按时间分区表策略,优化大量日志的存储和查询性能
|
||||||
|
|
||||||
|
## 6. 测试计划
|
||||||
|
|
||||||
|
### 6.1 功能测试
|
||||||
|
|
||||||
|
- 验证各类消息的正确记录
|
||||||
|
- 验证消息内容完整性
|
||||||
|
- 验证不同格式消息的解析效果
|
||||||
|
- 验证业务关联信息的正确提取
|
||||||
|
|
||||||
|
### 6.2 性能测试
|
||||||
|
|
||||||
|
- 高并发消息下的记录性能
|
||||||
|
- 验证日志记录对主业务的性能影响(应小于5%)
|
||||||
|
- 验证大数据量下的查询性能
|
||||||
|
|
||||||
|
### 6.3 稳定性测试
|
||||||
|
|
||||||
|
- 长时间运行测试
|
||||||
|
- 异常情况下的日志记录可靠性测试
|
||||||
|
- 系统重启后的连续性测试
|
||||||
|
|
||||||
|
## 7. 注意事项与风险
|
||||||
|
|
||||||
|
### 7.1 技术风险
|
||||||
|
|
||||||
|
- **性能影响**:确保日志记录不成为系统瓶颈
|
||||||
|
- 解决方案:异步记录,独立线程池,必要时考虑消息缓冲
|
||||||
|
|
||||||
|
- **存储膨胀**:MQTT消息量大可能导致数据库快速增长
|
||||||
|
- 解决方案:设置合理的日志保留策略,定期清理或归档
|
||||||
|
|
||||||
|
- **消息完整性**:某些异常情况可能导致日志不完整
|
||||||
|
- 解决方案:增加重试机制,添加监控告警
|
||||||
|
|
||||||
|
### 7.2 业务风险
|
||||||
|
|
||||||
|
- **敏感信息**:日志可能包含敏感业务数据
|
||||||
|
- 解决方案:实现字段级别的脱敏,严格控制日志访问权限
|
||||||
|
|
||||||
|
- **一致性**:日志状态与实际业务状态可能不一致
|
||||||
|
- 解决方案:明确标注日志仅用于参考,不作为业务状态的权威来源
|
||||||
|
|
||||||
|
## 8. 后续优化方向
|
||||||
|
|
||||||
|
- 实现更复杂的消息分析功能
|
||||||
|
- 添加可视化图表,直观展示通信模式和趋势
|
||||||
|
- 实现异常模式的自动检测和告警
|
||||||
|
- 与系统监控平台集成,提供更全面的系统健康视图
|
||||||
|
|
||||||
|
## 9. 项目计划
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 时间估计 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 设计与准备 | 详细设计、环境准备 | 2天 |
|
||||||
|
| 后端开发 | 核心记录功能实现 | 3天 |
|
||||||
|
| 前端开发 | 日志查询界面实现 | 3天 |
|
||||||
|
| 测试与优化 | 功能测试、性能优化 | 2天 |
|
||||||
|
| 文档与部署 | 编写文档、系统部署 | 1天 |
|
||||||
|
|
||||||
|
**总计划时间**:约11个工作日
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
Navicat Premium Data Transfer
|
Navicat Premium Dump SQL
|
||||||
|
|
||||||
Source Server : yuyun
|
Source Server : yuyun-us1.stormrain.cn
|
||||||
Source Server Type : MySQL
|
Source Server Type : MySQL
|
||||||
Source Server Version : 50744
|
Source Server Version : 50744 (5.7.44)
|
||||||
Source Host : yuyun-us1.stormrain.cn:3306
|
Source Host : yuyun-us1.stormrain.cn:3306
|
||||||
Source Schema : mqtt_power
|
Source Schema : mqtt_power
|
||||||
|
|
||||||
Target Server Type : MySQL
|
Target Server Type : MySQL
|
||||||
Target Server Version : 50744
|
Target Server Version : 50744 (5.7.44)
|
||||||
File Encoding : 65001
|
File Encoding : 65001
|
||||||
|
|
||||||
Date: 18/05/2025 19:54:49
|
Date: 22/05/2025 19:36:58
|
||||||
*/
|
*/
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
@@ -89,6 +89,33 @@ CREATE TABLE `charging_session` (
|
|||||||
PRIMARY KEY (`id`) USING BTREE
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '充电记录表' ROW_FORMAT = Dynamic;
|
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_german2_ci COMMENT = '充电记录表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for mqtt_communication_log
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `mqtt_communication_log`;
|
||||||
|
CREATE TABLE `mqtt_communication_log` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||||
|
`message_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '消息的唯一标识 (例如 MQTT v5 的 Message ID 或应用生成的UUID)',
|
||||||
|
`direction` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息方向: UPSTREAM (设备->服务器), DOWNSTREAM (服务器->设备)',
|
||||||
|
`client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '相关的客户端ID',
|
||||||
|
`topic` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'MQTT 主题',
|
||||||
|
`payload_format` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'TEXT' COMMENT 'Payload 格式 (TEXT, JSON, BINARY)',
|
||||||
|
`payload` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '消息原文 (Payload)',
|
||||||
|
`qos` tinyint(4) NULL DEFAULT NULL COMMENT '消息QoS级别',
|
||||||
|
`is_retained` tinyint(1) NULL DEFAULT NULL COMMENT '是否为保留消息',
|
||||||
|
`log_timestamp` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '日志记录时间戳 (精确到毫秒)',
|
||||||
|
`backend_processing_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '后端处理状态 (RECEIVED, PROCESSING, SUCCESS, FAILED)',
|
||||||
|
`backend_processing_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '后端处理附加信息 (如错误信息、关联业务ID)',
|
||||||
|
`related_session_id` bigint(20) NULL DEFAULT NULL COMMENT '关联的充电会话ID (如果适用)',
|
||||||
|
`related_task_id` bigint(20) NULL DEFAULT NULL COMMENT '关联的机器人任务ID (如果适用)',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `idx_log_timestamp`(`log_timestamp`) USING BTREE,
|
||||||
|
INDEX `idx_topic`(`topic`(255)) USING BTREE,
|
||||||
|
INDEX `idx_client_id`(`client_id`) USING BTREE,
|
||||||
|
INDEX `idx_related_session_id`(`related_session_id`) USING BTREE,
|
||||||
|
INDEX `idx_related_task_id`(`related_task_id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MQTT通信日志表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for parking_spot
|
-- Table structure for parking_spot
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.yupi.project.aop;
|
||||||
|
|
||||||
|
import com.yupi.project.config.properties.MqttLoggerProperties;
|
||||||
|
import com.yupi.project.model.entity.RobotTask;
|
||||||
|
import com.yupi.project.service.MqttCommunicationLogService;
|
||||||
|
import com.yupi.project.service.RobotTaskService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class MqttPublishLogAspect {
|
||||||
|
|
||||||
|
private final MqttCommunicationLogService logService;
|
||||||
|
private final MqttLoggerProperties loggerProperties;
|
||||||
|
private final RobotTaskService robotTaskService;
|
||||||
|
private final MqttClient mainMqttClient;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MqttPublishLogAspect(MqttCommunicationLogService logService,
|
||||||
|
MqttLoggerProperties loggerProperties,
|
||||||
|
RobotTaskService robotTaskService,
|
||||||
|
@Qualifier("mqttClientBean") MqttClient mainMqttClient) {
|
||||||
|
this.logService = logService;
|
||||||
|
this.loggerProperties = loggerProperties;
|
||||||
|
this.robotTaskService = robotTaskService;
|
||||||
|
this.mainMqttClient = mainMqttClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Around("execution(* com.yupi.project.service.impl.MqttServiceImpl.sendCommand(..)) || execution(* com.yupi.project.service.MqttService.sendCommand(..))")
|
||||||
|
public Object logMqttSendCommand(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
if (!loggerProperties.isEnabled()) {
|
||||||
|
return joinPoint.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[] args = joinPoint.getArgs();
|
||||||
|
String robotIdArg = null;
|
||||||
|
String commandTypeArg = null;
|
||||||
|
String payloadJsonArg = null;
|
||||||
|
Long sessionIdArg = null;
|
||||||
|
Long taskIdForLog = null;
|
||||||
|
|
||||||
|
if (args.length >= 3) {
|
||||||
|
robotIdArg = (String) args[0];
|
||||||
|
commandTypeArg = args[1].toString();
|
||||||
|
payloadJsonArg = (String) args[2];
|
||||||
|
if (args.length >= 4 && args[3] instanceof Long) {
|
||||||
|
sessionIdArg = (Long) args[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String topic = null;
|
||||||
|
Integer qos = null;
|
||||||
|
Boolean retained = false;
|
||||||
|
String messageIdForLog = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
Object result = null;
|
||||||
|
try {
|
||||||
|
result = joinPoint.proceed();
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
logService.asyncLogDownstreamMessage(
|
||||||
|
"UNKNOWN_TOPIC_PRE_SEND_FAILURE",
|
||||||
|
payloadJsonArg,
|
||||||
|
qos,
|
||||||
|
retained,
|
||||||
|
mainMqttClient.getClientId(),
|
||||||
|
messageIdForLog,
|
||||||
|
sessionIdArg,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
log.error("Exception during sendCommand, logged with placeholder data. RobotId: {}, Command: {}", robotIdArg, commandTypeArg, throwable);
|
||||||
|
throw throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (robotIdArg != null && sessionIdArg !=null) {
|
||||||
|
RobotTask latestTask = robotTaskService.findLatestTaskByRobotIdAndSessionId(robotIdArg,sessionIdArg);
|
||||||
|
if(latestTask != null){
|
||||||
|
taskIdForLog = latestTask.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logService.asyncLogDownstreamMessage(
|
||||||
|
args.length > 0 ? ("CMD_TO_" + args[0]) : "UNKNOWN_TOPIC_POST_SEND",
|
||||||
|
payloadJsonArg,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
mainMqttClient.getClientId(),
|
||||||
|
messageIdForLog,
|
||||||
|
sessionIdArg,
|
||||||
|
taskIdForLog
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.yupi.project.config;
|
||||||
|
|
||||||
|
import com.yupi.project.config.properties.MqttLoggerProperties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
||||||
|
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MqttLoggerAsyncConfigurer implements AsyncConfigurer {
|
||||||
|
|
||||||
|
private final MqttLoggerProperties mqttLoggerProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Bean(name = "mqttLoggerThreadPoolTaskExecutor")
|
||||||
|
public Executor getAsyncExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
MqttLoggerProperties.Async asyncProps = mqttLoggerProperties.getAsync();
|
||||||
|
executor.setCorePoolSize(asyncProps.getCorePoolSize());
|
||||||
|
executor.setMaxPoolSize(asyncProps.getMaxPoolSize());
|
||||||
|
executor.setQueueCapacity(asyncProps.getQueueCapacity());
|
||||||
|
executor.setThreadNamePrefix(asyncProps.getThreadNamePrefix());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
|
||||||
|
return new SimpleAsyncUncaughtExceptionHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.yupi.project.config;
|
||||||
|
|
||||||
|
import com.yupi.project.config.properties.MqttLoggerProperties;
|
||||||
|
import com.yupi.project.config.properties.MqttProperties;
|
||||||
|
import com.yupi.project.mqtt.MqttLoggerCallbackHandler;
|
||||||
|
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.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MqttLoggerClientConfig {
|
||||||
|
|
||||||
|
private final MqttProperties mainMqttProperties;
|
||||||
|
private final MqttLoggerProperties mqttLoggerProperties;
|
||||||
|
private final MqttConnectOptions mqttConnectOptions;
|
||||||
|
|
||||||
|
@Bean(name = "mqttLoggerClient")
|
||||||
|
@Lazy
|
||||||
|
public MqttClient mqttLoggerClient(@Qualifier("mqttLoggerCallbackHandler") MqttLoggerCallbackHandler loggerCallbackHandler)
|
||||||
|
throws MqttException {
|
||||||
|
if (!mqttLoggerProperties.isEnabled()) {
|
||||||
|
log.info("MQTT Logger Client is disabled via configuration.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientId = mqttLoggerProperties.getClientIdPrefix() + UUID.randomUUID().toString().replace("-", "");
|
||||||
|
log.info("Initializing MQTT Logger Client with Broker URL: {} and Client ID: {}", mainMqttProperties.getBrokerUrl(), clientId);
|
||||||
|
|
||||||
|
MqttClient client = new MqttClient(mainMqttProperties.getBrokerUrl(), clientId, new MemoryPersistence());
|
||||||
|
client.setCallback(loggerCallbackHandler);
|
||||||
|
loggerCallbackHandler.setMqttClient(client);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.yupi.project.config.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "mqtt.logger")
|
||||||
|
public class MqttLoggerProperties {
|
||||||
|
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String clientIdPrefix;
|
||||||
|
private List<String> topics;
|
||||||
|
private Async async = new Async();
|
||||||
|
private Retention retention = new Retention();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Async {
|
||||||
|
private int corePoolSize = 1;
|
||||||
|
private int maxPoolSize = 5;
|
||||||
|
private int queueCapacity = 100;
|
||||||
|
private String threadNamePrefix = "mqtt-log-async-";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Retention {
|
||||||
|
private int days = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.yupi.project.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.yupi.project.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.mqttlog.MqttLogQueryRequest;
|
||||||
|
import com.yupi.project.model.entity.MqttCommunicationLog;
|
||||||
|
import com.yupi.project.service.MqttCommunicationLogService;
|
||||||
|
import com.yupi.project.annotation.AuthCheck;
|
||||||
|
import com.yupi.project.constant.UserConstant;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/mqtt-log")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MqttCommunicationLogController {
|
||||||
|
|
||||||
|
private final MqttCommunicationLogService mqttCommunicationLogService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取MQTT通信日志列表
|
||||||
|
*
|
||||||
|
* @param mqttLogQueryRequest
|
||||||
|
* @param request
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping("/list/page")
|
||||||
|
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||||
|
public BaseResponse<Page<MqttCommunicationLog>> listMqttCommunicationLogsByPage(@RequestBody MqttLogQueryRequest mqttLogQueryRequest, HttpServletRequest request) {
|
||||||
|
if (mqttLogQueryRequest == null) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
long current = mqttLogQueryRequest.getCurrent();
|
||||||
|
long size = mqttLogQueryRequest.getPageSize();
|
||||||
|
// 限制爬虫
|
||||||
|
if (size > 50) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求数据量过大");
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryWrapper<MqttCommunicationLog> queryWrapper = getQueryWrapper(mqttLogQueryRequest);
|
||||||
|
Page<MqttCommunicationLog> logPage = mqttCommunicationLogService.page(new Page<>(current, size), queryWrapper);
|
||||||
|
return ResultUtils.success(logPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryWrapper<MqttCommunicationLog> getQueryWrapper(MqttLogQueryRequest queryRequest) {
|
||||||
|
QueryWrapper<MqttCommunicationLog> queryWrapper = new QueryWrapper<>();
|
||||||
|
if (queryRequest == null) {
|
||||||
|
return queryWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
String messageId = queryRequest.getMessageId();
|
||||||
|
String direction = queryRequest.getDirection();
|
||||||
|
String clientId = queryRequest.getClientId();
|
||||||
|
String topic = queryRequest.getTopic();
|
||||||
|
String payloadContains = queryRequest.getPayloadContains();
|
||||||
|
Integer qos = queryRequest.getQos();
|
||||||
|
Date startTime = queryRequest.getStartTime();
|
||||||
|
Date endTime = queryRequest.getEndTime();
|
||||||
|
Long relatedSessionId = queryRequest.getRelatedSessionId();
|
||||||
|
Long relatedTaskId = queryRequest.getRelatedTaskId();
|
||||||
|
String sortField = queryRequest.getSortField();
|
||||||
|
String sortOrder = queryRequest.getSortOrder();
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(messageId)) {
|
||||||
|
queryWrapper.like("message_id", messageId);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(direction)) {
|
||||||
|
queryWrapper.eq("direction", direction);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(clientId)) {
|
||||||
|
queryWrapper.like("client_id", clientId);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(topic)) {
|
||||||
|
queryWrapper.like("topic", topic);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(payloadContains)) {
|
||||||
|
queryWrapper.like("payload", payloadContains);
|
||||||
|
}
|
||||||
|
if (qos != null) {
|
||||||
|
queryWrapper.eq("qos", qos);
|
||||||
|
}
|
||||||
|
if (startTime != null) {
|
||||||
|
queryWrapper.ge("log_timestamp", startTime);
|
||||||
|
}
|
||||||
|
if (endTime != null) {
|
||||||
|
queryWrapper.le("log_timestamp", endTime);
|
||||||
|
}
|
||||||
|
if (relatedSessionId != null) {
|
||||||
|
queryWrapper.eq("related_session_id", relatedSessionId);
|
||||||
|
}
|
||||||
|
if (relatedTaskId != null) {
|
||||||
|
queryWrapper.eq("related_task_id", relatedTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认按日志时间降序排序
|
||||||
|
if (StringUtils.isNotBlank(sortField)) {
|
||||||
|
boolean isAsc = "asc".equalsIgnoreCase(sortOrder);
|
||||||
|
queryWrapper.orderBy(true, isAsc, sortField);
|
||||||
|
} else {
|
||||||
|
queryWrapper.orderByDesc("log_timestamp");
|
||||||
|
}
|
||||||
|
return queryWrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.yupi.project.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.yupi.project.model.entity.MqttCommunicationLog;
|
||||||
|
|
||||||
|
public interface MqttCommunicationLogMapper extends BaseMapper<MqttCommunicationLog> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.yupi.project.model.dto.mqttlog;
|
||||||
|
|
||||||
|
import com.yupi.project.common.PageRequest;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class MqttLogQueryRequest extends PageRequest implements Serializable {
|
||||||
|
|
||||||
|
private String messageId;
|
||||||
|
private String direction;
|
||||||
|
private String clientId;
|
||||||
|
private String topic;
|
||||||
|
private String payloadContains;
|
||||||
|
private Integer qos;
|
||||||
|
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date endTime;
|
||||||
|
|
||||||
|
private Long relatedSessionId;
|
||||||
|
private Long relatedTaskId;
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.yupi.project.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@TableName(value = "mqtt_communication_log")
|
||||||
|
@Data
|
||||||
|
public class MqttCommunicationLog implements Serializable {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
private String direction; // UPSTREAM, DOWNSTREAM
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
private String payloadFormat; // TEXT, JSON, BINARY
|
||||||
|
|
||||||
|
private String payload;
|
||||||
|
|
||||||
|
private Integer qos;
|
||||||
|
|
||||||
|
private Boolean isRetained;
|
||||||
|
|
||||||
|
private Date logTimestamp;
|
||||||
|
|
||||||
|
private String backendProcessingStatus; // RECEIVED, PROCESSING, SUCCESS, FAILED
|
||||||
|
|
||||||
|
private String backendProcessingInfo;
|
||||||
|
|
||||||
|
private Long relatedSessionId;
|
||||||
|
|
||||||
|
private Long relatedTaskId;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -1,24 +1,31 @@
|
|||||||
package com.yupi.project.mqtt;
|
package com.yupi.project.mqtt;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||||
import org.springframework.beans.factory.DisposableBean;
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.context.event.ContextRefreshedEvent;
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MqttConnectionManager implements ApplicationListener<ContextRefreshedEvent>, DisposableBean {
|
public class MqttConnectionManager implements ApplicationListener<ContextRefreshedEvent>, DisposableBean {
|
||||||
|
|
||||||
private final MqttClient mqttClient;
|
private final MqttClient mqttClient;
|
||||||
private final MqttConnectOptions mqttConnectOptions;
|
private final MqttConnectOptions mqttConnectOptions;
|
||||||
// private final MqttProperties mqttProperties; // Injected if needed for logging brokerUrl, or get from mqttClient.getServerURI()
|
// private final MqttProperties mqttProperties; // Injected if needed for logging brokerUrl, or get from mqttClient.getServerURI()
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MqttConnectionManager(@Qualifier("mqttClientBean") MqttClient mqttClient,
|
||||||
|
MqttConnectOptions mqttConnectOptions) {
|
||||||
|
this.mqttClient = mqttClient;
|
||||||
|
this.mqttConnectOptions = mqttConnectOptions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
// Ensure this logic runs only once, for the root application context
|
// Ensure this logic runs only once, for the root application context
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.yupi.project.mqtt;
|
||||||
|
|
||||||
|
import com.yupi.project.config.properties.MqttLoggerProperties;
|
||||||
|
import com.yupi.project.config.properties.MqttProperties; // 主业务的 properties,获取QoS等
|
||||||
|
import com.yupi.project.service.MqttCommunicationLogService;
|
||||||
|
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.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component("mqttLoggerCallbackHandler") // 指定Bean名称
|
||||||
|
public class MqttLoggerCallbackHandler implements MqttCallbackExtended {
|
||||||
|
|
||||||
|
private final MqttCommunicationLogService logService;
|
||||||
|
private final MqttLoggerProperties loggerProperties;
|
||||||
|
private final MqttProperties mainMqttProperties; // 用于获取默认QoS等
|
||||||
|
private MqttClient mqttLoggerClient; // 通过setter注入
|
||||||
|
|
||||||
|
public MqttLoggerCallbackHandler(MqttCommunicationLogService logService,
|
||||||
|
MqttLoggerProperties loggerProperties,
|
||||||
|
MqttProperties mainMqttProperties) {
|
||||||
|
this.logService = logService;
|
||||||
|
this.loggerProperties = loggerProperties;
|
||||||
|
this.mainMqttProperties = mainMqttProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMqttClient(@Qualifier("mqttLoggerClient") MqttClient mqttLoggerClient) {
|
||||||
|
this.mqttLoggerClient = mqttLoggerClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectComplete(boolean reconnect, String serverURI) {
|
||||||
|
log.info("MQTT Logger Client connection {} to broker: {}", reconnect ? "re-established" : "established", serverURI);
|
||||||
|
if (!loggerProperties.isEnabled() || mqttLoggerClient == null) {
|
||||||
|
log.warn("MQTT Logger is disabled or client not initialized. Cannot subscribe to topics for logging.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<String> topicsToLog = loggerProperties.getTopics();
|
||||||
|
if (topicsToLog != null && !topicsToLog.isEmpty()) {
|
||||||
|
for (String topicFilter : topicsToLog) {
|
||||||
|
// 使用主配置的QoS等级进行订阅,或者在loggerProperties中单独配置
|
||||||
|
mqttLoggerClient.subscribe(topicFilter, mainMqttProperties.getDefaultQos());
|
||||||
|
log.info("MQTT Logger Client subscribed to topic: {} for logging.", topicFilter);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("No topics configured for MQTT Logger Client to subscribe.");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error subscribing MQTT Logger Client to topics: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionLost(Throwable cause) {
|
||||||
|
log.error("MQTT Logger Client connection lost!", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageArrived(String topic, MqttMessage message) throws Exception {
|
||||||
|
if (!loggerProperties.isEnabled()) {
|
||||||
|
return; // 如果日志功能被禁用,则不处理
|
||||||
|
}
|
||||||
|
// 异步记录上行消息 (UPSTREAM)
|
||||||
|
// clientId 可以从 mqttLoggerClient.getClientId() 获取,表示是日志客户端收到的
|
||||||
|
// messageIdStr可以尝试从 MQTTv5 属性获取,如果使用的是 MQTTv3,Paho 的 message.getId() 是内部ID,对于 broker 可能无意义
|
||||||
|
// 对于日志记录,如果 message.getId() > 0,可以用它,否则生成一个UUID
|
||||||
|
String messageIdStr = message.getId() > 0 ? String.valueOf(message.getId()) : UUID.randomUUID().toString();
|
||||||
|
logService.asyncLogMessage(topic, message, "UPSTREAM", mqttLoggerClient.getClientId(), messageIdStr, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deliveryComplete(IMqttDeliveryToken token) {
|
||||||
|
// Logger client typically does not publish, so this might not be very relevant
|
||||||
|
// unless it's used for some diagnostic publishing.
|
||||||
|
try {
|
||||||
|
if (token != null && token.isComplete() && token.getMessage() != null) {
|
||||||
|
log.trace("MQTT Logger Client: Delivery complete for message ID: {}", token.getMessageId());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error in MQTT Logger Client deliveryComplete: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.yupi.project.mqtt;
|
||||||
|
|
||||||
|
import com.yupi.project.config.properties.MqttLoggerProperties;
|
||||||
|
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.springframework.beans.factory.DisposableBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MqttLoggerConnectionManager implements ApplicationListener<ContextRefreshedEvent>, DisposableBean {
|
||||||
|
|
||||||
|
@Qualifier("mqttLoggerClient")
|
||||||
|
private final MqttClient mqttLoggerClient; // 注入日志专用客户端
|
||||||
|
private final MqttConnectOptions mqttConnectOptions; // 可复用主连接配置
|
||||||
|
private final MqttLoggerProperties mqttLoggerProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
|
if (event.getApplicationContext().getParent() == null) { // Ensure root context
|
||||||
|
if (mqttLoggerProperties.isEnabled() && mqttLoggerClient != null) {
|
||||||
|
connectToMqtt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectToMqtt() {
|
||||||
|
try {
|
||||||
|
if (!mqttLoggerClient.isConnected()) {
|
||||||
|
log.info("Attempting to connect MQTT Logger Client: {} to broker: {}", mqttLoggerClient.getClientId(), mqttLoggerClient.getServerURI());
|
||||||
|
mqttLoggerClient.connect(mqttConnectOptions);
|
||||||
|
// Subscription logic is handled by MqttLoggerCallbackHandler.connectComplete
|
||||||
|
} else {
|
||||||
|
log.info("MQTT Logger Client {} is already connected.", mqttLoggerClient.getClientId());
|
||||||
|
}
|
||||||
|
} catch (MqttException e) {
|
||||||
|
log.error("Error connecting MQTT Logger Client: ", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Unexpected error during MQTT Logger Client connection: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() throws Exception {
|
||||||
|
if (mqttLoggerProperties.isEnabled() && mqttLoggerClient != null) {
|
||||||
|
disconnectFromMqtt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disconnectFromMqtt() {
|
||||||
|
try {
|
||||||
|
if (mqttLoggerClient.isConnected()) {
|
||||||
|
log.info("Disconnecting MQTT Logger Client: {} from broker: {}", mqttLoggerClient.getClientId(), mqttLoggerClient.getServerURI());
|
||||||
|
mqttLoggerClient.disconnect();
|
||||||
|
log.info("MQTT Logger Client {} disconnected successfully.", mqttLoggerClient.getClientId());
|
||||||
|
}
|
||||||
|
} catch (MqttException e) {
|
||||||
|
log.error("Error disconnecting MQTT Logger Client: ", e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
log.info("Closing MQTT Logger Client: {}", mqttLoggerClient.getClientId());
|
||||||
|
mqttLoggerClient.close();
|
||||||
|
log.info("MQTT Logger Client {} closed successfully.", mqttLoggerClient.getClientId());
|
||||||
|
} catch (MqttException e) {
|
||||||
|
log.error("Error closing MQTT Logger Client: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.yupi.project.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.yupi.project.model.entity.MqttCommunicationLog;
|
||||||
|
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||||
|
|
||||||
|
public interface MqttCommunicationLogService extends IService<MqttCommunicationLog> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步记录MQTT消息
|
||||||
|
*
|
||||||
|
* @param topic 主题
|
||||||
|
* @param message MQTT消息对象
|
||||||
|
* @param direction 方向 (UPSTREAM/DOWNSTREAM)
|
||||||
|
* @param clientId 客户端ID
|
||||||
|
* @param messageIdStr MQTT v5 的 Message ID 或应用生成的UUID
|
||||||
|
* @param backendProcessingStatus 后端处理状态 (可选)
|
||||||
|
* @param backendProcessingInfo 后端处理信息 (可选)
|
||||||
|
* @param relatedSessionId 关联的会话ID (可选)
|
||||||
|
* @param relatedTaskId 关联的任务ID (可选)
|
||||||
|
*/
|
||||||
|
void asyncLogMessage(String topic, MqttMessage message, String direction, String clientId,
|
||||||
|
String messageIdStr, String backendProcessingStatus,
|
||||||
|
String backendProcessingInfo, Long relatedSessionId, Long relatedTaskId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步记录出站 (DOWNSTREAM) MQTT消息的简化版本。
|
||||||
|
* 通常在消息发布后调用。
|
||||||
|
*/
|
||||||
|
void asyncLogDownstreamMessage(String topic, String payload, Integer qos, Boolean retained, String clientId, String messageIdStr, Long relatedSessionId, Long relatedTaskId);
|
||||||
|
}
|
||||||
@@ -101,4 +101,13 @@ public interface RobotTaskService extends IService<RobotTask> {
|
|||||||
*/
|
*/
|
||||||
boolean markTaskAsFailed(Long taskId, String errorMessage, Date failedTime);
|
boolean markTaskAsFailed(Long taskId, String errorMessage, Date failedTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据机器人ID和会话ID查找最新的任务。
|
||||||
|
*
|
||||||
|
* @param robotId 机器人ID
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
* @return 如果找到,则返回任务实体,否则返回 null
|
||||||
|
*/
|
||||||
|
RobotTask findLatestTaskByRobotIdAndSessionId(String robotId, Long sessionId);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.yupi.project.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.yupi.project.mapper.MqttCommunicationLogMapper;
|
||||||
|
import com.yupi.project.model.entity.MqttCommunicationLog;
|
||||||
|
import com.yupi.project.service.MqttCommunicationLogService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class MqttCommunicationLogServiceImpl extends ServiceImpl<MqttCommunicationLogMapper, MqttCommunicationLog>
|
||||||
|
implements MqttCommunicationLogService {
|
||||||
|
|
||||||
|
private static final String DIRECTION_UPSTREAM = "UPSTREAM";
|
||||||
|
private static final String DIRECTION_DOWNSTREAM = "DOWNSTREAM";
|
||||||
|
private static final String PAYLOAD_FORMAT_TEXT = "TEXT";
|
||||||
|
private static final String PAYLOAD_FORMAT_JSON = "JSON"; // Assume JSON if parseable
|
||||||
|
private static final String PAYLOAD_FORMAT_BINARY = "BINARY";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Async("mqttLoggerThreadPoolTaskExecutor") // Ensure this bean name matches your AsyncConfigurer
|
||||||
|
public void asyncLogMessage(String topic, MqttMessage message, String direction, String clientId,
|
||||||
|
String messageIdStr, String backendProcessingStatus,
|
||||||
|
String backendProcessingInfo, Long relatedSessionId, Long relatedTaskId) {
|
||||||
|
try {
|
||||||
|
MqttCommunicationLog logEntry = new MqttCommunicationLog();
|
||||||
|
logEntry.setMessageId(messageIdStr != null ? messageIdStr : (message.getId() > 0 ? String.valueOf(message.getId()) : null) );
|
||||||
|
logEntry.setDirection(direction);
|
||||||
|
logEntry.setClientId(clientId);
|
||||||
|
logEntry.setTopic(topic);
|
||||||
|
|
||||||
|
byte[] payloadBytes = message.getPayload();
|
||||||
|
String payloadString = new String(payloadBytes, StandardCharsets.UTF_8);
|
||||||
|
logEntry.setPayload(payloadString); // Store as string
|
||||||
|
|
||||||
|
// Basic payload format detection (can be enhanced)
|
||||||
|
if (isJson(payloadString)) {
|
||||||
|
logEntry.setPayloadFormat(PAYLOAD_FORMAT_JSON);
|
||||||
|
} else {
|
||||||
|
logEntry.setPayloadFormat(PAYLOAD_FORMAT_TEXT); // Default to TEXT, could be BINARY if not UTF-8 decodable well
|
||||||
|
}
|
||||||
|
// For true binary, would need different handling or indication
|
||||||
|
|
||||||
|
logEntry.setQos(message.getQos());
|
||||||
|
logEntry.setIsRetained(message.isRetained());
|
||||||
|
logEntry.setLogTimestamp(new Date()); // Record time when log entry is created
|
||||||
|
|
||||||
|
logEntry.setBackendProcessingStatus(backendProcessingStatus);
|
||||||
|
logEntry.setBackendProcessingInfo(backendProcessingInfo);
|
||||||
|
logEntry.setRelatedSessionId(relatedSessionId);
|
||||||
|
logEntry.setRelatedTaskId(relatedTaskId);
|
||||||
|
|
||||||
|
this.save(logEntry);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to asynchronously log MQTT message. Topic: {}, Direction: {}, ClientId: {}", topic, direction, clientId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Async("mqttLoggerThreadPoolTaskExecutor")
|
||||||
|
public void asyncLogDownstreamMessage(String topic, String payload, Integer qos, Boolean retained, String clientId, String messageIdStr, Long relatedSessionId, Long relatedTaskId) {
|
||||||
|
try {
|
||||||
|
MqttCommunicationLog logEntry = new MqttCommunicationLog();
|
||||||
|
logEntry.setMessageId(messageIdStr != null ? messageIdStr : UUID.randomUUID().toString()); // Generate UUID if not provided
|
||||||
|
logEntry.setDirection(DIRECTION_DOWNSTREAM);
|
||||||
|
logEntry.setClientId(clientId);
|
||||||
|
logEntry.setTopic(topic);
|
||||||
|
logEntry.setPayload(payload);
|
||||||
|
|
||||||
|
if (isJson(payload)) {
|
||||||
|
logEntry.setPayloadFormat(PAYLOAD_FORMAT_JSON);
|
||||||
|
} else {
|
||||||
|
logEntry.setPayloadFormat(PAYLOAD_FORMAT_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry.setQos(qos);
|
||||||
|
logEntry.setIsRetained(retained != null ? retained : false);
|
||||||
|
logEntry.setLogTimestamp(new Date());
|
||||||
|
|
||||||
|
// For downstream, processing status/info is typically not set at the moment of logging send attempt
|
||||||
|
// It might be updated later if a response is expected and tracked.
|
||||||
|
logEntry.setRelatedSessionId(relatedSessionId);
|
||||||
|
logEntry.setRelatedTaskId(relatedTaskId);
|
||||||
|
|
||||||
|
this.save(logEntry);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to asynchronously log downstream MQTT message. Topic: {}, ClientId: {}", topic, clientId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isJson(String str) {
|
||||||
|
if (str == null || str.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String trimmedStr = str.trim();
|
||||||
|
return (trimmedStr.startsWith("{") && trimmedStr.endsWith("}")) || (trimmedStr.startsWith("[") && trimmedStr.endsWith("]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -18,13 +19,20 @@ import java.util.Date;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MqttServiceImpl implements MqttService {
|
public class MqttServiceImpl implements MqttService {
|
||||||
|
|
||||||
private final MqttClient mqttClient; // Autowired by Spring from MqttConfig
|
private final MqttClient mqttClient; // Autowired by Spring from MqttConfig
|
||||||
private final MqttProperties mqttProperties;
|
private final MqttProperties mqttProperties;
|
||||||
private final RobotTaskService robotTaskService;
|
private final RobotTaskService robotTaskService;
|
||||||
|
|
||||||
|
public MqttServiceImpl(@Qualifier("mqttClientBean") MqttClient mqttClient,
|
||||||
|
MqttProperties mqttProperties,
|
||||||
|
RobotTaskService robotTaskService) {
|
||||||
|
this.mqttClient = mqttClient;
|
||||||
|
this.mqttProperties = mqttProperties;
|
||||||
|
this.robotTaskService = robotTaskService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class) // Ensure rollback if MQTT publish fails after task creation
|
@Transactional(rollbackFor = Exception.class) // Ensure rollback if MQTT publish fails after task creation
|
||||||
public boolean sendCommand(String robotId, CommandTypeEnum commandType, String payloadJson, Long sessionId) throws Exception {
|
public boolean sendCommand(String robotId, CommandTypeEnum commandType, String payloadJson, Long sessionId) throws Exception {
|
||||||
|
|||||||
@@ -316,4 +316,25 @@ public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask
|
|||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RobotTask findLatestTaskByRobotIdAndSessionId(String robotId, Long sessionId) {
|
||||||
|
if (robotId == null || sessionId == null) {
|
||||||
|
log.warn("Cannot find latest task: robotId or sessionId is null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
QueryWrapper<RobotTask> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("robot_id", robotId)
|
||||||
|
.eq("related_session_id", sessionId)
|
||||||
|
.orderByDesc("create_time")
|
||||||
|
.last("LIMIT 1");
|
||||||
|
|
||||||
|
RobotTask task = this.getOne(queryWrapper);
|
||||||
|
if (task != null) {
|
||||||
|
log.info("Found latest task with ID {} for Robot ID: {} and Session ID: {}.", task.getId(), robotId, sessionId);
|
||||||
|
} else {
|
||||||
|
log.debug("No task found for Robot ID: {} and Session ID: {}.", robotId, sessionId);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -63,3 +63,19 @@ mqtt:
|
|||||||
task: # Task specific configurations
|
task: # Task specific configurations
|
||||||
timeoutSeconds: 300 # Default 300 seconds (5 minutes) for a task to be considered timed out
|
timeoutSeconds: 300 # Default 300 seconds (5 minutes) for a task to be considered timed out
|
||||||
timeoutCheckRateMs: 60000 # Default 60000 ms (1 minute) for how often to check for timed out tasks
|
timeoutCheckRateMs: 60000 # Default 60000 ms (1 minute) for how often to check for timed out tasks
|
||||||
|
logger:
|
||||||
|
enabled: true
|
||||||
|
client-id-prefix: backend-yupi-mqtt-power-logger- # Ensure this is different from the main client-id-prefix
|
||||||
|
# Topics to subscribe to for logging. Use '#' for all sub-topics.
|
||||||
|
# These should cover all topics the main application interacts with for upstream messages.
|
||||||
|
topics:
|
||||||
|
- "yupi_mqtt_power_project/robot/status/#" # Example: status messages from robots
|
||||||
|
# Add other topics for upstream messages (e.g., heartbeats, specific acknowledgements if not covered by status)
|
||||||
|
# - "yupi_mqtt_power_project/robot/heartbeat/#" # If heartbeats are on a separate root
|
||||||
|
async: # Async properties for logging tasks
|
||||||
|
core-pool-size: 2
|
||||||
|
max-pool-size: 5
|
||||||
|
queue-capacity: 1000 # Queue for logging tasks
|
||||||
|
thread-name-prefix: "mqtt-log-async-"
|
||||||
|
retention:
|
||||||
|
days: 30 # Log retention period in days (for future cleanup tasks)
|
||||||
Reference in New Issue
Block a user