From 03e320af9e1192bdf9ececb99584c77ba86a742c Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Thu, 7 May 2026 17:14:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(log):=20=E5=A2=9E=E5=BC=BA=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD=E4=B8=8E=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增模块级别日志控制和采样配置 - 为操作日志添加requestId字段支持全链路追踪 - 优化请求日志中间件,增强结构化记录能力 - 实现日志降级机制,数据库不可用时写入文件 - 添加相关数据库迁移脚本和模型索引 --- CODE_WIKI.md | 2288 +++++++++++++++++ backend/.env.example | 11 + backend/middleware/requestLogger.js | 51 +- backend/models/OperationLog.js | 12 + backend/scripts/migrate-all.js | 51 + .../migrate-operation-log-requestId.js | 115 + backend/utils/logger.js | 174 +- backend/utils/operationLogger.js | 165 +- 8 files changed, 2809 insertions(+), 58 deletions(-) create mode 100644 CODE_WIKI.md create mode 100644 backend/scripts/migrate-operation-log-requestId.js diff --git a/CODE_WIKI.md b/CODE_WIKI.md new file mode 100644 index 0000000..20ccea9 --- /dev/null +++ b/CODE_WIKI.md @@ -0,0 +1,2288 @@ +# IDC设备资产系统 - Code Wiki + +> 版本:2.0.0 | 更新日期:2026-05-07 | 项目地址:[GitHub](https://github.com/gituib/idc_assest) + +--- + +## 目录 + +1. [系统概述](#1-系统概述) +2. [整体架构](#2-整体架构) +3. [前端模块结构](#3-前端模块结构) +4. [后端模块结构](#4-后端模块结构) +5. [数据模型关系](#5-数据模型关系) +6. [API接口规范](#6-api接口规范) +7. [关键模块详解](#7-关键模块详解) +8. [依赖关系](#8-依赖关系) +9. [运行环境与配置](#9-运行环境与配置) +10. [部署指南](#10-部署指南) +11. [系统关系图](#11-系统关系图) + +--- + +## 1. 系统概述 + +### 1.1 项目简介 + +IDC设备资产系统(IDC Device Management System)是一个现代化的数据中心设备全生命周期管理平台,提供机房、机柜、设备的完整管理能力,并具备3D可视化展示功能。 + +### 1.2 技术栈概览 + +| 层级 | 技术选型 | 版本 | +|------|----------|------| +| **前端框架** | React + Vite | 18.2.0 / 4.4.9 | +| **UI组件库** | Ant Design | 5.8.6 | +| **3D渲染** | Three.js + @react-three/fiber | 0.183.2 / 8.18.0 | +| **后端框架** | Express | 4.18.2 | +| **ORM框架** | Sequelize | 6.37.8 | +| **数据库** | SQLite / MySQL | 5.1.7 / 8.0+ | +| **认证** | JWT | 9.0.3 | + +### 1.3 核心功能模块 + +| 模块 | 功能描述 | 页面入口 | +|------|----------|----------| +| **机房管理** | 多机房分类、平面图可视化 | /rooms | +| **机柜管理** | 机柜CRUD、容量统计、3D可视化 | /racks | +| **设备管理** | 设备全生命周期、批量导入导出 | /devices | +| **端口管理** | 设备端口配置、网卡绑定 | /ports | +| **线缆管理** | 机柜间线缆连接可视化追踪 | /cables | +| **网络拓扑** | ReactFlow拓扑可视化 | - | +| **工单管理** | 故障报修、维护工单全流程 | /tickets | +| **耗材管理** | 库存、领用、SN序列号追踪 | /consumables | +| **盘点管理** | 盘点计划、任务分配、盘盈设备 | /inventory | +| **数据看板** | 实时监控、趋势图表、功率监控 | / | +| **3D可视化** | Three.js机柜可视化、LOD优化 | /visualization-3d | +| **备份管理** | 本地/远程备份、定时任务 | /backup | +| **操作日志** | 审计日志、统计分析 | /operation-logs | +| **用户权限** | RBAC角色控制 | /users | + +--- + +## 2. 整体架构 + +### 2.1 系统架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 客户端层 (Client) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 浏览器 │ │ 移动端 │ │ Web App │ │ PWA │ │ +│ │ (Chrome) │ │ (响应式) │ │ (SPA) │ │ (可选) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 前端层 (Frontend) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ React 18 + Vite │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ 页面组件 │ │ 3D组件 │ │ 通用组件 │ │ Hooks │ │ │ +│ │ │ /pages │ │ /3d │ │/components│ │ /hooks │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ Context │ │ API │ │ Utils │ │ Config │ │ │ +│ │ │状态管理 │ │封装 │ │工具函数 │ │配置中心 │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + │ RESTful API │ + │ /api/* │ + └───────────────┬───────────────┘ + │ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 后端层 (Backend) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Express.js Server │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │中间件层 │ │路由层 │ │业务逻辑 │ │数据访问 │ │ │ +│ │ │/middleware│ │/routes │ │/routes │ │/models │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │验证层 │ │认证授权 │ │日志审计 │ │备份恢复 │ │ │ +│ │ │/validation│ │/middleware│ │/utils │ │/utils │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 数据层 (Data Layer) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ SQLite │ │ MySQL │ │ +│ │ (开发环境) │◄────────────────►│ (生产环境) │ │ +│ │ idc_management.db│ │ MySQL 8.0+ │ │ +│ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 前端项目结构 + +``` +frontend/ +├── public/ # 静态公共资源 +│ ├── assets/ +│ │ └── 3d/ +│ │ └── env.hdr # 3D环境贴图 +│ └── png/ # 设备图标资源 +│ ├── 服务器.png +│ ├── 交换机.png +│ ├── 路由器.png +│ └── 防火墙.png +├── src/ # 源代码目录 +│ ├── api/ # API接口封装 +│ │ ├── index.js # Axios实例、API方法 +│ │ └── cache.js # 请求缓存管理 +│ ├── components/ # React组件库 +│ │ ├── 3d/ # 3D可视化组件 +│ │ │ ├── materials/ # 3D材质组件 +│ │ │ │ ├── TextureCache.js # 纹理缓存管理 +│ │ │ │ ├── constants.jsx # 3D常量定义 +│ │ │ │ ├── devicePanel.jsx # 设备信息面板 +│ │ │ │ ├── index.jsx # 材质导出 +│ │ │ │ ├── rackFrame.jsx # 机柜框架材质 +│ │ │ │ └── utils.jsx # 3D工具函数 +│ │ │ ├── CascadingRackPanel.jsx # 级联机柜面板 +│ │ │ ├── DeviceModel.jsx # 设备3D模型 +│ │ │ ├── LODManager.jsx # LOD细节层次管理 +│ │ │ ├── RackModel.jsx # 机柜3D模型 +│ │ │ ├── RackSelectorHeader.jsx # 机柜选择器 +│ │ │ └── Scene.jsx # 3D主场景 +│ │ ├── dashboard/ # 数据看板组件 +│ │ │ ├── AnimatedCounter.jsx # 数字动画 +│ │ │ ├── CircularProgress.jsx # 环形进度 +│ │ │ ├── DeviceTrendChart.jsx # 设备趋势图 +│ │ │ ├── NavigationGrid.jsx # 导航网格 +│ │ │ ├── PowerGauge.jsx # 功率仪表 +│ │ │ ├── QuickStats.jsx # 快速统计 +│ │ │ ├── StatCard.jsx # 统计卡片 +│ │ │ ├── StatusLegend.jsx # 状态图例 +│ │ │ └── SystemInfo.jsx # 系统信息 +│ │ ├── device/ # 设备管理组件 +│ │ │ ├── BatchStatusModal.jsx # 批量状态修改 +│ │ │ ├── DeviceDetailModal.jsx # 设备详情 +│ │ │ ├── DeviceFormModal.jsx # 设备表单 +│ │ │ ├── ExportModal.jsx # 导出弹窗 +│ │ │ ├── FieldConfigModal.jsx # 字段配置 +│ │ │ ├── ImportModal.jsx # 导入弹窗 +│ │ │ └── ResizableTitle.jsx # 可调整标题 +│ │ ├── floorplan/ # 机房平面图组件 +│ │ │ ├── canvas/ # Canvas渲染 +│ │ │ │ ├── CanvasConstants.js +│ │ │ │ ├── CanvasInteraction.js +│ │ │ │ ├── CanvasRenderer.js +│ │ │ │ ├── FloorPlanCanvas.jsx +│ │ │ │ └── index.js +│ │ │ ├── panels/ # 侧边面板 +│ │ │ ├── toolbar/ # 工具栏 +│ │ │ ├── index.js +│ │ │ └── styles.js +│ │ ├── rack/ # 机柜组件 +│ │ ├── topology/ # 网络拓扑组件 +│ │ │ ├── hooks/ # 拓扑Hooks +│ │ │ │ ├── useTopologyData.js +│ │ │ │ └── useTopologyLayout.js +│ │ │ └── nodes/ # 拓扑节点 +│ │ ├── CableCreateModal.jsx # 线缆创建 +│ │ ├── CableWizardModal.jsx # 线缆向导 +│ │ ├── DeviceDetailDrawer.jsx # 设备详情抽屉 +│ │ ├── ErrorBoundary.jsx # 错误边界 +│ │ ├── NetworkCardPanel.jsx # 网卡面板 +│ │ ├── PortManagementPanel.jsx # 端口管理面板 +│ │ ├── ProtectedRoute.jsx # 路由保护 +│ │ └── VirtualDeviceList.jsx # 虚拟设备列表 +│ ├── config/ # 配置文件 +│ │ ├── api.js # API基础配置 +│ │ ├── dangerousOperationConfig.js # 危险操作配置 +│ │ └── theme.js # 主题配置 +│ ├── constants/ # 常量定义 +│ │ └── deviceManagementConstants.js +│ ├── context/ # React Context +│ │ ├── AuthContext.jsx # 认证上下文 +│ │ ├── ConfigContext.jsx # 配置上下文 +│ │ ├── FloorPlanContext.jsx # 平面图上下文 +│ │ └── Scene3DContext.jsx # 3D场景上下文 +│ ├── hooks/ # 自定义Hooks +│ │ ├── floorplan/ # 平面图Hooks +│ │ │ ├── useFloorPlanContext.js +│ │ │ └── useFloorPlanData.js +│ │ ├── useApi.js # API请求Hook +│ │ ├── useDangerousOperation.js # 危险操作Hook +│ │ ├── useDebounce.js # 防抖Hook +│ │ ├── useDesignTokens.js # 设计令牌Hook +│ │ ├── useIdleTimeout.js # 空闲超时Hook +│ │ ├── useResponsiveLayout.js # 响应式布局Hook +│ │ ├── useSWR.js # SWR数据请求Hook +│ │ └── useSortedRacks.js # 机柜排序Hook +│ ├── pages/ # 页面组件 +│ │ ├── Dashboard.jsx # 数据看板主页 +│ │ ├── Login.jsx # 登录页 +│ │ ├── RoomManagement.jsx # 机房管理 +│ │ ├── RoomFloorPlan.jsx # 机房平面图 +│ │ ├── RackManagement.jsx # 机柜管理 +│ │ ├── DeviceManagement.jsx # 设备管理 +│ │ ├── DeviceFieldManagement.jsx # 设备字段管理 +│ │ ├── IdleDeviceManagement.jsx # 空闲设备管理 +│ │ ├── PendingDeviceManagement.jsx # 待审核设备 +│ │ ├── PortManagement.jsx # 端口管理 +│ │ ├── CableManagement.jsx # 线缆管理 +│ │ ├── TicketManagement.jsx # 工单管理 +│ │ ├── TicketCategoryManagement.jsx # 工单分类 +│ │ ├── TicketFieldManagement.jsx # 工单字段 +│ │ ├── TicketStatistics.jsx # 工单统计 +│ │ ├── ConsumableManagement.jsx # 耗材管理 +│ │ ├── CategoryManagement.jsx # 耗材分类 +│ │ ├── ConsumableLogs.jsx # 耗材日志 +│ │ ├── ConsumableStatistics.jsx # 耗材统计 +│ │ ├── InventoryManagement.jsx # 盘点管理 +│ │ ├── InventoryTaskExecution.jsx # 盘点任务执行 +│ │ ├── OperationLogs.jsx # 操作日志 +│ │ ├── UserManagement.jsx # 用户管理 +│ │ ├── SystemSettings.jsx # 系统设置 +│ │ ├── BackupManagement.jsx # 备份管理 +│ │ ├── AutoBackupSettings.jsx # 自动备份设置 +│ │ ├── RemoteBackupSettings.jsx # 远程备份设置 +│ │ ├── Rack3DVisualization.jsx # 3D可视化 +│ │ └── ErrorBoundaryTest.jsx # 错误边界测试 +│ ├── styles/ # 样式文件 +│ │ └── deviceManagementStyles.js +│ ├── utils/ # 工具函数 +│ │ ├── common.js # 通用工具 +│ │ ├── crypto.js # 加密工具 +│ │ ├── deviceUtils.jsx # 设备相关工具 +│ │ ├── logger.js # 日志工具 +│ │ └── secureStorage.js # 安全存储 +│ ├── App.jsx # 应用入口 +│ ├── main.jsx # 渲染入口 +│ └── index.css # 全局样式 +├── vite.config.mjs # Vite配置 +├── package.json # 前端依赖 +└── .env.example # 环境变量示例 +``` + +### 2.3 后端项目结构 + +``` +backend/ +├── config/ # 配置模块 +│ ├── index.js # 配置统一导出 +│ ├── constants.js # 常量配置 +│ ├── routes.js # 路由注册配置 +│ ├── security.js # 安全配置 +│ ├── auto-backup-settings.json # 自动备份设置 +│ └── remote-backup-configs.json # 远程备份配置 +├── middleware/ # 中间件 +│ ├── auth.js # JWT认证中间件 +│ ├── maintenance.js # 维护模式中间件 +│ ├── requestLogger.js # 请求日志中间件 +│ └── validation.js # 验证中间件 +├── models/ # Sequelize数据模型 +│ ├── index.js # 模型关联定义 +│ ├── Room.js # 机房模型 +│ ├── Rack.js # 机柜模型 +│ ├── Device.js # 设备模型 +│ ├── DeviceField.js # 设备字段模型 +│ ├── DevicePort.js # 设备端口模型 +│ ├── NetworkCard.js # 网卡模型 +│ ├── Cable.js # 线缆模型 +│ ├── Ticket.js # 工单模型 +│ ├── TicketField.js # 工单字段模型 +│ ├── TicketCategory.js # 工单分类模型 +│ ├── TicketOperationRecord.js # 工单操作记录 +│ ├── FaultCategory.js # 故障分类模型 +│ ├── Consumable.js # 耗材模型 +│ ├── ConsumableCategory.js # 耗材分类模型 +│ ├── ConsumableRecord.js # 耗材领用记录 +│ ├── ConsumableLog.js # 耗材日志模型 +│ ├── ConsumableLogArchive.js # 耗材日志归档 +│ ├── InventoryPlan.js # 盘点计划模型 +│ ├── InventoryTask.js # 盘点任务模型 +│ ├── InventoryRecord.js # 盘点记录模型 +│ ├── User.js # 用户模型 +│ ├── Role.js # 角色模型 +│ ├── Permission.js # 权限模型 +│ ├── UserRole.js # 用户角色关联 +│ ├── SystemSetting.js # 系统设置模型 +│ ├── PendingDevice.js # 待审核设备模型 +│ ├── OperationLog.js # 操作日志模型 +│ ├── BackupLog.js # 备份日志模型 +│ ├── Warehouse.js # 仓库模型 +│ ├── Business.js # 业务模型 +│ ├── DeviceBusiness.js # 设备业务关联 +│ └── ticketIndex.js # 工单索引模型 +├── routes/ # API路由 +│ ├── auth.js # 认证路由 +│ ├── rooms.js # 机房路由 +│ ├── racks.js # 机柜路由 +│ ├── devices.js # 设备路由 +│ ├── deviceFields.js # 设备字段路由 +│ ├── devicePorts.js # 设备端口路由 +│ ├── networkCards.js # 网卡路由 +│ ├── cables.js # 线缆路由 +│ ├── tickets.js # 工单路由 +│ ├── ticketCategories.js # 工单分类路由 +│ ├── ticketFields.js # 工单字段路由 +│ ├── consumables.js # 耗材路由 +│ ├── consumableCategories.js # 耗材分类路由 +│ ├── consumableRecords.js # 耗材记录路由 +│ ├── consumableImport.js # 耗材导入路由 +│ ├── inventory.js # 盘点路由 +│ ├── users.js # 用户路由 +│ ├── roles.js # 角色路由 +│ ├── systemSettings.js # 系统设置路由 +│ ├── background.js # 背景配置路由 +│ ├── backup.js # 备份管理路由 +│ ├── statistics.js # 统计分析路由 +│ ├── operationLogs.js # 操作日志路由 +│ ├── idleDevices.js # 空闲设备路由 +│ ├── warehouses.js # 仓库路由 +│ ├── dangerousOperations.js # 危险操作路由 +│ └── topology.js # 拓扑路由 +├── scripts/ # 数据库脚本 +│ ├── init-database.js # 数据库初始化 +│ ├── migrate-all.js # 执行所有迁移 +│ ├── fix-foreign-keys.js # 外键修复 +│ ├── backup.js # 备份脚本 +│ ├── restore.js # 恢复脚本 +│ ├── create_indexes.js # 创建索引 +│ ├── generate-rack-import-template.js # 生成导入模板 +│ └── archive/ # 归档迁移脚本 +│ ├── migrate-v2.js +│ ├── migrate-consumable-logs.js +│ ├── migrate-consumable-version.js +│ ├── migrate-consumable-log-decouple.js +│ ├── migrate-consumable-log-archive.js +│ ├── remove-consumable-log-fk.js +│ └── migrate-add-pending-status.js +├── utils/ # 工具函数 +│ ├── autoBackupScheduler.js # 自动备份调度器 +│ ├── backup.js # 备份工具 +│ ├── remoteBackup.js # 远程备份 +│ ├── remoteBackupConfig.js # 远程备份配置 +│ ├── backupLog.js # 备份日志 +│ ├── dangerousOperationLogger.js # 危险操作日志 +│ ├── errorHandler.js # 错误处理器 +│ ├── healthCheck.js # 健康检查 +│ ├── idGenerator.js # ID生成器 +│ ├── importJobManager.js # 导入任务管理 +│ ├── logger.js # Winston日志 +│ ├── maintenanceMode.js # 维护模式 +│ ├── operationLogger.js # 操作日志 +│ └── routeLoader.js # 路由加载器 +├── validation/ # Joi验证Schema +│ ├── deviceSchema.js +│ ├── rackSchema.js +│ └── roomSchema.js +├── uploads/ # 文件上传目录 +│ └── avatars/ # 用户头像 +├── tests/ # 测试文件 +├── temp/ # 临时文件目录 +├── server.js # 服务入口 +├── db.js # 数据库连接 +├── initConfig.js # 初始化配置 +├── initDeviceFields.js # 初始化设备字段 +├── initTicketFields.js # 初始化工单字段 +├── swagger.js # Swagger配置 +├── swagger_docs.yaml # Swagger文档 +├── swagger_index.html # Swagger页面 +├── package.json # 后端依赖 +├── .env.example # 环境变量示例 +└── jest.config.js # Jest配置 +``` + +--- + +## 3. 前端模块结构 + +### 3.1 组件架构 + +前端采用**组件化架构**,核心组件分类如下: + +#### 3.1.1 页面组件 (pages/) + +| 页面组件 | 文件路径 | 功能描述 | +|----------|----------|----------| +| Dashboard | pages/Dashboard.jsx | 数据看板主页,展示统计卡片、趋势图、功率仪表 | +| Login | pages/Login.jsx | 用户登录/注册页面 | +| RoomManagement | pages/RoomManagement.jsx | 机房增删改查,支持图片上传 | +| RackManagement | pages/RackManagement.jsx | 机柜管理,支持批量导入导出 | +| DeviceManagement | pages/DeviceManagement.jsx | 设备全生命周期管理 | +| PortManagement | pages/PortManagement.jsx | 设备端口配置管理 | +| CableManagement | pages/CableManagement.jsx | 线缆连接管理 | +| TicketManagement | pages/TicketManagement.jsx | 工单全流程管理 | +| ConsumableManagement | pages/ConsumableManagement.jsx | 耗材库存管理 | +| InventoryManagement | pages/InventoryManagement.jsx | 盘点计划与执行 | +| Rack3DVisualization | pages/Rack3DVisualization.jsx | Three.js 3D可视化 | +| RoomFloorPlan | pages/RoomFloorPlan.jsx | Canvas机房平面图 | +| BackupManagement | pages/BackupManagement.jsx | 数据库备份恢复 | +| UserManagement | pages/UserManagement.jsx | 用户与权限管理 | + +#### 3.1.2 3D可视化组件 (components/3d/) + +| 组件 | 文件 | 功能描述 | +|------|------|----------| +| Scene | Scene.jsx | Three.js主场景,管理相机、灯光、渲染器 | +| RackModel | RackModel.jsx | 机柜3D模型,支持LOD | +| DeviceModel | DeviceModel.jsx | 设备3D模型,根据类型渲染 | +| LODManager | LODManager.jsx | 细节层次管理,优化性能 | +| CascadingRackPanel | CascadingRackPanel.jsx | 级联机柜信息面板 | +| TextureCache | materials/TextureCache.js | 纹理缓存,减少加载时间 | +| devicePanel | materials/devicePanel.jsx | 设备悬停信息面板 | + +#### 3.1.3 数据看板组件 (components/dashboard/) + +| 组件 | 功能描述 | +|------|----------| +| StatCard | 统计卡片,展示单个指标 | +| AnimatedCounter | 数字动画组件 | +| CircularProgress | 环形进度指示器 | +| DeviceTrendChart | 设备趋势图表(ECharts) | +| PowerGauge | 功率监控仪表盘 | +| StatusLegend | 设备状态图例 | +| NavigationGrid | 快捷导航网格 | +| SystemInfo | 系统运行信息 | + +### 3.2 Context状态管理 + +```javascript +// AuthContext - 认证状态管理 +AuthContext { + user: Object, // 当前用户信息 + token: String, // JWT令牌 + loading: Boolean, // 加载状态 + initialized: Boolean, // 初始化完成 + login(), // 登录方法 + register(), // 注册方法 + logout(), // 登出方法 + hasPermission(), // 权限检查 +} + +// ConfigContext - 全局配置管理 +ConfigContext { + config: Object, // 系统配置 + updateConfig(), // 更新配置 +} + +// Scene3DContext - 3D场景状态 +Scene3DContext { + scene: Object, // Three.js场景 + camera: Object, // 相机 + selectedRack: Object, // 选中机柜 + setSelectedRack(), // 设置选中 +} + +// FloorPlanContext - 平面图状态 +FloorPlanContext { + canvas: Object, // Canvas对象 + roomData: Object, // 机房数据 + racks: Array, // 机柜列表 +} +``` + +### 3.3 关键Hooks + +| Hook | 文件 | 功能描述 | +|------|------|----------| +| useApi | hooks/useApi.js | 封装API请求,支持 loading/error 状态 | +| useSWR | hooks/useSWR.js | SWR数据获取与缓存 | +| useDebounce | hooks/useDebounce.js | 防抖处理 | +| useIdleTimeout | hooks/useIdleTimeout.js | 空闲超时检测 | +| useDangerousOperation | hooks/useDangerousOperation.js | 危险操作二次确认 | +| useDesignTokens | hooks/useDesignTokens.js | Ant Design主题令牌 | +| useResponsiveLayout | hooks/useResponsiveLayout.js | 响应式布局 | +| useSortedRacks | hooks/useSortedRacks.js | 机柜排序 | + +### 3.4 API封装 (api/index.js) + +```javascript +// Axios实例配置 +const api = axios.create({ + baseURL: '/api', // API基础URL + timeout: 30000, // 超时30秒 + headers: { 'Content-Type': 'application/json' } +}); + +// 请求拦截器 - 自动添加Token +api.interceptors.request.use(config => { + const token = secureStorage.get(TOKEN_KEY); + if (token) config.headers.Authorization = `Bearer ${token}`; + return config; +}); + +// 响应拦截器 - 统一错误处理 +api.interceptors.response.use( + response => response.data, + error => { + if (error.response?.status === 401) { + // Token过期,跳转登录 + } + return Promise.reject(error); + } +); + +// API模块导出 +export const authAPI = { login, register, getProfile, ... }; +export const deviceAPI = { list, get, create, update, delete, ... }; +export const ticketAPI = { list, get, create, assign, process, ... }; +export const backupAPI = { list, create, restore, download, ... }; +// ... 更多API模块 +``` + +--- + +## 4. 后端模块结构 + +### 4.1 服务入口 (server.js) + +```javascript +// server.js - 应用入口流程 +async function initializeApp() { + // 1. 数据库同步 + await syncDatabase(); // 连接并同步表结构 + + // 2. 初始化设备字段 + await initDeviceFields(); + + // 3. 初始化工单字段 + await initTicketFields(); + + // 4. 初始化工单模型关联 + await initTicketModels(); + + // 5. 同步耗材模型 + await syncConsumableModels(); + + // 6. 同步盘点模型 + await syncInventoryModels(); + + // 7. 初始化系统设置 + await initDefaultSystemSettings(); + + // 8. 初始化故障分类 + await initFaultCategories(); + + // 9. 初始化自动备份调度器 + await initAutoBackupScheduler(); +} +``` + +### 4.2 数据库连接 (db.js) + +```javascript +// 支持 SQLite 和 MySQL 双数据库 +const DB_TYPE = process.env.DB_TYPE || 'sqlite'; + +if (DB_TYPE === 'mysql') { + sequelize = new Sequelize({ + dialect: 'mysql', + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT, + database: process.env.MYSQL_DATABASE, + username: process.env.MYSQL_USERNAME, + password: process.env.MYSQL_PASSWORD, + pool: { max: 10, min: 2, acquire: 30000, idle: 10000 } + }); +} else { + sequelize = new Sequelize({ + dialect: 'sqlite', + storage: process.env.DB_PATH || './idc_management.db' + }); +} +``` + +### 4.3 中间件层 + +| 中间件 | 文件 | 功能描述 | +|--------|------|----------| +| authMiddleware | middleware/auth.js | JWT令牌验证、用户认证 | +| maintenanceMiddleware | middleware/maintenance.js | 维护模式拦截 | +| requestLogger | middleware/requestLogger.js | HTTP请求日志记录 | +| validationMiddleware | middleware/validation.js | Joi数据验证 | + +### 4.4 路由模块 (routes/) + +| 路由模块 | 基础路径 | 功能描述 | +|----------|----------|----------| +| auth | /api/auth | 登录、注册、Token管理 | +| rooms | /api/rooms | 机房CRUD | +| racks | /api/racks | 机柜CRUD | +| devices | /api/devices | 设备CRUD、批量操作 | +| deviceFields | /api/deviceFields | 设备自定义字段 | +| devicePorts | /api/device-ports | 设备端口管理 | +| networkCards | /api/network-cards | 网卡管理 | +| cables | /api/cables | 线缆连接管理 | +| tickets | /api/tickets | 工单全流程 | +| consumables | /api/consumables | 耗材管理 | +| inventory | /api/inventory | 盘点管理 | +| users | /api/users | 用户管理 | +| roles | /api/roles | 角色权限 | +| systemSettings | /api/system-settings | 系统配置 | +| backup | /api/backup | 备份恢复 | +| statistics | /api/statistics | 统计分析 | +| operationLogs | /api/operation-logs | 操作审计 | +| topology | /api/topology | 网络拓扑 | + +--- + +## 5. 数据模型关系 + +### 5.1 核心模型关系图 + +``` +┌─────────────┐ +│ Room │ ──1:N──► ┌─────────┐ +│ (机房) │ │ Rack │ +└─────────────┘ │ (机柜) │ + │ └────┬────┘ + │ │ + │ │ 1:N + │ ▼ + │ ┌─────────┐ + │ │ Device │ + │ │ (设备) │ + │ └────┬────┘ + │ │ + │ ┌───────────┼───────────┐ + │ │ │ │ + │ ▼ ▼ ▼ + │ ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ │NetworkCard│ │DevicePort│ │DeviceField│ + │ │ (网卡) │ │ (端口) │ │ (字段) │ + │ └────┬─────┘ └────┬─────┘ └──────────┘ + │ │ │ + │ │ │ N:1 + │ │ ▼ + │ │ ┌──────────┐ + └──────────┼─────►│ Cable │ + │ │ (线缆) │ + │ └──────────┘ + │ + ▼ + ┌──────────────┐ + │ Ticket │ + │ (工单) │ + └──────────────┘ + +┌──────────────┐ ┌──────────────┐ +│ User │ ─N:M─► │ Role │ +│ (用户) │ │ (角色) │ +└──────────────┘ └──────────────┘ + +┌──────────────┐ ┌──────────────┐ +│ Consumable │ ─N:M─► │ConsumableCat │ +│ (耗材) │ │ (分类) │ +└──────┬───────┘ └──────────────┘ + │ + ▼ +┌──────────────┐ +│ConsumableLog │ +│ (日志) │ +└──────────────┘ + +┌──────────────┐ +│InventoryPlan │ ─1:N──► ┌──────────────┐ +│ (盘点计划) │ │InventoryTask │ +└──────────────┘ │ (盘点任务) │ + └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │InventoryRecord│ + │ (盘点记录) │ + └──────────────┘ +``` + +### 5.2 主要数据模型 + +#### 5.2.1 机房模型 (Room) + +```javascript +// backend/models/Room.js +Room.init({ + roomId: { type: DataTypes.STRING, primaryKey: true }, // 机房ID + name: { type: DataTypes.STRING, allowNull: false }, // 机房名称 + location: DataTypes.STRING, // 地理位置 + area: DataTypes.FLOAT, // 面积(平方米) + manager: DataTypes.STRING, // 负责人 + contact: DataTypes.STRING, // 联系方式 + status: DataTypes.ENUM('active', 'inactive'), // 状态 + description: DataTypes.TEXT, // 描述 + imageUrl: DataTypes.STRING, // 机房图片 + floorPlanData: DataTypes.JSON, // 平面图数据 +}, { + tableName: 'rooms', + timestamps: true, +}); + +// 关联关系 +Room.hasMany(Rack, { foreignKey: 'roomId', as: 'Racks' }); +``` + +#### 5.2.2 机柜模型 (Rack) + +```javascript +// backend/models/Rack.js +Rack.init({ + rackId: { type: DataTypes.STRING, primaryKey: true }, + name: { type: DataTypes.STRING, allowNull: false }, + roomId: { type: DataTypes.STRING, references: { model: 'rooms' } }, + totalU: { type: DataTypes.INTEGER, defaultValue: 42 }, // 总U数 + usedU: { type: DataTypes.INTEGER, defaultValue: 0 }, // 已用U数 + powerCapacity: { type: DataTypes.FLOAT }, // 功率容量 + currentPower: { type: DataTypes.FLOAT }, // 当前功率 + status: DataTypes.ENUM('active', 'maintenance', 'offline'), + positionX: DataTypes.FLOAT, // 平面图X坐标 + positionY: DataTypes.FLOAT, // 平面图Y坐标 +}, { + tableName: 'racks', + timestamps: true, +}); + +// 关联 +Rack.belongsTo(Room, { foreignKey: 'roomId' }); +Rack.hasMany(Device, { foreignKey: 'rackId', as: 'Devices' }); +``` + +#### 5.2.3 设备模型 (Device) + +```javascript +// backend/models/Device.js +Device.init({ + deviceId: { type: DataTypes.STRING, primaryKey: true }, + name: { type: DataTypes.STRING, allowNull: false }, + type: DataTypes.ENUM('server', 'switch', 'router', 'storage', 'firewall', 'other'), + brand: DataTypes.STRING, // 品牌 + model: DataTypes.STRING, // 型号 + serialNumber: DataTypes.STRING, // 序列号 + rackId: { type: DataTypes.STRING, references: { model: 'racks' } }, + position: DataTypes.INTEGER, // U位 + height: { type: DataTypes.INTEGER, defaultValue: 1 }, // 设备高度(U) + status: DataTypes.ENUM('purchasing', 'pending', 'running', 'maintenance', 'scrapped'), + ipAddress: DataTypes.STRING, // IP地址 + macAddress: DataTypes.STRING, // MAC地址 + powerConsumption: DataTypes.FLOAT, // 功率(W) + purchaseDate: DataTypes.DATE, // 采购日期 + warrantyExpiry: DataTypes.DATE, // 保修到期 + customFields: DataTypes.JSON, // 自定义字段 +}, { + tableName: 'devices', + timestamps: true, +}); + +// 关联 +Device.belongsTo(Rack, { foreignKey: 'rackId' }); +Device.hasMany(DevicePort, { foreignKey: 'deviceId' }); +Device.hasMany(NetworkCard, { foreignKey: 'deviceId' }); +Device.hasMany(Cable, { foreignKey: 'sourceDeviceId' }); +Device.hasMany(Cable, { foreignKey: 'targetDeviceId' }); +``` + +#### 5.2.4 工单模型 (Ticket) + +```javascript +// backend/models/Ticket.js +Ticket.init({ + ticketId: { type: DataTypes.STRING, primaryKey: true }, + title: { type: DataTypes.STRING, allowNull: false }, + description: DataTypes.TEXT, + type: DataTypes.ENUM('fault', 'maintenance', 'change', 'other'), + priority: DataTypes.ENUM('urgent', 'high', 'medium', 'low'), + status: DataTypes.ENUM('open', 'assigned', 'processing', 'resolved', 'closed', 'reopened'), + categoryId: DataTypes.STRING, // 故障分类ID + reporterId: { type: DataTypes.STRING, references: { model: 'users' } }, + assigneeId: { type: DataTypes.STRING, references: { model: 'users' } }, + deviceId: { type: DataTypes.STRING, references: { model: 'devices' } }, + customFields: DataTypes.JSON, // 自定义字段 + rating: DataTypes.INTEGER, // 评价(1-5) + resolution: DataTypes.TEXT, // 解决方案 + resolvedAt: DataTypes.DATE, + closedAt: DataTypes.DATE, +}, { + tableName: 'tickets', + timestamps: true, +}); +``` + +#### 5.2.5 耗材模型 (Consumable) + +```javascript +// backend/models/Consumable.js +Consumable.init({ + consumableId: { type: DataTypes.STRING, primaryKey: true }, + name: { type: DataTypes.STRING, allowNull: false }, + categoryId: { type: DataTypes.STRING, references: { model: 'consumable_categories' } }, + sku: DataTypes.STRING, // SKU编码 + unit: DataTypes.STRING, // 单位 + stock: { type: DataTypes.INTEGER, defaultValue: 0 }, // 库存 + minStock: { type: DataTypes.INTEGER, defaultValue: 0 }, // 最低库存 + location: DataTypes.STRING, // 存放位置 + warehouseId: { type: DataTypes.STRING, references: { model: 'warehouses' } }, + price: DataTypes.FLOAT, // 单价 + snList: DataTypes.JSON, // SN序列号列表 + version: { type: DataTypes.INTEGER, defaultValue: 0 }, // 乐观锁版本 +}, { + tableName: 'consumables', + timestamps: true, +}); +``` + +### 5.3 模型关联初始化 (models/index.js) + +```javascript +// backend/models/index.js +const Room = require('./Room'); +const Rack = require('./Rack'); +const Device = require('./Device'); +const DevicePort = require('./DevicePort'); +const NetworkCard = require('./NetworkCard'); +const Cable = require('./Cable'); +const Ticket = require('./Ticket'); +const InventoryRecord = require('./InventoryRecord'); +const Warehouse = require('./Warehouse'); + +// 建立核心关联 +Room.hasMany(Rack, { foreignKey: 'roomId', as: 'Racks' }); +Rack.belongsTo(Room, { foreignKey: 'roomId' }); + +Rack.hasMany(Device, { foreignKey: 'rackId', as: 'Devices' }); +Device.belongsTo(Rack, { foreignKey: 'rackId' }); + +// 设备关联端口和网卡 +Device.hasMany(DevicePort, { foreignKey: 'deviceId', as: 'Ports' }); +Device.hasMany(NetworkCard, { foreignKey: 'deviceId', as: 'NetworkCards' }); + +// 线缆关联 +Device.hasMany(Cable, { foreignKey: 'sourceDeviceId', as: 'SourceCables' }); +Device.hasMany(Cable, { foreignKey: 'targetDeviceId', as: 'TargetCables' }); + +module.exports = { + Room, Rack, Device, DevicePort, NetworkCard, Cable, Ticket, + InventoryRecord, Warehouse +}; +``` + +--- + +## 6. API接口规范 + +### 6.1 接口基础规范 + +| 项目 | 规范 | +|------|------| +| 基础URL | `http://localhost:8000/api` | +| 认证方式 | JWT Bearer Token | +| Content-Type | `application/json` | +| 编码 | UTF-8 | + +### 6.2 主要API模块 + +#### 6.2.1 设备管理API (/devices) + +```javascript +// GET /api/devices - 获取设备列表 +// Query: keyword, status, type, rackId, roomId, page, pageSize +// Response: { success, data: { list, total, page, pageSize } } + +// GET /api/devices/all - 获取所有设备(不分页,最大50000条) +GET /api/devices/all?keyword=&status=&type=&rackId=&roomId= + +// GET /api/devices/:deviceId - 获取设备详情 +// Response: { success, data: device } + +// POST /api/devices - 创建设备 +// Body: { name, type, rackId, position, height, ... } + +// PUT /api/devices/:deviceId - 更新设备 + +// DELETE /api/devices/:deviceId - 删除设备 + +// POST /api/devices/check-position - 检查U位可用性 +// Body: { rackId, position, height, excludeDeviceId? } + +// POST /api/devices/import-preview - 导入预览 +// Body: FormData (csvFile) + +// POST /api/devices/import - 执行导入 + +// GET /api/devices/import-template - 下载导入模板 + +// GET /api/devices/export - 导出设备 +// Query: format(csv|xlsx), keyword, status, type, rackId, roomId +``` + +#### 6.2.2 工单管理API (/tickets) + +```javascript +// GET /api/tickets - 获取工单列表 +// Query: status, priority, categoryId, assigneeId, page, pageSize + +// GET /api/tickets/:ticketId - 获取工单详情 + +// POST /api/tickets - 创建工单 +// Body: { title, description, type, priority, categoryId, deviceId } + +// PUT /api/tickets/:ticketId - 更新工单 + +// PUT /api/tickets/:ticketId/assign - 分配工单 +// Body: { assigneeId } + +// PUT /api/tickets/:ticketId/process - 处理工单 +// Body: { resolution, nextStep? } + +// PUT /api/tickets/:ticketId/close - 关闭工单 + +// PUT /api/tickets/:ticketId/reopen - 重开工单 + +// GET /api/tickets/:ticketId/operations - 获取操作记录 +``` + +#### 6.2.3 备份管理API (/backup) + +```javascript +// GET /api/backup/list - 获取备份列表 + +// POST /api/backup - 创建备份 +// Body: { name?, description? } + +// GET /api/backup/validate/:filename - 验证备份文件 + +// POST /api/backup/restore - 恢复备份 +// Body: { filename, options? } + +// GET /api/backup/download/:filename - 下载备份 + +// DELETE /api/backup/:filename - 删除备份 + +// POST /api/backup/upload - 上传备份文件 +// Body: FormData (backup) + +// GET /api/backup/info - 获取备份信息 + +// 自动备份 +// GET /api/backup/auto/status - 获取自动备份状态 +// POST /api/backup/auto/settings - 更新自动备份设置 +// POST /api/backup/auto/execute - 立即执行备份 +``` + +#### 6.2.4 统计分析API (/statistics) + +```javascript +// GET /api/statistics/overview - 获取概览统计 +// Response: { deviceCount, roomCount, rackCount, ticketStats } + +// GET /api/statistics/devices - 设备统计 +// Query: roomId?, rackId?, type?, period? + +// GET /api/statistics/tickets - 工单统计 +// Query: period?, categoryId? + +// GET /api/statistics/consumables - 耗材统计 +``` + +### 6.3 响应格式 + +```javascript +// 成功响应 +{ + "success": true, + "data": { ... }, + "message": "操作成功" +} + +// 失败响应 +{ + "success": false, + "message": "错误描述", + "code": "ERROR_CODE" +} + +// 分页响应 +{ + "success": true, + "data": { + "list": [...], + "total": 100, + "page": 1, + "pageSize": 20 + } +} +``` + +--- + +## 7. 关键模块详解 + +### 7.1 认证模块 + +#### 7.1.1 后端认证 (middleware/auth.js) + +```javascript +// JWT认证中间件 +const authMiddleware = async (req, res, next) => { + // 1. 从请求头获取Token + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + success: false, + message: '未提供认证令牌' + }); + } + + // 2. 验证Token + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // 3. 获取用户信息 + const User = require('../models/User'); + const user = await User.findByPk(decoded.userId, { + include: [{ model: Role, as: 'roles' }] + }); + + if (!user) { + return res.status(401).json({ + success: false, + message: '用户不存在' + }); + } + + // 4. 挂载用户到请求对象 + req.user = user; + next(); + } catch (error) { + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + success: false, + message: '令牌已过期' + }); + } + return res.status(401).json({ + success: false, + message: '无效的认证令牌' + }); + } +}; +``` + +#### 7.1.2 前端认证 (context/AuthContext.jsx) + +```javascript +// 认证Provider +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + + // 初始化 - 从本地存储恢复认证状态 + useEffect(() => { + const initializeAuth = async () => { + const storedToken = await secureStorage.loadFromStorage(TOKEN_KEY); + if (storedToken) { + setToken(storedToken); + // 验证Token有效性 + const response = await authAPI.getProfile(); + if (response.success) { + setUser(response.data.user); + } + } + }; + initializeAuth(); + }, []); + + const login = async (username, password) => { + const response = await authAPI.login({ username, password }); + if (response.success) { + await secureStorage.set(TOKEN_KEY, response.data.token); + setToken(response.data.token); + setUser(response.data.user); + } + return response; + }; + + const logout = () => { + secureStorage.remove(TOKEN_KEY); + setToken(null); + setUser(null); + }; + + const hasPermission = (permission) => { + if (!user) return false; + const roles = user.roles || []; + if (roles.some(r => r.roleCode === 'admin')) return true; + return roles.some(r => r.roleCode === permission); + }; + + return ( + + {children} + + ); +}; +``` + +### 7.2 3D可视化模块 + +#### 7.2.1 Three.js场景 (components/3d/Scene.jsx) + +```javascript +// 3D主场景组件 +const Scene = () => { + const { selectedRack, setSelectedRack } = useContext(Scene3DContext); + + return ( + + {/* 灯光 */} + + + + {/* 机柜模型 */} + {racks.map(rack => ( + setSelectedRack(rack)} + /> + ))} + + {/* OrbitControls */} + + + ); +}; +``` + +#### 7.2.2 机柜模型 (components/3d/RackModel.jsx) + +```javascript +// 机柜3D模型 +const RackModel = ({ rack, isSelected, onSelect }) => { + const [hovered, setHovered] = useState(false); + + return ( + + {/* 机柜框架 */} + setHovered(true)} + onPointerOut={() => setHovered(false)} + > + + + + + {/* 设备模型 */} + {rack.devices?.map(device => ( + + ))} + + {/* LOD优化 - 远处隐藏细节 */} + + + + + + + ); +}; +``` + +### 7.3 备份模块 + +#### 7.3.1 本地备份 (utils/backup.js) + +```javascript +// 备份工具 +const backupDatabase = async (options = {}) => { + const { DB_TYPE, dbDialect } = require('../db'); + + if (dbDialect === 'mysql') { + // MySQL备份 + const { MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE, MYSQL_USERNAME, MYSQL_PASSWORD } = process.env; + + const filename = `backup_${dayjs().format('YYYYMMDD_HHmmss')}.sql`; + const filepath = path.join(BACKUP_DIR, filename); + + // mysqldump命令 + const command = `mysqldump -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USERNAME}${MYSQL_PASSWORD ? `-p${MYSQL_PASSWORD}` : ''} ${MYSQL_DATABASE} > ${filepath}`; + + await exec(command); + return { filename, filepath }; + } else { + // SQLite备份 - 复制数据库文件 + const filename = `backup_${dayjs().format('YYYYMMDD_HHmmss')}.db`; + const filepath = path.join(BACKUP_DIR, filename); + + await fs.copy( + process.env.DB_PATH || './idc_management.db', + filepath + ); + + return { filename, filepath }; + } +}; +``` + +#### 7.3.2 远程备份 (utils/remoteBackup.js) + +```javascript +// 支持多种协议的远程备份 +const uploadToRemote = async (localFile, config) => { + const { protocol, host, port, username, password, path: remotePath } = config; + + switch (protocol) { + case 'ftp': + const ftpClient = require('basic-ftp'); + const ftp = new ftpClient(); + await ftp.access({ host, port, user: username, password }); + await ftp.uploadFrom(localFile, `${remotePath}/${path.basename(localFile)}`); + await ftp.close(); + break; + + case 'sftp': + const sftpClient = require('ssh2-sftp-client'); + const sftp = new sftpClient(); + await sftp.connect({ host, port, username, password }); + await sftp.put(localFile, `${remotePath}/${path.basename(localFile)}`); + await sftp.end(); + break; + + case 'webdav': + const { WebDAVClient } = require('webdav'); + const webdav = new WebDAVClient(`http://${host}:${port}`); + await webdav.putFileContents( + `${remotePath}/${path.basename(localFile)}`, + fs.createReadStream(localFile) + ); + break; + + case 'smb': + const SMB2 = require('smb2'); + const smb = new SMB2({ share: `\\\\${host}\\${remotePath}` }); + await smb.writeFile(path.basename(localFile), fs.readFileSync(localFile)); + break; + } +}; +``` + +### 7.4 操作日志模块 + +#### 7.4.1 后端操作日志 (utils/operationLogger.js) + +```javascript +// 操作日志记录 +const logDeviceOperation = async (req, action, deviceData, userId) => { + const OperationLog = require('../models/OperationLog'); + + await OperationLog.create({ + logId: generateId({ prefix: 'LOG' }), + userId, + module: 'device', + action, + targetId: deviceData.deviceId, + targetName: deviceData.name, + details: { + before: deviceData.before, + after: deviceData.after, + ip: req.ip, + userAgent: req.get('user-agent') + }, + result: 'success' + }); +}; + +// 生成设备变更描述 +const generateDeviceDescription = (action, deviceData) => { + const descriptions = { + create: `创建设备「${deviceData.name}」`, + update: `更新设备「${deviceData.name}」信息`, + delete: `删除设备「${deviceData.name}」`, + move: `移动设备「${deviceData.name}」到新位置`, + status: `修改设备「${deviceData.name}」状态为${deviceData.status}` + }; + return descriptions[action] || `${action}设备「${deviceData.name}」`; +}; +``` + +### 7.5 设备导入模块 + +#### 7.5.1 导入预览验证 (routes/devices.js) + +```javascript +// POST /api/devices/import-preview +router.post('/import-preview', upload.single('csvFile'), async (req, res) => { + const results = []; + const errors = []; + const warnings = []; + + // 1. 解析CSV + await new Promise((resolve, reject) => { + fs.createReadStream(req.file.path) + .pipe(csv()) + .on('data', (row) => results.push(row)) + .on('end', resolve) + .on('error', reject); + }); + + // 2. 逐行验证 + for (let i = 0; i < results.length; i++) { + const row = results[i]; + const rowNum = i + 2; // 跳过表头 + + // 必填字段检查 + if (!row.name) { + errors.push({ row: rowNum, field: 'name', message: '设备名称不能为空' }); + } + + // 格式验证 + if (row.position && !/^\d+$/.test(row.position)) { + errors.push({ row: rowNum, field: 'position', message: 'U位必须是数字' }); + } + + // 冲突检查 + if (row.rackId && row.position) { + const { available, reason } = await checkPositionAvailable( + row.rackId, row.position, row.height || 1 + ); + if (!available) { + warnings.push({ row: rowNum, message: reason }); + } + } + } + + // 3. 返回预览结果 + res.json({ + success: true, + data: { + total: results.length, + errors: errors.slice(0, 20), // 最多返回20条错误 + warnings: warnings.slice(0, 20), + preview: results.slice(0, PREVIEW_COUNT) + } + }); +}); +``` + +--- + +## 8. 依赖关系 + +### 8.1 前端依赖 (frontend/package.json) + +| 依赖 | 版本 | 用途 | +|------|------|------| +| react | ^18.2.0 | 前端框架 | +| react-dom | ^18.2.0 | DOM渲染 | +| react-router-dom | ^6.15.0 | 路由管理 | +| antd | ^5.8.6 | UI组件库 | +| @ant-design/icons | ^6.1.0 | 图标库 | +| axios | ^1.13.6 | HTTP客户端 | +| three | ^0.183.2 | 3D渲染引擎 | +| @react-three/fiber | ^8.18.0 | React Three.js集成 | +| @react-three/drei | ^9.122.0 | Three.js工具库 | +| reactflow | ^11.11.4 | 流程图/拓扑图 | +| swr | ^2.4.0 | 数据请求与缓存 | +| styled-components | ^6.3.9 | CSS-in-JS | +| framer-motion | ^12.34.0 | 动画库 | +| dayjs | ^1.11.19 | 日期处理 | +| xlsx | ^0.18.5 | Excel处理 | +| papaparse | ^5.5.3 | CSV解析 | +| dagre | ^0.8.5 | 图形布局算法 | + +### 8.2 后端依赖 (backend/package.json) + +| 依赖 | 版本 | 用途 | +|------|------|------| +| express | ^4.18.2 | Web框架 | +| sequelize | ^6.37.8 | ORM框架 | +| sqlite3 | ^5.1.7 | SQLite驱动 | +| mysql2 | ^3.19.1 | MySQL驱动 | +| jsonwebtoken | ^9.0.3 | JWT认证 | +| bcryptjs | ^3.0.3 | 密码加密 | +| winston | ^3.19.0 | 日志管理 | +| winston-daily-rotate-file | ^5.0.0 | 日志轮转 | +| joi | ^18.0.2 | 数据验证 | +| multer | ^2.1.1 | 文件上传 | +| express-fileupload | ^1.5.2 | 文件上传中间件 | +| cors | ^2.8.5 | 跨域资源共享 | +| node-cron | ^4.2.1 | 定时任务 | +| swagger-jsdoc | ^6.2.8 | Swagger文档生成 | +| swagger-ui-express | ^5.0.1 | Swagger UI | +| xlsx | ^0.18.5 | Excel处理 | +| csv-parser | ^3.2.0 | CSV解析 | +| csv-writer | ^1.6.0 | CSV生成 | +| basic-ftp | ^5.0.3 | FTP客户端 | +| ssh2-sftp-client | ^9.1.0 | SFTP客户端 | +| webdav | ^5.3.1 | WebDAV客户端 | +| smb2 | ^0.2.2 | SMB客户端 | +| dayjs | ^1.11.19 | 日期处理 | +| dotenv | ^17.2.3 | 环境变量 | + +### 8.3 依赖关系图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 项目根目录 │ +│ (concurrently ^9.2.1) │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ │ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────┐ +│ frontend/ │ │ backend/ │ +│ ^2.0.0 │ │ ^2.0.0 │ +├─────────────────────┤ ├─────────────────────┤ +│ React 18.2.0 │ │ Express 4.18.2 │ +│ ├─ react-router-dom │ │ ├─ sequelize 6.37 │ +│ ├─ antd 5.8.6 │ │ │ ├─ sqlite3 │ +│ │ └─ @ant-design │ │ │ └─ mysql2 │ +│ ├─ three.js 0.183 │ │ ├─ jsonwebtoken │ +│ │ ├─ @react-three │ │ ├─ bcryptjs │ +│ │ └─ @react-three │ │ ├─ winston │ +│ ├─ axios 1.13 │─────────────│ ├─ axios │ +│ ├─ swr 2.4 │ 共享HTTP │ ├─ dayjs │ +│ └─ styled-components│ │ └─ xlsx │ +│ │ │ │ +│ 开发依赖: │ │ 开发依赖: │ +│ ├─ vite 4.4 │ │ ├─ nodemon │ +│ ├─ eslint │ │ ├─ jest │ +│ └─ vitest 4.0 │ │ └─ supertest │ +└─────────────────────┘ └─────────────────────┘ +``` + +--- + +## 9. 运行环境与配置 + +### 9.1 环境要求 + +| 项目 | 最低要求 | 推荐配置 | +|------|----------|----------| +| 操作系统 | Windows 10, macOS 12, Ubuntu 20.04 | 同最低 | +| Node.js | ≥14.0.0 | 20.x LTS | +| npm | ≥6.0.0 | 10.x | +| 内存 | 4GB | 8GB+ | +| 磁盘空间 | 2GB | 10GB+ | + +### 9.2 环境变量配置 + +#### 9.2.1 前端环境变量 (frontend/.env) + +```bash +# 前端环境配置 +VITE_API_BASE_URL=http://localhost:8000/api +VITE_APP_TITLE=IDC设备管理系统 +VITE_APP_VERSION=2.0.0 +``` + +#### 9.2.2 后端环境变量 (backend/.env) + +```bash +# 服务配置 +NODE_ENV=development +PORT=8000 + +# 数据库配置 +DB_TYPE=sqlite # sqlite 或 mysql +DB_PATH=./idc_management.db # SQLite数据库路径 + +# MySQL配置 (当DB_TYPE=mysql时) +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_DATABASE=idc_management +MYSQL_USERNAME=root +MYSQL_PASSWORD=your_password + +# JWT配置 +JWT_SECRET=your_jwt_secret_key_here +JWT_EXPIRES_IN=7d + +# 文件上传 +FILE_UPLOAD_MAX_SIZE=10485760 # 10MB + +# 备份配置 +BACKUP_DIR=./backups +AUTO_BACKUP_ENABLED=false +AUTO_BACKUP_CRON=0 2 * * * # 每天凌晨2点 + +# 远程备份 (可选) +REMOTE_BACKUP_ENABLED=false +REMOTE_BACKUP_PROTOCOL=ftp +REMOTE_BACKUP_HOST= +REMOTE_BACKUP_PORT=21 +REMOTE_BACKUP_USERNAME= +REMOTE_BACKUP_PASSWORD= +REMOTE_BACKUP_PATH=/backups +``` + +### 9.3 启动方式 + +#### 9.3.1 一键启动 (开发环境) + +```bash +# 在项目根目录 +npm start + +# 效果等同于同时运行: +# - 后端: cd backend && npm run dev (端口8000) +# - 前端: cd frontend && npm run dev (端口3000) +``` + +#### 9.3.2 分别启动 + +```bash +# 终端1: 启动后端 +cd backend +npm run dev +# 监听端口: http://localhost:8000 + +# 终端2: 启动前端 +cd frontend +npm run dev +# 监听端口: http://localhost:3000 +``` + +#### 9.3.3 生产环境部署 + +```bash +# 1. 构建前端 +cd frontend +npm run build + +# 2. 使用PM2启动后端 +cd ../backend +pm2 start server.js --name idc-backend + +# 3. 使用Nginx反向代理 +# 或使用PM2 serve提供静态文件 +pm2 serve ./frontend/dist 3000 --name idc-frontend +``` + +--- + +## 10. 部署指南 + +### 10.1 生产环境部署流程 + +#### 10.1.1 数据库配置 + +```bash +# 1. 安装MySQL 8.0+ +# Ubuntu +sudo apt update +sudo apt install mysql-server + +# 2. 创建数据库 +mysql -u root -p +CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'idc_user'@'localhost' IDENTIFIED BY 'your_password'; +GRANT ALL PRIVILEGES ON idc_management.* TO 'idc_user'@'localhost'; +FLUSH PRIVILEGES; +``` + +#### 10.1.2 Nginx配置 + +```nginx +# /etc/nginx/sites-available/idc + +server { + listen 80; + server_name your_domain.com; + + # 前端静态文件 + root /var/www/idc/frontend/dist; + index index.html; + + # Gzip压缩 + gzip on; + gzip_types text/plain text/css application/json application/javascript; + + # 前端路由 + location / { + try_files $uri $uri/ /index.html; + } + + # API代理 + location /api { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # 上传文件 + location /uploads { + alias /var/www/idc/backend/uploads; + autoindex on; + } + + # Swagger文档 + location /api-docs { + proxy_pass http://127.0.0.1:8000; + } +} + +# HTTPS配置 (推荐) +server { + listen 443 ssl http2; + server_name your_domain.com; + + ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem; + + # ... 同上配置 +} +``` + +#### 10.1.3 PM2进程管理 + +```bash +# 安装PM2 +npm install -g pm2 + +# 启动后端服务 +cd backend +pm2 start server.js --name idc-backend \ + --env production \ + --max-memory-restart 1G + +# 配置负载均衡 (多核CPU) +pm2 start server.js -i max --name idc-backend + +# 保存进程列表 +pm2 save + +# 设置开机自启 +pm2 startup +pm2 install ubuntu-upstart # Ubuntu +``` + +### 10.2 Docker部署 (可选) + +```dockerfile +# backend/Dockerfile +FROM node:20-alpine + +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . +EXPOSE 8000 + +CMD ["node", "server.js"] +``` + +```yaml +# docker-compose.yml +version: '3.8' + +services: + backend: + build: ./backend + ports: + - "8000:8000" + environment: + - NODE_ENV=production + - DB_TYPE=mysql + - MYSQL_HOST=db + depends_on: + - db + + frontend: + build: ./frontend + ports: + - "3000:80" + depends_on: + - backend + + db: + image: mysql:8.0 + environment: + MYSQL_DATABASE: idc_management + MYSQL_ROOT_PASSWORD: root_password + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: +``` + +--- + +## 11. 系统关系图 + +### 11.1 整体架构关系图 + +``` + ┌────────────────────────────────────────────────────┐ + │ 用户交互层 │ + │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ + │ │ 浏览器 │ │ 管理员 │ │ 运维人员 │ │ + │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ + └───────┼────────────┼────────────┼────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 前端应用层 (React + Vite) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 页面组件 (Pages) │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │Dashboard│ │Devices │ │ Racks │ │Tickets │ │Backup │ │ │ +│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ +│ └───────┼──────────┼──────────┼──────────┼──────────┼────────────────────┘ │ +│ │ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 通用组件 (Components) │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ 3D组件 │ │看板组件 │ │设备组件 │ │拓扑组件 │ │表单组件 │ │ │ +│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ +│ └───────┼──────────┼──────────┼──────────┼──────────┼────────────────────┘ │ +│ │ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 状态管理与Hooks │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │AuthContext│ │ConfigCtx │ │ useApi │ │ useSWR │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────┘ + │ + │ HTTP/REST API + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 后端服务层 (Express.js) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 中间件层 (Middleware) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 认证 │ │ 授权 │ │ 验证 │ │ 日志 │ │ 维护 │ │ │ +│ │ │ Middle │ │ Middle │ │ Middle │ │ Middle │ │ Middle │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 路由层 (Routes) │ │ +│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ +│ │ │ /auth │ │/devices│ │ /racks │ │/tickets│ │ /backup│ │/stats │ │ │ +│ │ └────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘ │ │ +│ └───────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────────────┘ │ +│ │ │ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 业务逻辑层 (Services) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 设备服务 │ │ 工单服务 │ │ 备份服务 │ │ 统计服务 │ │ 导入服务 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 数据访问层 (Sequelize ORM) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Device │ │ Ticket │ │Consumable│ │ User │ │ Rack │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 数据存储层 (Database) │ +│ │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ SQLite │ │ MySQL │ │ +│ │ (开发环境) │◄──►│ (生产环境) │ │ +│ │ idc_management.db │ │ MySQL 8.0+ │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ 数据备份层 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 本地备份 │ │ FTP备份 │ │ SFTP备份 │ │WebDAV备份│ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### 11.2 数据模型ER图 + +``` +┌──────────────────┐ ┌──────────────────┐ +│ Room │ │ Rack │ +├──────────────────┤ ├──────────────────┤ +│ PK roomId │──1:N──│ FK roomId │ +│ name │ │ PK rackId │ +│ location │ │ name │ +│ area │ │ totalU │ +│ manager │ │ usedU │ +│ status │ │ powerCapacity │ +│ floorPlanData │ │ status │ +└──────────────────┘ │ positionX │ + │ │ positionY │ + │ └────────┬─────────┘ + │ │ + │ │ 1:N + │ ▼ + │ ┌──────────────────┐ + │ │ Device │ + │ ├──────────────────┤ + │ │ PK deviceId │ + │ │ FK rackId │ + │ │ name │ + │ │ type │ + │ │ status │ + │ │ position │ + │ │ height │ + │ │ serialNumber │ + │ │ customFields │ + │ └────────┬─────────┘ + │ │ + │ ┌──────────────┼──────────────┐ + │ │ │ │ + │ ▼ ▼ ▼ + │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ + │ │ NetworkCard │ │ DevicePort │ │ DeviceField │ + │ ├──────────────┤ ├──────────────┤ ├──────────────┤ + │ │ PK nicId │ │ PK portId │ │ PK fieldId │ + │ │ FK deviceId │ │ FK deviceId │ │ FK deviceId │ + │ │ name │ │ FK nicId │ │ name │ + │ │ type │ │ name │ │ type │ + │ │ speed │ │ status │ │ value │ + │ └──────────────┘ └──────────────┘ └──────────────┘ + │ │ │ + │ │ │ N:1 + │ │ ▼ + │ │ ┌──────────────┐ + │ └────►│ Cable │ + │ ├──────────────┤ + │ │ PK cableId │ + │ │ FK sourceId │ + │ │ FK targetId │ + │ │ type │ + │ │ length │ + │ │ status │ + │ └──────────────┘ + +┌──────────────────┐ ┌──────────────────┐ +│ User │ │ Role │ +├──────────────────┤ ├──────────────────┤ +│ PK userId │──N:M──│ PK roleId │ +│ username │ │ name │ +│ password │ │ code │ +│ email │ │ description │ +│ status │ └──────────────────┘ +│ isSystem │ │ +└──────────────────┘ │ + │ │ + │ ▼ + │ ┌──────────────────┐ + │ │ Permission │ + │ ├──────────────────┤ + │ │ PK permissionId │ + │ │ FK roleId │ + │ │ resource │ + │ │ action │ + │ └──────────────────┘ + +┌──────────────────┐ ┌──────────────────┐ +│ Ticket │ │ TicketCategory │ +├──────────────────┤ ├──────────────────┤ +│ PK ticketId │──N:1──│ PK categoryId │ +│ title │ │ name │ +│ description │ │ priority │ +│ type │ └──────────────────┘ +│ priority │ +│ status │ ┌──────────────────┐ +│ FK reporterId │ │ FaultCategory │ +│ FK assigneeId │ ├──────────────────┤ +│ FK deviceId │ │ PK categoryId │ +│ FK categoryId │ │ name │ +│ rating │ │ priority │ +│ resolution │ │ solutions │ +└──────────────────┘ └──────────────────┘ + +┌──────────────────┐ ┌──────────────────┐ +│ Consumable │ │ConsumableCategory│ +├──────────────────┤ ├──────────────────┤ +│ PK consumableId │──N:1──│ PK categoryId │ +│ name │ │ name │ +│ FK categoryId │ │ parentId │ +│ sku │ │ description │ +│ unit │ └──────────────────┘ +│ stock │ +│ minStock │ ┌──────────────────┐ +│ price │ │ ConsumableLog │ +│ snList │ ├──────────────────┤ +│ version │ │ PK logId │ +└──────────────────┘──1:N───│ FK consumableId │ + │ action │ + │ quantity │ + │ operatorId │ + │ createdAt │ + └──────────────────┘ + +┌──────────────────┐ ┌──────────────────┐ +│ InventoryPlan │ │ InventoryTask │ +├──────────────────┤ ├──────────────────┤ +│ PK planId │──1:N──│ PK taskId │ +│ name │ │ FK planId │ +│ startDate │ │ FK assigneeId │ +│ endDate │ │ FK rackId │ +│ status │ │ status │ +│ description │ │ progress │ +└──────────────────┘ └──────────────────┘ +``` + +### 11.3 API调用流程图 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 前端组件 (React) │ +│ │ +│ const DeviceList = () => { │ +│ const { data, error, isLoading } = useSWR('/api/devices', fetcher); │ +│ // ... │ +│ }; │ +└────────────────────────────────┬────────────────────────────────────────┘ + │ useSWR('/api/devices') + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ API层 (axios + interceptor) │ +│ │ +│ api.get('/devices') │ +│ │ │ +│ ├──► 请求拦截器 │ +│ │ ├─ 添加 Authorization: Bearer {token} │ +│ │ └─ 添加 Content-Type: application/json │ +│ │ │ +│ ▼ │ +│ └──► 发送HTTP请求到 /api/devices │ +└────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Express路由 (routes/devices.js) │ +│ │ +│ router.get('/devices', async (req, res) => { │ +│ │ │ +│ ▼ │ +│ ├─► 中间件链: auth → validation → logging │ +│ │ │ +│ ▼ │ +│ └─► 控制器函数: getDevices(req, res) │ +│ │ │ +│ ▼ │ +│ ├─► 参数验证 (Joi schema) │ +│ │ │ +│ ▼ │ +│ ├─► 调用Service层 │ +│ │ │ +│ ▼ │ +│ └─► 返回JSON响应 │ +└────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Service层 (业务逻辑) │ +│ │ +│ async function getDevices(params) { │ +│ │ │ +│ ▼ │ +│ ├─► 构建查询条件 (Sequelize Op) │ +│ │ { where: { status, type, ... } } │ +│ │ │ +│ ▼ │ +│ ├─► 分页处理 │ +│ │ { limit, offset } │ +│ │ │ +│ ▼ │ +│ └─► 调用DAO层 │ +└────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ DAO层 (Sequelize ORM) │ +│ │ +│ Device.findAndCountAll({ │ +│ where: conditions, │ +│ include: [Room, Rack, NetworkCard, ...], │ +│ limit, │ +│ offset, │ +│ order: [['createdAt', 'DESC']] │ +│ }) │ +│ │ │ +│ ▼ │ +│ └─► 执行SQL查询 │ +└────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 数据库 (SQLite / MySQL) │ +│ │ +│ SELECT * FROM devices │ +│ JOIN racks ON devices.rackId = racks.rackId │ +│ JOIN rooms ON racks.roomId = rooms.roomId │ +│ WHERE ... │ +│ ORDER BY createdAt DESC │ +│ LIMIT 20 OFFSET 0 │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 11.4 用户权限控制流程 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 用户权限控制流程 │ +│ │ +│ ┌─────────────┐ │ +│ │ 用户登录 │ │ +│ └──────┬──────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ JWT Token 生成 │ │ +│ │ jwt.sign({ userId, roles: [...] }, secret, { expiresIn: '7d' }) │ │ +│ └──────┬────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ 请求API资源 │ │ +│ └──────┬──────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ Auth Middleware │ │ +│ │ 1. 提取 Authorization Header │ │ +│ │ 2. 验证 JWT Token │ │ +│ │ 3. 从数据库加载用户信息和角色 │ │ +│ │ 4. 挂载 req.user │ │ +│ └──────┬────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 权限检查 (hasPermission) │ │ +│ │ │ │ +│ │ User.roles = [Role{code: 'admin'}, Role{code: 'device_manager'}]│ │ +│ │ │ │ +│ │ hasPermission('admin') → true (拥有admin角色) │ │ +│ │ hasPermission('device_mgr') → true (拥有device_manager角色) │ │ +│ │ hasPermission('system') → false (无此角色) │ │ +│ │ │ │ +│ │ // 权限层级 │ │ +│ │ admin > device_manager > operator > viewer │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 允许访问 │ │ 拒绝访问 │ │ 404 Not │ │ +│ │ 返回数据 │ │ 403 Forbidden│ │ Found │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 11.5 备份恢复流程 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 备份恢复系统架构 │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 备份触发机制 │ │ +│ │ │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ 手动触发 │ │ 定时触发 │ │ 远程同步触发 │ │ │ +│ │ │ /backup │ │ node-cron │ │ webhook │ │ │ +│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ +│ │ │ │ │ │ │ +│ └─────────┼─────────────────┼─────────────────┼────────────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 备份管理器 (BackupManager) │ │ +│ │ │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ 本地备份 │ │ 远程备份 │ │ 备份日志 │ │ │ +│ │ │ backup.js │ │ remoteBackup.js│ │ backupLog.js │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────────────────┐│ │ +│ │ │ 支持的远程协议 ││ │ +│ │ │ FTP │ SFTP │ WebDAV │ SMB/CIFS │ Amazon S3 │ 阿里云OSS │ ││ │ +│ │ └──────────────────────────────────────────────────────────────┘│ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 备份存储层 │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 备份文件 │ │ │ +│ │ │ backup_YYYYMMDD_HHmmss.sql (MySQL) │ │ │ +│ │ │ backup_YYYYMMDD_HHmmss.db (SQLite) │ │ │ +│ │ │ │ │ │ +│ │ │ 存储位置: │ │ │ +│ │ │ ├── 本地: ./backups/ │ │ │ +│ │ │ ├── FTP: /remote/backups/ │ │ │ +│ │ │ ├── SFTP: /var/backups/ │ │ │ +│ │ │ └── OSS: oss://idc-backup/ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 备份元数据 │ │ │ +│ │ │ { │ │ │ +│ │ │ filename: "backup_20260507_120000.sql", │ │ │ +│ │ │ size: 1048576, │ │ │ +│ │ │ createdAt: "2026-05-07T12:00:00Z", │ │ │ +│ │ │ database: "idc_management", │ │ │ +│ │ │ tables: ["devices", "racks", "tickets", ...], │ │ │ +│ │ │ status: "completed" │ │ │ +│ │ │ } │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ 恢复执行流程 │ +│ │ +│ POST /api/backup/restore │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 1. 验证备份文件 │ │ +│ │ GET /api/backup/validate/:filename │ │ +│ └──────────────────────────┬───────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 2. 创建临时恢复点 (可选) │ │ +│ │ - 备份当前数据库状态 │ │ +│ │ - 确保可回滚 │ │ +│ └──────────────────────────┬───────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 3. 关闭现有连接 │ │ +│ │ - await sequelize.close() │ │ +│ └──────────────────────────┬───────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 4. 执行恢复 │ │ +│ │ │ │ +│ │ MySQL: │ │ +│ │ mysql -u user -p dbname < backup_xxx.sql │ │ +│ │ │ │ +│ │ SQLite: │ │ +│ │ cp backup_xxx.db idc_management.db │ │ +│ │ │ │ +│ └──────────────────────────┬───────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 5. 重建连接 │ │ +│ │ sequelize = new Sequelize(...) │ │ +│ │ await sequelize.authenticate() │ │ +│ └──────────────────────────┬───────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 6. 验证数据完整性 │ │ +│ │ - 检查关键表是否存在 │ │ +│ │ - 验证记录数 │ │ +│ └──────────────────────────┬───────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ 7. 返回恢复结果 │ │ +│ │ { success: true, message: "恢复成功", backupId: "xxx" } │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 附录 + +### A. 常用命令 + +```bash +# 安装依赖 +npm run install:all + +# 启动开发环境 +npm start + +# 后端测试 +cd backend && npm test + +# 前端构建 +cd frontend && npm run build + +# 代码检查 +cd backend && npm run lint +cd frontend && npm run lint + +# 数据库迁移 +cd backend && node scripts/migrate-all.js +``` + +### B. 目录约定 + +| 目录 | 说明 | +|------|------| +| frontend/src/components | React组件 | +| frontend/src/pages | 页面组件 | +| frontend/src/hooks | 自定义Hooks | +| frontend/src/context | Context状态管理 | +| backend/models | Sequelize模型 | +| backend/routes | Express路由 | +| backend/middleware | Express中间件 | +| backend/utils | 工具函数 | +| backend/scripts | 数据库脚本 | + +### C. 命名约定 + +| 类型 | 约定 | 示例 | +|------|------|------| +| 组件文件 | PascalCase.jsx | DeviceManagement.jsx | +| 工具文件 | camelCase.js | secureStorage.js | +| 数据模型 | PascalCase | Device.js | +| 数据库表 | snake_case | device_ports | +| API路径 | camelCase | /api/devices | +| Context | PascalCase | AuthContext.jsx | +| Hooks | use{camelCase}.js | useApi.js | + +--- + +**文档维护说明:** +- 本文档由Code Wiki自动生成工具维护 +- 更新日期:2026-05-07 +- 版本:2.0.0 +- 如有更新建议,请提交Issue diff --git a/backend/.env.example b/backend/.env.example index 24a0968..51d5eab 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -117,6 +117,17 @@ LOG_MAX_FILE_SIZE=20m # 日志文件最大保留天数 LOG_MAX_FILES=30d +# 模块级别日志控制(JSON格式,可选) +# 允许针对特定模块设置不同的日志级别 +# 示例:{"device":"debug","backup":"warn","HTTP":"info"} +MODULE_LOG_LEVELS= + +# 日志采样配置(JSON格式,可选) +# 高频日志采样输出,rate表示采样率(0-1) +# error和warn级别不受采样影响 +# 示例:{"HTTP":0.1,"device":0.5} +LOG_SAMPLING= + # ============================================== # 前端配置 # ============================================== diff --git a/backend/middleware/requestLogger.js b/backend/middleware/requestLogger.js index c5bee4f..5733fa6 100644 --- a/backend/middleware/requestLogger.js +++ b/backend/middleware/requestLogger.js @@ -1,29 +1,53 @@ /** * HTTP 请求日志中间件 - * 为每个请求生成唯一 ID,记录请求/响应信息 + * 支持: + * - 唯一请求ID生成(UUID格式,避免碰撞) + * - 全链路请求追踪 + * - 请求/响应信息结构化记录 */ const logger = require('../utils/logger'); const crypto = require('crypto'); +/** + * 生成唯一请求ID + * 使用UUID格式确保全局唯一性 + * @returns {string} 请求ID + */ +const generateRequestId = () => { + if (crypto.randomUUID) { + return crypto.randomUUID(); + } + return `${Date.now().toString(36)}-${crypto.randomBytes(16).toString('hex')}`; +}; + /** * 请求日志中间件 - * 记录每个 HTTP 请求的方法、URL、状态码、耗时、用户ID 等信息 + * 为每个请求生成唯一ID,记录请求/响应信息 + * 支持全链路追踪:requestId贯穿请求生命周期 */ const requestLogger = (req, res, next) => { - const requestId = crypto.randomBytes(4).toString('hex'); + const requestId = generateRequestId(); const startTime = Date.now(); req.requestId = requestId; + const reqLogger = logger.withRequestId(requestId, 'HTTP'); + + reqLogger.debug('请求开始', { + method: req.method, + url: req.originalUrl, + ip: req.ip || req.connection.remoteAddress, + userAgent: req.get('User-Agent'), + userId: req.user?.userId || null, + }); + res.on('finish', () => { const duration = Date.now() - startTime; const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'debug'; - logger[level](`${req.method} ${req.originalUrl}`, { - module: 'HTTP', - requestId, + const logData = { method: req.method, url: req.originalUrl, statusCode: res.statusCode, @@ -31,6 +55,21 @@ const requestLogger = (req, res, next) => { ip: req.ip || req.connection.remoteAddress, userAgent: req.get('User-Agent'), userId: req.user?.userId || null, + contentLength: res.get('Content-Length') || 0, + }; + + if (res.statusCode >= 400) { + logData.errorMessage = res.locals.errorMessage || null; + logData.errorStack = res.locals.errorStack || null; + } + + reqLogger[level](`${req.method} ${req.originalUrl}`, logData); + }); + + res.on('error', (error) => { + reqLogger.error('响应错误', { + error: error.message, + stack: error.stack, }); }); diff --git a/backend/models/OperationLog.js b/backend/models/OperationLog.js index 5d535ab..eefa112 100644 --- a/backend/models/OperationLog.js +++ b/backend/models/OperationLog.js @@ -1,3 +1,9 @@ +/** + * 操作日志模型 + * 支持请求追踪ID(requestId)关联 + * 支持复合索引优化查询性能 + */ + const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); @@ -72,6 +78,10 @@ const OperationLog = sequelize.define( type: DataTypes.STRING, comment: '用户代理', }, + requestId: { + type: DataTypes.STRING, + comment: '请求追踪ID,关联HTTP请求日志', + }, metadata: { type: DataTypes.JSON, defaultValue: {}, @@ -87,6 +97,8 @@ const OperationLog = sequelize.define( { fields: ['targetId'] }, { fields: ['operatorId'] }, { fields: ['createdAt'] }, + { fields: ['requestId'] }, + { fields: ['module', 'createdAt'] }, ], } ); diff --git a/backend/scripts/migrate-all.js b/backend/scripts/migrate-all.js index aa51746..9e2109a 100644 --- a/backend/scripts/migrate-all.js +++ b/backend/scripts/migrate-all.js @@ -128,6 +128,11 @@ const migrations = [ description: '为 racks 表添加 rowPos、colPos、facing 字段,支持机柜位置定位', migrate: migrateRackPositionFields, }, + { + name: '操作日志请求追踪', + description: '为 operation_logs 表添加 requestId 字段和复合索引,支持请求追踪', + migrate: migrateOperationLogRequestId, + }, ]; async function runMigrations() { @@ -900,6 +905,52 @@ async function migrateRackPositionFields() { console.log(' 机柜位置字段迁移完成'); } +async function addIndexIfNotExists(tableName, indexName, fields) { + const dialect = sequelize.getDialect(); + try { + if (dialect === 'sqlite') { + await sequelize.query( + `CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${fields.join(', ')})` + ); + } else { + const [indexes] = await sequelize.query( + `SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME = ? AND INDEX_NAME = ? AND TABLE_SCHEMA = DATABASE()`, + { replacements: [tableName, indexName], type: sequelize.QueryTypes.SELECT } + ); + if (!indexes || indexes.length === 0) { + await sequelize.query( + `CREATE INDEX \`${indexName}\` ON \`${tableName}\`(\`${fields.join('`, `')}\`)` + ); + } else { + console.log(` 索引 ${indexName} 已存在,跳过`); + return; + } + } + console.log(` 索引 ${indexName} 创建成功`); + } catch (error) { + if (error.message.includes('already exists') || error.message.includes('Duplicate key name')) { + console.log(` 索引 ${indexName} 已存在,跳过`); + } else { + throw error; + } + } +} + +async function migrateOperationLogRequestId() { + const tableName = 'operation_logs'; + + if (!(await tableExists(tableName))) { + console.log(` ${tableName} 表不存在,跳过`); + return; + } + + await addColumnIfNotExists(tableName, 'requestId', 'VARCHAR(255)'); + await addIndexIfNotExists(tableName, 'operation_logs_requestId', ['requestId']); + await addIndexIfNotExists(tableName, 'operation_logs_module_createdAt', ['module', 'createdAt']); + + console.log(' 操作日志requestId字段和索引迁移完成'); +} + // 执行迁移 runMigrations().catch(error => { console.error('迁移执行失败:', error); diff --git a/backend/scripts/migrate-operation-log-requestId.js b/backend/scripts/migrate-operation-log-requestId.js new file mode 100644 index 0000000..d5078f1 --- /dev/null +++ b/backend/scripts/migrate-operation-log-requestId.js @@ -0,0 +1,115 @@ +/** + * 操作日志表添加 requestId 字段和索引 + * 支持请求追踪ID关联,优化查询性能 + * 支持 SQLite 和 MySQL,幂等执行 + */ + +require('dotenv').config({ path: require('path').join(__dirname, '../.env') }); + +const { sequelize, dbDialect } = require('../db'); + +async function getTableColumns(tableName) { + const dialect = sequelize.getDialect(); + if (dialect === 'sqlite') { + const tableInfo = await sequelize.query(`PRAGMA table_info(${tableName})`, { + type: sequelize.QueryTypes.SELECT, + }); + return tableInfo.map(col => col.name); + } + const tableInfo = await sequelize.query(`SHOW COLUMNS FROM ${tableName}`, { + type: sequelize.QueryTypes.SELECT, + }); + return tableInfo.map(col => col.Field); +} + +async function tableExists(tableName) { + const dialect = sequelize.getDialect(); + if (dialect === 'sqlite') { + const tables = await sequelize.query( + "SELECT name FROM sqlite_master WHERE type='table' AND name=?", + { replacements: [tableName], type: sequelize.QueryTypes.SELECT } + ); + return tables.length > 0; + } + const tables = await sequelize.query('SHOW TABLES LIKE ?', { + replacements: [tableName], + type: sequelize.QueryTypes.SELECT, + }); + return tables.length > 0; +} + +async function addColumnIfNotExists(tableName, columnName, columnDef) { + const columns = await getTableColumns(tableName); + if (!columns.includes(columnName)) { + await sequelize.query(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`); + console.log(` ${tableName} 表添加 ${columnName} 字段成功`); + } else { + console.log(` ${tableName} 表 ${columnName} 字段已存在,跳过`); + } +} + +async function addIndexIfNotExists(tableName, indexName, fields) { + const dialect = sequelize.getDialect(); + try { + if (dialect === 'sqlite') { + await sequelize.query( + `CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${fields.join(', ')})` + ); + } else { + const [indexes] = await sequelize.query( + `SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME = ? AND INDEX_NAME = ? AND TABLE_SCHEMA = DATABASE()`, + { replacements: [tableName, indexName], type: sequelize.QueryTypes.SELECT } + ); + if (!indexes || indexes.length === 0) { + await sequelize.query( + `CREATE INDEX \`${indexName}\` ON \`${tableName}\`(\`${fields.join('`, `')}\`)` + ); + } else { + console.log(` 索引 ${indexName} 已存在,跳过`); + return; + } + } + console.log(` 索引 ${indexName} 创建成功`); + } catch (error) { + if (error.message.includes('already exists') || error.message.includes('Duplicate key name')) { + console.log(` 索引 ${indexName} 已存在,跳过`); + } else { + throw error; + } + } +} + +async function migrateOperationLogRequestId() { + const tableName = 'operation_logs'; + + if (!(await tableExists(tableName))) { + console.log(` ${tableName} 表不存在,跳过`); + return; + } + + await addColumnIfNotExists(tableName, 'requestId', 'VARCHAR(255)'); + + await addIndexIfNotExists(tableName, 'operation_logs_requestId', ['requestId']); + await addIndexIfNotExists(tableName, 'operation_logs_module_createdAt', ['module', 'createdAt']); + + console.log(' 操作日志requestId字段和索引迁移完成'); +} + +async function run() { + console.log('========================================'); + console.log(' 操作日志表添加requestId字段迁移脚本 '); + console.log('========================================'); + console.log(`数据库类型: ${dbDialect}`); + + try { + await migrateOperationLogRequestId(); + console.log('\n迁移完成'); + } catch (error) { + console.error('迁移失败:', error.message); + process.exit(1); + } finally { + await sequelize.close(); + } +} + +run(); diff --git a/backend/utils/logger.js b/backend/utils/logger.js index 1a6abe9..26ea7de 100644 --- a/backend/utils/logger.js +++ b/backend/utils/logger.js @@ -1,6 +1,11 @@ /** * 统一日志模块 - * 基于 winston 的日志系统,支持文件轮转、级别控制、模块标识 + * 基于 winston 的日志系统,支持: + * - 文件轮转、级别控制、模块标识 + * - JSON结构化输出(生产环境) + * - 请求追踪ID传递 + * - 模块级别精细控制 + * - 日志采样过滤 * 全项目唯一日志入口 */ @@ -8,32 +13,107 @@ const { createLogger, format, transports } = require('winston'); const DailyRotateFile = require('winston-daily-rotate-file'); const path = require('path'); -// 日志目录 const LOG_DIR = process.env.LOG_DIR || path.join(__dirname, '../logs'); -// 日志级别 const LOG_LEVEL = process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'); -// 单个日志文件最大大小 const LOG_MAX_FILE_SIZE = process.env.LOG_MAX_FILE_SIZE || '20m'; -// 日志文件最大保留天数 const LOG_MAX_FILES = process.env.LOG_MAX_FILES || '30d'; /** - * 统一日志格式:时间 | 级别 | 模块 | 消息 | 元数据 + * 模块级别配置 + * 支持按模块精细控制日志级别,JSON格式 + * 示例:MODULE_LOG_LEVELS={"device":"debug","backup":"warn"} */ -const logFormat = format.combine( +let MODULE_LOG_LEVELS = {}; +try { + MODULE_LOG_LEVELS = process.env.MODULE_LOG_LEVELS + ? JSON.parse(process.env.MODULE_LOG_LEVELS) + : {}; +} catch { + MODULE_LOG_LEVELS = {}; +} + +/** + * 日志采样配置 + * 高频日志采样输出,rate表示采样率(0-1) + * 示例:LOG_SAMPLING={"HTTP":0.1,"device":0.5} + */ +let LOG_SAMPLING = {}; +try { + LOG_SAMPLING = process.env.LOG_SAMPLING + ? JSON.parse(process.env.LOG_SAMPLING) + : {}; +} catch { + LOG_SAMPLING = {}; +} + +const samplingCounters = {}; + +/** + * 获取模块日志级别 + * @param {string} moduleName - 模块名称 + * @returns {string} 日志级别 + */ +const getModuleLevel = (moduleName) => { + return MODULE_LOG_LEVELS[moduleName] || LOG_LEVEL; +}; + +/** + * 日志级别优先级映射 + */ +const LEVEL_PRIORITY = { error: 0, warn: 1, info: 2, debug: 3 }; + +/** + * 判断是否应该输出该级别日志 + * @param {string} level - 当前日志级别 + * @param {string} minLevel - 最低输出级别 + * @returns {boolean} + */ +const shouldLog = (level, minLevel) => { + return (LEVEL_PRIORITY[level] || 99) <= (LEVEL_PRIORITY[minLevel] || 99); +}; + +/** + * 日志采样判断 + * @param {string} moduleName - 模块名称 + * @param {string} level - 日志级别 + * @returns {boolean} 是否应该输出 + */ +const shouldSample = (moduleName, level) => { + if (level === 'error' || level === 'warn') return true; + + const samplingConfig = LOG_SAMPLING[moduleName]; + if (!samplingConfig || samplingConfig.rate >= 1) return true; + + samplingCounters[moduleName] = (samplingCounters[moduleName] || 0) + 1; + return samplingCounters[moduleName] % Math.floor(1 / samplingConfig.rate) === 0; +}; + +/** + * JSON格式输出(生产环境文件日志) + */ +const jsonFormat = format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), format.errors({ stack: true }), - format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'module'] }), - format.printf(({ timestamp, level, message, module, metadata, stack }) => { + format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'module', 'requestId'] }), + format.json() +); + +/** + * 文本格式输出(开发环境控制台) + */ +const consoleFormat = format.combine( + format.colorize(), + format.timestamp({ format: 'HH:mm:ss.SSS' }), + format.printf(({ timestamp, level, message, module, requestId, metadata }) => { const moduleTag = module ? `[${module}]` : ''; + const requestTag = requestId ? `[${requestId}]` : ''; const metaStr = metadata && Object.keys(metadata).length ? ' ' + JSON.stringify(metadata) : ''; - const stackStr = stack ? `\n${stack}` : ''; - return `${timestamp} ${level.toUpperCase().padEnd(5)} ${moduleTag} ${message}${metaStr}${stackStr}`; + return `${timestamp} ${level} ${moduleTag}${requestTag} ${message}${metaStr}`; }) ); @@ -42,19 +122,17 @@ const logFormat = format.combine( */ const logger = createLogger({ level: LOG_LEVEL, - format: logFormat, + format: jsonFormat, defaultMeta: { service: 'idc-management' }, transports: [ - // 应用日志 - 按天轮转 new DailyRotateFile({ dirname: path.join(LOG_DIR, 'app'), filename: 'application-%DATE%.log', datePattern: 'YYYY-MM-DD', maxFiles: LOG_MAX_FILES, maxSize: LOG_MAX_FILE_SIZE, - level: 'info', + level: 'debug', }), - // 错误日志 - 独立存储 new DailyRotateFile({ dirname: path.join(LOG_DIR, 'error'), filename: 'error-%DATE%.log', @@ -63,8 +141,15 @@ const logger = createLogger({ maxSize: LOG_MAX_FILE_SIZE, level: 'error', }), + new DailyRotateFile({ + dirname: path.join(LOG_DIR, 'request'), + filename: 'request-%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxFiles: '15d', + maxSize: LOG_MAX_FILE_SIZE, + level: 'debug', + }), ], - // 未捕获异常处理 exceptionHandlers: [ new DailyRotateFile({ dirname: path.join(LOG_DIR, 'error'), @@ -73,7 +158,6 @@ const logger = createLogger({ maxFiles: LOG_MAX_FILES, }), ], - // 未处理 Promise 拒绝处理 rejectionHandlers: [ new DailyRotateFile({ dirname: path.join(LOG_DIR, 'error'), @@ -84,30 +168,56 @@ const logger = createLogger({ ], }); -// 开发环境添加控制台输出(带颜色) if (process.env.NODE_ENV !== 'production') { logger.add(new transports.Console({ - format: format.combine( - format.colorize(), - format.timestamp({ format: 'HH:mm:ss' }), - format.printf(({ timestamp, level, message, module }) => { - const moduleTag = module ? `[${module}]` : ''; - return `${timestamp} ${level} ${moduleTag} ${message}`; - }) - ), + format: consoleFormat, + level: 'debug', })); } /** * 创建带模块名的子 logger + * 支持模块级别精细控制和日志采样 * @param {string} moduleName - 模块名称 * @returns {Object} 包含 debug/info/warn/error 方法的对象 */ -logger.module = (moduleName) => ({ - debug: (msg, meta) => logger.debug(msg, { module: moduleName, ...meta }), - info: (msg, meta) => logger.info(msg, { module: moduleName, ...meta }), - warn: (msg, meta) => logger.warn(msg, { module: moduleName, ...meta }), - error: (msg, meta) => logger.error(msg, { module: moduleName, ...meta }), -}); +logger.module = (moduleName) => { + const moduleLevel = getModuleLevel(moduleName); + + const logWithLevel = (level, msg, meta = {}) => { + if (!shouldLog(level, moduleLevel)) return; + if (!shouldSample(moduleName, level)) return; + logger.log(level, msg, { module: moduleName, ...meta }); + }; + + return { + debug: (msg, meta) => logWithLevel('debug', msg, meta), + info: (msg, meta) => logWithLevel('info', msg, meta), + warn: (msg, meta) => logWithLevel('warn', msg, meta), + error: (msg, meta) => logWithLevel('error', msg, meta), + }; +}; + +/** + * 创建带请求ID的 logger + * 用于全链路请求追踪 + * @param {string} requestId - 请求追踪ID + * @param {string} moduleName - 模块名称(可选) + * @returns {Object} 包含日志方法的对象 + */ +logger.withRequestId = (requestId, moduleName = null) => { + const baseLogger = moduleName ? logger.module(moduleName) : logger; + + const wrapMethod = (method) => (msg, meta = {}) => { + baseLogger[method](msg, { requestId, ...meta }); + }; + + return { + debug: wrapMethod('debug'), + info: wrapMethod('info'), + warn: wrapMethod('warn'), + error: wrapMethod('error'), + }; +}; module.exports = logger; diff --git a/backend/utils/operationLogger.js b/backend/utils/operationLogger.js index 68d0d69..61adf0f 100644 --- a/backend/utils/operationLogger.js +++ b/backend/utils/operationLogger.js @@ -1,9 +1,27 @@ +/** + * 操作日志记录器 + * 支持: + * - 关联请求追踪ID(requestId) + * - 数据库写入失败时降级到文件日志 + * - 统一错误处理(使用logger替代console.error) + */ + const OperationLog = require('../models/OperationLog'); +const logger = require('./logger'); +const fs = require('fs'); +const path = require('path'); + +const logModule = logger.module('OperationLogger'); const generateRecordId = () => { return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`; }; +/** + * 获取操作人信息 + * @param {Object} req - Express请求对象 + * @returns {Object} 操作人信息 + */ const getOperatorInfo = req => { if (!req || !req.user) { return { @@ -19,9 +37,14 @@ const getOperatorInfo = req => { }; }; +/** + * 获取客户端信息 + * @param {Object} req - Express请求对象 + * @returns {Object} 客户端信息(含requestId) + */ const getClientInfo = req => { if (!req) { - return { ipAddress: null, userAgent: null }; + return { ipAddress: null, userAgent: null, requestId: null }; } const ipAddress = req.headers['x-forwarded-for'] || @@ -29,8 +52,11 @@ const getClientInfo = req => { req.connection?.remoteAddress || req.ip || null; - const userAgent = req.headers['user-agent'] || null; - return { ipAddress, userAgent }; + return { + ipAddress, + userAgent: req.headers['user-agent'] || null, + requestId: req.requestId || null, + }; }; const DEVICE_TYPE_MAP = { @@ -43,6 +69,13 @@ const DEVICE_TYPE_MAP = { other: '其他设备', }; +/** + * 生成设备操作描述 + * @param {string} operation - 操作类型描述 + * @param {Object} device - 设备信息 + * @param {Object} options - 选项 + * @returns {string} 操作描述 + */ const generateDeviceDescription = (operation, device, options = {}) => { const { includeRack = true, @@ -82,6 +115,12 @@ const generateDeviceDescription = (operation, device, options = {}) => { return `${operation}${parts.join(',')}`; }; +/** + * 构建设备元数据 + * @param {Object} device - 设备信息 + * @param {Object} extra - 额外字段 + * @returns {Object} 元数据 + */ const buildDeviceMetadata = (device, extra = {}) => { return { deviceId: device.deviceId || null, @@ -99,6 +138,46 @@ const buildDeviceMetadata = (device, extra = {}) => { }; }; +/** + * 降级写入文件日志 + * 当数据库不可用时,将操作日志写入文件作为降级方案 + * @param {Object} logData - 日志数据 + */ +const fallbackToFile = (logData) => { + try { + const fallbackDir = path.join(process.env.LOG_DIR || './logs', 'fallback'); + if (!fs.existsSync(fallbackDir)) { + fs.mkdirSync(fallbackDir, { recursive: true }); + } + const fallbackPath = path.join(fallbackDir, `operation-fallback-${new Date().toISOString().slice(0, 10)}.log`); + const fallbackEntry = { + timestamp: new Date().toISOString(), + level: 'warn', + module: 'OperationLogger', + message: '操作日志数据库写入失败(降级记录)', + data: logData, + }; + fs.appendFileSync(fallbackPath, JSON.stringify(fallbackEntry) + '\n'); + } catch { + // 文件写入也失败时静默忽略,避免级联错误 + } +}; + +/** + * 记录操作日志(核心方法) + * @param {Object} params - 参数对象 + * @param {string} params.module - 模块名称 + * @param {string} params.operationType - 操作类型 + * @param {string} params.operationDescription - 操作描述 + * @param {string} params.targetId - 目标ID + * @param {string} params.targetName - 目标名称 + * @param {Object} params.beforeState - 操作前状态 + * @param {Object} params.afterState - 操作后状态 + * @param {string} params.result - 操作结果 + * @param {Object} params.req - 请求对象 + * @param {Object} params.metadata - 附加元数据 + * @returns {Promise} 创建的日志记录 + */ async function logOperation({ module, operationType, @@ -111,32 +190,64 @@ async function logOperation({ req, metadata = {}, }) { - try { - const operatorInfo = getOperatorInfo(req); - const clientInfo = getClientInfo(req); + const operatorInfo = getOperatorInfo(req); + const clientInfo = getClientInfo(req); - await OperationLog.create({ + const logData = { + module, + operationType, + operationDescription, + targetId: targetId || null, + targetName: targetName || null, + operatorId: operatorInfo.operatorId, + operatorName: operatorInfo.operatorName, + operatorRole: operatorInfo.operatorRole, + beforeState: beforeState || null, + afterState: afterState || null, + result, + ipAddress: clientInfo.ipAddress, + userAgent: clientInfo.userAgent, + requestId: clientInfo.requestId, + metadata, + }; + + try { + const operationLog = await OperationLog.create({ recordId: generateRecordId(), + ...logData, + }); + + logModule.debug('操作日志记录成功', { + recordId: operationLog.recordId, module, operationType, - operationDescription, - targetId: targetId || null, - targetName: targetName || null, - operatorId: operatorInfo.operatorId, - operatorName: operatorInfo.operatorName, - operatorRole: operatorInfo.operatorRole, - beforeState: beforeState || null, - afterState: afterState || null, - result, - ipAddress: clientInfo.ipAddress, - userAgent: clientInfo.userAgent, - metadata, + targetId, + requestId: clientInfo.requestId, }); + + return operationLog; } catch (error) { - console.error('记录操作日志失败:', error); + logModule.error('记录操作日志失败', { + error: error.message, + module, + operationType, + targetId, + requestId: clientInfo.requestId, + }); + + fallbackToFile(logData); + + return null; } } +/** + * 记录设备操作日志 + * @param {string} operationType - 操作类型 + * @param {string} operationDescription - 操作描述 + * @param {Object} params - 参数 + * @returns {Promise} + */ async function logDeviceOperation( operationType, operationDescription, @@ -156,6 +267,13 @@ async function logDeviceOperation( }); } +/** + * 记录用户操作日志 + * @param {string} operationType - 操作类型 + * @param {string} operationDescription - 操作描述 + * @param {Object} params - 参数 + * @returns {Promise} + */ async function logUserOperation( operationType, operationDescription, @@ -175,6 +293,13 @@ async function logUserOperation( }); } +/** + * 记录角色操作日志 + * @param {string} operationType - 操作类型 + * @param {string} operationDescription - 操作描述 + * @param {Object} params - 参数 + * @returns {Promise} + */ async function logRoleOperation( operationType, operationDescription,