Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9bff9beb3 | ||
|
|
864efb7f9d | ||
|
|
94ebe643b9 | ||
|
|
add1275d1c | ||
|
|
d0761503b7 | ||
|
|
6f4bff9bd0 | ||
|
|
755712bdb6 | ||
|
|
e57df45e48 | ||
|
|
5e3998736d | ||
|
|
321c691402 | ||
|
|
413babb238 | ||
|
|
42ec16824c |
@@ -3,11 +3,11 @@
|
|||||||
# 复制此文件为 .env,填入实际值
|
# 复制此文件为 .env,填入实际值
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 后端镜像地址(阿里云容器镜像服务个人版)
|
# 后端镜像地址(从 ghcr.io 拉取公开镜像)
|
||||||
BACKEND_IMAGE=crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-backend:latest
|
BACKEND_IMAGE=ghcr.io/xinmi/yunrui_asset-backend:latest
|
||||||
|
|
||||||
# 前端镜像地址(阿里云容器镜像服务个人版)
|
# 前端镜像地址(从 ghcr.io 拉取公开镜像)
|
||||||
FRONTEND_IMAGE=crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-frontend:latest
|
FRONTEND_IMAGE=ghcr.io/xinmi/yunrui_asset-frontend:latest
|
||||||
|
|
||||||
# 指定特定版本(可选,默认 latest)
|
# 指定特定版本(可选,默认 latest)
|
||||||
# BACKEND_VERSION=v1.0.0
|
# BACKEND_VERSION=v1.0.0
|
||||||
@@ -15,6 +15,6 @@ FRONTEND_IMAGE=crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-as
|
|||||||
|
|
||||||
# ---- MySQL 容器配置(如启用本地 MySQL) ----
|
# ---- MySQL 容器配置(如启用本地 MySQL) ----
|
||||||
# MYSQL_ROOT_PASSWORD=your_root_password
|
# MYSQL_ROOT_PASSWORD=your_root_password
|
||||||
# MYSQL_DATABASE=idc_management
|
# MYSQL_DATABASE=yunrui_management
|
||||||
# MYSQL_USER=idc_user
|
# MYSQL_USER=yunrui_user
|
||||||
# MYSQL_PASSWORD=your_mysql_password
|
# MYSQL_PASSWORD=your_mysql_password
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# ============================================
|
||||||
|
# 构建并推送 Docker 镜像到 GitHub Container Registry
|
||||||
|
# ============================================
|
||||||
name: Build and Push Docker Images
|
name: Build and Push Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -12,10 +15,10 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com
|
REGISTRY: ghcr.io
|
||||||
NAMESPACE: ${{ secrets.ALIYUN_NAMESPACE }}
|
NAMESPACE: ${{ github.repository_owner }}
|
||||||
BACKEND_IMAGE: idc-backend
|
BACKEND_IMAGE: yunrui_asset-backend
|
||||||
FRONTEND_IMAGE: idc-frontend
|
FRONTEND_IMAGE: yunrui_asset-frontend
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-backend:
|
build-backend:
|
||||||
@@ -27,12 +30,12 @@ jobs:
|
|||||||
- name: 检出代码
|
- name: 检出代码
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 登录阿里云 Docker Registry
|
- name: 登录 GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ secrets.ALIYUN_USERNAME }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: 提取版本号
|
- name: 提取版本号
|
||||||
id: meta
|
id: meta
|
||||||
@@ -69,12 +72,12 @@ jobs:
|
|||||||
- name: 检出代码
|
- name: 检出代码
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 登录阿里云 Docker Registry
|
- name: 登录 GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ secrets.ALIYUN_USERNAME }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: 提取版本号
|
- name: 提取版本号
|
||||||
id: meta
|
id: meta
|
||||||
@@ -114,8 +117,12 @@ jobs:
|
|||||||
echo "=== 构建结果 ==="
|
echo "=== 构建结果 ==="
|
||||||
echo "后端镜像: ${{ needs.build-backend.result }}"
|
echo "后端镜像: ${{ needs.build-backend.result }}"
|
||||||
echo "前端镜像: ${{ needs.build-frontend.result }}"
|
echo "前端镜像: ${{ needs.build-frontend.result }}"
|
||||||
|
echo ""
|
||||||
|
echo "ghcr.io 镜像地址:"
|
||||||
|
echo " 后端: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.BACKEND_IMAGE }}:latest"
|
||||||
|
echo " 前端: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.FRONTEND_IMAGE }}:latest"
|
||||||
if [[ "${{ needs.build-backend.result }}" == "success" && "${{ needs.build-frontend.result }}" == "success" ]]; then
|
if [[ "${{ needs.build-backend.result }}" == "success" && "${{ needs.build-frontend.result }}" == "success" ]]; then
|
||||||
echo "所有镜像构建成功"
|
echo "所有镜像构建成功,已推送到 ghcr.io"
|
||||||
else
|
else
|
||||||
echo "部分镜像构建失败"
|
echo "部分镜像构建失败"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -119,3 +119,5 @@ docs/install-split-plan.md
|
|||||||
# Local-only references (本地参考文档)
|
# Local-only references (本地参考文档)
|
||||||
# ==============================================
|
# ==============================================
|
||||||
deploy/nginx-troubleshooting.md
|
deploy/nginx-troubleshooting.md
|
||||||
|
docs/trae-creativity-contest-proposal.html
|
||||||
|
TRAE-创意提案-云睿资产管理系统.html
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# 工作流执行状态
|
||||||
|
|
||||||
|
## 当前进度
|
||||||
|
|
||||||
|
- **当前步骤**:步骤 11
|
||||||
|
- **状态**:completed
|
||||||
|
- **开始时间**:2026-6-18 10:18:34
|
||||||
|
- **完成时间**:
|
||||||
|
- **项目信息**:
|
||||||
|
- 项目名称:云睿资产
|
||||||
|
- 项目目录:C:\Users\L1822\xinmi\yunrui_assetet
|
||||||
|
- Git 地址:https://code.xinmi.cloud/yyunrui_assett
|
||||||
|
- 端口范围:12000-12010
|
||||||
|
- 状态文档:C:\Users\L1822\xinmi\yunrui_assetet\.work\工作流状态.md
|
||||||
|
|
||||||
|
## 步骤执行记录
|
||||||
|
|
||||||
|
### 步骤 0:克隆项目
|
||||||
|
- **状态**:⏭️ 未开始
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:
|
||||||
|
|
||||||
|
### 步骤 1:项目完整度评估
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:项目评估完成,品牌名已确定为:云睿资产。
|
||||||
|
|
||||||
|
### 步骤 2:项目整体分析
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:项目分析完成,端口范围已确定为:12000-12010。
|
||||||
|
|
||||||
|
### 步骤 3:Docker配置生成
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:Docker 配置已生成,包含 docker-compose.yml 和 docker-compose.full.yml。
|
||||||
|
|
||||||
|
### 步骤 4:品牌与界面标准化定制
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:品牌与界面标准化定制完成,已更新站点名称并添加新觅源码库链接。
|
||||||
|
|
||||||
|
### 步骤 5:核心应用页面建设
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:核心应用页面建设完成,已优化登录页并新增关于页面。
|
||||||
|
|
||||||
|
### 步骤 6:示例数据
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:示例数据种子脚本已生成:backend/scripts/seed-example-data.js。
|
||||||
|
|
||||||
|
### 步骤 7:CORS配置检查
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:CORS 配置检查完成,后端已通过 app.use(cors()) 允许跨域。
|
||||||
|
|
||||||
|
### 步骤 8:端口检查与分配
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:端口检查与分配完成,已确认 Docker 映射及 Nginx 代理配置。
|
||||||
|
|
||||||
|
### 步骤 9:构建与启动
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:构建已启动,由于环境 Docker 守护进程不稳定,服务状态将在健康验证步骤中确认。
|
||||||
|
|
||||||
|
### 步骤 10:健康验证
|
||||||
|
- **状态**:failed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:健康验证失败:由于本地 Docker 守护进程不稳定,构建任务中断。
|
||||||
|
|
||||||
|
### 步骤 11:提交
|
||||||
|
- **状态**:completed
|
||||||
|
- **完成时间**:
|
||||||
|
- **说明**:工作流执行完毕,代码已提交。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
无
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
无
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# 错误记录
|
||||||
|
|
||||||
|
## 2026-06-18
|
||||||
|
- **步骤**:9/10 构建与启动 / 健康验证
|
||||||
|
- **错误信息**:`failed to receive status: rpc error: code = Unavailable desc = error reading from server: EOF`
|
||||||
|
- **原因分析**:本地 Docker 守护进程在构建过程中多次崩溃(返回 500 错误),导致构建中断。可能由于系统资源占用过高或 Docker Desktop 稳定性问题。
|
||||||
|
- **尝试措施**:重试了 3 次(包含背景构建和前台构建),均在前端构建阶段由于 RPC 错误中断。
|
||||||
|
- **当前状态**:代码已完成标准化定制(品牌、端口、配置、示例数据脚本、关于页面),但本地 Docker 部署无法完成。建议在资源更充足的环境下重试 `docker compose -f docker-compose.full.yml up -d --build`。
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
《项目完整度评估报告》
|
||||||
|
|
||||||
|
## 评估结果
|
||||||
|
- 项目结构是否完整:是
|
||||||
|
- 项目类型:Web应用(前后端分离)
|
||||||
|
- **是否适合 Docker 部署**:是
|
||||||
|
- 主要技术栈:Node.js / Express / MongoDB / React / Vite
|
||||||
|
|
||||||
|
## 有效文件列表
|
||||||
|
### 源码文件
|
||||||
|
- `backend/`: 后端源码目录,包含 `server.js`、`routes/`、`models/` 等。
|
||||||
|
- `frontend/src/`: 前端源码目录,基于 Vite + React。
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
|
- `backend/package.json`: 后端依赖及脚本。
|
||||||
|
- `frontend/package.json`: 前端依赖及脚本。
|
||||||
|
- `docker-compose.prod.yml`: 生产环境 Docker 编排配置。
|
||||||
|
- `backend/Dockerfile`: 后端 Docker 镜像构建配置。
|
||||||
|
- `frontend/Dockerfile`: 前端 Docker 镜像构建配置。
|
||||||
|
- `.env.example`: 环境变量模板。
|
||||||
|
|
||||||
|
### 依赖文件
|
||||||
|
- `backend/package-lock.json`
|
||||||
|
- `frontend/package-lock.json`
|
||||||
|
|
||||||
|
## 缺失关键信息
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 组件代码缺失处理
|
||||||
|
- 查找过程:
|
||||||
|
- 已阅读 README.md。
|
||||||
|
- 检查了根目录,已包含 `backend` 和 `frontend` 两个核心目录。
|
||||||
|
- 确认项目完整。
|
||||||
|
- 查找结果:未发现缺失组件。
|
||||||
|
|
||||||
|
## 建议
|
||||||
|
- 项目结构完整,适合进行后续的 Xinmi 标准化定制与部署。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
《项目整体分析报告》
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
本项目是一个 云睿资产管理系统,主要提供机房、机柜、设备、端口、线缆等资产的精细化管理功能,支持工单系统、耗材管理、盘点任务以及 3D 可视化展示。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
- 编程语言:JavaScript (Node.js v20+)
|
||||||
|
- 后端框架:Express 4.19.2
|
||||||
|
- 前端框架:React 18.2.0 (Vite 构建)
|
||||||
|
- 数据库:SQLite (默认) / MySQL
|
||||||
|
- 中间件:Swagger (API 文档), Sequelize (ORM), Three.js (3D 可视化)
|
||||||
|
|
||||||
|
## 服务架构
|
||||||
|
### 业务服务
|
||||||
|
- 后端服务 (Backend):端口 8000,负责 API 接口、数据库交互、文件上传及系统逻辑。
|
||||||
|
- 前端服务 (Frontend):端口 3000,负责用户界面交互及 3D 模型展示。
|
||||||
|
|
||||||
|
### 外部依赖
|
||||||
|
- 数据库 (SQLite):本地文件 `yunrui_management.db`。
|
||||||
|
- 数据库 (MySQL):可选,外部连接。
|
||||||
|
|
||||||
|
## 服务依赖关系
|
||||||
|
1. 数据库:后端启动时自动连接 SQLite 文件或 MySQL 实例。
|
||||||
|
2. 后端服务:执行数据库同步、字段初始化后启动。
|
||||||
|
3. 前端服务:依赖后端提供的 API 接口进行数据展示。
|
||||||
|
|
||||||
|
## 部署策略判断
|
||||||
|
- **是否适合 Docker 部署**:是
|
||||||
|
- **判断依据**:项目采用前后端分离架构,已提供 Dockerfile 及 docker-compose 配置,环境依赖明确(Node.js, SQLite/MySQL)。
|
||||||
|
- **项目类型**:Web 应用
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
IDC Device Asset Management System — full-stack app for data center device lifecycle management with 3D visualization. Monorepo: `backend/` (Express + Sequelize), `frontend/` (React + Vite + Ant Design + Three.js).
|
YunRui Asset Management System — full-stack app for data center device lifecycle management with 3D visualization. Monorepo: `backend/` (Express + Sequelize), `frontend/` (React + Vite + Ant Design + Three.js).
|
||||||
|
|
||||||
## 交互规则
|
## 交互规则
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# IDC设备资产系统 - Code Wiki
|
# 云睿资产系统 - Code Wiki
|
||||||
|
|
||||||
> 版本:2.0.0 | 更新日期:2026-05-07 | 项目地址:[GitHub](https://github.com/gituib/idc_assest)
|
> 版本:2.0.0 | 更新日期:2026-05-07 | 项目地址:[源码仓库](https://code.xinmi.cloud/yunyunrui_asset
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
### 1.1 项目简介
|
### 1.1 项目简介
|
||||||
|
|
||||||
IDC设备资产系统(IDC Device Management System)是一个现代化的数据中心设备全生命周期管理平台,提供机房、机柜、设备的完整管理能力,并具备3D可视化展示功能。
|
云睿资产系统(YunRui Asset Management System)是一个现代化的数据中心设备全生命周期管理平台,提供机房、机柜、设备的完整管理能力,并具备3D可视化展示功能。
|
||||||
|
|
||||||
### 1.2 技术栈概览
|
### 1.2 技术栈概览
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ IDC设备资产系统(IDC Device Management System)是一个现代化的数
|
|||||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
│ │ SQLite │ │ MySQL │ │
|
│ │ SQLite │ │ MySQL │ │
|
||||||
│ │ (开发环境) │◄────────────────►│ (生产环境) │ │
|
│ │ (开发环境) │◄────────────────►│ (生产环境) │ │
|
||||||
│ │ idc_management.db│ │ MySQL 8.0+ │ │
|
│ │ yunrui_management.db│ │ MySQL 8.0+ │ │
|
||||||
│ └─────────────────┘ └─────────────────┘ │
|
│ └─────────────────┘ └─────────────────┘ │
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
@@ -592,7 +592,7 @@ if (DB_TYPE === 'mysql') {
|
|||||||
} else {
|
} else {
|
||||||
sequelize = new Sequelize({
|
sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: process.env.DB_PATH || './idc_management.db'
|
storage: process.env.DB_PATH || './yunrui_management.db'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1235,7 +1235,7 @@ const backupDatabase = async (options = {}) => {
|
|||||||
const filepath = path.join(BACKUP_DIR, filename);
|
const filepath = path.join(BACKUP_DIR, filename);
|
||||||
|
|
||||||
await fs.copy(
|
await fs.copy(
|
||||||
process.env.DB_PATH || './idc_management.db',
|
process.env.DB_PATH || './yunrui_management.db',
|
||||||
filepath
|
filepath
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1493,7 +1493,7 @@ router.post('/import-preview', upload.single('csvFile'), async (req, res) => {
|
|||||||
```bash
|
```bash
|
||||||
# 前端环境配置
|
# 前端环境配置
|
||||||
VITE_API_BASE_URL=http://localhost:8000/api
|
VITE_API_BASE_URL=http://localhost:8000/api
|
||||||
VITE_APP_TITLE=IDC设备管理系统
|
VITE_APP_TITLE=云睿资产管理系统
|
||||||
VITE_APP_VERSION=2.0.0
|
VITE_APP_VERSION=2.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1506,12 +1506,12 @@ PORT=8000
|
|||||||
|
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
DB_TYPE=sqlite # sqlite 或 mysql
|
DB_TYPE=sqlite # sqlite 或 mysql
|
||||||
DB_PATH=./idc_management.db # SQLite数据库路径
|
DB_PATH=./yunrui_management.db # SQLite数据库路径
|
||||||
|
|
||||||
# MySQL配置 (当DB_TYPE=mysql时)
|
# MySQL配置 (当DB_TYPE=mysql时)
|
||||||
MYSQL_HOST=localhost
|
MYSQL_HOST=localhost
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_DATABASE=idc_management
|
MYSQL_DATABASE=yunrui_management
|
||||||
MYSQL_USERNAME=root
|
MYSQL_USERNAME=root
|
||||||
MYSQL_PASSWORD=your_password
|
MYSQL_PASSWORD=your_password
|
||||||
|
|
||||||
@@ -1573,11 +1573,11 @@ npm run build
|
|||||||
|
|
||||||
# 2. 使用PM2启动后端
|
# 2. 使用PM2启动后端
|
||||||
cd ../backend
|
cd ../backend
|
||||||
pm2 start server.js --name idc-backend
|
pm2 start server.js --name yunrui_asset-backend
|
||||||
|
|
||||||
# 3. 使用Nginx反向代理
|
# 3. 使用Nginx反向代理
|
||||||
# 或使用PM2 serve提供静态文件
|
# 或使用PM2 serve提供静态文件
|
||||||
pm2 serve ./frontend/dist 3000 --name idc-frontend
|
pm2 serve ./frontend/dist 3000 --name yunrui_asset-frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1596,23 +1596,23 @@ sudo apt install mysql-server
|
|||||||
|
|
||||||
# 2. 创建数据库
|
# 2. 创建数据库
|
||||||
mysql -u root -p
|
mysql -u root -p
|
||||||
CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
CREATE DATABASE yunrui_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
CREATE USER 'idc_user'@'localhost' IDENTIFIED BY 'your_password';
|
CREATE USER 'yunrui_user'@'localhost' IDENTIFIED BY 'your_password';
|
||||||
GRANT ALL PRIVILEGES ON idc_management.* TO 'idc_user'@'localhost';
|
GRANT ALL PRIVILEGES ON yunrui_management.* TO 'yunrui_user'@'localhost';
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 10.1.2 Nginx配置
|
#### 10.1.2 Nginx配置
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
# /etc/nginx/sites-available/idc
|
# /etc/nginx/sites-available/yunrui_asset
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name your_domain.com;
|
server_name your_domain.com;
|
||||||
|
|
||||||
# 前端静态文件
|
# 前端静态文件
|
||||||
root /var/www/idc/frontend/dist;
|
root /var/www/yunrui_asset/frontend/dist;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# Gzip压缩
|
# Gzip压缩
|
||||||
@@ -1636,7 +1636,7 @@ server {
|
|||||||
|
|
||||||
# 上传文件
|
# 上传文件
|
||||||
location /uploads {
|
location /uploads {
|
||||||
alias /var/www/idc/backend/uploads;
|
alias /var/www/yunrui_asset/backend/uploads;
|
||||||
autoindex on;
|
autoindex on;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1666,12 +1666,12 @@ npm install -g pm2
|
|||||||
|
|
||||||
# 启动后端服务
|
# 启动后端服务
|
||||||
cd backend
|
cd backend
|
||||||
pm2 start server.js --name idc-backend \
|
pm2 start server.js --name yunrui_asset-backend \
|
||||||
--env production \
|
--env production \
|
||||||
--max-memory-restart 1G
|
--max-memory-restart 1G
|
||||||
|
|
||||||
# 配置负载均衡 (多核CPU)
|
# 配置负载均衡 (多核CPU)
|
||||||
pm2 start server.js -i max --name idc-backend
|
pm2 start server.js -i max --name yunrui_asset-backend
|
||||||
|
|
||||||
# 保存进程列表
|
# 保存进程列表
|
||||||
pm2 save
|
pm2 save
|
||||||
@@ -1723,7 +1723,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: idc_management
|
MYSQL_DATABASE: yunrui_management
|
||||||
MYSQL_ROOT_PASSWORD: root_password
|
MYSQL_ROOT_PASSWORD: root_password
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
@@ -1819,7 +1819,7 @@ volumes:
|
|||||||
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
||||||
│ │ SQLite │ │ MySQL │ │
|
│ │ SQLite │ │ MySQL │ │
|
||||||
│ │ (开发环境) │◄──►│ (生产环境) │ │
|
│ │ (开发环境) │◄──►│ (生产环境) │ │
|
||||||
│ │ idc_management.db │ │ MySQL 8.0+ │ │
|
│ │ yunrui_management.db │ │ MySQL 8.0+ │ │
|
||||||
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
||||||
@@ -2160,7 +2160,7 @@ volumes:
|
|||||||
│ │ │ filename: "backup_20260507_120000.sql", │ │ │
|
│ │ │ filename: "backup_20260507_120000.sql", │ │ │
|
||||||
│ │ │ size: 1048576, │ │ │
|
│ │ │ size: 1048576, │ │ │
|
||||||
│ │ │ createdAt: "2026-05-07T12:00:00Z", │ │ │
|
│ │ │ createdAt: "2026-05-07T12:00:00Z", │ │ │
|
||||||
│ │ │ database: "idc_management", │ │ │
|
│ │ │ database: "yunrui_management", │ │ │
|
||||||
│ │ │ tables: ["devices", "racks", "tickets", ...], │ │ │
|
│ │ │ tables: ["devices", "racks", "tickets", ...], │ │ │
|
||||||
│ │ │ status: "completed" │ │ │
|
│ │ │ status: "completed" │ │ │
|
||||||
│ │ │ } │ │ │
|
│ │ │ } │ │ │
|
||||||
@@ -2200,7 +2200,7 @@ volumes:
|
|||||||
│ │ mysql -u user -p dbname < backup_xxx.sql │ │
|
│ │ mysql -u user -p dbname < backup_xxx.sql │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ SQLite: │ │
|
│ │ SQLite: │ │
|
||||||
│ │ cp backup_xxx.db idc_management.db │ │
|
│ │ cp backup_xxx.db yunrui_management.db │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ └──────────────────────────┬───────────────────────────────────────┘ │
|
│ └──────────────────────────┬───────────────────────────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# IDC设备管理系统 - 环境配置
|
# 云睿资产管理系统 - 环境配置
|
||||||
# 生成时间: 2026-02-26T05:07:19.876Z
|
# 生成时间: 2026-02-26T05:07:19.876Z
|
||||||
|
|
||||||
# ==============================================
|
# ==============================================
|
||||||
@@ -19,16 +19,16 @@ NODE_ENV=development
|
|||||||
DB_TYPE=sqlite
|
DB_TYPE=sqlite
|
||||||
|
|
||||||
# SQLite 配置(当 DB_TYPE=sqlite 时使用)
|
# SQLite 配置(当 DB_TYPE=sqlite 时使用)
|
||||||
DB_PATH=./idc_management.db
|
DB_PATH=./yunrui_management.db
|
||||||
|
|
||||||
# MySQL 配置(当 DB_TYPE=mysql 时使用)
|
# MySQL 配置(当 DB_TYPE=mysql 时使用)
|
||||||
# 注意:使用 MySQL 前请确保 MySQL 服务已安装并创建相应的数据库
|
# 注意:使用 MySQL 前请确保 MySQL 服务已安装并创建相应的数据库
|
||||||
# 数据库创建命令:CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
# 数据库创建命令:CREATE DATABASE yunrui_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
MYSQL_HOST=localhost
|
MYSQL_HOST=localhost
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_USERNAME=root
|
MYSQL_USERNAME=root
|
||||||
MYSQL_PASSWORD=
|
MYSQL_PASSWORD=
|
||||||
MYSQL_DATABASE=idc_management
|
MYSQL_DATABASE=yunrui_management
|
||||||
|
|
||||||
# ==============================================
|
# ==============================================
|
||||||
# 安全配置
|
# 安全配置
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# IDC 设备管理系统 - 后端 Dockerfile
|
# 云睿资产管理系统 - 后端 Dockerfile
|
||||||
# 基于 Node.js 20 Alpine 构建
|
# 基于 Node.js 20 Alpine 构建
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ COPY package.json ./
|
|||||||
COPY package-lock.json* yarn.lock* ./
|
COPY package-lock.json* yarn.lock* ./
|
||||||
|
|
||||||
# 安装生产依赖(不安装 devDependencies)
|
# 安装生产依赖(不安装 devDependencies)
|
||||||
RUN npm install --omit=dev --ignore-scripts
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
# 复制后端代码
|
# 复制后端代码
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const sqlLogger = (msg) => logger.debug(msg);
|
|||||||
if (DB_TYPE === 'mysql') {
|
if (DB_TYPE === 'mysql') {
|
||||||
// MySQL 配置
|
// MySQL 配置
|
||||||
sequelize = new Sequelize(
|
sequelize = new Sequelize(
|
||||||
process.env.MYSQL_DATABASE || 'idc_management',
|
process.env.MYSQL_DATABASE || 'yunrui_management',
|
||||||
process.env.MYSQL_USERNAME || 'root',
|
process.env.MYSQL_USERNAME || 'root',
|
||||||
process.env.MYSQL_PASSWORD || '',
|
process.env.MYSQL_PASSWORD || '',
|
||||||
{
|
{
|
||||||
@@ -38,7 +38,7 @@ if (DB_TYPE === 'mysql') {
|
|||||||
} else {
|
} else {
|
||||||
sequelize = new Sequelize({
|
sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: process.env.DB_PATH || './idc_management.db',
|
storage: process.env.DB_PATH || './yunrui_management.db',
|
||||||
logging: sqlLogger,
|
logging: sqlLogger,
|
||||||
pool: {
|
pool: {
|
||||||
max: 5,
|
max: 5,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function ensureJwtSecret() {
|
|||||||
fs.writeFileSync(ENV_FILE_PATH, updatedContent, 'utf-8');
|
fs.writeFileSync(ENV_FILE_PATH, updatedContent, 'utf-8');
|
||||||
console.log('✓ JWT_SECRET 已自动生成并保存到 .env 文件');
|
console.log('✓ JWT_SECRET 已自动生成并保存到 .env 文件');
|
||||||
} else {
|
} else {
|
||||||
const defaultContent = `# IDC设备管理系统 - 环境配置
|
const defaultContent = `# 云睿资产管理系统 - 环境配置
|
||||||
# 自动生成时间: ${new Date().toISOString()}
|
# 自动生成时间: ${new Date().toISOString()}
|
||||||
|
|
||||||
JWT_SECRET=${newSecret}
|
JWT_SECRET=${newSecret}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "idc-backend",
|
"name": "yunrui-backend",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "idc-backend",
|
"name": "yunrui-backend",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"all": "^0.0.0",
|
"all": "^0.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "idc-backend",
|
"name": "yunrui_asset-backend",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"description": "IDC设备管理系统后端",
|
"description": "云睿资产管理系统后端",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon server.js",
|
"dev": "nodemon server.js",
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"test": "jest --runInBand",
|
"test": "jest --runInBand",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"test:operation-logs": "jest tests/operationLog.model.test.js tests/operationLogger.test.js tests/operationLogs.api.test.js --runInBand"
|
"test:operation-logs": "jest tests/operationLog.model.test.js tests/operationLogger.test.js tests/operationLogs.api.test.js --runInBand",
|
||||||
|
"seed": "node scripts/seed-example-data.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"all": "^0.0.0",
|
"all": "^0.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const logger = require('../utils/logger').module('DevicePortsRoute');
|
const logger = require('../utils/logger').module('DevicePortsRoute');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { Op } = require('sequelize');
|
const { Op, Sequelize } = require('sequelize');
|
||||||
const DevicePort = require('../models/DevicePort');
|
const DevicePort = require('../models/DevicePort');
|
||||||
const Device = require('../models/Device');
|
const Device = require('../models/Device');
|
||||||
const NetworkCard = require('../models/NetworkCard');
|
const NetworkCard = require('../models/NetworkCard');
|
||||||
@@ -489,6 +489,107 @@ router.get('/export/all', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 获取单个端口
|
// 获取单个端口
|
||||||
|
// 按设备分组返回端口,支持设备维度分页
|
||||||
|
// 解决端口数超过 pageSize 时部分设备不显示的问题
|
||||||
|
router.get('/grouped', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deviceId, page = 1, pageSize = 10 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const pageSizeNum = Math.max(1, parseInt(pageSize) || 10);
|
||||||
|
const offset = (pageNum - 1) * pageSizeNum;
|
||||||
|
|
||||||
|
// 设备筛选条件
|
||||||
|
const deviceWhere = {};
|
||||||
|
if (deviceId) {
|
||||||
|
deviceWhere.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 端口总数(不受设备分页影响,用于前端统计与导出)
|
||||||
|
const portWhere = {};
|
||||||
|
if (deviceId) {
|
||||||
|
portWhere.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
const totalPorts = await DevicePort.count({ where: portWhere });
|
||||||
|
|
||||||
|
// 查询有端口的设备(用 EXISTS 子查询,避免 hasMany JOIN 导致 limit 作用于端口行)
|
||||||
|
// 若用 include+JOIN,limit 会作用于 JOIN 后的行数,端口多的设备会占满 limit
|
||||||
|
const { count, rows } = await Device.findAndCountAll({
|
||||||
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
|
where: {
|
||||||
|
...deviceWhere,
|
||||||
|
[Op.and]: [
|
||||||
|
Sequelize.where(
|
||||||
|
Sequelize.literal(
|
||||||
|
`EXISTS (SELECT 1 FROM device_ports WHERE device_ports.deviceId = Device.deviceId)`
|
||||||
|
),
|
||||||
|
'=',
|
||||||
|
1
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
order: [['name', 'ASC']],
|
||||||
|
offset,
|
||||||
|
limit: pageSizeNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前页设备ID列表
|
||||||
|
const pagedDeviceIds = rows.map(d => d.deviceId);
|
||||||
|
|
||||||
|
if (pagedDeviceIds.length === 0) {
|
||||||
|
return res.json({
|
||||||
|
total: count,
|
||||||
|
totalPorts,
|
||||||
|
page: pageNum,
|
||||||
|
pageSize: pageSizeNum,
|
||||||
|
groups: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询当前页设备的所有端口(含网卡关联)
|
||||||
|
const ports = await DevicePort.findAll({
|
||||||
|
where: { deviceId: { [Op.in]: pagedDeviceIds } },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Device,
|
||||||
|
as: 'device',
|
||||||
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: NetworkCard,
|
||||||
|
as: 'networkCard',
|
||||||
|
attributes: ['nicId', 'name'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按设备分组,保持设备排序(与分页查询一致)
|
||||||
|
const deviceMap = new Map();
|
||||||
|
rows.forEach(d => {
|
||||||
|
deviceMap.set(d.deviceId, { device: d, ports: [] });
|
||||||
|
});
|
||||||
|
ports.forEach(port => {
|
||||||
|
const group = deviceMap.get(port.deviceId);
|
||||||
|
if (group) {
|
||||||
|
group.ports.push(port);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = pagedDeviceIds.map(id => deviceMap.get(id));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
total: count,
|
||||||
|
totalPorts,
|
||||||
|
page: pageNum,
|
||||||
|
pageSize: pageSizeNum,
|
||||||
|
groups,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('按设备分组获取端口失败', { error: error.message, stack: error.stack });
|
||||||
|
res.status(500).json({ error: error.message, errorType: error.name });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/:portId', async (req, res) => {
|
router.get('/:portId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const port = await DevicePort.findByPk(req.params.portId, {
|
const port = await DevicePort.findByPk(req.params.portId, {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { Op } = require('sequelize');
|
|||||||
const { authMiddleware, clearMaintenanceCache } = require('../middleware/auth');
|
const { authMiddleware, clearMaintenanceCache } = require('../middleware/auth');
|
||||||
|
|
||||||
// 读取 package.json 获取版本号
|
// 读取 package.json 获取版本号
|
||||||
const packageJsonPath = path.join(__dirname, '../../package.json');
|
const packageJsonPath = path.join(__dirname, '../package.json');
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
const APP_VERSION = packageJson.version || '1.0.0';
|
const APP_VERSION = packageJson.version || '1.0.0';
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const initDefaultSettings = async () => {
|
|||||||
// 基本设置 - 站点信息
|
// 基本设置 - 站点信息
|
||||||
{
|
{
|
||||||
settingKey: 'site_name',
|
settingKey: 'site_name',
|
||||||
settingValue: JSON.stringify('机柜管理系统'),
|
settingValue: JSON.stringify('云睿资产管理系统'),
|
||||||
settingType: 'string',
|
settingType: 'string',
|
||||||
category: 'general',
|
category: 'general',
|
||||||
description: '网站名称',
|
description: '网站名称',
|
||||||
@@ -93,7 +93,7 @@ const initDefaultSettings = async () => {
|
|||||||
// 关于页面
|
// 关于页面
|
||||||
{
|
{
|
||||||
settingKey: 'app_version',
|
settingKey: 'app_version',
|
||||||
settingValue: JSON.stringify('1.2.0'),
|
settingValue: JSON.stringify(APP_VERSION),
|
||||||
settingType: 'string',
|
settingType: 'string',
|
||||||
category: 'about',
|
category: 'about',
|
||||||
description: '应用版本',
|
description: '应用版本',
|
||||||
@@ -133,7 +133,7 @@ const initDefaultSettings = async () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
settingKey: 'system_description',
|
settingKey: 'system_description',
|
||||||
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
|
settingValue: JSON.stringify('云睿资产管理系统 - 专业的数据中心设备管理解决方案'),
|
||||||
settingType: 'string',
|
settingType: 'string',
|
||||||
category: 'about',
|
category: 'about',
|
||||||
description: '系统描述',
|
description: '系统描述',
|
||||||
@@ -374,7 +374,7 @@ router.post('/reset/:key', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
site_name: '机柜管理系统',
|
site_name: '云睿资产管理系统',
|
||||||
site_logo: '',
|
site_logo: '',
|
||||||
idle_timeout: 30,
|
idle_timeout: 30,
|
||||||
max_login_attempts: 5,
|
max_login_attempts: 5,
|
||||||
@@ -385,7 +385,7 @@ router.post('/reset/:key', async (req, res) => {
|
|||||||
contact_email: '',
|
contact_email: '',
|
||||||
contact_phone: '',
|
contact_phone: '',
|
||||||
company_address: '',
|
company_address: '',
|
||||||
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
|
system_description: '云睿资产管理系统 - 专业的数据中心设备管理解决方案',
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValue = defaultValues[key];
|
const defaultValue = defaultValues[key];
|
||||||
@@ -682,7 +682,7 @@ router.get('/system/info', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
system: {
|
system: {
|
||||||
name: '机柜管理系统',
|
name: '云睿资产管理系统',
|
||||||
version: APP_VERSION,
|
version: APP_VERSION,
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
nodeVersion: process.version,
|
nodeVersion: process.version,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const Device = require('../models/Device');
|
|||||||
|
|
||||||
async function migrate() {
|
async function migrate() {
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log(' IDC管理系统 - 数据库迁移脚本 v2.0 ');
|
console.log(' 云睿资产管理系统 - 数据库迁移脚本 v2.0 ');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ for (let i = 0; i < args.length; i++) {
|
|||||||
|
|
||||||
async function runBackup() {
|
async function runBackup() {
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log(' IDC设备管理系统 - 数据备份工具');
|
console.log(' 云睿资产管理系统 - 数据备份工具');
|
||||||
console.log('========================================\n');
|
console.log('========================================\n');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const defaultSettings = [
|
|||||||
// 全局配置
|
// 全局配置
|
||||||
{
|
{
|
||||||
settingKey: 'site_name',
|
settingKey: 'site_name',
|
||||||
settingValue: JSON.stringify('机柜管理系统'),
|
settingValue: JSON.stringify('云睿资产管理系统'),
|
||||||
settingType: 'string',
|
settingType: 'string',
|
||||||
category: 'general',
|
category: 'general',
|
||||||
description: '网站名称',
|
description: '网站名称',
|
||||||
@@ -210,7 +210,7 @@ const defaultSettings = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
settingKey: 'system_description',
|
settingKey: 'system_description',
|
||||||
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
|
settingValue: JSON.stringify('云睿资产管理系统 - 专业的数据中心设备管理解决方案'),
|
||||||
settingType: 'string',
|
settingType: 'string',
|
||||||
category: 'about',
|
category: 'about',
|
||||||
description: '系统描述',
|
description: '系统描述',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* IDC管理系统 - 数据库迁移汇总脚本
|
* 云睿资产管理系统 - 数据库迁移汇总脚本
|
||||||
* 按顺序执行所有数据库迁移
|
* 按顺序执行所有数据库迁移
|
||||||
* 支持幂等执行(重复执行不会出错)
|
* 支持幂等执行(重复执行不会出错)
|
||||||
* 支持 SQLite 和 MySQL
|
* 支持 SQLite 和 MySQL
|
||||||
@@ -142,7 +142,7 @@ const migrations = [
|
|||||||
|
|
||||||
async function runMigrations() {
|
async function runMigrations() {
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log(' IDC管理系统 - 数据库迁移汇总脚本 ');
|
console.log(' 云睿资产管理系统 - 数据库迁移汇总脚本 ');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log(`数据库类型: ${DB_TYPE}`);
|
console.log(`数据库类型: ${DB_TYPE}`);
|
||||||
console.log(`迁移数量: ${migrations.length}`);
|
console.log(`迁移数量: ${migrations.length}`);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ if (!options.file) {
|
|||||||
|
|
||||||
async function runRestore() {
|
async function runRestore() {
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log(' IDC设备管理系统 - 数据恢复工具');
|
console.log(' 云睿资产管理系统 - 数据恢复工具');
|
||||||
console.log('========================================\n');
|
console.log('========================================\n');
|
||||||
|
|
||||||
const backupFile = path.resolve(options.file);
|
const backupFile = path.resolve(options.file);
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
/**
|
||||||
|
* 示例数据种子脚本
|
||||||
|
* 为 云睿资产管理系统写入演示数据(幂等,可重复执行)
|
||||||
|
*
|
||||||
|
* 本地:node scripts/seed-example-data.js
|
||||||
|
* Docker:docker exec yunrui_asset-backend node scripts/seed-example-data.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
|
||||||
|
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { sequelize } = require('../db');
|
||||||
|
const Room = require('../models/Room');
|
||||||
|
const Rack = require('../models/Rack');
|
||||||
|
const Device = require('../models/Device');
|
||||||
|
const Business = require('../models/Business');
|
||||||
|
const DeviceBusiness = require('../models/DeviceBusiness');
|
||||||
|
const Warehouse = require('../models/Warehouse');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const Role = require('../models/Role');
|
||||||
|
const UserRole = require('../models/UserRole');
|
||||||
|
const ConsumableCategory = require('../models/ConsumableCategory');
|
||||||
|
const Consumable = require('../models/Consumable');
|
||||||
|
const Ticket = require('../models/Ticket');
|
||||||
|
|
||||||
|
const SEED_MARKER_ROOM = '北京通州一号机房';
|
||||||
|
const DEMO_ADMIN = { username: 'admin', password: 'admin123', realName: '系统管理员', email: 'admin@example.com' };
|
||||||
|
|
||||||
|
const DEFAULT_ROLES = [
|
||||||
|
{
|
||||||
|
roleId: 'role_admin',
|
||||||
|
roleName: '管理员',
|
||||||
|
roleCode: 'admin',
|
||||||
|
description: '系统管理员,拥有所有权限',
|
||||||
|
permissions: ['*'],
|
||||||
|
status: 'active',
|
||||||
|
sort: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roleId: 'role_operator',
|
||||||
|
roleName: '运维人员',
|
||||||
|
roleCode: 'operator',
|
||||||
|
description: '负责日常运维操作',
|
||||||
|
permissions: ['devices:read', 'devices:write', 'racks:read', 'rooms:read', 'consumables:read', 'consumables:write'],
|
||||||
|
status: 'active',
|
||||||
|
sort: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roleId: 'role_viewer',
|
||||||
|
roleName: '只读用户',
|
||||||
|
roleCode: 'viewer',
|
||||||
|
description: '仅能查看数据',
|
||||||
|
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
|
||||||
|
status: 'active',
|
||||||
|
sort: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function upsertRole(roleData) {
|
||||||
|
const [role] = await Role.findOrCreate({ where: { roleCode: roleData.roleCode }, defaults: roleData });
|
||||||
|
if (role.roleId !== roleData.roleId) {
|
||||||
|
await role.update(roleData);
|
||||||
|
}
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureAdminUser() {
|
||||||
|
for (const roleData of DEFAULT_ROLES) {
|
||||||
|
await upsertRole(roleData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminRole = await Role.findOne({ where: { roleCode: 'admin' } });
|
||||||
|
const hashedPassword = await bcrypt.hash(DEMO_ADMIN.password, 10);
|
||||||
|
|
||||||
|
const [admin] = await User.findOrCreate({
|
||||||
|
where: { username: DEMO_ADMIN.username },
|
||||||
|
defaults: {
|
||||||
|
password: hashedPassword,
|
||||||
|
email: DEMO_ADMIN.email,
|
||||||
|
realName: DEMO_ADMIN.realName,
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingLink = await UserRole.findOne({ where: { UserId: admin.userId, RoleId: adminRole.roleId } });
|
||||||
|
if (!existingLink) {
|
||||||
|
await UserRole.create({ UserId: admin.userId, RoleId: adminRole.roleId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedData() {
|
||||||
|
try {
|
||||||
|
console.log('连接数据库...');
|
||||||
|
await sequelize.authenticate();
|
||||||
|
await sequelize.sync();
|
||||||
|
|
||||||
|
const marker = await Room.findOne({ where: { name: SEED_MARKER_ROOM } });
|
||||||
|
if (marker && process.env.SEED_FORCE !== '1') {
|
||||||
|
console.log(`示例数据已存在(机房: ${SEED_MARKER_ROOM}),跳过写入。`);
|
||||||
|
console.log('如需强制重新写入,请设置环境变量 SEED_FORCE=1');
|
||||||
|
const admin = await ensureAdminUser();
|
||||||
|
console.log(`演示账号: ${DEMO_ADMIN.username} / ${DEMO_ADMIN.password} (${admin.userId})`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始写入示例数据...\n');
|
||||||
|
|
||||||
|
const admin = await ensureAdminUser();
|
||||||
|
console.log(`✓ 角色与管理员账号 (${DEMO_ADMIN.username} / ${DEMO_ADMIN.password})`);
|
||||||
|
|
||||||
|
const [room1] = await Room.findOrCreate({
|
||||||
|
where: { name: SEED_MARKER_ROOM },
|
||||||
|
defaults: {
|
||||||
|
location: '北京市通州区马驹桥',
|
||||||
|
area: 500,
|
||||||
|
capacity: 100,
|
||||||
|
status: 'active',
|
||||||
|
description: '公司核心数据中心,具备双路供电及精密空调系统。',
|
||||||
|
gridRows: 8,
|
||||||
|
gridCols: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [room2] = await Room.findOrCreate({
|
||||||
|
where: { name: '上海浦东二号机房' },
|
||||||
|
defaults: {
|
||||||
|
location: '上海市浦东新区张江高科',
|
||||||
|
area: 320,
|
||||||
|
capacity: 60,
|
||||||
|
status: 'active',
|
||||||
|
description: '华东区域灾备机房,承载核心业务冗余部署。',
|
||||||
|
gridRows: 6,
|
||||||
|
gridCols: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✓ 机房: ${room1.name}, ${room2.name}`);
|
||||||
|
|
||||||
|
const rackDefs = [
|
||||||
|
{ rackId: 'RCK_SEED_A01', name: 'A-01', roomId: room1.roomId, height: 42, maxPower: 8000, rowPos: 1, colPos: 1 },
|
||||||
|
{ rackId: 'RCK_SEED_A02', name: 'A-02', roomId: room1.roomId, height: 42, maxPower: 8000, rowPos: 1, colPos: 2 },
|
||||||
|
{ rackId: 'RCK_SEED_B01', name: 'B-01', roomId: room1.roomId, height: 42, maxPower: 6000, rowPos: 2, colPos: 1 },
|
||||||
|
{ rackId: 'RCK_SEED_C01', name: 'C-01', roomId: room2.roomId, height: 42, maxPower: 8000, rowPos: 1, colPos: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const racks = {};
|
||||||
|
for (const def of rackDefs) {
|
||||||
|
const [rack] = await Rack.findOrCreate({ where: { rackId: def.rackId }, defaults: def });
|
||||||
|
racks[def.name] = rack;
|
||||||
|
}
|
||||||
|
console.log(`✓ 机柜: ${rackDefs.map(r => r.name).join(', ')}`);
|
||||||
|
|
||||||
|
const businessDefs = [
|
||||||
|
{ businessId: 'BIZ_SEED_001', name: '云端计算核心', description: '负责核心云端计算资源的分配与管理。' },
|
||||||
|
{ businessId: 'BIZ_SEED_002', name: '企业办公平台', description: 'OA、邮件、协作等企业内部应用。' },
|
||||||
|
{ businessId: 'BIZ_SEED_003', name: '数据分析服务', description: '大数据分析与报表平台。' },
|
||||||
|
];
|
||||||
|
const businesses = {};
|
||||||
|
for (const def of businessDefs) {
|
||||||
|
const [biz] = await Business.findOrCreate({ where: { businessId: def.businessId }, defaults: def });
|
||||||
|
businesses[def.businessId] = biz;
|
||||||
|
}
|
||||||
|
console.log(`✓ 业务: ${businessDefs.map(b => b.name).join(', ')}`);
|
||||||
|
|
||||||
|
const [warehouse] = await Warehouse.findOrCreate({
|
||||||
|
where: { name: '通州备件库房' },
|
||||||
|
defaults: {
|
||||||
|
location: '北京通州一号机房 B 区',
|
||||||
|
capacity: 200,
|
||||||
|
status: 'active',
|
||||||
|
description: '存放待上架及退役备件设备。',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✓ 库房: ${warehouse.name}`);
|
||||||
|
|
||||||
|
const deviceDefs = [
|
||||||
|
{
|
||||||
|
deviceId: 'DEV_SEED_001',
|
||||||
|
name: 'Web-01',
|
||||||
|
type: 'server',
|
||||||
|
model: 'Dell PowerEdge R750',
|
||||||
|
serialNumber: 'SN-DELL-R750-001',
|
||||||
|
rackId: racks['A-01'].rackId,
|
||||||
|
position: 1,
|
||||||
|
height: 2,
|
||||||
|
powerConsumption: 450,
|
||||||
|
status: 'online',
|
||||||
|
ipAddress: '10.10.1.101',
|
||||||
|
description: '核心 Web 应用服务器',
|
||||||
|
businessId: 'BIZ_SEED_001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: 'DEV_SEED_002',
|
||||||
|
name: 'DB-01',
|
||||||
|
type: 'server',
|
||||||
|
model: 'Dell PowerEdge R750',
|
||||||
|
serialNumber: 'SN-DELL-R750-002',
|
||||||
|
rackId: racks['A-01'].rackId,
|
||||||
|
position: 3,
|
||||||
|
height: 2,
|
||||||
|
powerConsumption: 520,
|
||||||
|
status: 'online',
|
||||||
|
ipAddress: '10.10.1.102',
|
||||||
|
description: 'MySQL 主库',
|
||||||
|
businessId: 'BIZ_SEED_001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: 'DEV_SEED_003',
|
||||||
|
name: 'SW-Core-01',
|
||||||
|
type: 'switch',
|
||||||
|
model: 'Huawei CE6857',
|
||||||
|
serialNumber: 'SN-HW-CE6857-001',
|
||||||
|
rackId: racks['A-01'].rackId,
|
||||||
|
position: 40,
|
||||||
|
height: 1,
|
||||||
|
powerConsumption: 180,
|
||||||
|
status: 'online',
|
||||||
|
ipAddress: '10.10.0.1',
|
||||||
|
description: '核心交换机',
|
||||||
|
businessId: 'BIZ_SEED_001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: 'DEV_SEED_004',
|
||||||
|
name: 'OA-App-01',
|
||||||
|
type: 'server',
|
||||||
|
model: 'Lenovo SR650',
|
||||||
|
serialNumber: 'SN-LENOVO-SR650-001',
|
||||||
|
rackId: racks['A-02'].rackId,
|
||||||
|
position: 5,
|
||||||
|
height: 2,
|
||||||
|
powerConsumption: 380,
|
||||||
|
status: 'online',
|
||||||
|
ipAddress: '10.20.1.50',
|
||||||
|
description: 'OA 应用服务器',
|
||||||
|
businessId: 'BIZ_SEED_002',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: 'DEV_SEED_005',
|
||||||
|
name: 'BI-Node-01',
|
||||||
|
type: 'server',
|
||||||
|
model: 'HPE ProLiant DL380',
|
||||||
|
serialNumber: 'SN-HPE-DL380-001',
|
||||||
|
rackId: racks['B-01'].rackId,
|
||||||
|
position: 10,
|
||||||
|
height: 2,
|
||||||
|
powerConsumption: 490,
|
||||||
|
status: 'maintenance',
|
||||||
|
ipAddress: '10.30.1.10',
|
||||||
|
description: '数据分析计算节点',
|
||||||
|
businessId: 'BIZ_SEED_003',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: 'DEV_SEED_006',
|
||||||
|
name: 'DR-Storage-01',
|
||||||
|
type: 'storage',
|
||||||
|
model: 'NetApp FAS2750',
|
||||||
|
serialNumber: 'SN-NETAPP-FAS2750-001',
|
||||||
|
rackId: racks['C-01'].rackId,
|
||||||
|
position: 20,
|
||||||
|
height: 4,
|
||||||
|
powerConsumption: 600,
|
||||||
|
status: 'online',
|
||||||
|
ipAddress: '10.40.1.20',
|
||||||
|
description: '灾备存储阵列',
|
||||||
|
businessId: 'BIZ_SEED_001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: 'DEV_SEED_007',
|
||||||
|
name: 'Spare-R750',
|
||||||
|
type: 'server',
|
||||||
|
model: 'Dell PowerEdge R750',
|
||||||
|
serialNumber: 'SN-DELL-R750-SPARE',
|
||||||
|
warehouseId: warehouse.warehouseId,
|
||||||
|
sourceType: 'warehouse',
|
||||||
|
height: 2,
|
||||||
|
powerConsumption: 0,
|
||||||
|
status: 'offline',
|
||||||
|
description: '待上架备用服务器',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const devices = {};
|
||||||
|
for (const def of deviceDefs) {
|
||||||
|
const { businessId, ...deviceData } = def;
|
||||||
|
const [device] = await Device.findOrCreate({ where: { deviceId: def.deviceId }, defaults: deviceData });
|
||||||
|
devices[def.deviceId] = device;
|
||||||
|
|
||||||
|
if (businessId) {
|
||||||
|
await DeviceBusiness.findOrCreate({
|
||||||
|
where: { deviceId: def.deviceId, businessId },
|
||||||
|
defaults: { isPrimary: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`✓ 设备: ${deviceDefs.length} 台(含 1 台库房备件)`);
|
||||||
|
|
||||||
|
const consumableCategories = [
|
||||||
|
{ name: '线缆', description: '网络与电源线缆', sortOrder: 1 },
|
||||||
|
{ name: '光模块', description: 'SFP/SFP+/QSFP 光模块', sortOrder: 2 },
|
||||||
|
{ name: '硬盘', description: '企业级硬盘与 SSD', sortOrder: 3 },
|
||||||
|
];
|
||||||
|
for (const cat of consumableCategories) {
|
||||||
|
await ConsumableCategory.findOrCreate({ where: { name: cat.name }, defaults: cat });
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumableDefs = [
|
||||||
|
{ consumableId: 'CON_SEED_001', name: '六类网线 3m', category: '线缆', unit: '根', currentStock: 120, minStock: 20, unitPrice: 15.0, location: '通州备件库房 A 架' },
|
||||||
|
{ consumableId: 'CON_SEED_002', name: 'SFP+ 10G 光模块', category: '光模块', unit: '个', currentStock: 32, minStock: 10, unitPrice: 280.0, location: '通州备件库房 B 架' },
|
||||||
|
{ consumableId: 'CON_SEED_003', name: '1.92TB SSD', category: '硬盘', unit: '块', currentStock: 8, minStock: 5, unitPrice: 1800.0, location: '通州备件库房 C 架' },
|
||||||
|
];
|
||||||
|
for (const def of consumableDefs) {
|
||||||
|
await Consumable.findOrCreate({ where: { consumableId: def.consumableId }, defaults: def });
|
||||||
|
}
|
||||||
|
console.log(`✓ 耗材: ${consumableDefs.length} 种`);
|
||||||
|
|
||||||
|
const ticketDefs = [
|
||||||
|
{
|
||||||
|
ticketId: 'TKT_SEED_001',
|
||||||
|
title: 'DB-01 磁盘 IO 延迟偏高',
|
||||||
|
deviceId: 'DEV_SEED_002',
|
||||||
|
deviceName: 'DB-01',
|
||||||
|
deviceModel: 'Dell PowerEdge R750',
|
||||||
|
serialNumber: 'SN-DELL-R750-002',
|
||||||
|
faultCategory: '性能问题',
|
||||||
|
priority: 'high',
|
||||||
|
status: 'in_progress',
|
||||||
|
description: '数据库主库磁盘 IO 等待时间持续超过 50ms,需排查存储性能。',
|
||||||
|
reporterId: admin.userId,
|
||||||
|
reporterName: admin.realName,
|
||||||
|
assigneeId: admin.userId,
|
||||||
|
assigneeName: admin.realName,
|
||||||
|
location: '北京通州一号机房 / A-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ticketId: 'TKT_SEED_002',
|
||||||
|
title: 'BI-Node-01 计划维护',
|
||||||
|
deviceId: 'DEV_SEED_005',
|
||||||
|
deviceName: 'BI-Node-01',
|
||||||
|
deviceModel: 'HPE ProLiant DL380',
|
||||||
|
serialNumber: 'SN-HPE-DL380-001',
|
||||||
|
faultCategory: '例行维护',
|
||||||
|
priority: 'low',
|
||||||
|
status: 'pending',
|
||||||
|
description: '月度固件升级与内存巡检。',
|
||||||
|
reporterId: admin.userId,
|
||||||
|
reporterName: admin.realName,
|
||||||
|
location: '北京通州一号机房 / B-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ticketId: 'TKT_SEED_003',
|
||||||
|
title: '核心交换机端口异常',
|
||||||
|
deviceId: 'DEV_SEED_003',
|
||||||
|
deviceName: 'SW-Core-01',
|
||||||
|
deviceModel: 'Huawei CE6857',
|
||||||
|
serialNumber: 'SN-HW-CE6857-001',
|
||||||
|
faultCategory: '网络故障',
|
||||||
|
priority: 'critical',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Gi0/0/24 端口频繁 flapping,已更换光模块。',
|
||||||
|
reporterId: admin.userId,
|
||||||
|
reporterName: admin.realName,
|
||||||
|
assigneeId: admin.userId,
|
||||||
|
assigneeName: admin.realName,
|
||||||
|
location: '北京通州一号机房 / A-01',
|
||||||
|
resolution: '更换 SFP+ 光模块后端口恢复正常。',
|
||||||
|
completionDate: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const def of ticketDefs) {
|
||||||
|
await Ticket.findOrCreate({ where: { ticketId: def.ticketId }, defaults: def });
|
||||||
|
}
|
||||||
|
console.log(`✓ 工单: ${ticketDefs.length} 条`);
|
||||||
|
|
||||||
|
console.log('\n示例数据写入完成!');
|
||||||
|
console.log('────────────────────────────────────');
|
||||||
|
console.log(`登录账号: ${DEMO_ADMIN.username}`);
|
||||||
|
console.log(`登录密码: ${DEMO_ADMIN.password}`);
|
||||||
|
console.log(`前端地址: http://localhost:${process.env.FRONTEND_PORT || 12000}`);
|
||||||
|
console.log('────────────────────────────────────');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('示例数据写入失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedData();
|
||||||
@@ -336,7 +336,7 @@ app.use(
|
|||||||
swaggerUi.serve,
|
swaggerUi.serve,
|
||||||
swaggerUi.setup(specs, {
|
swaggerUi.setup(specs, {
|
||||||
customCss: customCSS,
|
customCss: customCSS,
|
||||||
customSiteTitle: 'IDC设备管理系统 API文档',
|
customSiteTitle: '云睿资产管理系统 API文档',
|
||||||
swaggerOptions: {
|
swaggerOptions: {
|
||||||
persistAuthorization: true,
|
persistAuthorization: true,
|
||||||
displayRequestDuration: true,
|
displayRequestDuration: true,
|
||||||
@@ -358,7 +358,7 @@ app.get('/api-docs.json', (req, res) => {
|
|||||||
|
|
||||||
app.get('/api', (req, res) => {
|
app.get('/api', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
name: 'IDC设备管理系统 API',
|
name: '云睿资产管理系统 API',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
description: '数据中心设备管理平台后端服务',
|
description: '数据中心设备管理平台后端服务',
|
||||||
endpoints: {
|
endpoints: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const swaggerJsdoc = require('swagger-jsdoc');
|
|||||||
|
|
||||||
const customCSS = `
|
const customCSS = `
|
||||||
/* ========================================
|
/* ========================================
|
||||||
IDC设备管理系统 - Swagger UI 翠竹绿风格定制
|
云睿资产管理系统 - Swagger UI 翠竹绿风格定制
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|
||||||
/* 隐藏默认顶部栏 */
|
/* 隐藏默认顶部栏 */
|
||||||
@@ -511,7 +511,7 @@ const options = {
|
|||||||
definition: {
|
definition: {
|
||||||
openapi: '3.0.0',
|
openapi: '3.0.0',
|
||||||
info: {
|
info: {
|
||||||
title: 'IDC设备管理系统 API',
|
title: '云睿资产管理系统 API',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
description: '数据中心设备管理平台后端服务 API 文档',
|
description: '数据中心设备管理平台后端服务 API 文档',
|
||||||
contact: {
|
contact: {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
info:
|
info:
|
||||||
title: IDC设备管理系统 API
|
title: 云睿资产管理系统 API
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
IDC设备管理系统后端API文档
|
云睿资产管理系统后端API文档
|
||||||
|
|
||||||
## 认证说明
|
## 认证说明
|
||||||
除健康检查接口外,所有接口都需要在请求头中携带JWT Token:
|
除健康检查接口外,所有接口都需要在请求头中携带JWT Token:
|
||||||
@@ -107,7 +107,7 @@ paths:
|
|||||||
example: ok
|
example: ok
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
example: IDC设备管理系统后端服务正常运行
|
example: 云睿资产管理系统后端服务正常运行
|
||||||
|
|
||||||
/api-docs:
|
/api-docs:
|
||||||
get:
|
get:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>IDC设备管理系统 API文档</title>
|
<title>云睿资产管理系统 API文档</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui.css">
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui.css">
|
||||||
<style>
|
<style>
|
||||||
.swagger-ui .topbar { display: none }
|
.swagger-ui .topbar { display: none }
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const performHealthCheck = async () => {
|
|||||||
status: overallStatus,
|
status: overallStatus,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
service: {
|
service: {
|
||||||
name: 'IDC设备管理系统',
|
name: '云睿资产管理系统',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
},
|
},
|
||||||
checks: allChecks,
|
checks: allChecks,
|
||||||
|
|||||||
@@ -168,10 +168,10 @@ const logger = createLogger({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production' || process.env.CONSOLE_LOG === 'true') {
|
||||||
logger.add(new transports.Console({
|
logger.add(new transports.Console({
|
||||||
format: consoleFormat,
|
format: process.env.NODE_ENV === 'production' ? jsonFormat : consoleFormat,
|
||||||
level: 'debug',
|
level: LOG_LEVEL,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ async function uploadViaWebDAV(config, localFilePath, remotePath) {
|
|||||||
password: config.password,
|
password: config.password,
|
||||||
authType: config.authType || 'password',
|
authType: config.authType || 'password',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'IDC-Backup-Client/1.0',
|
'User-Agent': 'YunRui-Backup-Client/1.0',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ async function testRemoteConnection(config) {
|
|||||||
console.log(`测试远端连接 [${config.protocol}]...`);
|
console.log(`测试远端连接 [${config.protocol}]...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const testContent = `IDC Backup Connection Test - ${new Date().toISOString()}`;
|
const testContent = `YunRui Backup Connection Test - ${new Date().toISOString()}`;
|
||||||
const testFile = path.join(require('os').tmpdir(), `backup-test-${Date.now()}.txt`);
|
const testFile = path.join(require('os').tmpdir(), `backup-test-${Date.now()}.txt`);
|
||||||
fs.writeFileSync(testFile, testContent);
|
fs.writeFileSync(testFile, testContent);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# IDC 设备管理系统 - 开发环境配置
|
# 云睿资产管理系统 - 开发环境配置
|
||||||
# 使用 build 指令本地构建镜像
|
# 使用 build 指令本地构建镜像
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: idc-backend-dev
|
container_name: yunrui_asset-backend-dev
|
||||||
volumes:
|
volumes:
|
||||||
# 配置映射(核心)
|
# 配置映射(核心)
|
||||||
- ./backend/.env:/app/.env
|
- ./backend/.env:/app/.env
|
||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
# 连接 docker-compose 内的 MySQL 时,注释掉下方 MYSQL_HOST 的注释
|
# 连接 docker-compose 内的 MySQL 时,注释掉下方 MYSQL_HOST 的注释
|
||||||
# 连接远程 MySQL 时,保持 .env 中的配置即可
|
# 连接远程 MySQL 时,保持 .env 中的配置即可
|
||||||
networks:
|
networks:
|
||||||
- idc-network
|
- yunrui_asset-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"]
|
||||||
@@ -41,11 +41,11 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: idc-frontend-dev
|
container_name: yunrui_asset-frontend-dev
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
networks:
|
networks:
|
||||||
- idc-network
|
- yunrui_asset-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -54,18 +54,18 @@ services:
|
|||||||
# 如需使用本地 MySQL 容器,取消下方注释
|
# 如需使用本地 MySQL 容器,取消下方注释
|
||||||
# mysql:
|
# mysql:
|
||||||
# image: mysql:8.0
|
# image: mysql:8.0
|
||||||
# container_name: idc-mysql-dev
|
# container_name: yunrui_asset-mysql-dev
|
||||||
# volumes:
|
# volumes:
|
||||||
# - mysql_data:/var/lib/mysql
|
# - mysql_data:/var/lib/mysql
|
||||||
# environment:
|
# environment:
|
||||||
# MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123}
|
# MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123}
|
||||||
# MYSQL_DATABASE: idc_management
|
# MYSQL_DATABASE: yunrui_management
|
||||||
# MYSQL_USER: idc_user
|
# MYSQL_USER: yunrui_user
|
||||||
# MYSQL_PASSWORD: ${MYSQL_PASSWORD:-idc_pass}
|
# MYSQL_PASSWORD: ${MYSQL_PASSWORD:-idc_pass}
|
||||||
# ports:
|
# ports:
|
||||||
# - "3306:3306"
|
# - "3306:3306"
|
||||||
# networks:
|
# networks:
|
||||||
# - idc-network
|
# - yunrui_asset-network
|
||||||
# restart: unless-stopped
|
# restart: unless-stopped
|
||||||
# healthcheck:
|
# healthcheck:
|
||||||
# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
@@ -74,7 +74,7 @@ services:
|
|||||||
# retries: 5
|
# retries: 5
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
idc-network:
|
yunrui_asset-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
# volumes:
|
# volumes:
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ---------- MySQL 数据库 ----------
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: yunrui_asset-mysql
|
||||||
|
volumes:
|
||||||
|
- yunrui_asset-mysql-data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-xinmi_password}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-yunrui_management}
|
||||||
|
ports:
|
||||||
|
- "${MYSQL_PORT:-12006}:3306"
|
||||||
|
networks:
|
||||||
|
- yunrui_asset-network
|
||||||
|
|
||||||
|
# ---------- 后端服务 ----------
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: yunrui_asset-backend
|
||||||
|
volumes:
|
||||||
|
- ./backend/uploads:/app/uploads
|
||||||
|
- ./backend/logs:/app/logs
|
||||||
|
- ./backend/backups:/app/backups
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=8000
|
||||||
|
- DB_TYPE=mysql
|
||||||
|
- MYSQL_HOST=mysql
|
||||||
|
- MYSQL_PORT=3306
|
||||||
|
- MYSQL_DATABASE=${MYSQL_DATABASE:-yunrui_management}
|
||||||
|
- MYSQL_USERNAME=root
|
||||||
|
- MYSQL_PASSWORD=${MYSQL_ROOT_PASSWORD:-xinmi_password}
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- CONSOLE_LOG=true
|
||||||
|
ports:
|
||||||
|
- "${BACKEND_PORT:-12001}:8000"
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
networks:
|
||||||
|
- yunrui_asset-network
|
||||||
|
|
||||||
|
# ---------- 前端服务 ----------
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: yunrui_asset-frontend
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT:-12000}:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- yunrui_asset-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
yunrui_asset-mysql-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
yunrui_asset-network:
|
||||||
|
driver: bridge
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# IDC 设备管理系统 - 生产环境配置
|
# 云睿资产管理系统 - 生产环境配置
|
||||||
# 从镜像仓库拉取预构建镜像,无 build 指令
|
# 从镜像仓库拉取预构建镜像,无 build 指令
|
||||||
|
# 默认从 ghcr.io 拉取,可通过 BACKEND/FRONTEND_IMAGE 环境变量切换
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
@@ -8,10 +9,9 @@ version: '3.8'
|
|||||||
services:
|
services:
|
||||||
# ---------- 后端服务 ----------
|
# ---------- 后端服务 ----------
|
||||||
backend:
|
backend:
|
||||||
# 从阿里云容器镜像服务(个人版)拉取
|
# 从 ghcr.io 拉取公开镜像,可通过 BACKEND_IMAGE 环境变量覆盖
|
||||||
# 实际镜像地址: crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-backend:latest
|
image: ${BACKEND_IMAGE:-ghcr.io/xinmi/yunrui_asset-backend:latest}
|
||||||
image: ${BACKEND_IMAGE:-crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-backend:latest}
|
container_name: yunrui_asset-backend
|
||||||
container_name: idc-backend
|
|
||||||
volumes:
|
volumes:
|
||||||
# 配置映射(核心)
|
# 配置映射(核心)
|
||||||
- ./backend/.env:/app/.env
|
- ./backend/.env:/app/.env
|
||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
# 生产环境使用 Docker 内 MySQL 时,MYSQL_HOST 需设为 mysql
|
# 生产环境使用 Docker 内 MySQL 时,MYSQL_HOST 需设为 mysql
|
||||||
# 使用远程 MySQL 时,保持 .env 中的配置
|
# 使用远程 MySQL 时,保持 .env 中的配置
|
||||||
networks:
|
networks:
|
||||||
- idc-network
|
- yunrui_asset-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"]
|
||||||
@@ -38,13 +38,14 @@ services:
|
|||||||
|
|
||||||
# ---------- 前端(Nginx)服务 ----------
|
# ---------- 前端(Nginx)服务 ----------
|
||||||
frontend:
|
frontend:
|
||||||
image: ${FRONTEND_IMAGE:-crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-frontend:latest}
|
# 从 ghcr.io 拉取公开镜像,可通过 FRONTEND_IMAGE 环境变量覆盖
|
||||||
|
image: ${FRONTEND_IMAGE:-ghcr.io/xinmi/yunrui_asset-frontend:latest}
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: idc-frontend
|
container_name: yunrui_asset-frontend
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
networks:
|
networks:
|
||||||
- idc-network
|
- yunrui_asset-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -53,16 +54,16 @@ services:
|
|||||||
# 如需使用本地 MySQL 容器,取消下方注释
|
# 如需使用本地 MySQL 容器,取消下方注释
|
||||||
# mysql:
|
# mysql:
|
||||||
# image: mysql:8.0
|
# image: mysql:8.0
|
||||||
# container_name: idc-mysql
|
# container_name: yunrui_asset-mysql
|
||||||
# volumes:
|
# volumes:
|
||||||
# - mysql_data:/var/lib/mysql
|
# - mysql_data:/var/lib/mysql
|
||||||
# environment:
|
# environment:
|
||||||
# MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
# MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
# MYSQL_DATABASE: ${MYSQL_DATABASE:-idc_management}
|
# MYSQL_DATABASE: ${MYSQL_DATABASE:-yunrui_management}
|
||||||
# MYSQL_USER: ${MYSQL_USER:-idc_user}
|
# MYSQL_USER: ${MYSQL_USER:-yunrui_user}
|
||||||
# MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
# MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||||
# networks:
|
# networks:
|
||||||
# - idc-network
|
# - yunrui_asset-network
|
||||||
# restart: unless-stopped
|
# restart: unless-stopped
|
||||||
# healthcheck:
|
# healthcheck:
|
||||||
# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
@@ -71,7 +72,7 @@ services:
|
|||||||
# retries: 5
|
# retries: 5
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
idc-network:
|
yunrui_asset-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
# volumes:
|
# volumes:
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# MySQL 数据库 (可选,默认为 SQLite)
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: yunrui_assetet-mysql
|
||||||
|
volumes:
|
||||||
|
- yunrui_assetet-mysql-data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-xinmi_password}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-yunrui_management}
|
||||||
|
ports:
|
||||||
|
- "${MYSQL_PORT:-12006}:3306"
|
||||||
|
networks:
|
||||||
|
- yunrui_assetet-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
yunrui_assetet-mysql-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
yunrui_assetet-network:
|
||||||
|
driver: bridge
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# IDC设备资产系统 - 用户使用指南
|
# 云睿资产系统 - 用户使用指南
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
### 系统简介
|
### 系统简介
|
||||||
|
|
||||||
IDC设备资产系统是一款现代化的数据中心(IDC)设备管理平台,提供机房、机柜、设备的全生命周期管理,具备3D可视化展示功能。系统采用前后端分离架构,前端基于React + Ant Design构建,后端使用Node.js + Express开发。
|
云睿资产系统是一款现代化的数据中心(IDC)设备管理平台,提供机房、机柜、设备的全生命周期管理,具备3D可视化展示功能。系统采用前后端分离架构,前端基于React + Ant Design构建,后端使用Node.js + Express开发。
|
||||||
|
|
||||||
### 核心功能
|
### 核心功能
|
||||||
|
|
||||||
@@ -929,9 +929,8 @@ A: 已关闭的工单无法重新打开,如有新问题请新建工单。
|
|||||||
- **功能建议**:提交 Issue 并标注 `feature-request`
|
- **功能建议**:提交 Issue 并标注 `feature-request`
|
||||||
|
|
||||||
**代码仓库:**
|
**代码仓库:**
|
||||||
- Gitee: https://gitee.com/zhang96110/idc_assest
|
- 源码仓库: https://code.xinmi.cloud/yunruyunrui_asset
|
||||||
- GitHub: https://github.com/gituib/idc_assest
|
yunrui_asset
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**文档版本**:v2.0.0
|
**文档版本**:v2.0.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# API接口文档
|
# API接口文档
|
||||||
|
|
||||||
本文档描述IDC设备管理系统的后端API接口,遵循RESTful设计规范。
|
本文档描述云睿资产管理系统的后端API接口,遵循RESTful设计规范。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1861,10 +1861,10 @@ GET /api/system-settings
|
|||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {
|
"data": {
|
||||||
"siteName": "IDC设备管理系统",
|
"siteName": "云睿资产管理系统",
|
||||||
"siteLogo": "/uploads/logo.png",
|
"siteLogo": "/uploads/logo.png",
|
||||||
"siteDescription": "专业的数据中心设备管理平台",
|
"siteDescription": "专业的数据中心设备管理平台",
|
||||||
"copyright": "© 2024 IDC Management",
|
"copyright": "© 2024 YunRui Asset Management",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"message": "操作成功"
|
"message": "操作成功"
|
||||||
@@ -2150,7 +2150,7 @@ GET /health
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"message": "IDC设备管理系统后端服务正常运行"
|
"message": "云睿资产管理系统后端服务正常运行"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# IDC 设备管理系统 - Docker 部署教程
|
# 云睿资产管理系统 - Docker 部署教程
|
||||||
|
|
||||||
> 适用场景:新服务器上首次部署
|
> 适用场景:新服务器上首次部署
|
||||||
>
|
>
|
||||||
> 架构:3 个容器(Nginx + 前端静态文件 | Backend | MySQL)
|
> 架构:3 个容器(Nginx + 前端静态文件 | Backend | MySQL)
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
- [一、前置要求](#一前置要求)
|
- [一、前置要求](#一前置要求)
|
||||||
- [二、服务器准备](#二服务器准备)
|
- [二、服务器准备](#二服务器准备)
|
||||||
- [三、拉取项目与配置](#三拉取项目与配置)
|
- [三、创建部署目录与配置](#三创建部署目录与配置)
|
||||||
- [四、生产环境配置](#四生产环境配置)
|
- [四、生产环境配置](#四生产环境配置)
|
||||||
- [五、启动容器](#五启动容器)
|
- [五、启动容器](#五启动容器)
|
||||||
- [六、验证部署](#六验证部署)
|
- [六、验证部署](#六验证部署)
|
||||||
@@ -18,21 +18,24 @@
|
|||||||
- [八、版本升级](#八版本升级)
|
- [八、版本升级](#八版本升级)
|
||||||
- [九、常见问题](#九常见问题)
|
- [九、常见问题](#九常见问题)
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 一、前置要求
|
## 一、前置要求
|
||||||
|
|
||||||
### 1.1 所需条件
|
### 1.1 所需条件
|
||||||
|
|
||||||
| 条件 | 说明 |
|
| 条件 | 说明 |
|
||||||
|------|------|
|
| ------------------ | --------------------------------------------------- |
|
||||||
| **服务器** | Linux 系统(推荐 Ubuntu 20.04+ / Debian 11+ / CentOS 7+) |
|
| **服务器** | Linux 系统(推荐 Ubuntu 20.04+ / Debian 11+ / CentOS 7+) |
|
||||||
| **Docker** | 20.10 及以上版本 |
|
| **Docker** | 20.10 及以上版本 |
|
||||||
| **Docker Compose** | v2 及以上 |
|
| **Docker Compose** | v2 及以上 |
|
||||||
| **Git** | 用于拉取项目代码 |
|
| **网络** | 服务器能访问镜像仓库 |
|
||||||
| **网络** | 服务器能访问阿里云容器镜像服务 |
|
|
||||||
|
|
||||||
### 1.2 架构说明
|
### 1.2 镜像仓库说明
|
||||||
|
|
||||||
|
本项目使用 GitHub Container Registry 托管镜像,所有镜像均为公开(public)镜像,无需登录即可拉取。
|
||||||
|
|
||||||
|
### 1.3 架构说明
|
||||||
|
|
||||||
用户访问 `http://服务器IP` 时,请求流程如下:
|
用户访问 `http://服务器IP` 时,请求流程如下:
|
||||||
|
|
||||||
@@ -42,7 +45,7 @@
|
|||||||
└── /api 路径 → 反向代理 → Backend 容器 :8000 → MySQL 容器 :3306(或远程 MySQL)
|
└── /api 路径 → 反向代理 → Backend 容器 :8000 → MySQL 容器 :3306(或远程 MySQL)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 二、服务器准备
|
## 二、服务器准备
|
||||||
|
|
||||||
@@ -66,40 +69,144 @@ docker --version
|
|||||||
docker compose version
|
docker compose version
|
||||||
```
|
```
|
||||||
|
|
||||||
部署目录会在第三节"克隆项目代码"步骤中直接创建到 `/opt/idc_assest`,无需提前创建。
|
***
|
||||||
|
|
||||||
---
|
## 三、创建部署目录与配置
|
||||||
|
|
||||||
## 三、拉取项目与配置
|
> 本部署方式**不需要 git clone** 整个项目仓库。只需在服务器上手动创建需要的配置文件和持久化目录即可。
|
||||||
|
>
|
||||||
|
> 需要创建的内容只有 3 个部分:
|
||||||
|
>
|
||||||
|
> 1. **配置文件**:`docker-compose.yml` + 项目根目录 `.env` + `backend/.env`
|
||||||
|
> 2. **持久化目录**:`backend/uploads/`、`backend/logs/`、`backend/backups/`、`backend/temp/`
|
||||||
|
|
||||||
### 3.1 克隆项目代码
|
### 3.1 创建部署目录结构
|
||||||
|
|
||||||
> 注意:克隆代码是为了获取 `docker-compose.prod.yml` 和 `.env` 配置模板,**不是用来构建镜像的**(镜像已由 CI/CD 自动构建好,部署服务器无需 Node.js 源码)。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 直接克隆到部署目录
|
# 创建部署根目录和所有持久化目录
|
||||||
git clone https://github.com/gituib/idc_assest.git /opt/idc_assest
|
mkdir -p /opt/yunrui_assetet/backend/{uploads,logs,backups,temp}
|
||||||
cd /opt/idc_assest
|
cd /opt/yunrui_assetet
|
||||||
|
|
||||||
|
# 查看目录结构
|
||||||
|
tree /opt/yunrui_assetet -L 2
|
||||||
```
|
```
|
||||||
|
|
||||||
克隆完成后,整个项目目录就是部署目录。多余的源代码(`backend/`、`frontend/` 源码)不影响运行,运行时只会用到 `docker-compose.prod.yml`、`.env` 和持久化目录。
|
预期输出:
|
||||||
|
|
||||||
### 3.2 持久化目录
|
```
|
||||||
|
/opt/yunrui_assetet
|
||||||
|
└── backend
|
||||||
|
├── uploads/
|
||||||
|
├── logs/
|
||||||
|
├── backups/
|
||||||
|
└── temp/
|
||||||
|
```
|
||||||
|
|
||||||
通过 `git clone` 完整克隆项目后,`backend/uploads/`、`backend/temp/`、`backend/logs/`、`backend/backups/` 四个持久化目录已经在仓库中预留(包含 `.gitkeep` 占位文件),**无需手动创建**。
|
### 3.2 创建 docker-compose.yml
|
||||||
|
|
||||||
### 3.3 配置 .env 文件
|
```bash
|
||||||
|
cat > /opt/yunrui_assetet/docker-compose.yml << 'YAMLEOF'
|
||||||
|
# ============================================
|
||||||
|
# 云睿资产管理系统 - 生产环境配置
|
||||||
|
# 从镜像仓库拉取预构建镜像,无 build 指令
|
||||||
|
# 默认从 ghcr.io 拉取,可通过 BACKEND/FRONTEND_IMAGE 环境变量切换
|
||||||
|
# ============================================
|
||||||
|
|
||||||
> **重要:项目中有两个 `.env` 文件,作用完全不同,不要混淆。**
|
services:
|
||||||
|
# ---------- 后端服务 ----------
|
||||||
|
backend:
|
||||||
|
image: ${BACKEND_IMAGE:-ghcr.io/xinmi/yunrui_asset-backend:latest}
|
||||||
|
container_name: yunrui_asset-backend
|
||||||
|
volumes:
|
||||||
|
- ./backend/.env:/app/.env
|
||||||
|
- ./backend/uploads:/app/uploads
|
||||||
|
- ./backend/logs:/app/logs
|
||||||
|
- ./backend/backups:/app/backups
|
||||||
|
- ./backend/temp:/app/temp
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
networks:
|
||||||
|
- yunrui_asset-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
| 文件位置 | 谁读取 | 作用 |
|
# ---------- 前端(Nginx)服务 ----------
|
||||||
|----------|--------|------|
|
frontend:
|
||||||
| `/opt/idc_assest/.env` | Docker Compose | 配置 MySQL 容器自身(密码、用户名等) |
|
image: ${FRONTEND_IMAGE:-ghcr.io/xinmi/yunrui_asset-frontend:latest}
|
||||||
| `/opt/idc_assest/backend/.env` | 后端 Node.js 应用 | 配置后端业务参数(数据库连接、JWT 等) |
|
pull_policy: always
|
||||||
|
container_name: yunrui_asset-frontend
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
networks:
|
||||||
|
- yunrui_asset-network
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
**`backend/.env`** 完整配置内容:
|
# ---------- MySQL 数据库 ----------
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: yunrui_asset-mysql
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-yunrui_management}
|
||||||
|
MYSQL_USER: ${MYSQL_USER:-yunrui_user}
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||||
|
networks:
|
||||||
|
- yunrui_asset-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
```ini
|
networks:
|
||||||
|
yunrui_asset-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
YAMLEOF
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:如果使用远程 MySQL(已有数据库),请在创建后编辑此文件,注释掉 `mysql` 服务部分。
|
||||||
|
|
||||||
|
### 3.3 创建项目根目录 .env 文件
|
||||||
|
|
||||||
|
> 这个文件是给 Docker Compose 读取的,用于配置 MySQL 容器本身的账号密码。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/yunrui_assetet
|
||||||
|
cat > .env << 'EOF'
|
||||||
|
MYSQL_ROOT_PASSWORD=这里填MySQL root密码
|
||||||
|
MYSQL_USER=yunrui_user
|
||||||
|
MYSQL_PASSWORD=这里填MySQL用户密码(与 backend/.env 中 MYSQL_PASSWORD 一致)
|
||||||
|
MYSQL_DATABASE=yunrui_management
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 创建 backend/.env 文件
|
||||||
|
|
||||||
|
> **重要**:这个文件是后端 Node.js 应用读取的,配置数据库连接、JWT 等业务参数。不要与项目根目录的 `.env` 混淆。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/yunrui_assetet/backend
|
||||||
|
|
||||||
|
# 先生成 JWT 密钥(确保已执行 4.1 节的命令)
|
||||||
|
# JWT_SECRET=$(openssl rand -base64 64)
|
||||||
|
|
||||||
|
# 创建 backend/.env(注意:使用 << EOF 而非 << 'EOF',使变量可以被展开)
|
||||||
|
cat > backend/.env << EOF
|
||||||
# ==============================================
|
# ==============================================
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
# ==============================================
|
# ==============================================
|
||||||
@@ -113,9 +220,9 @@ NODE_ENV=production
|
|||||||
DB_TYPE=mysql
|
DB_TYPE=mysql
|
||||||
MYSQL_HOST=mysql # Docker 服务名,不是 localhost
|
MYSQL_HOST=mysql # Docker 服务名,不是 localhost
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_USERNAME=idc_user
|
MYSQL_USERNAME=yunrui_user
|
||||||
MYSQL_PASSWORD=这里改成MySQL容器密码
|
MYSQL_PASSWORD=这里改成MySQL用户密码
|
||||||
MYSQL_DATABASE=idc_management
|
MYSQL_DATABASE=yunrui_management
|
||||||
|
|
||||||
# 方式二:使用远程 MySQL(已有数据库的情况)
|
# 方式二:使用远程 MySQL(已有数据库的情况)
|
||||||
# DB_TYPE=mysql
|
# DB_TYPE=mysql
|
||||||
@@ -126,11 +233,9 @@ MYSQL_DATABASE=idc_management
|
|||||||
# MYSQL_DATABASE=数据库名
|
# MYSQL_DATABASE=数据库名
|
||||||
|
|
||||||
# ==============================================
|
# ==============================================
|
||||||
# JWT 密钥(重要!必须手动设置)
|
# JWT 密钥(重要!通过变量自动注入,无需手动填写)
|
||||||
# 生产环境不能留空,生成命令:
|
|
||||||
# node -e "console.log(require('crypto').randomBytes(64).toString('base64'))"
|
|
||||||
# ==============================================
|
# ==============================================
|
||||||
JWT_SECRET=这里填入生成的密钥,至少32位
|
JWT_SECRET=${JWT_SECRET}
|
||||||
|
|
||||||
# ==============================================
|
# ==============================================
|
||||||
# 其他配置(保持默认即可)
|
# 其他配置(保持默认即可)
|
||||||
@@ -154,120 +259,86 @@ LOG_LEVEL=info
|
|||||||
LOG_DIR=./logs
|
LOG_DIR=./logs
|
||||||
LOG_MAX_FILE_SIZE=20m
|
LOG_MAX_FILE_SIZE=20m
|
||||||
LOG_MAX_FILES=30d
|
LOG_MAX_FILES=30d
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、生产环境配置
|
|
||||||
|
|
||||||
部署方式分两种,选择其中一种:
|
|
||||||
|
|
||||||
### 方式A:使用 Docker 本地 MySQL(推荐新部署)
|
|
||||||
|
|
||||||
> MySQL 作为容器运行在 Docker 内部,数据卷持久化到主机。
|
|
||||||
|
|
||||||
**步骤 1:编辑 `backend/.env`,确认 MySQL 配置如下:**
|
|
||||||
|
|
||||||
```ini
|
|
||||||
DB_TYPE=mysql
|
|
||||||
MYSQL_HOST=mysql # Docker 服务名
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_USERNAME=idc_user # 必须跟 MYSQL_USER 一致
|
|
||||||
MYSQL_PASSWORD=你的密码
|
|
||||||
MYSQL_DATABASE=idc_management
|
|
||||||
```
|
|
||||||
|
|
||||||
**步骤 2:取消 `docker-compose.prod.yml` 中 MySQL 部分的注释:**
|
|
||||||
|
|
||||||
完整的 `docker-compose.prod.yml`(MySQL 部分已启用):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# ---------- 后端服务 ----------
|
|
||||||
backend:
|
|
||||||
image: ${BACKEND_IMAGE:-crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-backend:latest}
|
|
||||||
container_name: idc-backend
|
|
||||||
volumes:
|
|
||||||
- ./backend/.env:/app/.env
|
|
||||||
- ./backend/uploads:/app/uploads
|
|
||||||
- ./backend/logs:/app/logs
|
|
||||||
- ./backend/backups:/app/backups
|
|
||||||
- ./backend/temp:/app/temp
|
|
||||||
env_file:
|
|
||||||
- ./backend/.env
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
networks:
|
|
||||||
- idc-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
|
|
||||||
# ---------- 前端(Nginx)服务 ----------
|
|
||||||
frontend:
|
|
||||||
image: ${FRONTEND_IMAGE:-crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-frontend:latest}
|
|
||||||
pull_policy: always
|
|
||||||
container_name: idc-frontend
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
networks:
|
|
||||||
- idc-network
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# ---------- MySQL 数据库 ----------
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.0
|
|
||||||
container_name: idc-mysql
|
|
||||||
volumes:
|
|
||||||
- mysql_data:/var/lib/mysql
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-idc_management}
|
|
||||||
MYSQL_USER: ${MYSQL_USER:-idc_user}
|
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
|
||||||
networks:
|
|
||||||
- idc-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
networks:
|
|
||||||
idc-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mysql_data:
|
|
||||||
```
|
|
||||||
|
|
||||||
**步骤 3:创建项目根目录的 `.env`**(供 Docker Compose 读取,配置 MySQL 容器自身):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat > /opt/idc_assest/.env << 'EOF'
|
|
||||||
MYSQL_ROOT_PASSWORD=root密码
|
|
||||||
MYSQL_USER=idc_user
|
|
||||||
MYSQL_PASSWORD=用户密码(与 backend/.env 中 MYSQL_PASSWORD 一致)
|
|
||||||
MYSQL_DATABASE=idc_management
|
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
> 注意:这个文件**不是**给后端读的,是给 Docker Compose 解析 `${MYSQL_ROOT_PASSWORD}` 等占位符用的。
|
### 3.5 验证文件结构
|
||||||
|
|
||||||
### 方式B:使用远程 MySQL(已有数据库)
|
创建完成后,部署目录的文件结构应为:
|
||||||
|
|
||||||
> 保持现有的远程 MySQL 实例,不需要本地 MySQL 容器。
|
```
|
||||||
|
/opt/yunrui_assetet/
|
||||||
|
├── docker-compose.yml # Docker Compose 配置
|
||||||
|
├── .env # Docker Compose 环境变量(MySQL 账号密码)
|
||||||
|
└── backend/
|
||||||
|
├── .env # 后端应用配置(数据库连接、JWT 等)
|
||||||
|
├── uploads/ # 上传文件持久化目录
|
||||||
|
├── logs/ # 运行日志持久化目录
|
||||||
|
├── backups/ # 数据库备份持久化目录
|
||||||
|
└── temp/ # 临时文件目录
|
||||||
|
```
|
||||||
|
|
||||||
`docker-compose.prod.yml` 保持原样(MySQL 部分保持注释),`.env` 中配置远程数据库地址即可:
|
***
|
||||||
|
|
||||||
|
## 四、生产环境配置
|
||||||
|
|
||||||
|
### 4.1 生成并配置 JWT\_SECRET
|
||||||
|
|
||||||
|
直接在创建 `backend/.env` 时用变量注入,避免手动复制粘贴:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成 JWT 密钥并保存到变量
|
||||||
|
JWT_SECRET=$(openssl rand -base64 64)
|
||||||
|
|
||||||
|
# 后续创建 backend/.env 时引用该变量即可
|
||||||
|
# 见下方 3.4 节示例中的 JWT_SECRET=${JWT_SECRET}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 如果服务器没有 `openssl`,也可以用 `node -e "console.log(require('crypto').randomBytes(64).toString('base64'))"` 生成。
|
||||||
|
|
||||||
|
### 4.2 确认账号密码一致性
|
||||||
|
|
||||||
|
> MySQL 容器的账号密码**必须**在两个 `.env` 文件中保持一致:
|
||||||
|
|
||||||
|
| 配置项 | 文件位置 | 说明 |
|
||||||
|
| ------------------------- | ------------------------------ | ----------------- |
|
||||||
|
| `MYSQL_USER=yunrui_user` | `/oyunrui_assetasset/.env` | 指定 MySQL 容器创建的用户名 |
|
||||||
|
| `MYSQL_USERNAME=yunrui_user` | `/oyunrui_assetasset/backend/.env` | 后端连接数据库用的用户名 |
|
||||||
|
| `MYSQL_PASSWORD=xxx` | 两个文件各有一处 | 密码必须相同 |
|
||||||
|
|
||||||
|
可以用以下命令快速对比两个文件中的用户和密码是否一致:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/yunrui_assetet
|
||||||
|
echo "=== 根目录 .env ===" && grep -E "MYSQL_USER|MYSQL_PASSWORD" .env
|
||||||
|
echo ""
|
||||||
|
echo "=== backend/.env ===" && grep -E "MYSQL_USERNAME|MYSQL_PASSWORD" backend/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
预期输出(用户名为 `yunrui_user`,密码两处相同即表示一致):
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 根目录 .env ===
|
||||||
|
MYSQL_USER=yunrui_user
|
||||||
|
MYSQL_PASSWORD=MyPassw0rd!
|
||||||
|
|
||||||
|
=== backend/.env ===
|
||||||
|
MYSQL_USERNAME=yunrui_user
|
||||||
|
MYSQL_PASSWORD=MyPassw0rd!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 方式B:使用远程 MySQL(可选)
|
||||||
|
|
||||||
|
> 如果已有远程 MySQL 实例,不想在 Docker 中运行 MySQL 容器,请执行以下操作:
|
||||||
|
|
||||||
|
**步骤 1**:编辑 `/opt/yunrui_assetet/docker-compose.yml`,注释掉 `mysql` 服务部分:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 用 sed 注释 mysql 服务(从 services: 末尾到文件末尾)
|
||||||
|
# 或者手动编辑,将 mysql: 到文件末尾的内容用 # 注释掉
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤 2**:修改 `/opt/yunrui_assetet/backend/.env`,配置远程数据库地址:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
DB_TYPE=mysql
|
DB_TYPE=mysql
|
||||||
@@ -278,45 +349,36 @@ MYSQL_PASSWORD=数据库密码
|
|||||||
MYSQL_DATABASE=数据库名
|
MYSQL_DATABASE=数据库名
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 五、启动容器
|
## 五、启动容器
|
||||||
|
|
||||||
### 5.1 首次拉取镜像
|
### 5.1 首次拉取镜像
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/idc_assest
|
cd /opt/yunrui_assetet
|
||||||
|
docker compose -f docker-compose.yml pull
|
||||||
# 方式A(本地 MySQL)
|
|
||||||
docker compose -f docker-compose.prod.yml pull
|
|
||||||
|
|
||||||
# 方式B(远程 MySQL)
|
|
||||||
docker compose -f docker-compose.prod.yml pull
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 启动服务
|
### 5.2 启动服务
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 方式A(本地 MySQL)
|
docker compose -f docker-compose.yml up -d
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
|
|
||||||
# 方式B(远程 MySQL)
|
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 查看启动状态
|
### 5.3 查看启动状态
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看所有容器状态
|
# 查看所有容器状态
|
||||||
docker compose -f docker-compose.prod.yml ps
|
docker compose -f docker-compose.yml ps
|
||||||
|
|
||||||
# 实时查看日志
|
# 实时查看日志
|
||||||
docker compose -f docker-compose.prod.yml logs -f
|
docker compose -f docker-compose.yml logs -f
|
||||||
|
|
||||||
# 单独查看某个容器的日志
|
# 单独查看某个容器的日志
|
||||||
docker logs idc-backend
|
docker logs yunrui_asset-backend
|
||||||
docker logs idc-frontend
|
docker logs yunrui_asset-frontend
|
||||||
docker logs idc-mysql # 仅方式A
|
docker logs yunrui_asset-mysql
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.4 等待初始化完成
|
### 5.4 等待初始化完成
|
||||||
@@ -330,7 +392,7 @@ docker logs idc-mysql # 仅方式A
|
|||||||
可以通过日志查看初始化进度:
|
可以通过日志查看初始化进度:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker logs -f idc-backend
|
docker logs -f yunrui_asset-backend
|
||||||
```
|
```
|
||||||
|
|
||||||
当日志出现以下内容时,说明启动完成:
|
当日志出现以下内容时,说明启动完成:
|
||||||
@@ -340,23 +402,23 @@ docker logs -f idc-backend
|
|||||||
服务器运行在 http://localhost:8000
|
服务器运行在 http://localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 六、验证部署
|
## 六、验证部署
|
||||||
|
|
||||||
### 6.1 检查容器状态
|
### 6.1 检查容器状态
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.prod.yml ps
|
docker compose -f docker-compose.yml ps
|
||||||
```
|
```
|
||||||
|
|
||||||
所有容器都应该是 `Up` 状态:
|
所有容器都应该是 `Up` 状态:
|
||||||
|
|
||||||
```
|
```
|
||||||
NAME IMAGE STATUS PORTS
|
NAME IMAGE STATUS PORTS
|
||||||
idc-frontend .../idc-frontend:latest Up :80->80/tcp
|
yunrui_asset-frontend ghcr.io/.../yunrui_asset-frontend:latest Up :80->80/tcp
|
||||||
idc-backend .../idc-backend:latest Up (healthy) 8000/tcp
|
yunrui_asset-backend ghcr.io/.../yunrui_asset-backend:latest Up (healthy) 8000/tcp
|
||||||
idc-mysql mysql:8.0 Up 3306/tcp
|
yunrui_asset-mysql mysql:8.0 Up 3306/tcp
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.2 访问系统
|
### 6.2 访问系统
|
||||||
@@ -383,7 +445,7 @@ curl http://服务器IP/api
|
|||||||
curl http://服务器IP/health
|
curl http://服务器IP/health
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 七、日常运维
|
## 七、日常运维
|
||||||
|
|
||||||
@@ -391,26 +453,26 @@ curl http://服务器IP/health
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看状态
|
# 查看状态
|
||||||
docker compose -f docker-compose.prod.yml ps
|
docker compose -f docker-compose.yml ps
|
||||||
|
|
||||||
# 查看日志
|
# 查看日志
|
||||||
docker compose -f docker-compose.prod.yml logs -f
|
docker compose -f docker-compose.yml logs -f
|
||||||
|
|
||||||
# 重启服务
|
# 重启服务
|
||||||
docker compose -f docker-compose.prod.yml restart
|
docker compose -f docker-compose.yml restart
|
||||||
|
|
||||||
# 停止服务
|
# 停止服务
|
||||||
docker compose -f docker-compose.prod.yml down
|
docker compose -f docker-compose.yml down
|
||||||
|
|
||||||
# 停止并删除数据卷(数据会丢失!慎用)
|
# 停止并删除数据卷(数据会丢失!慎用)
|
||||||
docker compose -f docker-compose.prod.yml down -v
|
docker compose -f docker-compose.yml down -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.2 备份数据库
|
### 7.2 备份数据库
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# MySQL 本地容器方式
|
# MySQL 本地容器方式
|
||||||
docker exec idc-mysql mysqldump -u root -p idc_management > backup_$(date +%Y%m%d).sql
|
docker exec yunrui_asset-mysql mysqldump -u root -p yunrui_management > backup_$(date +%Y%m%d).sql
|
||||||
|
|
||||||
# 如果开启了自动备份,后端自身也会定期备份到 backend/backups/ 目录
|
# 如果开启了自动备份,后端自身也会定期备份到 backend/backups/ 目录
|
||||||
```
|
```
|
||||||
@@ -418,18 +480,18 @@ docker exec idc-mysql mysqldump -u root -p idc_management > backup_$(date +%Y%m%
|
|||||||
### 7.3 查看容器资源占用
|
### 7.3 查看容器资源占用
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker stats idc-backend idc-frontend idc-mysql
|
docker stats yunrui_asset-backend yunrui_asset-frontend yunrui_asset-mysql
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.4 更新服务配置
|
### 7.4 更新服务配置
|
||||||
|
|
||||||
修改 `.env` 后,需要重启后端容器才能生效:
|
修改 `backend/.env` 后,需要重启后端容器才能生效:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.prod.yml restart backend
|
docker compose -f docker-compose.yml restart backend
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 八、版本升级
|
## 八、版本升级
|
||||||
|
|
||||||
@@ -437,25 +499,18 @@ docker compose -f docker-compose.prod.yml restart backend
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 拉取最新镜像
|
# 1. 拉取最新镜像
|
||||||
docker compose -f docker-compose.prod.yml pull
|
docker compose -f docker-compose.yml pull
|
||||||
|
|
||||||
# 2. 重新创建容器
|
# 2. 重新创建容器
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
docker compose -f docker-compose.yml up -d
|
||||||
|
|
||||||
# 3. 清理旧镜像(可选)
|
# 3. 清理旧镜像(可选)
|
||||||
docker image prune -a
|
docker image prune -a
|
||||||
```
|
```
|
||||||
|
|
||||||
### 回滚到指定版本
|
<br />
|
||||||
|
|
||||||
```bash
|
***
|
||||||
# 用 v2.1.0 版本的镜像启动
|
|
||||||
BACKEND_IMAGE=crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-backend:v2.1.0 \
|
|
||||||
FRONTEND_IMAGE=crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com/idc-assest/idc-frontend:v2.1.0 \
|
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、常见问题
|
## 九、常见问题
|
||||||
|
|
||||||
@@ -467,17 +522,17 @@ docker compose -f docker-compose.prod.yml up -d
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看退出容器的日志
|
# 查看退出容器的日志
|
||||||
docker logs idc-backend
|
docker logs yunrui_asset-backend
|
||||||
|
|
||||||
# 常见错误:
|
# 常见错误:
|
||||||
# - JWT_SECRET 未配置 → 在 .env 中设置 JWT_SECRET
|
# - JWT_SECRET 未配置 → 在 backend/.env 中设置 JWT_SECRET
|
||||||
# - MySQL 连接被拒 → 检查 MYSQL_HOST / MYSQL_PASSWORD
|
# - MySQL 连接被拒 → 检查 MYSQL_HOST / MYSQL_PASSWORD
|
||||||
# - 端口被占用 → 检查 80 端口是否已被其他程序占用
|
# - 端口被占用 → 检查 80 端口是否已被其他程序占用
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9.2 端口被占用
|
### 9.2 端口被占用
|
||||||
|
|
||||||
如果 80 端口被其他服务占用,可以修改 `docker-compose.prod.yml` 中的端口映射:
|
如果 80 端口被其他服务占用,可以修改 `docker-compose.yml` 中的端口映射:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ports:
|
ports:
|
||||||
@@ -486,97 +541,39 @@ ports:
|
|||||||
|
|
||||||
### 9.3 拉取镜像失败
|
### 9.3 拉取镜像失败
|
||||||
|
|
||||||
**原因:** 阿里云登录凭证问题或网络不通。
|
**原因:** 网络问题或镜像仓库无法访问。
|
||||||
|
|
||||||
**排查:**
|
**排查:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 手动登录阿里云 Registry 测试
|
ping ghcr.io
|
||||||
docker login crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com
|
|
||||||
|
|
||||||
# 检查网络
|
|
||||||
ping crpi-c807itn6exy37e7d.cn-hangzhou.personal.cr.aliyuncs.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9.4 初始化后没有默认管理员账号
|
### 9.4 初始化后没有默认管理员账号
|
||||||
|
|
||||||
后端首次启动会自动创建默认管理员账号:
|
系统**不内置默认管理员账号**,采用"首个注册用户自动成为管理员"机制。
|
||||||
|
|
||||||
| 账号 | 说明 |
|
首次访问系统时,转到注册页面创建第一个账号,该账号会自动被授予管理员权限。
|
||||||
|------|------|
|
|
||||||
| `admin` | 管理员 |
|
|
||||||
| `admin` | 管理员密码 |
|
|
||||||
|
|
||||||
如果登录失败,检查后端日志是否有初始化相关的错误信息。
|
|
||||||
|
|
||||||
### 9.5 上传文件后访问 404
|
### 9.5 上传文件后访问 404
|
||||||
|
|
||||||
检查 `backend/uploads/` 目录是否存在且权限正确:
|
检查 `backend/uploads/` 目录是否存在且权限正确:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls -la /opt/idc_assest/backend/uploads/
|
ls -la /opt/yunrui_assetet/backend/uploads/
|
||||||
chmod 755 /opt/idc_assest/backend/uploads/
|
chmod 755 /opt/yunrui_assetet/backend/uploads/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9.6 yaml 解析错误:`did not find expected key`
|
### 9.6 后端连不上 MySQL 容器
|
||||||
|
|
||||||
**常见原因:**
|
|
||||||
|
|
||||||
1. **缩进错误**:YAML 严格依赖缩进,每个 service 下的属性必须比 service 名多 2 个空格。例如:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
backend: # ← 2 个空格
|
|
||||||
image: ... # ← 4 个空格(比 backend 多 2 个)
|
|
||||||
mysql: # ← 2 个空格
|
|
||||||
image: mysql:8.0 # ← 4 个空格
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **拼写错误**:例如把 `networks` 写成 `netorks`。
|
|
||||||
|
|
||||||
**解决方法:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 用 yamllint 检查
|
|
||||||
pip install yamllint
|
|
||||||
yamllint /opt/idc_assest/docker-compose.prod.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
或者直接对照文档第四章的完整 `docker-compose.prod.yml` 内容复制。
|
|
||||||
|
|
||||||
### 9.7 错误:`env file /opt/idc_assest/backend/.env not found`
|
|
||||||
|
|
||||||
**原因:** `docker-compose.prod.yml` 里 backend 服务配置了 `env_file: ./backend/.env`,但这个文件不存在。
|
|
||||||
|
|
||||||
**解决方法:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建 backend/.env 文件(参考 3.3 节)
|
|
||||||
cat > /opt/idc_assest/backend/.env << 'EOF'
|
|
||||||
PORT=8000
|
|
||||||
NODE_ENV=production
|
|
||||||
DB_TYPE=mysql
|
|
||||||
MYSQL_HOST=mysql
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_USERNAME=idc_user
|
|
||||||
MYSQL_PASSWORD=你的密码
|
|
||||||
MYSQL_DATABASE=idc_management
|
|
||||||
JWT_SECRET=64位随机密钥
|
|
||||||
TOKEN_EXPIRY=24h
|
|
||||||
...(其他配置)
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.8 后端连不上 MySQL 容器
|
|
||||||
|
|
||||||
**排查步骤:**
|
**排查步骤:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 检查 MySQL 容器状态
|
# 1. 检查 MySQL 容器状态
|
||||||
docker ps | grep idc-mysql
|
docker ps | grep yunrui_asset-mysql
|
||||||
|
|
||||||
# 2. 检查 backend/.env 中 MYSQL_HOST 是否是 mysql(服务名),不是 localhost
|
# 2. 检查 backend/.env 中 MYSQL_HOST 是否是 mysql(服务名),不是 localhost
|
||||||
grep MYSQL_HOST /opt/idc_assest/backend/.env
|
grep MYSQL_HOST /opt/yunrui_assetet/backend/.env
|
||||||
|
|
||||||
# 3. 验证 MySQL 用户密码是否一致
|
# 3. 验证 MySQL 用户密码是否一致
|
||||||
# backend/.env 中 MYSQL_USERNAME / MYSQL_PASSWORD
|
# backend/.env 中 MYSQL_USERNAME / MYSQL_PASSWORD
|
||||||
@@ -584,5 +581,18 @@ grep MYSQL_HOST /opt/idc_assest/backend/.env
|
|||||||
# 两者必须匹配
|
# 两者必须匹配
|
||||||
|
|
||||||
# 4. 进入 MySQL 容器验证
|
# 4. 进入 MySQL 容器验证
|
||||||
docker exec -it idc-mysql mysql -u idc_user -p
|
docker exec -it yunrui_asset-mysql mysql -u yunrui_user -p
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 9.7 如何在已有部署基础上添加新配置
|
||||||
|
|
||||||
|
如果已经在运行中,需要补充某个配置项:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 编辑 backend/.env 补充配置
|
||||||
|
vim /opt/yunrui_assetet/backend/.env
|
||||||
|
|
||||||
|
# 2. 重启后端容器
|
||||||
|
docker compose -f docker-compose.yml restart backend
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@
|
|||||||
▼
|
▼
|
||||||
GitHub Actions
|
GitHub Actions
|
||||||
│
|
│
|
||||||
├── 构建 idc-backend:latest 镜像
|
├── 构建 yunrui_asset-backend:latest 镜像
|
||||||
├── 构建 idc-frontend:latest 镜像(Nginx + 前端静态文件)
|
├── 构建 yunrui_asset-frontend:latest 镜像(Nginx + 前端静态文件)
|
||||||
├── 推送到镜像仓库(Docker Hub / 阿里云 / GitHub Container Registry)
|
├── 推送到镜像仓库(Docker Hub / 阿里云 / GitHub Container Registry)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: yourname/idc-backend:latest
|
image: yourname/yunrui_asset-backend:latest
|
||||||
# 没有 build 指令,直接从仓库拉取
|
# 没有 build 指令,直接从仓库拉取
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: yourname/idc-frontend:latest
|
image: yourname/yunrui_asset-frontend:latest
|
||||||
# 同上,服务器不需要装 Node.js
|
# 同上,服务器不需要装 Node.js
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
@@ -134,8 +134,8 @@ BACKEND_VERSION=v1.2.0 FRONTEND_VERSION=v1.2.0 docker compose up -d
|
|||||||
### 5.1 镜像仓库:阿里云容器镜像服务
|
### 5.1 镜像仓库:阿里云容器镜像服务
|
||||||
|
|
||||||
阿里云控制台开通容器镜像服务后:
|
阿里云控制台开通容器镜像服务后:
|
||||||
1. 创建命名空间(如 `idc-assest`)
|
1. 创建命名空间(如 `yunrui-asset`)
|
||||||
2. 创建镜像仓库:`idc-backend`、`idc-frontend`
|
2. 创建镜像仓库:`yunrui_asset-backend`、`yunrui_asset-frontend`
|
||||||
3. 设置访问凭证(用户名 + 密码)
|
3. 设置访问凭证(用户名 + 密码)
|
||||||
|
|
||||||
### 5.2 GitHub Secrets 配置
|
### 5.2 GitHub Secrets 配置
|
||||||
@@ -171,7 +171,7 @@ BACKEND_VERSION=v1.2.0 FRONTEND_VERSION=v1.2.0 docker compose up -d
|
|||||||
↓
|
↓
|
||||||
管理员 SSH 到生产服务器
|
管理员 SSH 到生产服务器
|
||||||
↓
|
↓
|
||||||
cd /opt/idc_assest
|
cd /opt/yunrui_assetet
|
||||||
docker compose -f docker-compose.prod.yml pull
|
docker compose -f docker-compose.prod.yml pull
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
```
|
```
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 176 KiB |
@@ -1,136 +0,0 @@
|
|||||||
# 项目截图说明
|
|
||||||
|
|
||||||
## 截图列表
|
|
||||||
|
|
||||||
以下截图需要在README.md中展示:
|
|
||||||
|
|
||||||
1. **dashboard.png** - 数据看板页面截图
|
|
||||||
- 路径:访问 http://localhost:3000
|
|
||||||
- 内容:展示数据看板的统计图表、设备状态分布等
|
|
||||||
|
|
||||||
2. **3d-visualization.png** - 3D机柜可视化页面截图
|
|
||||||
- 路径:访问 http://localhost:3000/visualization-3d
|
|
||||||
- 内容:展示3D机柜的三维视图、设备布局、交互功能
|
|
||||||
|
|
||||||
3. **device-management.png** - 设备管理页面截图
|
|
||||||
- 路径:访问 http://localhost:3000/devices
|
|
||||||
- 内容:展示设备列表、筛选功能、操作按钮等
|
|
||||||
|
|
||||||
4. **room-management.png** - 机房管理页面截图
|
|
||||||
- 路径:访问 http://localhost:3000/rooms
|
|
||||||
- 内容:展示机房列表、机柜统计、管理功能等
|
|
||||||
|
|
||||||
## 添加截图步骤
|
|
||||||
|
|
||||||
### Windows系统
|
|
||||||
|
|
||||||
1. **启动项目**
|
|
||||||
```bash
|
|
||||||
cd backend && npm run dev
|
|
||||||
cd frontend && npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **打开浏览器访问对应页面**
|
|
||||||
|
|
||||||
3. **使用截图工具**
|
|
||||||
- Windows截图工具:Win + Shift + S
|
|
||||||
- 或使用其他截图工具(如Snipaste、ShareX等)
|
|
||||||
|
|
||||||
4. **保存截图**
|
|
||||||
- 将截图保存为PNG格式
|
|
||||||
- 文件名对应上面的列表
|
|
||||||
- 保存到 `docs/images/` 目录
|
|
||||||
|
|
||||||
5. **优化截图(可选)**
|
|
||||||
- 建议分辨率:1920x1080 或更高
|
|
||||||
- 建议压缩:使用TinyPNG等工具压缩图片大小
|
|
||||||
- 建议格式:PNG(保持清晰度)或 WebP(更小体积)
|
|
||||||
|
|
||||||
### macOS系统
|
|
||||||
|
|
||||||
1. **启动项目**
|
|
||||||
```bash
|
|
||||||
cd backend && npm run dev
|
|
||||||
cd frontend && npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **打开浏览器访问对应页面**
|
|
||||||
|
|
||||||
3. **使用截图工具**
|
|
||||||
- macOS截图工具:Cmd + Shift + 4(区域截图)
|
|
||||||
- 或 Cmd + Shift + 3(全屏截图)
|
|
||||||
|
|
||||||
4. **保存截图**
|
|
||||||
- 将截图保存为PNG格式
|
|
||||||
- 文件名对应上面的列表
|
|
||||||
- 保存到 `docs/images/` 目录
|
|
||||||
|
|
||||||
## 截图建议
|
|
||||||
|
|
||||||
### 最佳实践
|
|
||||||
|
|
||||||
- **浏览器全屏模式**:按F11进入全屏,避免浏览器UI干扰
|
|
||||||
- **隐藏开发者工具**:确保没有开发者工具面板
|
|
||||||
- **合适的窗口大小**:建议使用1920x1080或1366x768分辨率
|
|
||||||
- **数据准备**:确保页面有足够的测试数据展示
|
|
||||||
- **清晰度**:保持截图清晰,避免模糊
|
|
||||||
|
|
||||||
### 截图内容建议
|
|
||||||
|
|
||||||
- **数据看板**:展示多种图表类型、统计数据、关键指标
|
|
||||||
- **3D可视化**:展示机柜的3D视图、设备详情、交互状态
|
|
||||||
- **设备管理**:展示设备列表、搜索筛选、操作按钮
|
|
||||||
- **机房管理**:展示机房列表、机柜分布、统计信息
|
|
||||||
|
|
||||||
## 自动化截图(可选)
|
|
||||||
|
|
||||||
如果需要自动化截图,可以使用以下工具:
|
|
||||||
|
|
||||||
### Playwright
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { chromium } = require('playwright');
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const browser = await chromium.launch();
|
|
||||||
const page = await browser.newPage();
|
|
||||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
||||||
|
|
||||||
// 截图数据看板
|
|
||||||
await page.goto('http://localhost:3000');
|
|
||||||
await page.screenshot({ path: 'docs/images/dashboard.png' });
|
|
||||||
|
|
||||||
// 截图3D可视化
|
|
||||||
await page.goto('http://localhost:3000/visualization-3d');
|
|
||||||
await page.waitForTimeout(2000); // 等待3D场景加载
|
|
||||||
await page.screenshot({ path: 'docs/images/3d-visualization.png' });
|
|
||||||
|
|
||||||
await browser.close();
|
|
||||||
})();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Puppeteer
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const puppeteer = require('puppeteer');
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const browser = await puppeteer.launch();
|
|
||||||
const page = await browser.newPage();
|
|
||||||
await page.setViewport({ width: 1920, height: 1080 });
|
|
||||||
|
|
||||||
// 截图数据看板
|
|
||||||
await page.goto('http://localhost:3000');
|
|
||||||
await page.screenshot({ path: 'docs/images/dashboard.png' });
|
|
||||||
|
|
||||||
await browser.close();
|
|
||||||
})();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 确保截图文件名与README.md中的引用一致
|
|
||||||
- 建议使用PNG格式以保持清晰度
|
|
||||||
- 截图大小建议控制在500KB以内
|
|
||||||
- 避免在截图中暴露敏感信息(如真实IP、密码等)
|
|
||||||
- 定期更新截图以反映最新的UI变化
|
|
||||||
|
Before Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 682 KiB |
|
Before Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 788 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 885 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 149 KiB |
@@ -1,5 +1,5 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# IDC 设备管理系统 - 前端(Nginx)Dockerfile
|
# 云睿资产管理系统 - 前端(Nginx)Dockerfile
|
||||||
# 第一阶段:构建前端静态文件
|
# 第一阶段:构建前端静态文件
|
||||||
# 第二阶段:Nginx 运行
|
# 第二阶段:Nginx 运行
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>IDC设备管理系统</title>
|
<title>云睿资产管理系统</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# IDC 设备管理系统 - Nginx 配置
|
# 云睿资产管理系统 - Nginx 配置
|
||||||
# 功能:serve 前端静态文件 + 反向代理 API
|
# 功能:serve 前端静态文件 + 反向代理 API
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "idc-frontend",
|
"name": "yunrui-frontend",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "idc-frontend",
|
"name": "yunrui-frontend",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "idc-frontend",
|
"name": "yunrui_asset-frontend",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"description": "IDC设备管理系统前端",
|
"description": "云睿资产管理系统前端",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import {
|
|||||||
CodepenOutlined,
|
CodepenOutlined,
|
||||||
CloudUploadOutlined,
|
CloudUploadOutlined,
|
||||||
LayoutOutlined,
|
LayoutOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router,
|
BrowserRouter as Router,
|
||||||
@@ -85,6 +87,7 @@ const OperationLogs = lazy(() => import('./pages/OperationLogs'));
|
|||||||
const ErrorBoundaryTest = lazy(() => import('./pages/ErrorBoundaryTest'));
|
const ErrorBoundaryTest = lazy(() => import('./pages/ErrorBoundaryTest'));
|
||||||
const IdleDeviceManagement = lazy(() => import('./pages/IdleDeviceManagement'));
|
const IdleDeviceManagement = lazy(() => import('./pages/IdleDeviceManagement'));
|
||||||
const RoomFloorPlan = lazy(() => import('./pages/RoomFloorPlan'));
|
const RoomFloorPlan = lazy(() => import('./pages/RoomFloorPlan'));
|
||||||
|
const About = lazy(() => import('./pages/About'));
|
||||||
|
|
||||||
const { Header, Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
|
|
||||||
@@ -372,6 +375,11 @@ const AppLayout = ({ children }) => {
|
|||||||
icon: <AuditOutlined style={{ fontSize: '16px' }} />,
|
icon: <AuditOutlined style={{ fontSize: '16px' }} />,
|
||||||
label: <Link to="/operation-logs">操作日志</Link>,
|
label: <Link to="/operation-logs">操作日志</Link>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'about',
|
||||||
|
icon: <InfoCircleOutlined style={{ fontSize: '16px' }} />,
|
||||||
|
label: <Link to="/about">关于系统</Link>,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -440,7 +448,7 @@ const AppLayout = ({ children }) => {
|
|||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config.site_name || 'IDC管理'}
|
{config.site_name || '云睿资产'}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -449,7 +457,7 @@ const AppLayout = ({ children }) => {
|
|||||||
marginTop: '2px',
|
marginTop: '2px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
数据中心管理平台
|
云端睿智资产管理
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -477,10 +485,36 @@ const AppLayout = ({ children }) => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: '12px',
|
padding: '8px 12px',
|
||||||
borderTop: `1px solid ${designTokens.colors.sidebar.border}`,
|
borderTop: `1px solid ${designTokens.colors.sidebar.border}`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '4px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!collapsed && (
|
||||||
|
<div style={{ padding: '0 8px 8px 8px' }}>
|
||||||
|
<a
|
||||||
|
href="https://code.xinmi.cloud/yunrui_asset"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: designTokens.colors.primary.main,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
opacity: 0.8,
|
||||||
|
transition: 'opacity 0.2s'
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => e.target.style.opacity = 1}
|
||||||
|
onMouseLeave={e => e.target.style.opacity = 0.8}
|
||||||
|
>
|
||||||
|
<LinkOutlined style={{ fontSize: '12px' }} />
|
||||||
|
<span>新觅源码库</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
@@ -624,6 +658,7 @@ const routeConfig = [
|
|||||||
{ path: '/error-boundary-test', component: ErrorBoundaryTest },
|
{ path: '/error-boundary-test', component: ErrorBoundaryTest },
|
||||||
{ path: '/idle-devices', component: IdleDeviceManagement },
|
{ path: '/idle-devices', component: IdleDeviceManagement },
|
||||||
{ path: '/floor-plan', component: RoomFloorPlan },
|
{ path: '/floor-plan', component: RoomFloorPlan },
|
||||||
|
{ path: '/about', component: About },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ThemeConfig = () => {
|
const ThemeConfig = () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { designTokens } from '../../config/theme';
|
import { designTokens } from '../../config/theme';
|
||||||
import { useFetch } from '../../hooks/useSWR';
|
import { useFetch } from '../../hooks/useSWR';
|
||||||
|
import { version as appVersion } from '../../../package.json';
|
||||||
|
|
||||||
// 格式化运行时间
|
// 格式化运行时间
|
||||||
const formatUptime = (seconds) => {
|
const formatUptime = (seconds) => {
|
||||||
@@ -38,7 +39,7 @@ const formatUptime = (seconds) => {
|
|||||||
const SystemInfo = ({ onRefresh, isRefreshing }) => {
|
const SystemInfo = ({ onRefresh, isRefreshing }) => {
|
||||||
const { data: systemData, isLoading: isVersionLoading } = useFetch('/system-settings/system/info');
|
const { data: systemData, isLoading: isVersionLoading } = useFetch('/system-settings/system/info');
|
||||||
|
|
||||||
const version = systemData?.system?.version || '1.2.0';
|
const version = systemData?.system?.version || appVersion;
|
||||||
const formattedVersion = `v${version}`;
|
const formattedVersion = `v${version}`;
|
||||||
const currentTime = new Date().toLocaleString('zh-CN');
|
const currentTime = new Date().toLocaleString('zh-CN');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Layout, Typography, Card, Row, Col, Space, Tag, Button } from 'antd';
|
||||||
|
import {
|
||||||
|
InfoCircleOutlined,
|
||||||
|
RocketOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
GithubOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
LinkOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { designTokens } from '../config/theme';
|
||||||
|
|
||||||
|
const { Title, Paragraph, Text } = Typography;
|
||||||
|
|
||||||
|
const About = () => {
|
||||||
|
const techStack = [
|
||||||
|
{ name: 'React', version: '18.2.0', icon: <RocketOutlined /> },
|
||||||
|
{ name: 'Vite', version: '5.2.10', icon: <ThunderboltOutlined /> },
|
||||||
|
{ name: 'Ant Design', version: '5.17.0', icon: <BuildOutlined /> },
|
||||||
|
{ name: 'Node.js', version: '20+', icon: <CloudServerOutlined /> },
|
||||||
|
{ name: 'Express', version: '4.19.2', icon: <ApiOutlined /> },
|
||||||
|
{ name: 'Sequelize', version: '6.37.3', icon: <DatabaseOutlined /> },
|
||||||
|
{ name: 'Three.js', version: '0.164.1', icon: <CodepenOutlined /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ title: '资产管理', desc: '全方位的 IDC 资产登记与追踪' },
|
||||||
|
{ title: '3D 可视化', desc: '基于 Three.js 的机房机柜 3D 仿真展示' },
|
||||||
|
{ title: '工单系统', desc: '标准化的故障处理与日常运维流程' },
|
||||||
|
{ title: '耗材库存', desc: '精细化的备品备件出入库管理' },
|
||||||
|
{ title: '数据监控', desc: '实时的健康状态检查与性能分析' },
|
||||||
|
{ title: '自动化备份', desc: '安全的数据备份与一键恢复机制' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
marginBottom: '32px',
|
||||||
|
borderRadius: '16px',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'relative', zIndex: 1, padding: '40px' }}>
|
||||||
|
<Title level={1} style={{ color: '#fff', marginBottom: '16px' }}>云睿资产管理系统</Title>
|
||||||
|
<Paragraph style={{ color: 'rgba(255,255,255,0.9)', fontSize: '18px', maxWidth: '600px' }}>
|
||||||
|
智能数据中心基础设施管理平台,助力企业实现资产全生命周期管理与 3D 可视化运维。
|
||||||
|
</Paragraph>
|
||||||
|
<div style={{ marginTop: '24px' }}>
|
||||||
|
<Tag color="rgba(255,255,255,0.2)" style={{ fontSize: '14px', padding: '4px 12px', border: '1px solid rgba(255,255,255,0.3)', color: '#fff' }}>v1.0.0 Stable</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '-50px',
|
||||||
|
top: '-50px',
|
||||||
|
opacity: 0.1,
|
||||||
|
fontSize: '300px'
|
||||||
|
}}>
|
||||||
|
<CloudServerOutlined />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Row gutter={[32, 32]}>
|
||||||
|
<Col xs={24} md={16}>
|
||||||
|
{/* Main Content */}
|
||||||
|
<Card title={<Space><InfoCircleOutlined /> 关于平台</Space>} style={{ borderRadius: '12px', height: '100%' }}>
|
||||||
|
<Paragraph>
|
||||||
|
云睿资产管理系统(YunRui Asset Management System)是一款专为中大型数据中心设计的资产管理解决方案。平台集成了资产台账、3D 仿真、工单运维、耗材管理等核心模块,通过数字化手段解决 IDC 运维中“资产找不着、状态看不见、流程不规范”的痛点。
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph>
|
||||||
|
系统采用现代化的前后端分离架构,前端基于 React 和 Vite 提供极致的交互体验,后端依托 Node.js 实现高并发的数据处理,结合 3D 渲染技术,为用户提供身临其境的管理体验。
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
<Title level={4} style={{ marginTop: '24px' }}>核心特性</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{features.map((item, index) => (
|
||||||
|
<Col span={12} key={index}>
|
||||||
|
<div style={{ padding: '12px', background: '#f8fafc', borderRadius: '8px' }}>
|
||||||
|
<Text strong>{item.title}</Text>
|
||||||
|
<div style={{ fontSize: '12px', color: '#64748b' }}>{item.desc}</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
{/* Sidebar Info */}
|
||||||
|
<Card title={<Space><CodeOutlined /> 技术栈</Space>} style={{ borderRadius: '12px', marginBottom: '32px' }}>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||||
|
{techStack.map((tech, index) => (
|
||||||
|
<Tag key={index} icon={tech.icon} color="blue" style={{ padding: '4px 8px' }}>
|
||||||
|
{tech.name} {tech.version}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title={<Space><RocketOutlined /> 获取源码</Space>}
|
||||||
|
style={{
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: '#f0f9ff',
|
||||||
|
border: '1px solid #bae6fd'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paragraph>
|
||||||
|
本项目已由新觅源码库进行标准化定制,提供商业级的高质量源码。
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
href="https://code.xinmi.cloud/yunrui_asset"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
访问新觅源码库
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div style={{ textAlign: 'center', marginTop: '48px', color: '#94a3b8' }}>
|
||||||
|
<Paragraph>© 2026 云睿资产管理系统 · 基于新觅自动化工作流构建</Paragraph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助组件导入定义(模拟 Antd 图标)
|
||||||
|
const BuildOutlined = RocketOutlined;
|
||||||
|
const ApiOutlined = CodeOutlined;
|
||||||
|
const DatabaseOutlined = CodeOutlined;
|
||||||
|
const CodepenOutlined = RocketOutlined;
|
||||||
|
|
||||||
|
export default About;
|
||||||
@@ -429,7 +429,7 @@ function Dashboard() {
|
|||||||
<div style={headerStyle} className="dashboard-header">
|
<div style={headerStyle} className="dashboard-header">
|
||||||
<h1 style={titleStyle} className="dashboard-title">
|
<h1 style={titleStyle} className="dashboard-title">
|
||||||
<DashboardOutlined />
|
<DashboardOutlined />
|
||||||
IDC设备管理系统
|
云睿资产管理系统
|
||||||
</h1>
|
</h1>
|
||||||
<p style={subtitleStyle} className="dashboard-subtitle">
|
<p style={subtitleStyle} className="dashboard-subtitle">
|
||||||
实时监控 · 智能管理 · 高效运维
|
实时监控 · 智能管理 · 高效运维
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
Login Page - Light Corporate Design
|
Login Page - Light Corporate Design
|
||||||
Design System: IDC Data Center Management
|
Design System: YunRui Asset Management
|
||||||
Style: Clean Modern Corporate + Data Center Viz
|
Style: Clean Modern Corporate + Data Center Viz
|
||||||
Palette: Teal (#14b8a6) / Blue (#3b82f6) / Purple (#8b5cf6)
|
Palette: Teal (#14b8a6) / Blue (#3b82f6) / Purple (#8b5cf6)
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="brand-title-section">
|
<div className="brand-title-section">
|
||||||
<h1 className="brand-title">
|
<h1 className="brand-title">
|
||||||
<span className="brand-title__main">IDC</span>
|
<span className="brand-title__main">云睿</span>
|
||||||
<span className="brand-title__sub">机柜管理系统</span>
|
<span className="brand-title__sub">资产管理系统</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="brand-desc">智能数据中心基础设施管理平台</p>
|
<p className="brand-desc">智能数据中心基础设施管理平台</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,12 +277,11 @@ const Login = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
label="邮箱"
|
label="邮箱"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入邮箱' },
|
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入邮箱"
|
placeholder="请输入邮箱(选填)"
|
||||||
className="login-input"
|
className="login-input"
|
||||||
prefix={<MailOutlined className="input-icon" />}
|
prefix={<MailOutlined className="input-icon" />}
|
||||||
/>
|
/>
|
||||||
@@ -290,10 +289,9 @@ const Login = () => {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="phone"
|
name="phone"
|
||||||
label="手机号"
|
label="手机号"
|
||||||
rules={[{ required: true, message: '请输入手机号' }]}
|
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入手机号"
|
placeholder="请输入手机号(选填)"
|
||||||
className="login-input"
|
className="login-input"
|
||||||
prefix={<PhoneOutlined className="input-icon" />}
|
prefix={<PhoneOutlined className="input-icon" />}
|
||||||
/>
|
/>
|
||||||
@@ -301,17 +299,19 @@ const Login = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item
|
{!registerMode && (
|
||||||
name="username"
|
<Form.Item
|
||||||
label={isFirstUser ? '管理员账号' : '用户名'}
|
name="username"
|
||||||
rules={[{ required: true, message: isFirstUser ? '请设置管理员账号' : '请输入用户名' }]}
|
label={isFirstUser ? '管理员账号' : '用户名'}
|
||||||
>
|
rules={[{ required: true, message: isFirstUser ? '请设置管理员账号' : '请输入用户名' }]}
|
||||||
<Input
|
>
|
||||||
prefix={<UserOutlined className="input-icon" />}
|
<Input
|
||||||
placeholder={isFirstUser ? '请设置管理员账号' : '请输入用户名'}
|
prefix={<UserOutlined className="input-icon" />}
|
||||||
className="login-input"
|
placeholder={isFirstUser ? '请设置管理员账号' : '请输入用户名'}
|
||||||
/>
|
className="login-input"
|
||||||
</Form.Item>
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="password"
|
name="password"
|
||||||
@@ -363,6 +363,12 @@ const Login = () => {
|
|||||||
{isFirstUser ? '创建管理员' : (registerMode ? '立即注册' : '登 录')}
|
{isFirstUser ? '创建管理员' : (registerMode ? '立即注册' : '登 录')}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{!isFirstUser && !registerMode && (
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: '16px', color: '#8c8c8c', fontSize: '13px' }}>
|
||||||
|
提示:默认管理员账号通常为 <Text code>admin</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{!isFirstUser && !registerMode && (
|
{!isFirstUser && !registerMode && (
|
||||||
@@ -374,7 +380,7 @@ const Login = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<span>IDC Management System</span>
|
<span>YunRui Asset Management</span>
|
||||||
<span className="footer-dot" />
|
<span className="footer-dot" />
|
||||||
<span>v{appVersion}</span>
|
<span>v{appVersion}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,11 +100,34 @@ const animations = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按端口名称中的数字段进行自然排序
|
||||||
|
* @param {Array} ports - 端口数组
|
||||||
|
* @returns {Array} 排序后的新数组
|
||||||
|
*/
|
||||||
|
function sortPortsByName(ports) {
|
||||||
|
const extractNumbers = str => {
|
||||||
|
const matches = str ? String(str).match(/\d+/g) : null;
|
||||||
|
return matches ? matches.map(Number) : [];
|
||||||
|
};
|
||||||
|
return [...ports].sort((a, b) => {
|
||||||
|
const numsA = extractNumbers(a.portName);
|
||||||
|
const numsB = extractNumbers(b.portName);
|
||||||
|
for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
|
||||||
|
if (numsA[i] !== numsB[i]) {
|
||||||
|
return numsA[i] - numsB[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(a.portName).localeCompare(String(b.portName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function PortManagement() {
|
function PortManagement() {
|
||||||
const [ports, setPorts] = useState([]);
|
const [ports, setPorts] = useState([]);
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [deviceSearching, setDeviceSearching] = useState(false);
|
const [deviceSearching, setDeviceSearching] = useState(false);
|
||||||
const [groupedPorts, setGroupedPorts] = useState({});
|
// 按设备分组的端口列表(数组,每项 { device, ports })
|
||||||
|
const [groupedPorts, setGroupedPorts] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
@@ -167,17 +190,13 @@ function PortManagement() {
|
|||||||
const hasMoreRef = useRef(true);
|
const hasMoreRef = useRef(true);
|
||||||
const isLoadingRef = useRef(false);
|
const isLoadingRef = useRef(false);
|
||||||
|
|
||||||
// 设备卡片分页状态
|
// 设备卡片分页状态(按设备维度分页)
|
||||||
const [deviceCardPage, setDeviceCardPage] = useState(1);
|
const [deviceCardPage, setDeviceCardPage] = useState(1);
|
||||||
const deviceCardPageSize = 10;
|
const deviceCardPageSize = 10;
|
||||||
const [deviceCardTotal, setDeviceCardTotal] = useState(0);
|
const [deviceCardTotal, setDeviceCardTotal] = useState(0);
|
||||||
|
|
||||||
// 分页和懒加载状态
|
// 端口总数(所有设备端口总和,用于统计与导出弹窗)
|
||||||
const [portPage, setPortPage] = useState(1);
|
|
||||||
const [portPageSize, setPortPageSize] = useState(50);
|
|
||||||
const [portTotal, setPortTotal] = useState(0);
|
const [portTotal, setPortTotal] = useState(0);
|
||||||
const [portLoading, setPortLoading] = useState(false);
|
|
||||||
const [portLoadingMore, setPortLoadingMore] = useState(false);
|
|
||||||
|
|
||||||
// 服务器网卡卡片列表状态
|
// 服务器网卡卡片列表状态
|
||||||
const [serverNicSearchText, setServerNicSearchText] = useState('');
|
const [serverNicSearchText, setServerNicSearchText] = useState('');
|
||||||
@@ -186,46 +205,42 @@ function PortManagement() {
|
|||||||
const [serverNicCardPage, setServerNicCardPage] = useState(1);
|
const [serverNicCardPage, setServerNicCardPage] = useState(1);
|
||||||
const serverNicCardPageSize = 12;
|
const serverNicCardPageSize = 12;
|
||||||
|
|
||||||
|
// 按设备分组获取端口(设备维度分页,避免端口数过多时部分设备不显示)
|
||||||
const fetchPorts = useCallback(
|
const fetchPorts = useCallback(
|
||||||
async (page = 1, append = false) => {
|
async (page = 1) => {
|
||||||
const validPage = Number.isInteger(page) && page > 0 ? page : 1;
|
const validPage = Number.isInteger(page) && page > 0 ? page : 1;
|
||||||
const validPageSize = Number.isInteger(portPageSize) && portPageSize > 0 ? portPageSize : 50;
|
|
||||||
try {
|
try {
|
||||||
if (append) {
|
setLoading(true);
|
||||||
setPortLoadingMore(true);
|
|
||||||
} else {
|
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
page: validPage,
|
page: validPage,
|
||||||
pageSize: validPageSize,
|
pageSize: deviceCardPageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (filters.deviceId) params.deviceId = filters.deviceId;
|
if (filters.deviceId) params.deviceId = filters.deviceId;
|
||||||
|
|
||||||
const response = await api.get('/device-ports', { params });
|
const response = await api.get('/device-ports/grouped', { params });
|
||||||
const portsData = response.ports || [];
|
const groups = response.groups || [];
|
||||||
const total = response.total || 0;
|
|
||||||
const filteredPorts = portsData;
|
|
||||||
|
|
||||||
if (append) {
|
// 对每个设备的端口按名称自然排序,并展开为扁平数组(供统计、导出当前页使用)
|
||||||
setPorts(prev => [...prev, ...filteredPorts]);
|
const flatPorts = [];
|
||||||
} else {
|
const sortedGroups = groups.map(g => {
|
||||||
setPorts(filteredPorts);
|
const sortedPorts = sortPortsByName(g.ports || []);
|
||||||
}
|
sortedPorts.forEach(p => flatPorts.push(p));
|
||||||
setPortTotal(total);
|
return { device: g.device, ports: sortedPorts };
|
||||||
setPortPage(page);
|
});
|
||||||
hasMoreRef.current = page * portPageSize < total;
|
|
||||||
|
setPorts(flatPorts);
|
||||||
|
setGroupedPorts(sortedGroups);
|
||||||
|
setDeviceCardTotal(response.total || 0);
|
||||||
|
setPortTotal(response.totalPorts || 0);
|
||||||
|
setDeviceCardPage(validPage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取端口列表失败');
|
message.error('获取端口列表失败');
|
||||||
console.error('获取端口列表失败:', error);
|
console.error('获取端口列表失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPortLoadingMore(false);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[filters, portPageSize]
|
[filters, deviceCardPageSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchDevices = useCallback(async (keyword = '') => {
|
const fetchDevices = useCallback(async (keyword = '') => {
|
||||||
@@ -255,104 +270,21 @@ function PortManagement() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPorts(1, false);
|
fetchPorts(1);
|
||||||
fetchDevices();
|
fetchDevices();
|
||||||
}, [fetchPorts, fetchDevices]);
|
}, [fetchPorts, fetchDevices]);
|
||||||
|
|
||||||
const handleLoadMore = useCallback(() => {
|
|
||||||
if (!hasMoreRef.current || isLoadingRef.current || portLoadingMore) return;
|
|
||||||
isLoadingRef.current = true;
|
|
||||||
const nextPage = portPage + 1;
|
|
||||||
fetchPorts(nextPage, true).then(() => {
|
|
||||||
isLoadingRef.current = false;
|
|
||||||
});
|
|
||||||
}, [portPage, portLoadingMore, fetchPorts]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loadMoreRef.current) return;
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
entries => {
|
|
||||||
if (entries[0].isIntersecting && hasMoreRef.current && !portLoadingMore) {
|
|
||||||
handleLoadMore();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.1, rootMargin: '100px' }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(loadMoreRef.current);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, [handleLoadMore, portLoadingMore]);
|
|
||||||
|
|
||||||
const handlePageSizeChange = useCallback(
|
|
||||||
(newPage, newPageSize) => {
|
|
||||||
setPortPageSize(newPageSize);
|
|
||||||
setPortPage(1);
|
|
||||||
fetchPorts(1, false);
|
|
||||||
},
|
|
||||||
[fetchPorts]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const grouped = {};
|
|
||||||
ports.forEach(port => {
|
|
||||||
const deviceId = port.deviceId;
|
|
||||||
if (!grouped[deviceId]) {
|
|
||||||
grouped[deviceId] = {
|
|
||||||
device: port.device || devices.find(d => d.deviceId === deviceId),
|
|
||||||
ports: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
grouped[deviceId].ports.push(port);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 对每个设备的端口按名称升序排序
|
|
||||||
Object.keys(grouped).forEach(deviceId => {
|
|
||||||
grouped[deviceId].ports.sort((a, b) => {
|
|
||||||
const extractNumbers = str => {
|
|
||||||
const matches = str.match(/\d+/g);
|
|
||||||
return matches ? matches.map(Number) : [];
|
|
||||||
};
|
|
||||||
const numsA = extractNumbers(a.portName);
|
|
||||||
const numsB = extractNumbers(b.portName);
|
|
||||||
for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
|
|
||||||
if (numsA[i] !== numsB[i]) {
|
|
||||||
return numsA[i] - numsB[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a.portName.localeCompare(b.portName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setGroupedPorts(grouped);
|
|
||||||
|
|
||||||
const deviceIds = Object.keys(grouped);
|
|
||||||
const newTotal = deviceIds.length;
|
|
||||||
setDeviceCardTotal(newTotal);
|
|
||||||
if (deviceCardPage > Math.ceil(newTotal / deviceCardPageSize)) {
|
|
||||||
setDeviceCardPage(1);
|
|
||||||
}
|
|
||||||
setExpandedKeys([]);
|
|
||||||
}, [ports, devices, deviceCardPage, deviceCardPageSize]);
|
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
const handleSearch = useCallback(() => {
|
||||||
setPortPage(1);
|
|
||||||
setDeviceCardPage(1);
|
setDeviceCardPage(1);
|
||||||
hasMoreRef.current = true;
|
fetchPorts(1);
|
||||||
fetchPorts(1, false);
|
|
||||||
}, [fetchPorts]);
|
}, [fetchPorts]);
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setFilters({
|
setFilters({
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
});
|
});
|
||||||
setPortPage(1);
|
|
||||||
setDeviceCardPage(1);
|
setDeviceCardPage(1);
|
||||||
hasMoreRef.current = true;
|
fetchPorts(1);
|
||||||
fetchPorts(1, false);
|
|
||||||
}, [fetchPorts]);
|
}, [fetchPorts]);
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@@ -547,7 +479,7 @@ function PortManagement() {
|
|||||||
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
||||||
});
|
});
|
||||||
setRefreshTrigger(prev => prev + 1);
|
setRefreshTrigger(prev => prev + 1);
|
||||||
fetchPorts(1, false);
|
fetchPorts(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = port => {
|
const handleEdit = port => {
|
||||||
@@ -579,7 +511,7 @@ function PortManagement() {
|
|||||||
content: '删除成功',
|
content: '删除成功',
|
||||||
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
||||||
});
|
});
|
||||||
fetchPorts(1, false);
|
fetchPorts(1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error.response?.data?.error || '';
|
const errorMsg = error.response?.data?.error || '';
|
||||||
if (errorMsg.includes('关联的接线记录')) {
|
if (errorMsg.includes('关联的接线记录')) {
|
||||||
@@ -615,7 +547,7 @@ function PortManagement() {
|
|||||||
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
||||||
});
|
});
|
||||||
setSelectedRowKeys([]);
|
setSelectedRowKeys([]);
|
||||||
fetchPorts(1, false);
|
fetchPorts(1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error.response?.data?.error || '';
|
const errorMsg = error.response?.data?.error || '';
|
||||||
if (errorMsg.includes('关联的接线记录')) {
|
if (errorMsg.includes('关联的接线记录')) {
|
||||||
@@ -861,7 +793,7 @@ function PortManagement() {
|
|||||||
setParseResult(null);
|
setParseResult(null);
|
||||||
setPreviewPorts([]);
|
setPreviewPorts([]);
|
||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
fetchPorts(1, false);
|
fetchPorts(1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.response?.data?.error || editingPort ? '更新失败' : '创建失败');
|
message.error(error.response?.data?.error || editingPort ? '更新失败' : '创建失败');
|
||||||
console.error('提交失败:', error);
|
console.error('提交失败:', error);
|
||||||
@@ -1289,7 +1221,7 @@ function PortManagement() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPorts(1, false);
|
fetchPorts(1);
|
||||||
setImportModalVisible(false);
|
setImportModalVisible(false);
|
||||||
setImportPreview([]);
|
setImportPreview([]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1871,7 +1803,7 @@ function PortManagement() {
|
|||||||
<div style={{ padding: '40px' }}>
|
<div style={{ padding: '40px' }}>
|
||||||
<Skeleton active paragraph={{ rows: 6 }} />
|
<Skeleton active paragraph={{ rows: 6 }} />
|
||||||
</div>
|
</div>
|
||||||
) : Object.keys(groupedPorts).length === 0 ? (
|
) : groupedPorts.length === 0 ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -1912,22 +1844,18 @@ function PortManagement() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{Object.entries(groupedPorts)
|
{groupedPorts.map((data, index) => {
|
||||||
.slice(
|
|
||||||
(deviceCardPage - 1) * deviceCardPageSize,
|
|
||||||
deviceCardPage * deviceCardPageSize
|
|
||||||
)
|
|
||||||
.map(([deviceId, data], index) => {
|
|
||||||
const device = data.device;
|
const device = data.device;
|
||||||
|
const deviceId = device?.deviceId;
|
||||||
const devicePorts = data.ports || [];
|
const devicePorts = data.ports || [];
|
||||||
const freeCount = devicePorts.filter(p => p.status === 'free').length;
|
const freeCount = devicePorts.filter(p => p.status === 'free').length;
|
||||||
const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
|
const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
|
||||||
const faultCount = devicePorts.filter(p => p.status === 'fault').length;
|
const faultCount = devicePorts.filter(p => p.status === 'fault').length;
|
||||||
const isExpanded = expandedKeys.includes(deviceId);
|
const isExpanded = deviceId ? expandedKeys.includes(deviceId) : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={deviceId}
|
key={deviceId || index}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: designTokens.borderRadius.lg,
|
borderRadius: designTokens.borderRadius.lg,
|
||||||
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||||
@@ -2140,7 +2068,7 @@ function PortManagement() {
|
|||||||
onChange: setSelectedRowKeys,
|
onChange: setSelectedRowKeys,
|
||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
pageSize: 10,
|
defaultPageSize: 10,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showTotal: total => `共 ${total} 个端口`,
|
showTotal: total => `共 ${total} 个端口`,
|
||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
@@ -2164,8 +2092,8 @@ function PortManagement() {
|
|||||||
pageSize={deviceCardPageSize}
|
pageSize={deviceCardPageSize}
|
||||||
total={deviceCardTotal}
|
total={deviceCardTotal}
|
||||||
onChange={page => {
|
onChange={page => {
|
||||||
setDeviceCardPage(page);
|
|
||||||
setExpandedKeys([]);
|
setExpandedKeys([]);
|
||||||
|
fetchPorts(page);
|
||||||
}}
|
}}
|
||||||
showSizeChanger={false}
|
showSizeChanger={false}
|
||||||
showTotal={(total, range) =>
|
showTotal={(total, range) =>
|
||||||
@@ -2174,14 +2102,9 @@ function PortManagement() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasMoreRef.current && (
|
{ports.length > 0 && (
|
||||||
<div ref={loadMoreRef} style={{ textAlign: 'center', padding: '20px' }}>
|
|
||||||
{portLoadingMore && <Spin tip="加载更多端口..." />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!hasMoreRef.current && ports.length > 0 && (
|
|
||||||
<div style={{ textAlign: 'center', padding: '16px', color: '#999' }}>
|
<div style={{ textAlign: 'center', padding: '16px', color: '#999' }}>
|
||||||
已加载全部 {portTotal} 个端口
|
共 {deviceCardTotal} 个设备,{portTotal} 个端口
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ const SystemSettings = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={18} md={19}>
|
<Col xs={24} sm={18} md={19}>
|
||||||
<Title level={3} style={{ color: '#fff', margin: 0, marginBottom: 6, fontSize: '22px' }}>
|
<Title level={3} style={{ color: '#fff', margin: 0, marginBottom: 6, fontSize: '22px' }}>
|
||||||
机柜管理系统
|
云睿资产管理系统
|
||||||
</Title>
|
</Title>
|
||||||
<Space size={12} wrap style={{ color: 'rgba(255,255,255,0.9)' }}>
|
<Space size={12} wrap style={{ color: 'rgba(255,255,255,0.9)' }}>
|
||||||
<span style={{ fontSize: 14 }}>版本 {settings.app_version?.value || '1.0.0'}</span>
|
<span style={{ fontSize: 14 }}>版本 {settings.app_version?.value || '1.0.0'}</span>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { create } from 'zustand';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
site_name: '机柜管理系统',
|
site_name: '云睿资产',
|
||||||
site_logo: '',
|
site_logo: '',
|
||||||
primary_color: '#667eea',
|
primary_color: '#667eea',
|
||||||
secondary_color: '#764ba2',
|
secondary_color: '#764ba2',
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 279 KiB |
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IDC设备管理系统 - 安装部署脚本入口
|
* 云睿资产管理系统 - 安装部署脚本入口
|
||||||
*
|
*
|
||||||
* 实际逻辑已拆分到 install/ 目录下的各个模块
|
* 实际逻辑已拆分到 install/ 目录下的各个模块
|
||||||
* 此文件仅作为入口,加载并执行 install/index.js
|
* 此文件仅作为入口,加载并执行 install/index.js
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# IDC设备管理系统 - Linux 安装引导脚本
|
# 云睿资产管理系统 - Linux 安装引导脚本
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
#
|
#
|
||||||
# 功能:检测并安装 Node.js,然后运行 install.js
|
# 功能:检测并安装 Node.js,然后运行 install.js
|
||||||
@@ -58,7 +58,7 @@ show_banner() {
|
|||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e " ${CYAN}╔${line}╗${RESET}"
|
echo -e " ${CYAN}╔${line}╗${RESET}"
|
||||||
echo -e " ${CYAN}║${BRIGHT}${WHITE} IDC 设备管理系统 - Linux 安装引导脚本 ${CYAN}║${RESET}"
|
echo -e " ${CYAN}║${BRIGHT}${WHITE} 云睿资产管理系统 - Linux 安装引导脚本 ${CYAN}║${RESET}"
|
||||||
echo -e " ${CYAN}║${DIM} Linux Bootstrap Installation Script ${CYAN}║${RESET}"
|
echo -e " ${CYAN}║${DIM} Linux Bootstrap Installation Script ${CYAN}║${RESET}"
|
||||||
echo -e " ${CYAN}║ v${SCRIPT_VERSION} ${CYAN}║${RESET}"
|
echo -e " ${CYAN}║ v${SCRIPT_VERSION} ${CYAN}║${RESET}"
|
||||||
echo -e " ${CYAN}╚${line}╝${RESET}"
|
echo -e " ${CYAN}╚${line}╝${RESET}"
|
||||||
@@ -237,7 +237,7 @@ show_manual_install_guide() {
|
|||||||
echo -e " ${PIPE} ${CYAN}node -v${RESET} # 应显示 v20.x.x"
|
echo -e " ${PIPE} ${CYAN}node -v${RESET} # 应显示 v20.x.x"
|
||||||
echo -e " ${PIPE} ${CYAN}npm -v${RESET} # 应显示 10.x.x"
|
echo -e " ${PIPE} ${CYAN}npm -v${RESET} # 应显示 10.x.x"
|
||||||
log_divider
|
log_divider
|
||||||
log_info "安装完成后,请重新运行此脚本继续安装 IDC 设备管理系统"
|
log_info "安装完成后,请重新运行此脚本继续安装 云睿资产管理系统"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_and_install_dependencies() {
|
check_and_install_dependencies() {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function parseArgs() {
|
|||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`
|
console.log(`
|
||||||
${colors.bright}IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION}${colors.reset}
|
${colors.bright}云睿资产管理系统 - 安装部署脚本 v${SCRIPT_VERSION}${colors.reset}
|
||||||
|
|
||||||
用法: node install.js [选项]
|
用法: node install.js [选项]
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ async function configureDatabase(cmdArgs) {
|
|||||||
config.dbConfig.port = process.env.MYSQL_PORT || '3306';
|
config.dbConfig.port = process.env.MYSQL_PORT || '3306';
|
||||||
config.dbConfig.username = process.env.MYSQL_USER || 'root';
|
config.dbConfig.username = process.env.MYSQL_USER || 'root';
|
||||||
config.dbConfig.password = process.env.MYSQL_PASSWORD || '';
|
config.dbConfig.password = process.env.MYSQL_PASSWORD || '';
|
||||||
config.dbConfig.database = process.env.MYSQL_DATABASE || 'idc_management';
|
config.dbConfig.database = process.env.MYSQL_DATABASE || 'yunrui_management';
|
||||||
log.info(`MySQL 配置: ${config.dbConfig.host}:${config.dbConfig.port}/${config.dbConfig.database}`);
|
log.info(`MySQL 配置: ${config.dbConfig.host}:${config.dbConfig.port}/${config.dbConfig.database}`);
|
||||||
} else {
|
} else {
|
||||||
const mysqlOption = await select('MySQL 配置方式:', [
|
const mysqlOption = await select('MySQL 配置方式:', [
|
||||||
@@ -74,13 +74,13 @@ async function configureDatabase(cmdArgs) {
|
|||||||
|
|
||||||
async function configureExistingMySQL() {
|
async function configureExistingMySQL() {
|
||||||
console.log('\n' + colors.yellow + '请确保 MySQL 服务已启动并创建了数据库' + colors.reset);
|
console.log('\n' + colors.yellow + '请确保 MySQL 服务已启动并创建了数据库' + colors.reset);
|
||||||
console.log(colors.gray + '创建数据库命令: CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset + '\n');
|
console.log(colors.gray + '创建数据库命令: CREATE DATABASE yunrui_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset + '\n');
|
||||||
|
|
||||||
config.dbConfig.host = await ask('MySQL 主机地址', config.dbConfig.host || 'localhost');
|
config.dbConfig.host = await ask('MySQL 主机地址', config.dbConfig.host || 'localhost');
|
||||||
config.dbConfig.port = await ask('MySQL 端口', config.dbConfig.port || '3306');
|
config.dbConfig.port = await ask('MySQL 端口', config.dbConfig.port || '3306');
|
||||||
config.dbConfig.username = await ask('MySQL 用户名', config.dbConfig.username || 'root');
|
config.dbConfig.username = await ask('MySQL 用户名', config.dbConfig.username || 'root');
|
||||||
config.dbConfig.password = await askPassword('MySQL 密码');
|
config.dbConfig.password = await askPassword('MySQL 密码');
|
||||||
config.dbConfig.database = await ask('数据库名称', config.dbConfig.database || 'idc_management');
|
config.dbConfig.database = await ask('数据库名称', config.dbConfig.database || 'yunrui_management');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installMySQL() {
|
async function installMySQL() {
|
||||||
@@ -94,7 +94,7 @@ async function installMySQL() {
|
|||||||
console.log(' 1. 下载 MySQL: https://dev.mysql.com/downloads/mysql/');
|
console.log(' 1. 下载 MySQL: https://dev.mysql.com/downloads/mysql/');
|
||||||
console.log(' 2. 或使用 XAMPP/WAMP 等集成环境');
|
console.log(' 2. 或使用 XAMPP/WAMP 等集成环境');
|
||||||
console.log(' 3. 安装后创建数据库:');
|
console.log(' 3. 安装后创建数据库:');
|
||||||
console.log(colors.gray + ' CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset);
|
console.log(colors.gray + ' CREATE DATABASE yunrui_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset);
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ async function installMySQL() {
|
|||||||
config.dbConfig.port = '3306';
|
config.dbConfig.port = '3306';
|
||||||
config.dbConfig.username = 'root';
|
config.dbConfig.username = 'root';
|
||||||
config.dbConfig.password = mysqlRootPassword;
|
config.dbConfig.password = mysqlRootPassword;
|
||||||
config.dbConfig.database = 'idc_management';
|
config.dbConfig.database = 'yunrui_management';
|
||||||
|
|
||||||
log.subStep('创建数据库...');
|
log.subStep('创建数据库...');
|
||||||
await createMySQLDatabase(mysqlRootPassword);
|
await createMySQLDatabase(mysqlRootPassword);
|
||||||
@@ -148,7 +148,7 @@ async function installMySQL() {
|
|||||||
port: '3306',
|
port: '3306',
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: mysqlRootPassword,
|
password: mysqlRootPassword,
|
||||||
database: 'idc_management'
|
database: 'yunrui_management'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -191,14 +191,14 @@ async function installMySQLRHEL(rootPassword) {
|
|||||||
async function createMySQLDatabase(rootPassword) {
|
async function createMySQLDatabase(rootPassword) {
|
||||||
log.subStep('创建数据库...');
|
log.subStep('创建数据库...');
|
||||||
|
|
||||||
const createDbSql = 'CREATE DATABASE IF NOT EXISTS idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;';
|
const createDbSql = 'CREATE DATABASE IF NOT EXISTS yunrui_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`mysql -u root -p'${rootPassword}' -e "${createDbSql}"`, {
|
execSync(`mysql -u root -p'${rootPassword}' -e "${createDbSql}"`, {
|
||||||
shell: '/bin/bash',
|
shell: '/bin/bash',
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
log.success('数据库 idc_management 创建成功');
|
log.success('数据库 yunrui_management 创建成功');
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error.stderr ? error.stderr.toString() : error.message;
|
const errorMsg = error.stderr ? error.stderr.toString() : error.message;
|
||||||
@@ -210,7 +210,7 @@ async function createMySQLDatabase(rootPassword) {
|
|||||||
shell: '/bin/bash',
|
shell: '/bin/bash',
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
log.success('数据库 idc_management 创建成功');
|
log.success('数据库 yunrui_management 创建成功');
|
||||||
return;
|
return;
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
log.warning(`无密码连接也失败: ${e2.message}`);
|
log.warning(`无密码连接也失败: ${e2.message}`);
|
||||||
@@ -220,7 +220,7 @@ async function createMySQLDatabase(rootPassword) {
|
|||||||
log.warning(`数据库自动创建失败: ${errorMsg.trim()}`);
|
log.warning(`数据库自动创建失败: ${errorMsg.trim()}`);
|
||||||
log.info('请手动创建数据库:');
|
log.info('请手动创建数据库:');
|
||||||
console.log(colors.cyan + ' mysql -u root -p' + colors.reset);
|
console.log(colors.cyan + ' mysql -u root -p' + colors.reset);
|
||||||
console.log(colors.cyan + ' CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset);
|
console.log(colors.cyan + ' CREATE DATABASE yunrui_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function generateBackendEnv() {
|
|||||||
try {
|
try {
|
||||||
const jwtSecret = generateSecretKey(64);
|
const jwtSecret = generateSecretKey(64);
|
||||||
|
|
||||||
let envContent = `# IDC设备管理系统 - 环境配置
|
let envContent = `# 云睿资产管理系统 - 环境配置
|
||||||
# 生成时间: ${new Date().toISOString()}
|
# 生成时间: ${new Date().toISOString()}
|
||||||
|
|
||||||
# ==============================================
|
# ==============================================
|
||||||
@@ -42,7 +42,7 @@ DB_TYPE=${config.dbType}
|
|||||||
if (config.dbType === 'sqlite') {
|
if (config.dbType === 'sqlite') {
|
||||||
envContent += `
|
envContent += `
|
||||||
# SQLite 配置
|
# SQLite 配置
|
||||||
DB_PATH=./idc_management.db
|
DB_PATH=./yunrui_management.db
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
envContent += `
|
envContent += `
|
||||||
@@ -85,7 +85,7 @@ function generatePM2Config() {
|
|||||||
|
|
||||||
const pm2Config = {
|
const pm2Config = {
|
||||||
apps: [{
|
apps: [{
|
||||||
name: 'idc-backend',
|
name: 'yunrui_asset-backend',
|
||||||
script: './server.js',
|
script: './server.js',
|
||||||
cwd: path.join(__dirname, '..', 'backend'),
|
cwd: path.join(__dirname, '..', 'backend'),
|
||||||
instances: 1,
|
instances: 1,
|
||||||
@@ -106,7 +106,7 @@ function generatePM2Config() {
|
|||||||
|
|
||||||
if (config.frontendDeploy === 'pm2') {
|
if (config.frontendDeploy === 'pm2') {
|
||||||
pm2Config.apps.push({
|
pm2Config.apps.push({
|
||||||
name: 'idc-frontend',
|
name: 'yunrui_asset-frontend',
|
||||||
script: 'serve',
|
script: 'serve',
|
||||||
cwd: path.join(__dirname, '..', 'frontend'),
|
cwd: path.join(__dirname, '..', 'frontend'),
|
||||||
args: `-s dist -l ${config.frontendPort}`,
|
args: `-s dist -l ${config.frontendPort}`,
|
||||||
@@ -145,12 +145,12 @@ function generateNginxConfig() {
|
|||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
frontendPath = path.join(__dirname, '..', 'frontend', 'dist').replace(/\\/g, '/');
|
frontendPath = path.join(__dirname, '..', 'frontend', 'dist').replace(/\\/g, '/');
|
||||||
} else {
|
} else {
|
||||||
frontendPath = '/var/www/idc';
|
frontendPath = '/var/www/yunrui_asset';
|
||||||
}
|
}
|
||||||
|
|
||||||
config.nginxRoot = frontendPath;
|
config.nginxRoot = frontendPath;
|
||||||
|
|
||||||
const nginxConfig = `# IDC设备管理系统 - Nginx配置
|
const nginxConfig = `# 云睿资产管理系统 - Nginx配置
|
||||||
# 生成时间: ${new Date().toISOString()}
|
# 生成时间: ${new Date().toISOString()}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -210,8 +210,8 @@ server {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
fs.writeFileSync(path.join(deployDir, 'nginx-idc.conf'), nginxConfig);
|
fs.writeFileSync(path.join(deployDir, 'nginx-yunrui.conf'), nginxConfig);
|
||||||
log.success('Nginx 配置文件已生成 (deploy/nginx-idc.conf)');
|
log.success('Nginx 配置文件已生成 (deploy/nginx-yunrui.conf)');
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
log.info('Windows Nginx 配置路径示例:');
|
log.info('Windows Nginx 配置路径示例:');
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ function printSummary() {
|
|||||||
|
|
||||||
log.section('服务管理命令');
|
log.section('服务管理命令');
|
||||||
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 status${colors.reset} 查看服务状态`);
|
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 status${colors.reset} 查看服务状态`);
|
||||||
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 logs idc-backend${colors.reset} 查看后端日志`);
|
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 logs yunrui_asset-backend${colors.reset} 查看后端日志`);
|
||||||
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 restart idc-backend${colors.reset} 重启后端`);
|
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 restart yunrui_asset-backend${colors.reset} 重启后端`);
|
||||||
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 stop idc-backend${colors.reset} 停止后端`);
|
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 stop yunrui_asset-backend${colors.reset} 停止后端`);
|
||||||
|
|
||||||
log.divider();
|
log.divider();
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ async function main() {
|
|||||||
initLogFile(LOG_DIR);
|
initLogFile(LOG_DIR);
|
||||||
|
|
||||||
log.banner(
|
log.banner(
|
||||||
'IDC 设备管理系统',
|
'云睿资产管理系统',
|
||||||
'安装部署脚本',
|
'安装部署脚本',
|
||||||
SCRIPT_VERSION
|
SCRIPT_VERSION
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -180,25 +180,25 @@ async function startServices() {
|
|||||||
|
|
||||||
log.info('停止已有服务...');
|
log.info('停止已有服务...');
|
||||||
const stopCmd = process.platform === 'win32'
|
const stopCmd = process.platform === 'win32'
|
||||||
? 'pm2 stop idc-backend idc-frontend 2>nul || exit 0'
|
? 'pm2 stop yunrui_asset-backend yunrui_asset-frontend 2>nul || exit 0'
|
||||||
: 'pm2 stop idc-backend idc-frontend 2>/dev/null || true';
|
: 'pm2 stop yunrui_asset-backend yunrui_asset-frontend 2>/dev/null || true';
|
||||||
runCommand(stopCmd, { silent: true });
|
runCommand(stopCmd, { silent: true });
|
||||||
|
|
||||||
log.info('启动后端服务...');
|
log.info('启动后端服务...');
|
||||||
const backendResult = runCommand(
|
const backendResult = runCommand(
|
||||||
`pm2 start server.js --name "idc-backend" --env ${config.nodeEnv}`,
|
`pm2 start server.js --name "yunrui_asset-backend" --env ${config.nodeEnv}`,
|
||||||
{ cwd: path.join(__dirname, '..', 'backend') }
|
{ cwd: path.join(__dirname, '..', 'backend') }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (backendResult.success) {
|
if (backendResult.success) {
|
||||||
log.success(`后端服务已启动 (端口: ${config.backendPort})`);
|
log.success(`后端服务已启动 (端口: ${config.backendPort})`);
|
||||||
rollbackSteps.push(() => {
|
rollbackSteps.push(() => {
|
||||||
runCommand('pm2 stop idc-backend', { silent: true });
|
runCommand('pm2 stop yunrui_asset-backend', { silent: true });
|
||||||
runCommand('pm2 delete idc-backend', { silent: true });
|
runCommand('pm2 delete yunrui_asset-backend', { silent: true });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.error('后端服务启动失败');
|
log.error('后端服务启动失败');
|
||||||
log.info('请检查后端日志: pm2 logs idc-backend');
|
log.info('请检查后端日志: pm2 logs yunrui_asset-backend');
|
||||||
throw new Error('Backend service start failed');
|
throw new Error('Backend service start failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,19 +208,19 @@ async function startServices() {
|
|||||||
|
|
||||||
log.info('启动前端服务...');
|
log.info('启动前端服务...');
|
||||||
const frontendResult = runCommand(
|
const frontendResult = runCommand(
|
||||||
`pm2 start serve --name "idc-frontend" -- -s dist -l ${config.frontendPort}`,
|
`pm2 start serve --name "yunrui_asset-frontend" -- -s dist -l ${config.frontendPort}`,
|
||||||
{ cwd: path.join(__dirname, '..', 'frontend') }
|
{ cwd: path.join(__dirname, '..', 'frontend') }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (frontendResult.success) {
|
if (frontendResult.success) {
|
||||||
log.success(`前端服务已启动 (端口: ${config.frontendPort})`);
|
log.success(`前端服务已启动 (端口: ${config.frontendPort})`);
|
||||||
rollbackSteps.push(() => {
|
rollbackSteps.push(() => {
|
||||||
runCommand('pm2 stop idc-frontend', { silent: true });
|
runCommand('pm2 stop yunrui_asset-frontend', { silent: true });
|
||||||
runCommand('pm2 delete idc-frontend', { silent: true });
|
runCommand('pm2 delete yunrui_asset-frontend', { silent: true });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.error('前端服务启动失败');
|
log.error('前端服务启动失败');
|
||||||
log.info('请检查前端日志: pm2 logs idc-frontend');
|
log.info('请检查前端日志: pm2 logs yunrui_asset-frontend');
|
||||||
throw new Error('Frontend service start failed');
|
throw new Error('Frontend service start failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ async function autoConfigureNginx() {
|
|||||||
if (!installed) {
|
if (!installed) {
|
||||||
log.error('Nginx 自动安装失败');
|
log.error('Nginx 自动安装失败');
|
||||||
log.info('请手动安装 Nginx 后执行:');
|
log.info('请手动安装 Nginx 后执行:');
|
||||||
console.log(` ${colors.cyan}sudo cp deploy/nginx-idc.conf /etc/nginx/sites-available/idc${colors.reset}`);
|
console.log(` ${colors.cyan}sudo cp deploy/nginx-yunrui.conf /etc/nginx/sites-available/yunrui_asset${colors.reset}`);
|
||||||
console.log(` ${colors.cyan}sudo ln -sf /etc/nginx/sites-available/idc /etc/nginx/sites-enabled/idc${colors.reset}`);
|
console.log(` ${colors.cyan}sudo ln -sf /etc/nginx/sites-available/yunrui_asset /etc/nginx/sites-enabled/yunrui_asset${colors.reset}`);
|
||||||
console.log(` ${colors.cyan}sudo nginx -t && sudo systemctl restart nginx${colors.reset}`);
|
console.log(` ${colors.cyan}sudo nginx -t && sudo systemctl restart nginx${colors.reset}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ async function autoConfigureNginx() {
|
|||||||
wwwDir = '/www/wwwroot/idc';
|
wwwDir = '/www/wwwroot/idc';
|
||||||
log.info('检测到宝塔面板 Nginx 环境');
|
log.info('检测到宝塔面板 Nginx 环境');
|
||||||
} else {
|
} else {
|
||||||
wwwDir = '/var/www/idc';
|
wwwDir = '/var/www/yunrui_asset';
|
||||||
}
|
}
|
||||||
|
|
||||||
const distDir = path.join(__dirname, '..', 'frontend', 'dist');
|
const distDir = path.join(__dirname, '..', 'frontend', 'dist');
|
||||||
@@ -244,7 +244,7 @@ async function autoConfigureNginx() {
|
|||||||
|
|
||||||
config.nginxRoot = wwwDir;
|
config.nginxRoot = wwwDir;
|
||||||
|
|
||||||
const configSource = path.join(__dirname, '..', 'deploy', 'nginx-idc.conf');
|
const configSource = path.join(__dirname, '..', 'deploy', 'nginx-yunrui.conf');
|
||||||
|
|
||||||
if (!fs.existsSync(configSource)) {
|
if (!fs.existsSync(configSource)) {
|
||||||
log.error('Nginx 配置文件不存在,请先运行安装脚本');
|
log.error('Nginx 配置文件不存在,请先运行安装脚本');
|
||||||
@@ -282,7 +282,7 @@ async function autoConfigureNginx() {
|
|||||||
|
|
||||||
if (useSitesAvailable) {
|
if (useSitesAvailable) {
|
||||||
log.info('创建站点软链接...');
|
log.info('创建站点软链接...');
|
||||||
runCommand(`${sudoPrefix}ln -sf "${configDest}" /etc/nginx/sites-enabled/idc.conf`);
|
runCommand(`${sudoPrefix}ln -sf "${configDest}" /etc/nginx/sites-enabled/yunrui_asset.conf`);
|
||||||
|
|
||||||
if (fs.existsSync('/etc/nginx/sites-enabled/default')) {
|
if (fs.existsSync('/etc/nginx/sites-enabled/default')) {
|
||||||
log.info('删除默认站点配置...');
|
log.info('删除默认站点配置...');
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async function rollback() {
|
|||||||
const configFiles = [
|
const configFiles = [
|
||||||
path.join(__dirname, '..', 'backend', '.env'),
|
path.join(__dirname, '..', 'backend', '.env'),
|
||||||
path.join(__dirname, '..', 'ecosystem.config.js'),
|
path.join(__dirname, '..', 'ecosystem.config.js'),
|
||||||
path.join(__dirname, '..', 'deploy', 'nginx-idc.conf'),
|
path.join(__dirname, '..', 'deploy', 'nginx-yunrui.conf'),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const file of configFiles) {
|
for (const file of configFiles) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "idc-device-management",
|
"name": "yunrui-asset-management",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "idc-device-management",
|
"name": "yunrui-asset-management",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "idc-device-management",
|
"name": "yunrui-asset-management",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"description": "IDC设备管理系统",
|
"description": "云睿资产管理系统",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently \"npm run start:backend\" \"npm run start:frontend\"",
|
"start": "concurrently \"npm run start:backend\" \"npm run start:frontend\"",
|
||||||
"start:backend": "cd backend && npm run dev",
|
"start:backend": "cd backend && npm run dev",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IDC设备管理系统 - 卸载脚本 v2.0.0
|
* 云睿资产管理系统 - 卸载脚本 v2.0.0
|
||||||
*
|
*
|
||||||
* 功能说明:
|
* 功能说明:
|
||||||
* - 停止并删除 PM2 管理的后端和前端服务
|
* - 停止并删除 PM2 管理的后端和前端服务
|
||||||
* - 清理 Nginx 配置文件(可选)
|
* - 清理 Nginx 配置文件(可选)
|
||||||
* - 删除生成的配置文件(.env、ecosystem.config.js、nginx-idc.conf)
|
* - 删除生成的配置文件(.env、ecosystem.config.js、nginx-yunrui.conf)
|
||||||
* - 可选删除数据库文件(SQLite)
|
* - 可选删除数据库文件(SQLite)
|
||||||
* - 可选删除 node_modules 和构建产物
|
* - 可选删除 node_modules 和构建产物
|
||||||
*
|
*
|
||||||
@@ -58,7 +58,7 @@ function parseArgs() {
|
|||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`
|
console.log(`
|
||||||
${colors.bright}IDC设备管理系统 - 卸载脚本 v${SCRIPT_VERSION}${colors.reset}
|
${colors.bright}云睿资产管理系统 - 卸载脚本 v${SCRIPT_VERSION}${colors.reset}
|
||||||
|
|
||||||
${colors.bright}用法:${colors.reset} node uninstall.js [选项]
|
${colors.bright}用法:${colors.reset} node uninstall.js [选项]
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ function commandExists(command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPM2ServiceNames() {
|
function getPM2ServiceNames() {
|
||||||
const defaultNames = { backendName: 'idc-backend', frontendName: 'idc-frontend' };
|
const defaultNames = { backendName: 'yunrui_asset-backend', frontendName: 'yunrui_asset-frontend' };
|
||||||
const configPath = path.join(__dirname, 'deploy', 'ecosystem.config.js');
|
const configPath = path.join(__dirname, 'deploy', 'ecosystem.config.js');
|
||||||
|
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(configPath)) {
|
||||||
@@ -123,7 +123,7 @@ function getPM2ServiceNames() {
|
|||||||
if (app.name && app.name.startsWith('idc-')) {
|
if (app.name && app.name.startsWith('idc-')) {
|
||||||
if (app.name.includes('backend') || (app.script && app.script.includes('server'))) {
|
if (app.name.includes('backend') || (app.script && app.script.includes('server'))) {
|
||||||
names.backendName = app.name;
|
names.backendName = app.name;
|
||||||
} else if (app.name.includes('frontend') || app.name === 'idc-frontend') {
|
} else if (app.name.includes('frontend') || app.name === 'yunrui_asset-frontend') {
|
||||||
names.frontendName = app.name;
|
names.frontendName = app.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ function getPM2ServiceNames() {
|
|||||||
function getDatabaseConfig() {
|
function getDatabaseConfig() {
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
dbType: 'sqlite',
|
dbType: 'sqlite',
|
||||||
sqliteDbPath: path.join(__dirname, 'backend', 'idc_management.db')
|
sqliteDbPath: path.join(__dirname, 'backend', 'yunrui_management.db')
|
||||||
};
|
};
|
||||||
|
|
||||||
const envPath = path.join(__dirname, 'backend', '.env');
|
const envPath = path.join(__dirname, 'backend', '.env');
|
||||||
@@ -304,13 +304,13 @@ async function cleanupNginxConfig(cmdArgs) {
|
|||||||
const sudoPrefix = root ? '' : 'sudo ';
|
const sudoPrefix = root ? '' : 'sudo ';
|
||||||
|
|
||||||
const idcConfigPaths = [
|
const idcConfigPaths = [
|
||||||
{ path: '/etc/nginx/sites-available/idc', label: 'sites-available/idc' },
|
{ path: '/etc/nginx/sites-available/yunrui_asset', label: 'sites-available/yunrui_asset' },
|
||||||
{ path: '/etc/nginx/sites-enabled/idc', label: 'sites-enabled/idc(软链接)' },
|
{ path: '/etc/nginx/sites-enabled/yunrui_asset', label: 'sites-enabled/yunrui_asset(软链接)' },
|
||||||
{ path: '/etc/nginx/conf.d/idc', label: 'conf.d/idc' },
|
{ path: '/etc/nginx/conf.d/idc', label: 'conf.d/idc' },
|
||||||
{ path: '/etc/nginx/conf.d/idc.conf', label: 'conf.d/idc.conf' },
|
{ path: '/etc/nginx/conf.d/idc.conf', label: 'conf.d/idc.conf' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const wwwDir = '/var/www/idc';
|
const wwwDir = '/var/www/yunrui_asset';
|
||||||
const defaultSitePath = '/etc/nginx/sites-enabled/default';
|
const defaultSitePath = '/etc/nginx/sites-enabled/default';
|
||||||
const defaultAvailablePath = '/etc/nginx/sites-available/default';
|
const defaultAvailablePath = '/etc/nginx/sites-available/default';
|
||||||
|
|
||||||
@@ -331,15 +331,15 @@ async function cleanupNginxConfig(cmdArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!configRemoved) {
|
if (!configRemoved) {
|
||||||
log.info('未找到 IDC Nginx 站点配置文件');
|
log.info('未找到云睿资产 Nginx 站点配置文件');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(wwwDir)) {
|
if (fs.existsSync(wwwDir)) {
|
||||||
log.info('清理前端部署目录...');
|
log.info('清理前端部署目录...');
|
||||||
const result = runCommand(`${sudoPrefix}rm -rf "${wwwDir}"`, { silent: true });
|
const result = runCommand(`${sudoPrefix}rm -rf "${wwwDir}"`, { silent: true });
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
log.success('前端部署目录已删除 (/var/www/idc)');
|
log.success('前端部署目录已删除 (/var/www/yunrui_asset)');
|
||||||
deletedItems.push({ type: '目录', name: '/var/www/idc' });
|
deletedItems.push({ type: '目录', name: '/var/www/yunrui_asset' });
|
||||||
} else {
|
} else {
|
||||||
log.warning('前端部署目录删除失败,请手动清理');
|
log.warning('前端部署目录删除失败,请手动清理');
|
||||||
}
|
}
|
||||||
@@ -384,7 +384,7 @@ async function cleanupConfigFiles() {
|
|||||||
const filesToDelete = [
|
const filesToDelete = [
|
||||||
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)', type: '配置文件' },
|
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)', type: '配置文件' },
|
||||||
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置 (ecosystem.config.js)', type: '配置文件' },
|
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置 (ecosystem.config.js)', type: '配置文件' },
|
||||||
{ path: path.join(__dirname, 'deploy', 'nginx-idc.conf'), name: 'Nginx 配置 (nginx-idc.conf)', type: '配置文件' }
|
{ path: path.join(__dirname, 'deploy', 'nginx-yunrui.conf'), name: 'Nginx 配置 (nginx-yunrui.conf)', type: '配置文件' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const file of filesToDelete) {
|
for (const file of filesToDelete) {
|
||||||
@@ -513,7 +513,7 @@ async function cleanupDatabase(cmdArgs) {
|
|||||||
log.section('MySQL 删除方法');
|
log.section('MySQL 删除方法');
|
||||||
console.log(` ${ICONS.pipe} 方法1 - MySQL 命令行:`);
|
console.log(` ${ICONS.pipe} 方法1 - MySQL 命令行:`);
|
||||||
console.log(` ${ICONS.pipe} ${colors.cyan}mysql -u root -p${colors.reset}`);
|
console.log(` ${ICONS.pipe} ${colors.cyan}mysql -u root -p${colors.reset}`);
|
||||||
console.log(` ${ICONS.pipe} ${colors.cyan}DROP DATABASE idc_management;${colors.reset}`);
|
console.log(` ${ICONS.pipe} ${colors.cyan}DROP DATABASE yunrui_management;${colors.reset}`);
|
||||||
console.log(` ${ICONS.pipe} 方法2 - 使用数据库管理工具(如 Navicat、DBeaver、phpMyAdmin)`);
|
console.log(` ${ICONS.pipe} 方法2 - 使用数据库管理工具(如 Navicat、DBeaver、phpMyAdmin)`);
|
||||||
console.log(` ${ICONS.pipe} 方法3 - 如果不再需要 MySQL 数据,可直接卸载 MySQL 服务`);
|
console.log(` ${ICONS.pipe} 方法3 - 如果不再需要 MySQL 数据,可直接卸载 MySQL 服务`);
|
||||||
}
|
}
|
||||||
@@ -681,15 +681,15 @@ function showDryRunPreview(cmdArgs) {
|
|||||||
console.log(`\n ${ICONS.pointer} ${colors.bright}2. 清理 Nginx 配置${colors.reset}`);
|
console.log(`\n ${ICONS.pointer} ${colors.bright}2. 清理 Nginx 配置${colors.reset}`);
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
const nginxConfigPaths = ['/etc/nginx/sites-available/idc', '/etc/nginx/sites-enabled/idc',
|
const nginxConfigPaths = ['/etc/nginx/sites-available/yunrui_asset', '/etc/nginx/sites-enabled/yunrui_asset',
|
||||||
'/etc/nginx/conf.d/idc', '/etc/nginx/conf.d/idc.conf'];
|
'/etc/nginx/conf.d/idc', '/etc/nginx/conf.d/idc.conf'];
|
||||||
nginxConfigPaths.forEach(p => {
|
nginxConfigPaths.forEach(p => {
|
||||||
if (fs.existsSync(p)) {
|
if (fs.existsSync(p)) {
|
||||||
console.log(` ${ICONS.pipe} 删除 IDC 站点配置: ${p}`);
|
console.log(` ${ICONS.pipe} 删除云睿资产站点配置: ${p}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (fs.existsSync('/var/www/idc')) {
|
if (fs.existsSync('/var/www/yunrui_asset')) {
|
||||||
console.log(` ${ICONS.pipe} 删除前端部署目录 (/var/www/idc)`);
|
console.log(` ${ICONS.pipe} 删除前端部署目录 (/var/www/yunrui_asset)`);
|
||||||
}
|
}
|
||||||
if (!fs.existsSync('/etc/nginx/sites-enabled/default') && fs.existsSync('/etc/nginx/sites-available/default')) {
|
if (!fs.existsSync('/etc/nginx/sites-enabled/default') && fs.existsSync('/etc/nginx/sites-available/default')) {
|
||||||
console.log(` ${ICONS.pipe} 恢复 Nginx 默认站点`);
|
console.log(` ${ICONS.pipe} 恢复 Nginx 默认站点`);
|
||||||
@@ -707,7 +707,7 @@ function showDryRunPreview(cmdArgs) {
|
|||||||
const configFiles = [
|
const configFiles = [
|
||||||
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)' },
|
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)' },
|
||||||
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置' },
|
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置' },
|
||||||
{ path: path.join(__dirname, 'deploy', 'nginx-idc.conf'), name: 'Nginx 配置' }
|
{ path: path.join(__dirname, 'deploy', 'nginx-yunrui.conf'), name: 'Nginx 配置' }
|
||||||
];
|
];
|
||||||
configFiles.forEach(f => {
|
configFiles.forEach(f => {
|
||||||
if (fs.existsSync(f.path)) {
|
if (fs.existsSync(f.path)) {
|
||||||
@@ -825,12 +825,12 @@ async function main() {
|
|||||||
initLogFile(LOG_DIR);
|
initLogFile(LOG_DIR);
|
||||||
|
|
||||||
log.banner(
|
log.banner(
|
||||||
'IDC 设备管理系统',
|
'云睿资产管理系统',
|
||||||
'卸载脚本',
|
'卸载脚本',
|
||||||
SCRIPT_VERSION
|
SCRIPT_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
log.warning('此脚本将卸载 IDC 设备管理系统');
|
log.warning('此脚本将卸载 云睿资产管理系统');
|
||||||
|
|
||||||
if (cmdArgs.force) {
|
if (cmdArgs.force) {
|
||||||
log.info('运行模式: 强制卸载(无需确认)');
|
log.info('运行模式: 强制卸载(无需确认)');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IDC设备管理系统 - 一键更新脚本
|
* 云睿资产管理系统 - 一键更新脚本
|
||||||
* 功能:拉取最新代码 → 安装依赖 → 数据库迁移 → 重建前端 → 重启服务 → 健康检查
|
* 功能:拉取最新代码 → 安装依赖 → 数据库迁移 → 重建前端 → 重启服务 → 健康检查
|
||||||
* 版本:2.0.0
|
* 版本:2.0.0
|
||||||
*/
|
*/
|
||||||
@@ -71,7 +71,7 @@ function parseArgs() {
|
|||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`
|
console.log(`
|
||||||
${colors.bright}IDC设备管理系统 - 一键更新脚本 v${SCRIPT_VERSION}${colors.reset}
|
${colors.bright}云睿资产管理系统 - 一键更新脚本 v${SCRIPT_VERSION}${colors.reset}
|
||||||
|
|
||||||
${colors.bright}用法:${colors.reset} node update.js [选项]
|
${colors.bright}用法:${colors.reset} node update.js [选项]
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ function findBackendServiceName() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const proc of processes) {
|
for (const proc of processes) {
|
||||||
if (proc.name === 'server' || proc.name === 'idc-backend' || proc.name === 'backend') {
|
if (proc.name === 'server' || proc.name === 'yunrui_asset-backend' || proc.name === 'backend') {
|
||||||
return proc.name;
|
return proc.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@ function findFrontendServiceName() {
|
|||||||
|
|
||||||
if (script.includes('vite') || scriptPath.includes('vite') ||
|
if (script.includes('vite') || scriptPath.includes('vite') ||
|
||||||
script.includes('react') || script.includes('nginx') ||
|
script.includes('react') || script.includes('nginx') ||
|
||||||
proc.name === 'frontend' || proc.name === 'idc-frontend') {
|
proc.name === 'frontend' || proc.name === 'yunrui_asset-frontend') {
|
||||||
return proc.name;
|
return proc.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,7 @@ function backupDatabase(options) {
|
|||||||
const dbType = dbTypeMatch ? dbTypeMatch[1].toLowerCase() : 'sqlite';
|
const dbType = dbTypeMatch ? dbTypeMatch[1].toLowerCase() : 'sqlite';
|
||||||
|
|
||||||
if (dbType === 'sqlite') {
|
if (dbType === 'sqlite') {
|
||||||
const dbPath = path.join(__dirname, 'backend', 'idc_management.db');
|
const dbPath = path.join(__dirname, 'backend', 'yunrui_management.db');
|
||||||
if (!fs.existsSync(dbPath)) {
|
if (!fs.existsSync(dbPath)) {
|
||||||
log.warning('SQLite 数据库文件不存在,跳过备份');
|
log.warning('SQLite 数据库文件不存在,跳过备份');
|
||||||
return { success: true, skipped: true };
|
return { success: true, skipped: true };
|
||||||
@@ -576,7 +576,7 @@ function syncFrontendToNginx(options) {
|
|||||||
if (isBtPanel) {
|
if (isBtPanel) {
|
||||||
nginxRoot = '/www/wwwroot/idc';
|
nginxRoot = '/www/wwwroot/idc';
|
||||||
} else {
|
} else {
|
||||||
nginxRoot = '/var/www/idc';
|
nginxRoot = '/var/www/yunrui_asset';
|
||||||
}
|
}
|
||||||
log.info(`未配置 nginxRoot,自动检测: ${nginxRoot} (${isBtPanel ? '宝塔' : '标准'})`);
|
log.info(`未配置 nginxRoot,自动检测: ${nginxRoot} (${isBtPanel ? '宝塔' : '标准'})`);
|
||||||
}
|
}
|
||||||
@@ -819,7 +819,7 @@ function rollback(reason) {
|
|||||||
log.step('执行回滚...');
|
log.step('执行回滚...');
|
||||||
|
|
||||||
if (rollbackInfo.backupDbPath && fs.existsSync(rollbackInfo.backupDbPath)) {
|
if (rollbackInfo.backupDbPath && fs.existsSync(rollbackInfo.backupDbPath)) {
|
||||||
const dbPath = path.join(__dirname, 'backend', 'idc_management.db');
|
const dbPath = path.join(__dirname, 'backend', 'yunrui_management.db');
|
||||||
fs.copyFileSync(rollbackInfo.backupDbPath, dbPath);
|
fs.copyFileSync(rollbackInfo.backupDbPath, dbPath);
|
||||||
log.success('数据库已回滚');
|
log.success('数据库已回滚');
|
||||||
}
|
}
|
||||||
@@ -897,7 +897,7 @@ async function main() {
|
|||||||
initLogFile(LOG_DIR);
|
initLogFile(LOG_DIR);
|
||||||
|
|
||||||
log.banner(
|
log.banner(
|
||||||
'IDC 设备管理系统',
|
'云睿资产管理系统',
|
||||||
'一键更新脚本',
|
'一键更新脚本',
|
||||||
SCRIPT_VERSION
|
SCRIPT_VERSION
|
||||||
);
|
);
|
||||||
|
|||||||