From 021f9f0e0deb377c40e939a8246eb5f09ec11ebd Mon Sep 17 00:00:00 2001 From: lingyunxsh Date: Fri, 23 May 2025 11:47:59 +0800 Subject: [PATCH] =?UTF-8?q?mqtt=E6=B6=88=E6=81=AF=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E6=8E=A7=E5=88=B6=E5=8F=B0=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LogBook.md | 64 ++++ README.md | 6 +- .../(authenticated)/admin/mqtt-logs/page.tsx | 265 ++++++++++++++++ .../src/app/(authenticated)/layout.tsx | 4 +- .../src/app/admin/mqtt-logs/page.tsx | 297 ------------------ .../mapper/MqttCommunicationLogMapper.java | 2 +- 6 files changed, 337 insertions(+), 301 deletions(-) create mode 100644 charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx delete mode 100644 charging_web_app/src/app/admin/mqtt-logs/page.tsx diff --git a/LogBook.md b/LogBook.md index 3f376c3..0646ed4 100644 --- a/LogBook.md +++ b/LogBook.md @@ -1,3 +1,67 @@ +## 2024-08-09 (全局样式优化 - 内容区域背景) + +- **目标**: 为所有认证后页面的主要内容区域设置统一的淡灰色背景和内边距,以提升视觉一致性。 +- **问题**: 之前 `bg-gray-100` 和特定内边距仅应用于部分页面(如管理员控制台),其他页面(如MQTT日志页面)未使用此背景。 +- **解决方案**: + - 修改 `charging_web_app/src/app/(authenticated)/layout.tsx` 文件。 + - 将 `
` 元素的类名从 `"flex-grow container mx-auto p-4 sm:p-6 lg:p-8"` 修改为 `"flex-grow container mx-auto bg-gray-100 py-8 px-4 md:px-6 lg:px-8"`。 + - 这使得所有通过 `AuthenticatedLayout` 渲染的子页面 (`{children}`) 的主内容区域都将继承 `bg-gray-100` 背景色和 `py-8 px-4 md:px-6 lg:px-8` 的内边距。 +- **状态**: 全局内容区域背景和内边距已统一。所有认证后的页面现在应具有一致的淡灰色背景和内边距。 +- **下一步**: 用户验证所有相关页面的视觉效果。 + +## 2024-08-09 (MQTT通信日志页面 - 对接真实API与UI增强) + +- **目标**: 将 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx` 从使用模拟数据改为对接真实的后端API,并使用Ant Design组件增强UI和功能。 +- **实施**: + - **移除模拟数据**: 删除了页面中用于生成和显示模拟日志的逻辑。 + - **引入Ant Design**: 页面全面改用Ant Design组件,包括 `Table`, `Form`, `Input`, `Select`, `Button`, `DatePicker`, `Tag`, `Tooltip`, `Card`, `Row`, `Col`, `Space`。 + - **API对接**: + - 导入了 `fetchMqttLogs` 服务 (来自 `logService.ts`) 和相关的类型定义 (`MqttCommunicationLog`, `MqttLogQueryRequest`, `PageResponse`, `BaseResponse` 来自 `log.types.ts`)。 + - `fetchData` 函数现在调用 `fetchMqttLogs`,并正确处理后端返回的数据结构,包括分页信息和错误处理。 + - 使用 `dayjs` 格式化时间戳。 + - **查询表单功能**: + - 创建了一个包含多个筛选条件的表单 (客户端ID, 主题, 方向, Payload内容, 时间范围, 关联会话ID, 关联任务ID)。 + - 实现了查询和重置功能,查询时会重新获取数据并重置到第一页。 + - 时间范围选择器使用 AntD `RangePicker`。 + - **表格功能增强**: + - 实现了服务器端分页和排序。 + - 表格列根据后端返回的数据模型 (`MqttCommunicationLog`) 进行定义,并对关键字段 (时间戳, 方向, Payload, Retained标记等) 进行了格式化和本地化显示。 + - Payload 列使用 `Tooltip` 展示完整内容,并通过 `pre` 标签保持格式。 + - 为表格启用了水平滚动条和边框。 + - **权限与加载状态**: 保留并优化了用户权限检查逻辑和加载状态的显示。 +- **状态**: MQTT通信日志页面现在能够从后端API获取、显示和筛选真实的日志数据,并拥有了基于Ant Design的完善的用户界面。与全局布局和导航保持一致。 +- **下一步**: + - 用户进行全面测试,包括查询、分页、排序、错误处理等。 + - 根据反馈进行可能的微调。 + +## 2024-08-09 (MQTT通信日志页面 - 修复路径冲突) + +- **问题**: Next.js 报错 "You cannot have two parallel pages that resolve to the same path",指向 `/src/app/admin`。 +- **原因**: + - 先前在 `charging_web_app/src/app/admin/mqtt-logs/page.tsx` 创建了MQTT日志页面。 + - 之后为了应用认证布局,在 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx` 又创建了同路径页面。 + - Next.js 的路由组 `(authenticated)` 不影响 URL 路径,导致两个文件都解析到 `/admin/mqtt-logs`,引发冲突。 +- **解决方案**: + - 删除了位于 `charging_web_app/src/app/admin/mqtt-logs/` 目录下的旧 `page.tsx` 文件及其父目录(如果为空)。 + - 保留了 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx`,因为它能正确应用期望的布局。 +- **状态**: 路径冲突已解决。MQTT日志页面现在由 `(authenticated)` 路由组内的文件唯一定义。 + +## 2024-08-09 (MQTT通信日志页面 - 布局优化) + +- **目标**: 确保 `charging_web_app` 中新创建的MQTT通信日志页面 (`/admin/mqtt-logs`) 与应用的全局布局(特别是顶部导航栏)保持一致。 +- **分析**: + - 全局的根布局 `charging_web_app/src/app/layout.tsx` 比较基础,主要提供全局样式和 `AuthProvider`。 + - 经过认证的用户界面布局(包括顶部导航栏、主内容区和页脚)定义在 `charging_web_app/src/app/(authenticated)/layout.tsx` 中。 + - `AuthenticatedLayout.tsx` 使用 Tailwind CSS,并包含了一个指向 `/admin/mqtt-logs` 的链接(仅管理员可见)。 +- **实施**: + - 在 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/` 目录下创建了 `page.tsx` 文件。 + - `page.tsx` 中实现了一个基本的MQTT日志展示组件,包括模拟数据加载、分页、权限检查和基本的Tailwind CSS样式,使其内容区域风格与整体应用一致。 + - 由于页面嵌套在 `(authenticated)/admin/` 路径下,它自动继承了 `(authenticated)/layout.tsx` 的布局,因此顶部导航栏和整体页面结构符合预期。 +- **状态**: MQTT通信日志页面已创建,并成功融入了现有的全局布局和导航结构中。管理员用户可以通过导航栏的链接访问此页面。 +- **下一步**: + - 将 `page.tsx` 中的模拟数据获取逻辑替换为真实的API调用(连接到先前开发的后端 `MqttCommunicationLogController`)。 + - 完善页面的筛选、排序和错误处理功能。 + ## 2024-08-08 (MQTT通信日志功能 - 开发完成) - **概述**: MQTT通信日志功能模块开发完成。该模块旨在记录系统与MQTT代理之间的所有通信消息,并提供前端界面供管理员查询和审计。 diff --git a/README.md b/README.md index 2320bcc..0534b84 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ * 充电桩/车位管理(状态监控、添加、编辑、删除) * 充电会话管理(查询、监控) * 激活码管理(生成、查询、删除) + * MQTT通信日志查看(查询、审计所有MQTT消息) * 系统概览与统计 * **硬件交互 (通过 MQTT)**: * 充电桩状态上报 (空闲、连接中、充电中、故障等) @@ -24,7 +25,7 @@ ## 2. 技术栈 -* **后端**: Spring Boot, Spring Security, MyBatis Plus, MySQL, Mosquitto (MQTT Broker) +* **后端**: Spring Boot, Spring Security, MyBatis Plus, MySQL, Mosquitto (MQTT Broker), Spring AOP (用于日志) * **前端**: Next.js, React, TypeScript, Ant Design, Axios * **数据库**: MySQL * **通信协议**: HTTP/HTTPS, MQTT @@ -247,5 +248,8 @@ * 完善各模块的单元测试和集成测试。 * 进一步优化 API 文档和错误处理机制。 * 根据实际运营需求,增加更细致的统计报表功能。 +* MQTT通信日志模块: + * (可选) 进一步优化Payload的展示,例如对JSON格式的Payload进行美化或提供折叠/展开功能。 + * (可选) 根据实际需求,考虑日志数据的定期归档或清理策略的实现。 * 考虑引入更高级的 MQTT 特性,如QoS、遗嘱消息等。 * 前端 UI/UX 持续打磨,提升用户体验。 \ No newline at end of file diff --git a/charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx b/charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx new file mode 100644 index 0000000..6c3be43 --- /dev/null +++ b/charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx @@ -0,0 +1,265 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; +import { + Table, Form, Input, Select, Button, DatePicker, message, Tag, Tooltip, Card, Row, Col, Space +} from 'antd'; +import type { TableProps, TablePaginationConfig } from 'antd'; +import type { FilterValue, SorterResult } from 'antd/es/table/interface'; +import { SearchOutlined, RedoOutlined } from '@ant-design/icons'; +import { useAuth } from '@/contexts/AuthContext'; +import { fetchMqttLogs, BaseResponse } from '@/services/logService'; +import { + MqttCommunicationLog, MqttLogQueryRequest, PageResponse +} from '@/types/log.types'; +import dayjs from 'dayjs'; + +const { RangePicker } = DatePicker; +const { Option } = Select; + +const MqttCommunicationLogPage: React.FC = () => { + const { user } = useAuth(); + const [form] = Form.useForm(); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 20, + total: 0, + showSizeChanger: true, + pageSizeOptions: ['10', '20', '50', '100'], + showTotal: (total) => `共 ${total} 条记录`, + }); + + const fetchData = useCallback(async (params: MqttLogQueryRequest = {}) => { + setLoading(true); + const queryParams: MqttLogQueryRequest = { + current: pagination.current, + pageSize: pagination.pageSize, + ...form.getFieldsValue(), // Get values from the form + ...params, // Override with any explicitly passed params (like sorters) + }; + + // Handle dateRange from AntD RangePicker + if (queryParams.dateRange && queryParams.dateRange[0] && queryParams.dateRange[1]) { + queryParams.startTime = queryParams.dateRange[0].toISOString(); + queryParams.endTime = queryParams.dateRange[1].toISOString(); + delete queryParams.dateRange; // Remove from params sent to backend + } + + try { + const response: BaseResponse> = await fetchMqttLogs(queryParams); + if (response.code === 0 && response.data) { + setLogs(response.data.records || []); + setPagination(prev => ({ + ...prev, + current: response.data.current, + pageSize: response.data.size, + total: response.data.total, + })); + } else { + message.error(response.message || '获取MQTT日志失败'); + setLogs([]); + setPagination(prev => ({ ...prev, total: 0, current: 1 })); + } + } catch (error) { + console.error('获取MQTT日志错误:', error); + message.error('无法连接到服务器或请求失败'); + setLogs([]); + setPagination(prev => ({ ...prev, total: 0, current: 1 })); + } finally { + setLoading(false); + } + }, [form, pagination.current, pagination.pageSize]); // Dependencies for useCallback + + useEffect(() => { + if (user?.role === 'admin') { + fetchData(); + } + }, [fetchData, user?.role]); + + const handleTableChange: TableProps['onChange'] = ( + newPagination: TablePaginationConfig, + filters: Record, + sorter: SorterResult | SorterResult[], + ) => { + const currentSorter = Array.isArray(sorter) ? sorter[0] : sorter; + const sortParams: MqttLogQueryRequest = {}; + if (currentSorter && currentSorter.field && currentSorter.order) { + sortParams.sortField = String(currentSorter.field); + sortParams.sortOrder = currentSorter.order; + } + + setPagination(prev => ({ + ...prev, + current: newPagination.current, + pageSize: newPagination.pageSize, + })); + + // fetchData will pick up new pagination from state + // and form values, and will merge sortParams + fetchData(sortParams); + }; + + const onFinish = () => { + // Reset to first page on new search + setPagination(prev => ({ ...prev, current: 1 })); + fetchData(); // fetchData will use current form values and new pagination + }; + + const handleReset = () => { + form.resetFields(); + setPagination(prev => ({ ...prev, current: 1 })); + // After reset, fetch with default (cleared) form values + fetchData({}); // Pass empty object to ensure sortField/sortOrder are not persisted from previous state if any + }; + + if (!user && !loading) { + return

