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 (
+
+ );
+};
+```
+
+#### 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