From 2905fb4fa307e2416af54de2bbc796502fccfd0d Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Tue, 17 Mar 2026 14:13:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(install):=20=E6=B7=BB=E5=8A=A0Linux?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=BC=95=E5=AF=BC=E8=84=9A=E6=9C=AC=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=89=E8=A3=85/=E5=8D=B8=E8=BD=BD?= =?UTF-8?q?=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加install.sh脚本支持Linux系统自动安装Node.js和依赖 优化install.js和uninstall.js脚本,增加非交互模式、健康检查、日志记录等功能 更新README.md和DEPLOYMENT.md文档,提供更详细的安装/卸载指南 在package.json中添加uninstall脚本命令 --- DEPLOYMENT.md | 145 ++++++++++++++--- README.md | 111 ++++++++++++- install.js | 438 ++++++++++++++++++++++++++++++++++++-------------- install.sh | 420 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- uninstall.js | 244 ++++++++++++++++++++++------ 6 files changed, 1159 insertions(+), 202 deletions(-) create mode 100644 install.sh diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index e5434d6..21efab1 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -25,6 +25,8 @@ 我们提供了交互式安装脚本,自动完成环境检测、依赖安装、数据库初始化和服务启动。 +#### Linux 用户(支持自动安装 Node.js) + ```bash # 克隆项目(选择其一) # Gitee(国内推荐) @@ -33,36 +35,63 @@ git clone https://gitee.com/zhang96110/idc_assest.git git clone https://github.com/gituib/idc_assest.git cd idc_assest -#如果没安装nodejs需要先安装 +# 方式1:使用 Shell 引导脚本(推荐,无需预装 Node.js) +chmod +x install.sh && ./install.sh -**Ubuntu/Debian:** -```bash -curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - -sudo apt-get install -y nodejs +# 方式2:一键安装(从远程) +curl -fsSL https://gitee.com/zhang96110/idc_assest/raw/main/install.sh | bash ``` -**CentOS/RHEL/Fedora:** -```bash -curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - -sudo yum install -y nodejs +**Shell 脚本功能:** +- ✅ 自动检测 Linux 发行版(Ubuntu/Debian/CentOS/Arch) +- ✅ 自动安装 Node.js 20.x(使用 NodeSource 或 nvm) +- ✅ 自动安装系统依赖(git、curl 等) +- ✅ 调用 install.js 完成后续安装 -系统已经安装了nodejs的可以直接执行一键安装脚本 +#### Windows 用户 -# 运行部署脚本 -npm run deploy -# 或 +```powershell +# 克隆项目 +git clone https://gitee.com/zhang96110/idc_assest.git +cd idc_assest + +# 运行安装脚本(需要先安装 Node.js) node install.js + +# 或使用 npm 命令 +npm run deploy +``` + +#### 非交互式安装(快速部署) + +```bash +# 使用默认配置快速安装 +node install.js -y # 交互式安装 + +# 指定数据库类型 +node install.js -y --db=mysql # 使用 MySQL +node install.js -y --db=sqlite # 使用 SQLite(默认) + +# 指定端口 +node install.js -y --port=3000 # 后端端口 3000 + +# 跳过前端构建 +node install.js -y --skip-build ``` **脚本功能:** - ✅ 自动检测 Node.js、npm、PM2、Nginx -- ✅ Linux 系统支持自动安装 Node.js(交互式) +- ✅ **Linux 自动安装 Node.js**(Ubuntu/Debian/CentOS/Arch) +- ✅ **MySQL 连接测试**(配置后自动验证) +- ✅ **安装后健康检查**(验证服务是否正常) - ✅ 交互式配置数据库(SQLite/MySQL) +- ✅ 交互式选择运行环境(development/production) - ✅ 交互式选择前端部署方式(Nginx/PM2 serve) - ✅ 自动安装项目依赖 - ✅ 自动初始化数据库 - ✅ 自动构建前端项目 - ✅ 使用 PM2 启动和管理服务 +- ✅ 日志持久化保存 --- @@ -775,13 +804,37 @@ time curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8000/health ## 更新升级 -### 方式一:一键更新脚本(推荐) +### 方式一:一键更新脚本(推荐)⭐ ```bash -npm run update +# 交互式更新 +node update.js + +# 查看帮助 +node update.js --help + +# 模拟运行(不执行实际操作) +node update.js --dry-run + +# 跳过某些步骤 +node update.js --skip-git # 跳过 Git 拉取 +node update.js --skip-backup # 跳过数据库备份 +node update.js --skip-migrate # 跳过数据库迁移 +node update.js --skip-build # 跳过前端构建 +node update.js --skip-deps # 跳过依赖安装 +node update.js --skip-restart # 跳过服务重启 + +# 强制执行(忽略锁文件) +node update.js --force ``` -功能:自动备份 → 拉取代码 → 更新依赖 → 重建前端 → 重启服务 +**智能检测功能:** +- ✅ **依赖安装智能跳过** - 检测 package.json 变化,无变化则跳过 +- ✅ **前端构建智能跳过** - 检测源码变化,无变化则跳过 +- ✅ **数据库自动备份** - 更新前自动备份,支持回滚 +- ✅ **健康检查验证** - 更新后验证服务是否正常 +- ✅ **日志持久化** - 更新日志保存到 `logs/update_*.log` +- ✅ **锁机制** - 防止多个更新进程同时运行 ### 方式二:手动更新 @@ -880,6 +933,61 @@ module.exports = { --- +## 卸载系统 + +### 方式一:一键卸载脚本(推荐)⭐ + +```bash +# 交互式卸载 +node uninstall.js + +# 查看帮助 +node uninstall.js --help + +# 强制卸载(无需确认) +node uninstall.js --force + +# 卸载前自动备份 +node uninstall.js --backup + +# 跳过某些步骤 +node uninstall.js --skip-db # 跳过数据库删除 +node uninstall.js --skip-deps # 跳过依赖删除 +``` + +**卸载功能:** +- ✅ 停止并删除 PM2 服务 +- ✅ 清理 Nginx 配置 +- ✅ 删除生成的配置文件 +- ✅ 可选删除数据库文件 +- ✅ 可选删除 node_modules +- ✅ 卸载前自动备份 +- ✅ 日志持久化保存 + +### 方式二:手动卸载 + +```bash +# 1. 停止服务 +pm2 stop idc-backend +pm2 delete idc-backend +pm2 save + +# 2. 删除配置文件 +rm backend/.env +rm deploy/ecosystem.config.js +rm deploy/nginx-idc.conf + +# 3. 删除数据库(可选) +rm backend/idc_management.db + +# 4. 删除依赖(可选) +rm -rf backend/node_modules +rm -rf frontend/node_modules +rm -rf frontend/dist +``` + +--- + ## 常见问题 ### 端口冲突 @@ -996,7 +1104,8 @@ idc_assest/ ├── docs/ # 项目文档 │ ├── api/ # 接口文档 │ └── images/ # 文档图片 -├── install.js # 交互式安装脚本 ⭐ +├── install.sh # Linux 安装引导脚本(Shell)⭐ +├── install.js # 交互式安装脚本(Node.js)⭐ ├── update.js # 一键更新脚本 ⭐ ├── uninstall.js # 卸载脚本 ├── check.js # 环境检查脚本 diff --git a/README.md b/README.md index 35c09da..cc75efc 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,9 @@ ### 方式一:一键部署脚本(推荐)⭐ -我们提供了交互式安装脚本,自动完成所有部署步骤: +我们提供了交互式安装脚本,自动完成所有部署步骤。 + +#### Linux 用户(支持自动安装 Node.js) ```bash # 克隆项目(选择其一) @@ -164,15 +166,49 @@ git clone https://gitee.com/zhang96110/idc_assest.git git clone https://github.com/gituib/idc_assest.git cd idc_assest -# 运行交互式安装脚本 -npm run deploy -# 或 +# 方式1:使用 Shell 引导脚本(推荐,无需预装 Node.js) +chmod +x install.sh && ./install.sh + +# 方式2:一键安装(从远程) +curl -fsSL https://gitee.com/zhang96110/idc_assest/raw/main/install.sh | bash +``` + +#### Windows 用户 + +```powershell +# 克隆项目 +git clone https://gitee.com/zhang96110/idc_assest.git +cd idc_assest + +# 运行安装脚本(需要先安装 Node.js) node install.js + +# 或使用 npm 命令 +npm run deploy +``` + +#### 非交互式安装(快速部署) + +```bash +# 使用默认配置快速安装 +node install.js -y # 交互式安装 + +# 指定数据库类型 +node install.js -y --db=mysql # 使用 MySQL +node install.js -y --db=sqlite # 使用 SQLite(默认) + +# 指定端口 +node install.js -y --port=3000 # 后端端口 3000 + +# 跳过前端构建 +node install.js -y --skip-build ``` **脚本功能:** - ✅ 自动检测 Node.js、npm、PM2、Nginx -- ✅ **Linux 支持自动安装 Node.js**(交互式) +- ✅ **Linux 自动安装 Node.js**(Ubuntu/Debian/CentOS/Arch) +- ✅ **MySQL 连接测试**(配置后自动验证) +- ✅ **安装后健康检查**(验证服务是否正常) - ✅ 交互式配置数据库(SQLite/MySQL) - ✅ 交互式选择运行环境(development/production) - ✅ 交互式选择前端部署方式(Nginx/PM2 serve) @@ -180,6 +216,7 @@ node install.js - ✅ 自动初始化数据库 - ✅ 自动构建前端项目 - ✅ 使用 PM2 启动和管理服务 +- ✅ 日志持久化保存 ### 方式二:手动安装 @@ -585,7 +622,8 @@ jigui/ │ └── USER_GUIDE.md # 用户指南 ├── scripts/ # 项目脚本 │ └── frontend-manager.js # 前端管理脚本 -├── install.js # 交互式安装脚本 ⭐ +├── install.sh # Linux 安装引导脚本(Shell)⭐ +├── install.js # 交互式安装脚本(Node.js)⭐ ├── update.js # 一键更新脚本 ⭐ ├── uninstall.js # 卸载脚本 ├── package.json # 项目根依赖 @@ -605,10 +643,34 @@ jigui/ ### 一键更新(推荐)⭐ ```bash -npm run update +# 交互式更新 +node update.js + +# 查看帮助 +node update.js --help + +# 模拟运行(不执行实际操作) +node update.js --dry-run + +# 跳过某些步骤 +node update.js --skip-git # 跳过 Git 拉取 +node update.js --skip-backup # 跳过数据库备份 +node update.js --skip-migrate # 跳过数据库迁移 +node update.js --skip-build # 跳过前端构建 +node update.js --skip-deps # 跳过依赖安装 +node update.js --skip-restart # 跳过服务重启 + +# 强制执行(忽略锁文件) +node update.js --force ``` -功能:自动备份 → 拉取代码 → 更新依赖 → 重建前端 → 重启服务 +**智能检测功能:** +- ✅ **依赖安装智能跳过** - 检测 package.json 变化,无变化则跳过 +- ✅ **前端构建智能跳过** - 检测源码变化,无变化则跳过 +- ✅ **数据库自动备份** - 更新前自动备份,支持回滚 +- ✅ **健康检查验证** - 更新后验证服务是否正常 +- ✅ **日志持久化** - 更新日志保存到 `logs/update_*.log` +- ✅ **锁机制** - 防止多个更新进程同时运行 ### 手动更新 @@ -628,6 +690,39 @@ pm2 restart idc-backend --- +## 卸载系统 + +### 一键卸载(推荐)⭐ + +```bash +# 交互式卸载 +node uninstall.js + +# 查看帮助 +node uninstall.js --help + +# 强制卸载(无需确认) +node uninstall.js --force + +# 卸载前自动备份 +node uninstall.js --backup + +# 跳过某些步骤 +node uninstall.js --skip-db # 跳过数据库删除 +node uninstall.js --skip-deps # 跳过依赖删除 +``` + +**卸载功能:** +- ✅ 停止并删除 PM2 服务 +- ✅ 清理 Nginx 配置 +- ✅ 删除生成的配置文件 +- ✅ 可选删除数据库文件 +- ✅ 可选删除 node_modules +- ✅ 卸载前自动备份 +- ✅ 日志持久化保存 + +--- + ## API接口 完整接口文档请参考 [docs/api/README.md](docs/api/README.md)。 diff --git a/install.js b/install.js index b862e0d..f45758c 100644 --- a/install.js +++ b/install.js @@ -15,12 +15,12 @@ * - 使用 PM2 启动和管理后端服务 * * 使用方法: - * node install.js - * 或 - * npm run deploy + * node install.js # 交互式安装 + * node install.js --help # 查看帮助 + * node install.js --non-interactive # 非交互式(使用默认配置) * * 系统要求: - * - Node.js >= 14.0.0 + * - Node.js >= 16.0.0 * - npm >= 6.0.0 * - Windows/Linux/macOS * @@ -29,54 +29,99 @@ * ============================================================================ */ -// Node.js 内置模块引入 -const readline = require('readline'); // 读取命令行输入 -const { execSync } = require('child_process'); // 执行系统命令 -const fs = require('fs'); // 文件系统操作 -const path = require('path'); // 路径处理 +const readline = require('readline'); +const { execSync, spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); -// ============================================================================= -// 工具函数:颜色输出和日志 -// ============================================================================= +const SCRIPT_VERSION = '2.0.0'; +const MIN_NODE_VERSION = 16; +const LOG_DIR = path.join(__dirname, 'logs'); -/** - * ANSI 颜色代码配置 - * 用于在终端输出彩色文字,提升可读性 - */ const colors = { - reset: '\x1b[0m', // 重置颜色 - bright: '\x1b[1m', // 高亮 - green: '\x1b[32m', // 绿色(成功) - yellow: '\x1b[33m', // 黄色(警告) - red: '\x1b[31m', // 红色(错误) - cyan: '\x1b[36m', // 青色(信息) - gray: '\x1b[90m' // 灰色(分隔线) + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', + cyan: '\x1b[36m', + gray: '\x1b[90m', + magenta: '\x1b[35m' }; -/** - * 日志输出工具对象 - * 提供统一格式的日志输出方法 - */ const log = { - /** 信息日志 - 青色 */ info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`), - - /** 成功日志 - 绿色带勾选 */ success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`), - - /** 警告日志 - 黄色带警告符号 */ warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), - - /** 错误日志 - 红色带叉号 */ error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`), - - /** 步骤日志 - 高亮青色带箭头 */ step: (msg) => console.log(`\n${colors.bright}${colors.cyan}▶ ${msg}${colors.reset}`), - - /** 分隔线 - 灰色横线 */ - divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`) + divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`), + subStep: (msg) => console.log(` ${colors.gray}└${colors.reset} ${msg}`) }; +let logFileStream = null; +let installStartTime = null; + +function parseArgs() { + const args = process.argv.slice(2); + return { + nonInteractive: args.includes('--non-interactive') || args.includes('-y'), + skipNginx: args.includes('--skip-nginx'), + skipBuild: args.includes('--skip-build'), + dbType: args.find(a => a.startsWith('--db='))?.split('=')[1], + backendPort: args.find(a => a.startsWith('--port='))?.split('=')[1], + help: args.includes('--help') || args.includes('-h') + }; +} + +function showHelp() { + console.log(` +${colors.bright}IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION}${colors.reset} + +用法: node install.js [选项] + +选项: + -y, --non-interactive 非交互式安装(使用默认配置) + --skip-nginx 跳过 Nginx 配置 + --skip-build 跳过前端构建 + --db= 数据库类型 (sqlite/mysql) + --port= 后端端口 + -h, --help 显示帮助信息 + +示例: + node install.js # 交互式安装 + node install.js -y # 使用默认配置快速安装 + node install.js -y --db=mysql # 使用 MySQL 数据库 + node install.js -y --port=3000 # 指定后端端口 +`); + process.exit(0); +} + +function initLogFile() { + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); + } + const logFileName = `install_${new Date().toISOString().replace(/[:.]/g, '-')}.log`; + const logFilePath = path.join(LOG_DIR, logFileName); + logFileStream = fs.createWriteStream(logFilePath, { flags: 'a' }); + + const originalConsoleLog = console.log; + console.log = (...args) => { + const timestamp = new Date().toISOString(); + const message = args.map(arg => + typeof arg === 'string' ? arg.replace(/\x1b\[[0-9;]*m/g, '') : String(arg) + ).join(' '); + logFileStream.write(`[${timestamp}] ${message}\n`); + originalConsoleLog.apply(console, args); + }; +} + +function closeLogFile() { + if (logFileStream) { + logFileStream.end(); + } +} + // ============================================================================= // 配置存储对象 // ============================================================================= @@ -286,12 +331,12 @@ function commandExists(command) { /** * 检查 Node.js 版本是否满足要求 * - * @returns {boolean} 版本是否 >= 14.0.0 + * @returns {boolean} 版本是否 >= MIN_NODE_VERSION */ function checkNodeVersion() { - const version = process.version; // 获取当前 Node 版本,如 "v20.10.0" - const major = parseInt(version.slice(1).split('.')[0]); // 提取主版本号 - return major >= 14; + const version = process.version; + const major = parseInt(version.slice(1).split('.')[0]); + return major >= MIN_NODE_VERSION; } /** @@ -785,31 +830,78 @@ async function checkEnvironment() { * 1. 选择数据库类型(SQLite / MySQL) * 2. 如选择 MySQL,提示输入连接参数 */ -async function configureDatabase() { +async function configureDatabase(cmdArgs) { log.step('数据库配置'); - // 选择数据库类型 - config.dbType = await select('选择数据库类型:', [ - { label: 'SQLite(零配置,适合开发/小规模)', value: 'sqlite' }, - { label: 'MySQL(生产环境推荐)', value: 'mysql' } - ]); + if (cmdArgs?.nonInteractive && cmdArgs?.dbType) { + config.dbType = cmdArgs.dbType; + log.info(`使用命令行参数: 数据库类型 = ${config.dbType}`); + } else if (cmdArgs?.nonInteractive) { + config.dbType = 'sqlite'; + log.info('使用默认配置: 数据库类型 = sqlite'); + } else { + config.dbType = await select('选择数据库类型:', [ + { label: 'SQLite(零配置,适合开发/小规模)', value: 'sqlite' }, + { label: 'MySQL(生产环境推荐)', value: 'mysql' } + ]); + } - // MySQL 配置 if (config.dbType === 'mysql') { 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'); - // 依次询问 MySQL 连接参数 - config.dbConfig.host = await ask('MySQL 主机地址', 'localhost'); - config.dbConfig.port = await ask('MySQL 端口', '3306'); - config.dbConfig.username = await ask('MySQL 用户名', 'root'); - config.dbConfig.password = await askPassword('MySQL 密码'); - config.dbConfig.database = await ask('数据库名称', 'idc_management'); + if (cmdArgs?.nonInteractive) { + config.dbConfig.host = process.env.MYSQL_HOST || 'localhost'; + config.dbConfig.port = process.env.MYSQL_PORT || '3306'; + config.dbConfig.username = process.env.MYSQL_USER || 'root'; + config.dbConfig.password = process.env.MYSQL_PASSWORD || ''; + config.dbConfig.database = process.env.MYSQL_DATABASE || 'idc_management'; + log.info(`MySQL 配置: ${config.dbConfig.host}:${config.dbConfig.port}/${config.dbConfig.database}`); + } else { + config.dbConfig.host = await ask('MySQL 主机地址', 'localhost'); + config.dbConfig.port = await ask('MySQL 端口', '3306'); + config.dbConfig.username = await ask('MySQL 用户名', 'root'); + config.dbConfig.password = await askPassword('MySQL 密码'); + config.dbConfig.database = await ask('数据库名称', 'idc_management'); + } + + const testResult = await testMySQLConnection(); + if (!testResult.success) { + log.error(`MySQL 连接测试失败: ${testResult.error}`); + const retry = cmdArgs?.nonInteractive ? false : await ask('是否重新配置? (Y/n)', 'Y'); + if (retry.toLowerCase() === 'y') { + return await configureDatabase(cmdArgs); + } + log.warning('将继续部署,但数据库可能无法正常工作'); + } } log.divider(); } +async function testMySQLConnection() { + log.subStep('测试 MySQL 连接...'); + + try { + const mysql = require('mysql2/promise'); + const connection = await mysql.createConnection({ + host: config.dbConfig.host, + port: parseInt(config.dbConfig.port), + user: config.dbConfig.username, + password: config.dbConfig.password, + database: config.dbConfig.database, + connectTimeout: 5000 + }); + + await connection.execute('SELECT 1'); + await connection.end(); + log.success('MySQL 连接测试成功'); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } +} + /** * 服务配置函数 * @@ -820,25 +912,40 @@ async function configureDatabase() { * 4. 如选择 Nginx 但未安装,提供安装选项 * 5. 配置前端端口和域名 */ -async function configureServices() { +async function configureServices(cmdArgs) { log.step('服务配置'); - // 1. 后端端口配置 - config.backendPort = await ask('后端服务端口', '8000'); + if (cmdArgs?.nonInteractive && cmdArgs?.backendPort) { + config.backendPort = cmdArgs.backendPort; + log.info(`使用命令行参数: 后端端口 = ${config.backendPort}`); + } else if (cmdArgs?.nonInteractive) { + config.backendPort = '8000'; + log.info('使用默认配置: 后端端口 = 8000'); + } else { + config.backendPort = await ask('后端服务端口', '8000'); + } - // 2. 运行环境选择 - config.nodeEnv = await select('选择运行环境:', [ - { label: 'production(生产模式,性能优化,推荐正式部署)', value: 'production' }, - { label: 'development(开发模式,详细日志,便于调试)', value: 'development' } - ]); + if (cmdArgs?.nonInteractive) { + config.nodeEnv = 'production'; + log.info('使用默认配置: 运行环境 = production'); + } else { + config.nodeEnv = await select('选择运行环境:', [ + { label: 'production(生产模式,性能优化,推荐正式部署)', value: 'production' }, + { label: 'development(开发模式,详细日志,便于调试)', value: 'development' } + ]); + } - // 3. 前端部署方式选择 - config.frontendDeploy = await select('前端部署方式:', [ - { label: 'Nginx(性能最优,推荐生产环境)', value: 'nginx' }, - { label: 'PM2 serve(简单快捷,无需额外安装)', value: 'pm2' } - ]); + if (cmdArgs?.nonInteractive || cmdArgs?.skipNginx) { + config.frontendDeploy = 'pm2'; + config.frontendPort = '3000'; + log.info('使用默认配置: 前端部署 = PM2 serve (端口 3000)'); + } else { + config.frontendDeploy = await select('前端部署方式:', [ + { label: 'Nginx(性能最优,推荐生产环境)', value: 'nginx' }, + { label: 'PM2 serve(简单快捷,无需额外安装)', value: 'pm2' } + ]); + } - // 3. 如果选择 Nginx,检查是否已安装 if (config.frontendDeploy === 'nginx') { const nginxInstalled = isNginxInstalled(); @@ -846,55 +953,54 @@ async function configureServices() { log.warning('未检测到 Nginx 安装'); if (process.platform === 'win32') { - // Windows 下询问是否自动安装 - const autoInstall = await ask('是否自动下载并安装 Nginx? (Y/n)', 'Y'); - if (autoInstall.toLowerCase() === 'y') { - await autoInstallNginx(); - } else { - log.info('已跳过自动安装,请手动安装 Nginx 后启动服务'); - console.log(` 下载地址: ${colors.cyan}https://nginx.org/en/download.html${colors.reset}`); - console.log(` 安装路径建议: ${colors.cyan}C:/nginx${colors.reset}`); + if (!cmdArgs?.nonInteractive) { + const autoInstall = await ask('是否自动下载并安装 Nginx? (Y/n)', 'Y'); + if (autoInstall.toLowerCase() === 'y') { + await autoInstallNginx(); + } else { + log.info('已跳过自动安装,请手动安装 Nginx 后启动服务'); + console.log(` 下载地址: ${colors.cyan}https://nginx.org/en/download.html${colors.reset}`); + console.log(` 安装路径建议: ${colors.cyan}C:/nginx${colors.reset}`); + } } } else if (process.platform === 'linux') { - // Linux 下询问是否自动安装 - const autoInstall = await ask('是否自动安装 Nginx? (Y/n)', 'Y'); - if (autoInstall.toLowerCase() === 'y') { - const installed = await autoInstallNginxLinux(); - if (!installed) { - log.error('Nginx 自动安装失败'); - log.info('请手动安装后重新运行脚本'); + if (!cmdArgs?.nonInteractive) { + const autoInstall = await ask('是否自动安装 Nginx? (Y/n)', 'Y'); + if (autoInstall.toLowerCase() === 'y') { + const installed = await autoInstallNginxLinux(); + if (!installed) { + log.error('Nginx 自动安装失败'); + log.info('请手动安装后重新运行脚本'); + showNginxInstallGuideLinux(); + process.exit(1); + } + } else { + log.info('已跳过自动安装'); showNginxInstallGuideLinux(); - process.exit(1); - } - } else { - log.info('已跳过自动安装'); - showNginxInstallGuideLinux(); - const continueDeploy = await ask('是否继续部署(后端将启动,前端需手动配置 Nginx)? (Y/n)', 'Y'); - if (continueDeploy.toLowerCase() !== 'y') { - log.info('已取消部署,请安装 Nginx 后重新运行'); - process.exit(0); + const continueDeploy = await ask('是否继续部署(后端将启动,前端需手动配置 Nginx)? (Y/n)', 'Y'); + if (continueDeploy.toLowerCase() !== 'y') { + log.info('已取消部署,请安装 Nginx 后重新运行'); + process.exit(0); + } } } } else { - // macOS 下提供安装命令 log.info('请使用 Homebrew 安装 Nginx:'); console.log(` ${colors.cyan}brew install nginx${colors.reset}`); - console.log(`\n安装完成后,请重新运行此脚本继续部署。\n`); - - const continueDeploy = await ask('是否继续部署(后端将启动,前端需手动配置 Nginx)? (Y/n)', 'Y'); - if (continueDeploy.toLowerCase() !== 'y') { - log.info('已取消部署,请安装 Nginx 后重新运行'); - process.exit(0); - } } } - // Nginx 端口和域名配置 - config.frontendPort = await ask('Nginx 监听端口', '80'); - config.domain = await ask('域名(没有则填localhost)', 'localhost'); + if (!cmdArgs?.nonInteractive) { + config.frontendPort = await ask('Nginx 监听端口', '80'); + config.domain = await ask('域名(没有则填localhost)', 'localhost'); + } else { + config.frontendPort = '80'; + config.domain = 'localhost'; + } } else { - // PM2 serve 端口配置 - config.frontendPort = await ask('前端服务端口', '3000'); + if (!cmdArgs?.nonInteractive) { + config.frontendPort = await ask('前端服务端口', '3000'); + } } log.divider(); @@ -1658,47 +1764,133 @@ function printManagementCommands() { * 10. 输出管理命令 */ async function main() { - // 显示欢迎信息 + installStartTime = Date.now(); + const cmdArgs = parseArgs(); + + if (cmdArgs.help) { + showHelp(); + return; + } + + initLogFile(); + console.log(` ${colors.bright}${colors.cyan} ╔══════════════════════════════════════════════════════════╗ -║ IDC设备管理系统 - 交互式安装部署脚本 ║ +║ IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION} ║ ║ Interactive Installation & Deployment Script ║ ╚══════════════════════════════════════════════════════════╝ ${colors.reset}`); + if (cmdArgs.nonInteractive) { + log.info('运行模式: 非交互式(使用默认配置)'); + } + try { - // 步骤1:环境检测 await checkEnvironment(); + await configureDatabase(cmdArgs); + await configureServices(cmdArgs); + + if (!cmdArgs.nonInteractive) { + await confirmConfiguration(); + } - // 步骤2:交互式配置 - await configureDatabase(); - await configureServices(); - await confirmConfiguration(); - - // 步骤3:生成配置文件 generateBackendEnv(); generatePM2Config(); generateNginxConfig(); - // 步骤4:安装和部署 await installDependencies(); await initDatabase(); - await buildFrontend(); + + if (!cmdArgs.skipBuild) { + await buildFrontend(); + } else { + log.info('已跳过前端构建'); + } + await startServices(); - - // 步骤5:输出管理命令 - printManagementCommands(); + await healthCheck(); + printSummary(); } catch (error) { log.error(`部署失败: ${error.message}`); console.error(error); process.exit(1); } finally { - // 关闭 readline 接口 rl.close(); + closeLogFile(); } } -// 运行主函数 +async function healthCheck() { + log.step('健康检查'); + log.subStep('检查后端服务状态...'); + + const maxRetries = 5; + const retryDelay = 3000; + + for (let i = 1; i <= maxRetries; i++) { + try { + const result = execSync('curl -s -o /dev/null -w "%{http_code}" http://localhost:' + config.backendPort + '/health 2>nul || echo "000"', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 5000 + }).trim(); + + if (result === '200') { + log.success('后端服务健康检查通过'); + return { success: true }; + } + + if (i < maxRetries) { + log.subStep(`第 ${i} 次检查失败 (HTTP ${result}),${retryDelay/1000}秒后重试...`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + } catch (error) { + if (i < maxRetries) { + log.subStep(`第 ${i} 次检查失败,${retryDelay/1000}秒后重试...`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + } + } + + log.warning('健康检查未通过,请手动验证服务状态'); + return { success: false }; +} + +function printSummary() { + const duration = ((Date.now() - installStartTime) / 1000).toFixed(1); + + console.log(` +${colors.bright}${colors.magenta} +╔══════════════════════════════════════════════════════════╗ +║ 安装摘要 ║ +╚══════════════════════════════════════════════════════════╝${colors.reset}`); + + console.log(`\n ${colors.cyan}安装耗时:${colors.reset} ${duration} 秒`); + console.log(` ${colors.cyan}数据库类型:${colors.reset} ${config.dbType}`); + console.log(` ${colors.cyan}后端端口:${colors.reset} ${config.backendPort}`); + console.log(` ${colors.cyan}前端部署:${colors.reset} ${config.frontendDeploy}`); + + console.log(`\n${colors.bright}服务管理命令:${colors.reset}`); + console.log(` ${colors.cyan}pm2 status${colors.reset} 查看服务状态`); + console.log(` ${colors.cyan}pm2 logs idc-backend${colors.reset} 查看后端日志`); + console.log(` ${colors.cyan}pm2 restart idc-backend${colors.reset} 重启后端`); + console.log(` ${colors.cyan}pm2 stop idc-backend${colors.reset} 停止后端`); + + console.log(`\n${colors.bright}访问地址:${colors.reset}`); + console.log(` 后端API: ${colors.cyan}http://localhost:${config.backendPort}/api${colors.reset}`); + if (config.frontendDeploy === 'nginx') { + console.log(` 前端页面: ${colors.cyan}http://${config.domain}:${config.frontendPort}${colors.reset}`); + } else { + console.log(` 前端页面: ${colors.cyan}http://localhost:${config.frontendPort}${colors.reset}`); + } + + console.log(`\n${colors.bright}更新升级:${colors.reset}`); + console.log(` ${colors.cyan}node update.js${colors.reset} 一键更新`); + + log.divider(); + log.success('安装部署完成!'); +} + main(); diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..2e71419 --- /dev/null +++ b/install.sh @@ -0,0 +1,420 @@ +#!/bin/bash + +# ============================================================================ +# IDC设备管理系统 - Linux 安装引导脚本 +# ============================================================================ +# +# 功能:检测并安装 Node.js,然后运行 install.js +# +# 使用方法: +# 方式1 - 直接运行: +# chmod +x install.sh && ./install.sh +# +# 方式2 - 一键安装(推荐): +# curl -fsSL https://your-domain/install.sh | bash +# +# 方式3 - 从 GitHub: +# curl -fsSL https://raw.githubusercontent.com/xxx/xxx/main/install.sh | bash +# +# ============================================================================ + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BRIGHT='\033[1m' +RESET='\033[0m' + +# 日志函数 +log_info() { echo -e "${CYAN}ℹ${RESET} $1"; } +log_success() { echo -e "${GREEN}✓${RESET} $1"; } +log_warning() { echo -e "${YELLOW}⚠${RESET} $1"; } +log_error() { echo -e "${RED}✗${RESET} $1"; } +log_step() { echo -e "\n${BRIGHT}${CYAN}▶ $1${RESET}"; } +log_divider() { echo -e "${YELLOW}$(printf '─%.0s' {1..60})${RESET}"; } + +# 脚本版本 +SCRIPT_VERSION="1.0.0" +MIN_NODE_VERSION=16 + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 显示欢迎信息 +show_banner() { + echo -e " +${BRIGHT}${CYAN} +╔══════════════════════════════════════════════════════════╗ +║ IDC设备管理系统 - Linux 安装引导脚本 v${SCRIPT_VERSION} ║ +║ Linux Bootstrap Installation Script ║ +╚══════════════════════════════════════════════════════════╝ +${RESET}" +} + +# 检测 Linux 发行版 +detect_distro() { + if [ -f /etc/os-release ]; then + . /etc/os-release + echo "$ID" + elif [ -f /etc/redhat-release ]; then + echo "rhel" + else + echo "unknown" + fi +} + +# 检查是否为 root 用户 +is_root() { + [ "$(id -u)" -eq 0 ] +} + +# 获取 sudo 前缀 +get_sudo() { + if is_root; then + echo "" + else + echo "sudo" + fi +} + +# 检查命令是否存在 +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# 获取 Node.js 版本号 +get_node_version() { + if command_exists node; then + node -e "console.log(process.versions.node.split('.')[0])" + else + echo "0" + fi +} + +# 检查 Node.js 版本 +check_node_version() { + local version=$(get_node_version) + [ "$version" -ge "$MIN_NODE_VERSION" ] +} + +# 安装 Node.js - Ubuntu/Debian +install_node_debian() { + local sudo=$(get_sudo) + log_info "使用 NodeSource 安装 Node.js 20.x..." + + # 安装 curl(如果没有) + if ! command_exists curl; then + log_info "安装 curl..." + $sudo apt-get update -qq + $sudo apt-get install -y -qq curl + fi + + # 添加 NodeSource 源 + log_info "添加 NodeSource 源..." + curl -fsSL https://deb.nodesource.com/setup_20.x | $sudo -E bash - + + # 安装 Node.js + log_info "安装 Node.js..." + $sudo apt-get install -y -qq nodejs + + # 验证安装 + if command_exists node; then + log_success "Node.js $(node -v) 安装成功" + return 0 + else + return 1 + fi +} + +# 安装 Node.js - CentOS/RHEL/Fedora +install_node_rhel() { + local sudo=$(get_sudo) + log_info "使用 NodeSource 安装 Node.js 20.x..." + + # 安装 curl(如果没有) + if ! command_exists curl; then + log_info "安装 curl..." + $sudo yum install -y -q curl + fi + + # 添加 NodeSource 源 + log_info "添加 NodeSource 源..." + curl -fsSL https://rpm.nodesource.com/setup_20.x | $sudo bash - + + # 安装 Node.js + log_info "安装 Node.js..." + $sudo yum install -y -q nodejs + + # 验证安装 + if command_exists node; then + log_success "Node.js $(node -v) 安装成功" + return 0 + else + return 1 + fi +} + +# 安装 Node.js - Arch Linux +install_node_arch() { + local sudo=$(get_sudo) + log_info "使用 pacman 安装 Node.js..." + + $sudo pacman -S --noconfirm nodejs npm + + if command_exists node; then + log_success "Node.js $(node -v) 安装成功" + return 0 + else + return 1 + fi +} + +# 安装 Node.js - 使用 nvm +install_node_nvm() { + log_info "使用 nvm 安装 Node.js..." + + # 安装 nvm + if [ ! -d "$HOME/.nvm" ]; then + log_info "安装 nvm..." + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + fi + + # 加载 nvm + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + + # 安装 Node.js 20 + log_info "安装 Node.js 20..." + nvm install 20 + nvm use 20 + nvm alias default 20 + + if command_exists node; then + log_success "Node.js $(node -v) 安装成功" + return 0 + else + return 1 + fi +} + +# 自动安装 Node.js +auto_install_node() { + local distro=$(detect_distro) + + log_step "自动安装 Node.js" + log_info "检测到 Linux 发行版: $distro" + + case $distro in + ubuntu|debian) + install_node_debian + ;; + centos|rhel|fedora|rocky|almalinux) + install_node_rhel + ;; + arch|manjaro) + install_node_arch + ;; + *) + log_warning "未知发行版,尝试使用 nvm 安装..." + install_node_nvm + ;; + esac +} + +# 显示手动安装指引 +show_manual_install_guide() { + echo -e " +${BRIGHT}Node.js 手动安装指引:${RESET} + +${YELLOW}Ubuntu/Debian:${RESET} + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + +${YELLOW}CentOS/RHEL/Fedora:${RESET} + curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - + sudo yum install -y nodejs + +${YELLOW}Arch Linux:${RESET} + sudo pacman -S nodejs npm + +${YELLOW}使用 nvm(推荐开发者):${RESET} + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + source ~/.bashrc + nvm install 20 + nvm use 20 + +${YELLOW}验证安装:${RESET} + node -v # 应显示 v20.x.x + npm -v # 应显示 10.x.x + +${CYAN}安装完成后,请重新运行此脚本继续安装 IDC设备管理系统${RESET} +" +} + +# 检查并安装依赖 +check_and_install_dependencies() { + log_step "检查系统依赖" + + local sudo=$(get_sudo) + local distro=$(detect_distro) + local needs_install=() + + # 检查 git + if ! command_exists git; then + needs_install+=("git") + fi + + # 检查 curl + if ! command_exists curl; then + needs_install+=("curl") + fi + + # 检查 wget(可选) + if ! command_exists wget; then + needs_install+=("wget") + fi + + if [ ${#needs_install[@]} -gt 0 ]; then + log_info "需要安装依赖: ${needs_install[*]}" + + case $distro in + ubuntu|debian) + $sudo apt-get update -qq + $sudo apt-get install -y -qq "${needs_install[@]}" + ;; + centos|rhel|fedora|rocky|almalinux) + $sudo yum install -y -q "${needs_install[@]}" + ;; + arch|manjaro) + $sudo pacman -S --noconfirm "${needs_install[@]}" + ;; + *) + log_warning "无法自动安装依赖,请手动安装: ${needs_install[*]}" + return 1 + ;; + esac + + log_success "依赖安装完成" + else + log_success "系统依赖已满足" + fi +} + +# 主函数 +main() { + show_banner + + # 检查是否在正确的目录 + if [ ! -f "$SCRIPT_DIR/install.js" ]; then + log_error "未找到 install.js 文件" + log_info "请在项目根目录运行此脚本" + exit 1 + fi + + # 检查 Node.js + log_step "检查 Node.js 环境" + + if command_exists node; then + local node_version=$(get_node_version) + log_info "检测到 Node.js: $(node -v)" + + if check_node_version; then + log_success "Node.js 版本满足要求 (>= v$MIN_NODE_VERSION)" + else + log_warning "Node.js 版本过低 (v$node_version < v$MIN_NODE_VERSION)" + + echo -e "\n${YELLOW}请选择操作:${RESET}" + echo " 1) 自动升级 Node.js 到 v20(推荐)" + echo " 2) 显示手动安装指引" + echo " 3) 退出" + + read -p "请选择 [1-3]: " choice + + case $choice in + 1) + auto_install_node || { + log_error "Node.js 升级失败" + show_manual_install_guide + exit 1 + } + # 重新加载环境 + export PATH="/usr/bin:$HOME/.nvm/versions/node/v20*/bin:$PATH" + hash -r + ;; + 2) + show_manual_install_guide + exit 0 + ;; + 3) + log_info "已取消安装" + exit 0 + ;; + *) + log_error "无效选择" + exit 1 + ;; + esac + fi + else + log_warning "未检测到 Node.js" + + echo -e "\n${YELLOW}请选择操作:${RESET}" + echo " 1) 自动安装 Node.js v20(推荐)" + echo " 2) 显示手动安装指引" + echo " 3) 退出" + + read -p "请选择 [1-3]: " choice + + case $choice in + 1) + check_and_install_dependencies + auto_install_node || { + log_error "Node.js 安装失败" + show_manual_install_guide + exit 1 + } + # 重新加载环境 + export PATH="/usr/bin:$HOME/.nvm/versions/node/v20*/bin:$PATH" + hash -r + ;; + 2) + show_manual_install_guide + exit 0 + ;; + 3) + log_info "已取消安装" + exit 0 + ;; + *) + log_error "无效选择" + exit 1 + ;; + esac + fi + + # 检查 npm + log_step "检查 npm" + if command_exists npm; then + log_success "npm $(npm -v)" + else + log_error "npm 未安装" + exit 1 + fi + + log_divider + log_success "环境检查通过!" + + # 运行 install.js + log_step "启动安装程序" + log_info "正在运行 install.js..." + echo "" + + cd "$SCRIPT_DIR" + node install.js "$@" +} + +# 运行主函数 +main "$@" diff --git a/package.json b/package.json index 3ed4799..48ba552 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "start:frontend": "cd frontend && npm run dev", "install:all": "npm install && cd backend && npm install && cd ../frontend && npm install", "deploy": "node install.js", - "update": "node update.js" + "update": "node update.js", + "uninstall": "node uninstall.js" }, "keywords": [ "idc", diff --git a/uninstall.js b/uninstall.js index 54f045c..0d82161 100644 --- a/uninstall.js +++ b/uninstall.js @@ -2,7 +2,7 @@ /** * ============================================================================ - * IDC设备管理系统 - 卸载脚本 + * IDC设备管理系统 - 卸载脚本 v2.0.0 * ============================================================================ * * 功能说明: @@ -13,24 +13,20 @@ * - 可选删除 node_modules 和构建产物 * * 使用方法: - * node uninstall.js - * - * 注意事项: - * - 卸载前请确保已备份重要数据 - * - 此脚本不会删除项目源代码 - * - MySQL 数据库不会被删除,需手动清理 - * ============================================================================ + * node uninstall.js # 交互式卸载 + * node uninstall.js --help # 查看帮助 + * node uninstall.js --force # 强制卸载(无需确认) + * node uninstall.js --backup # 卸载前自动备份 */ -// Node.js 内置模块引入 const readline = require('readline'); const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); -// ============================================================================= -// 工具函数:颜色输出和日志 -// ============================================================================= +const SCRIPT_VERSION = '2.0.0'; +const BACKUP_DIR = path.join(__dirname, 'backup'); +const LOG_DIR = path.join(__dirname, 'logs'); const colors = { reset: '\x1b[0m', @@ -39,7 +35,8 @@ const colors = { yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m', - gray: '\x1b[90m' + gray: '\x1b[90m', + magenta: '\x1b[35m' }; const log = { @@ -48,9 +45,72 @@ const log = { warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`), step: (msg) => console.log(`\n${colors.bright}${colors.cyan}▶ ${msg}${colors.reset}`), - divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`) + divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`), + subStep: (msg) => console.log(` ${colors.gray}└${colors.reset} ${msg}`) }; +let logFileStream = null; +let uninstallStartTime = null; +let deletedItems = []; +let backedUpItems = []; + +function parseArgs() { + const args = process.argv.slice(2); + return { + force: args.includes('--force') || args.includes('-f'), + backup: args.includes('--backup') || args.includes('-b'), + skipDb: args.includes('--skip-db'), + skipDeps: args.includes('--skip-deps'), + help: args.includes('--help') || args.includes('-h') + }; +} + +function showHelp() { + console.log(` +${colors.bright}IDC设备管理系统 - 卸载脚本 v${SCRIPT_VERSION}${colors.reset} + +用法: node uninstall.js [选项] + +选项: + -f, --force 强制卸载(无需确认) + -b, --backup 卸载前自动备份数据库 + --skip-db 跳过数据库删除 + --skip-deps 跳过依赖删除 + -h, --help 显示帮助信息 + +示例: + node uninstall.js # 交互式卸载 + node uninstall.js --force # 强制卸载 + node uninstall.js --backup # 卸载前备份 +`); + process.exit(0); +} + +function initLogFile() { + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); + } + const logFileName = `uninstall_${new Date().toISOString().replace(/[:.]/g, '-')}.log`; + const logFilePath = path.join(LOG_DIR, logFileName); + logFileStream = fs.createWriteStream(logFilePath, { flags: 'a' }); + + const originalConsoleLog = console.log; + console.log = (...args) => { + const timestamp = new Date().toISOString(); + const message = args.map(arg => + typeof arg === 'string' ? arg.replace(/\x1b\[[0-9;]*m/g, '') : String(arg) + ).join(' '); + logFileStream.write(`[${timestamp}] ${message}\n`); + originalConsoleLog.apply(console, args); + }; +} + +function closeLogFile() { + if (logFileStream) { + logFileStream.end(); + } +} + // ============================================================================= // 交互式输入函数 // ============================================================================= @@ -257,9 +317,9 @@ async function cleanupConfigFiles() { log.step('清理生成的配置文件'); const filesToDelete = [ - { path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)' }, - { path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置 (ecosystem.config.js)' }, - { path: path.join(__dirname, 'deploy', 'nginx-idc.conf'), name: 'Nginx 配置 (nginx-idc.conf)' } + { 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', 'nginx-idc.conf'), name: 'Nginx 配置 (nginx-idc.conf)', type: '配置文件' } ]; for (const file of filesToDelete) { @@ -267,6 +327,7 @@ async function cleanupConfigFiles() { try { fs.unlinkSync(file.path); log.success(`${file.name} 已删除`); + deletedItems.push({ type: file.type, name: file.name }); } catch (error) { log.error(`删除 ${file.name} 失败: ${error.message}`); } @@ -275,7 +336,6 @@ async function cleanupConfigFiles() { } } - // 检查 deploy 目录是否为空,如果是则删除 const deployDir = path.join(__dirname, 'deploy'); if (fs.existsSync(deployDir)) { try { @@ -283,6 +343,7 @@ async function cleanupConfigFiles() { if (files.length === 0) { fs.rmdirSync(deployDir); log.success('deploy 目录已删除'); + deletedItems.push({ type: '目录', name: 'deploy/' }); } else { log.info('deploy 目录不为空,保留'); } @@ -350,10 +411,10 @@ async function cleanupDatabase() { const confirm = await ask('\n是否删除 SQLite 数据库文件? (y/N)', 'N'); if (confirm.toLowerCase() === 'y') { try { - // 尝试停止服务后再删除(避免文件被占用) log.info('正在删除数据库文件...'); fs.unlinkSync(sqliteDbPath); log.success('SQLite 数据库已删除'); + deletedItems.push({ type: '数据库', name: `SQLite (${dbSize} MB)` }); } catch (error) { log.error(`删除失败: ${error.message}`); if (error.message.includes('EBUSY') || error.message.includes('resource busy')) { @@ -399,11 +460,11 @@ async function cleanupDatabase() { // 依赖和构建产物清理 // ============================================================================= -async function cleanupDependencies() { +async function cleanupDependencies(cmdArgs) { log.step('依赖和构建产物清理'); log.warning('此操作将删除 node_modules 和构建产物,需要重新安装依赖才能再次运行'); - const confirm = await ask('是否删除依赖和构建产物? (y/N)', 'N'); + const confirm = cmdArgs?.force ? 'y' : await ask('是否删除依赖和构建产物? (y/N)', 'N'); if (confirm.toLowerCase() !== 'y') { log.info('跳过依赖清理'); @@ -411,9 +472,9 @@ async function cleanupDependencies() { } const dirsToDelete = [ - { path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules' }, - { path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules' }, - { path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物 (dist)' } + { path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules', type: '依赖' }, + { path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules', type: '依赖' }, + { path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物 (dist)', type: '构建产物' } ]; for (const dir of dirsToDelete) { @@ -421,6 +482,7 @@ async function cleanupDependencies() { try { fs.rmSync(dir.path, { recursive: true, force: true }); log.success(`${dir.name} 已删除`); + deletedItems.push({ type: dir.type, name: dir.name }); } catch (error) { log.error(`删除 ${dir.name} 失败: ${error.message}`); } @@ -463,52 +525,130 @@ async function cleanupLogs() { // 主函数 // ============================================================================= +async function backupDatabase() { + log.step('备份数据库'); + + const sqliteDbPath = path.join(__dirname, 'backend', 'idc_management.db'); + if (!fs.existsSync(sqliteDbPath)) { + log.info('未找到 SQLite 数据库文件,跳过备份'); + return false; + } + + if (!fs.existsSync(BACKUP_DIR)) { + fs.mkdirSync(BACKUP_DIR, { recursive: true }); + } + + const backupPath = path.join(BACKUP_DIR, `database_backup_${Date.now()}.db`); + + try { + fs.copyFileSync(sqliteDbPath, backupPath); + backedUpItems.push({ type: '数据库', path: backupPath }); + log.success(`数据库已备份: ${path.basename(backupPath)}`); + return true; + } catch (error) { + log.error(`备份失败: ${error.message}`); + return false; + } +} + +function printSummary() { + const duration = ((Date.now() - uninstallStartTime) / 1000).toFixed(1); + + console.log(` +${colors.bright}${colors.magenta} +╔══════════════════════════════════════════════════════════╗ +║ 卸载摘要 ║ +╚══════════════════════════════════════════════════════════╝${colors.reset}`); + + console.log(`\n ${colors.cyan}卸载耗时:${colors.reset} ${duration} 秒`); + + if (deletedItems.length > 0) { + console.log(`\n ${colors.cyan}已删除项目:${colors.reset}`); + deletedItems.forEach(item => { + console.log(` - ${item.type}: ${item.name}`); + }); + } + + if (backedUpItems.length > 0) { + console.log(`\n ${colors.cyan}已备份项目:${colors.reset}`); + backedUpItems.forEach(item => { + console.log(` - ${item.type}: ${path.basename(item.path)}`); + }); + console.log(`\n ${colors.yellow}备份位置:${colors.reset} ${BACKUP_DIR}`); + } + + console.log(`\n ${colors.cyan}保留的文件:${colors.reset}`); + console.log(` - 项目源代码 (backend/, frontend/)`); + console.log(` - 上传的文件 (backend/uploads/)`); + + console.log(`\n ${colors.cyan}重新部署:${colors.reset}`); + console.log(` node install.js`); + + log.divider(); + log.success('卸载完成!'); +} + async function main() { + uninstallStartTime = Date.now(); + const cmdArgs = parseArgs(); + + if (cmdArgs.help) { + showHelp(); + return; + } + + initLogFile(); + console.log(` ${colors.bright}${colors.yellow} ╔══════════════════════════════════════════════════════════╗ -║ IDC设备管理系统 - 卸载脚本 ║ +║ IDC设备管理系统 - 卸载脚本 v${SCRIPT_VERSION} ║ ║ Uninstallation Script ║ ╚══════════════════════════════════════════════════════════╝ ${colors.reset}`); log.warning('此脚本将卸载 IDC设备管理系统'); - log.warning('请确保已备份重要数据!'); + + if (cmdArgs.force) { + log.info('运行模式: 强制卸载(无需确认)'); + } + log.divider(); - const confirm = await ask('确认要开始卸载? (y/N)', 'N'); - if (confirm.toLowerCase() !== 'y') { - log.info('已取消卸载'); - rl.close(); - return; + if (!cmdArgs.force) { + const confirm = await ask('确认要开始卸载? (y/N)', 'N'); + if (confirm.toLowerCase() !== 'y') { + log.info('已取消卸载'); + rl.close(); + closeLogFile(); + return; + } } try { - // 步骤1:停止并删除服务 + if (cmdArgs.backup) { + await backupDatabase(); + } + await stopAndDeleteServices(); - - // 步骤2:清理 Nginx 配置 await cleanupNginxConfig(); - - // 步骤3:清理配置文件 await cleanupConfigFiles(); - - // 步骤4:数据库清理 - await cleanupDatabase(); - - // 步骤5:日志清理 + + if (!cmdArgs.skipDb) { + await cleanupDatabase(); + } else { + log.info('已跳过数据库删除'); + } + await cleanupLogs(); + + if (!cmdArgs.skipDeps) { + await cleanupDependencies(); + } else { + log.info('已跳过依赖删除'); + } - // 步骤6:依赖和构建产物清理(可选) - await cleanupDependencies(); - - // 完成 - log.divider(); - log.success('卸载完成!'); - console.log(`\n${colors.bright}系统已完全卸载,以下文件保留:${colors.reset}`); - console.log(` - 项目源代码(backend/、frontend/)`); - console.log(` - 上传的文件(如有)`); - console.log(`\n如需重新部署,请运行: ${colors.cyan}node install.js${colors.reset}`); + printSummary(); } catch (error) { log.error(`卸载失败: ${error.message}`); @@ -516,8 +656,8 @@ ${colors.reset}`); process.exit(1); } finally { rl.close(); + closeLogFile(); } } -// 运行主函数 main();