请先登录。

; + } + + if (user?.role !== 'admin' && !loading) { + return ( + +

+ 抱歉,您没有权限访问此页面。此功能仅对管理员开放。 +

+
+ ); + } + + const columns: TableProps['columns'] = [ + { title: 'ID', dataIndex: 'id', key: 'id', width: 80, sorter: true }, + { + title: '时间戳', + dataIndex: 'logTimestamp', + key: 'logTimestamp', + sorter: true, + width: 180, + render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss.SSS'), + }, + { + title: '方向', + dataIndex: 'direction', + key: 'direction', + width: 100, + render: (direction: 'UPSTREAM' | 'DOWNSTREAM') => ( + + {direction === 'UPSTREAM' ? '上行' : '下行'} + + ), + }, + { title: '客户端ID', dataIndex: 'clientId', key: 'clientId', width: 150, sorter: true, ellipsis: true }, + { title: '主题', dataIndex: 'topic', key: 'topic', width: 250, sorter: true, ellipsis: true }, + { + title: 'Payload', + dataIndex: 'payload', + key: 'payload', + ellipsis: true, + render: (text: string) => ( + +
+            {text}
+          
+
+ ), + }, + { + title: '格式', + dataIndex: 'payloadFormat', + key: 'payloadFormat', + width: 80, + render: (format?: 'TEXT' | 'JSON' | 'BINARY') => format || 'N/A', + }, + { title: 'QoS', dataIndex: 'qos', key: 'qos', width: 60, sorter: true }, + { + title: 'Retained', + dataIndex: 'isRetained', + key: 'isRetained', + width: 90, + render: (isRetained?: boolean) => (isRetained ? : '否'), + }, + { title: '关联会话ID', dataIndex: 'relatedSessionId', key: 'relatedSessionId', width: 120, sorter: true }, + { title: '关联任务ID', dataIndex: 'relatedTaskId', key: 'relatedTaskId', width: 120, sorter: true }, + ]; + + return ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + ); +}; + +export default MqttCommunicationLogPage; \ No newline at end of file diff --git a/charging_web_app/src/app/(authenticated)/layout.tsx b/charging_web_app/src/app/(authenticated)/layout.tsx index 2c0eba4..ed70caa 100644 --- a/charging_web_app/src/app/(authenticated)/layout.tsx +++ b/charging_web_app/src/app/(authenticated)/layout.tsx @@ -47,7 +47,7 @@ export default function AuthenticatedLayout({ }; return ( -
+
-
+
{children}
diff --git a/charging_web_app/src/app/admin/mqtt-logs/page.tsx b/charging_web_app/src/app/admin/mqtt-logs/page.tsx deleted file mode 100644 index 6f4299a..0000000 --- a/charging_web_app/src/app/admin/mqtt-logs/page.tsx +++ /dev/null @@ -1,297 +0,0 @@ -'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(); - const [loading, setLoading] = useState(false); - const [logs, setLogs] = useState([]); - 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>, then we need to access response.data - const response = await fetchMqttLogs(params); - - // Assuming fetchMqttLogs returns BaseResponse> 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 is inside response.data - if (response && response.data) { // Check if response and response.data exist - const pageData: PageResponse = 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['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['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 上行; - if (direction === 'DOWNSTREAM') return 下行; - 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 ? ( - {text}}> -
-              {text.length > 100 ? `${text.substring(0, 100)}...` : text}
-            
-
- ) : '-' - ), - }, - { - 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 ( - - MQTT 通信日志 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
`总计 ${totalLogs} 条`, - }} - onChange={handleTableChange} - scroll={{ x: 1500 }} - /> - - ); -}; - -export default MqttLogsPage; \ No newline at end of file diff --git a/springboot-init-main/src/main/java/com/yupi/project/mapper/MqttCommunicationLogMapper.java b/springboot-init-main/src/main/java/com/yupi/project/mapper/MqttCommunicationLogMapper.java index 54d0679..cda9ecf 100644 --- a/springboot-init-main/src/main/java/com/yupi/project/mapper/MqttCommunicationLogMapper.java +++ b/springboot-init-main/src/main/java/com/yupi/project/mapper/MqttCommunicationLogMapper.java @@ -2,6 +2,6 @@ package com.yupi.project.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.yupi.project.model.entity.MqttCommunicationLog; - + public interface MqttCommunicationLogMapper extends BaseMapper { } \ No newline at end of file