#!/usr/bin/env node /** * ============================================================================ * IDC设备管理系统 - 交互式安装部署脚本 * ============================================================================ * * 功能说明: * - 交互式配置数据库(SQLite/MySQL) * - 交互式选择前端部署方式(Nginx/PM2 serve) * - 自动检测并可选安装 Nginx(Windows) * - 自动安装项目依赖 * - 自动初始化数据库 * - 自动构建前端项目 * - 使用 PM2 启动和管理后端服务 * * 使用方法: * node install.js # 交互式安装 * node install.js --help # 查看帮助 * node install.js --non-interactive # 非交互式(使用默认配置) * * 系统要求: * - Node.js >= 16.0.0 * - npm >= 6.0.0 * - Windows/Linux/macOS * * 作者:自动生成 * 日期:2026-02-05 * ============================================================================ */ 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'); const colors = { 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}`), 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(); } } // ============================================================================= // 配置存储对象 // ============================================================================= /** * 部署配置对象 * 存储用户交互过程中设置的所有配置参数 * * @property {string} dbType - 数据库类型:'sqlite' 或 'mysql' * @property {Object} dbConfig - MySQL 配置参数(当 dbType='mysql' 时使用) * @property {number} backendPort - 后端服务监听端口 * @property {string} nodeEnv - 运行环境:'development' 或 'production' * @property {string} frontendDeploy - 前端部署方式:'nginx' 或 'pm2' * @property {number} frontendPort - 前端服务监听端口 * @property {string} domain - 域名配置(Nginx 使用) */ const config = { dbType: 'sqlite', // 默认使用 SQLite dbConfig: {}, // MySQL 配置初始为空 backendPort: 8000, // 默认后端端口 nodeEnv: 'production', // 默认运行环境 frontendDeploy: 'nginx', // 默认使用 Nginx frontendPort: 80, // 默认 Nginx 端口 domain: 'localhost' // 默认域名 }; // ============================================================================= // 交互式输入函数 // ============================================================================= /** * 创建 readline 接口实例 * 用于在命令行中读取用户输入 */ const rl = readline.createInterface({ input: process.stdin, // 标准输入(键盘) output: process.stdout // 标准输出(屏幕) }); rl.on('close', () => { process.exit(0); }); rl.on('error', (err) => { log.error(`输入错误: ${err.message}`); process.exit(1); }); /** * 提问函数 - 获取用户输入 * * @param {string} question - 提示问题文本 * @param {string} defaultValue - 默认值(用户直接回车时使用) * @returns {Promise} 用户输入的内容 * * 使用示例: * const port = await ask('请输入端口', '8000'); */ function ask(question, defaultValue = '') { return new Promise((resolve) => { const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `; rl.question(prompt, (answer) => { resolve(answer.trim() || defaultValue); }); }); } /** * 密码输入函数 - 隐藏输入内容并显示星号 * * @param {string} question - 提示问题文本 * @param {boolean} requireConfirm - 是否需要确认密码 * @returns {Promise} 用户输入的密码 * * 使用示例: * const password = await askPassword('请输入密码'); * const password = await askPassword('请输入密码', true); // 需要确认 */ function askPassword(question, requireConfirm = false) { return new Promise((resolve) => { const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout }); rl2.question(`${question}: `, { silent: true }, (password1) => { if (requireConfirm) { rl2.question('请再次输入密码确认: `, { silent: true }, (password2) => { rl2.close(); if (password1 === password2) { resolve(password1); } else { log.error('两次输入的密码不一致,请重新输入'); resolve(askPassword(question, requireConfirm)); } }); } else { rl2.close(); resolve(password1); } }); }); } /** * 选择函数 - 显示选项列表并获取用户选择 * * @param {string} question - 提示问题文本 * @param {Array<{label: string, value: any}>} options - 选项数组 * @returns {Promise} 用户选择的值 * * 使用示例: * const dbType = await select('选择数据库:', [ * { label: 'SQLite', value: 'sqlite' }, * { label: 'MySQL', value: 'mysql' } * ]); */ async function select(question, options) { console.log(`\n${question}`); options.forEach((opt, idx) => { console.log(` ${colors.cyan}${idx + 1}.${colors.reset} ${opt.label}`); }); const answer = await ask('请选择', '1'); const index = parseInt(answer) - 1; return options[index]?.value || options[0].value; } /** * 生成随机密钥 * * @param {number} length - 密钥长度,默认64位 * @returns {string} 随机密钥 */ function generateSecretKey(length = 64) { const crypto = require('crypto'); return crypto.randomBytes(length).toString('hex'); } // ============================================================================= // 系统命令执行函数 // ============================================================================= /** * 执行系统命令 * * @param {string} command - 要执行的命令 * @param {Object} options - 执行选项 * @param {boolean} options.silent - 是否静默执行(不显示输出) * @param {string} options.cwd - 工作目录 * @returns {Object} 执行结果 { success: boolean, output?: string, error?: string } * * 使用示例: * const result = runCommand('npm install', { cwd: './backend' }); * if (result.success) console.log('安装成功'); */ function runCommand(command, options = {}) { try { // 使用 execSync 同步执行命令 const result = execSync(command, { encoding: 'utf8', // 输出编码为 UTF-8 stdio: options.silent ? 'pipe' : 'inherit', // 是否显示命令输出 cwd: options.cwd || process.cwd(), // 设置工作目录 shell: true // 使用系统 shell 执行 }); return { success: true, output: result }; } catch (error) { // 命令执行失败时返回错误信息 return { success: false, error: error.message }; } } /** * 检查命令是否存在于系统中 * * @param {string} command - 命令名称 * @returns {boolean} 是否存在 * * 使用示例: * if (commandExists('pm2')) console.log('PM2 已安装'); */ function commandExists(command) { try { // 尝试执行命令的 --version 参数 execSync(`${command} --version`, { stdio: 'pipe', shell: true }); return true; } catch { return false; } } // ============================================================================= // 环境检测函数 // ============================================================================= /** * 检查 Node.js 版本是否满足要求 * * @returns {boolean} 版本是否 >= MIN_NODE_VERSION */ function checkNodeVersion() { const version = process.version; const major = parseInt(version.slice(1).split('.')[0]); return major >= MIN_NODE_VERSION; } /** * 检测 Nginx 是否已安装 * * 检测逻辑: * 1. 尝试执行 nginx -v 命令 * 2. Windows 系统额外检查常见安装路径 * * @returns {boolean} 是否已安装 */ function isNginxInstalled() { try { // 方法1:尝试执行 nginx 命令 execSync('nginx -v', { stdio: 'pipe', shell: true }); return true; } catch { // 方法2:Windows 下检查常见安装路径 if (process.platform === 'win32') { const commonPaths = [ 'C:\\nginx\\nginx.exe', 'C:\\Program Files\\nginx\\nginx.exe', 'C:\\Program Files (x86)\\nginx\\nginx.exe' ]; for (const p of commonPaths) { if (fs.existsSync(p)) return true; } } return false; } } /** * 检查 Node.js 和 npm 是否已安装 * * @returns {Object} 检查结果 { nodeInstalled: boolean, npmInstalled: boolean, version?: string } */ function checkNodeAndNpm() { let nodeInstalled = false; let npmInstalled = false; let version = ''; try { // 检查 Node.js version = process.version; nodeInstalled = true; } catch { nodeInstalled = false; } // 检查 npm npmInstalled = commandExists('npm'); return { nodeInstalled, npmInstalled, version }; } /** * 检测 Linux 发行版类型 * * @returns {string} 发行版类型:'ubuntu' | 'debian' | 'centos' | 'rhel' | 'fedora' | 'arch' | 'unknown' */ function detectLinuxDistro() { try { // 尝试读取 /etc/os-release 文件 const osRelease = fs.readFileSync('/etc/os-release', 'utf8'); if (osRelease.includes('Ubuntu')) return 'ubuntu'; if (osRelease.includes('Debian')) return 'debian'; if (osRelease.includes('CentOS')) return 'centos'; if (osRelease.includes('Red Hat')) return 'rhel'; if (osRelease.includes('Fedora')) return 'fedora'; if (osRelease.includes('Arch')) return 'arch'; return 'unknown'; } catch { return 'unknown'; } } /** * 显示 Node.js 安装指引 * * 以 Linux 为主,同时支持 Windows 和 macOS * 根据操作系统显示对应的安装方式和下载链接 */ function showNodeInstallGuide() { const platform = process.platform; const isWindows = platform === 'win32'; const isMac = platform === 'darwin'; const isLinux = platform === 'linux'; console.log('\n' + colors.bright + 'Node.js 安装指引:' + colors.reset); if (isLinux) { // Linux 为主,优先显示 const distro = detectLinuxDistro(); console.log('\n' + colors.green + '【Linux 推荐安装方式】' + colors.reset); // 根据检测到的发行版显示对应命令 switch (distro) { case 'ubuntu': case 'debian': console.log('\n' + colors.yellow + `Ubuntu/Debian (${distro}):` + colors.reset); console.log(` ${colors.cyan}# 使用 NodeSource 源安装 Node.js 20.x` + colors.reset); console.log(` ${colors.cyan}curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -${colors.reset}`); console.log(` ${colors.cyan}sudo apt-get install -y nodejs${colors.reset}`); break; case 'centos': case 'rhel': case 'fedora': console.log('\n' + colors.yellow + `CentOS/RHEL/Fedora (${distro}):` + colors.reset); console.log(` ${colors.cyan}# 使用 NodeSource 源安装 Node.js 20.x` + colors.reset); console.log(` ${colors.cyan}curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -${colors.reset}`); console.log(` ${colors.cyan}sudo yum install -y nodejs${colors.reset}`); break; case 'arch': console.log('\n' + colors.yellow + 'Arch Linux:' + colors.reset); console.log(` ${colors.cyan}sudo pacman -S nodejs npm${colors.reset}`); break; default: console.log('\n' + colors.yellow + '通用 Linux 安装方式:' + colors.reset); console.log(` ${colors.cyan}# Ubuntu/Debian${colors.reset}`); console.log(` curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -`); console.log(` sudo apt-get install -y nodejs`); console.log(`\n ${colors.cyan}# CentOS/RHEL/Fedora${colors.reset}`); console.log(` curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -`); console.log(` sudo yum install -y nodejs`); } // 所有 Linux 都显示 nvm 方式 console.log('\n' + colors.yellow + '【开发者推荐】使用 nvm(Node Version Manager):' + colors.reset); console.log(` ${colors.cyan}# 1. 安装 nvm${colors.reset}`); console.log(` curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash`); console.log(` ${colors.cyan}# 2. 加载 nvm(或重新打开终端)${colors.reset}`); console.log(` export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"`); console.log(` ${colors.cyan}# 3. 安装并使用 Node.js 20${colors.reset}`); console.log(` nvm install 20`); console.log(` nvm use 20`); console.log(` ${colors.cyan}# 4. 验证安装${colors.reset}`); console.log(` node -v && npm -v`); // Docker 方式 console.log('\n' + colors.yellow + '【Docker 方式】' + colors.reset); console.log(` ${colors.cyan}# 使用官方 Node 镜像${colors.reset}`); console.log(` docker run -it --rm node:20-alpine node -v`); } else if (isWindows) { // Windows console.log('\n' + colors.green + '【Windows 安装方式】' + colors.reset); console.log('\n' + colors.yellow + '方式一:使用官方安装包(推荐新手)' + colors.reset); console.log(` 1. 访问: ${colors.cyan}https://nodejs.org/${colors.reset}`); console.log(` 2. 下载 LTS 版本(推荐 ${colors.cyan}v20.x${colors.reset})`); console.log(` 3. 运行安装包,按向导完成安装`); console.log(` 4. 安装完成后,${colors.yellow}重新打开终端${colors.reset}并再次运行此脚本`); console.log('\n' + colors.yellow + '方式二:使用 nvm-windows(推荐开发者)' + colors.reset); console.log(` 1. 下载安装 nvm-windows: ${colors.cyan}https://github.com/coreybutler/nvm-windows/releases${colors.reset}`); console.log(` 2. 打开新的 PowerShell 或 CMD 窗口`); console.log(` 3. 执行: ${colors.cyan}nvm install 20${colors.reset}`); console.log(` 4. 执行: ${colors.cyan}nvm use 20${colors.reset}`); console.log(` 5. 验证: ${colors.cyan}node -v${colors.reset}`); console.log('\n' + colors.yellow + '方式三:使用 Winget(Windows 10/11 自带)' + colors.reset); console.log(` ${colors.cyan}winget install OpenJS.NodeJS.LTS${colors.reset}`); } else if (isMac) { // macOS console.log('\n' + colors.green + '【macOS 安装方式】' + colors.reset); console.log('\n' + colors.yellow + '方式一:使用 Homebrew(推荐)' + colors.reset); console.log(` ${colors.cyan}brew install node@${colors.reset}`); console.log('\n' + colors.yellow + '方式二:使用官方安装包' + colors.reset); console.log(` 1. 访问: ${colors.cyan}https://nodejs.org/${colors.reset}`); console.log(` 2. 下载 macOS 安装包(.pkg)`); console.log(` 3. 双击安装包按向导完成安装`); console.log('\n' + colors.yellow + '方式三:使用 nvm(推荐开发者)' + colors.reset); console.log(` ${colors.cyan}# 1. 安装 nvm${colors.reset}`); console.log(` curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash`); console.log(` ${colors.cyan}# 2. 重新加载 shell 配置${colors.reset}`); console.log(` source ~/.bashrc # 或 source ~/.zshrc`); console.log(` ${colors.cyan}# 3. 安装 Node.js 20${colors.reset}`); console.log(` nvm install 20`); console.log(` nvm use 20`); } // 通用验证命令 console.log('\n' + colors.green + '【验证安装】' + colors.reset); console.log(` 安装完成后,运行以下命令验证:`); console.log(` ${colors.cyan}node -v${colors.reset} # 应显示 v20.x.x 或更高版本`); console.log(` ${colors.cyan}npm -v${colors.reset} # 应显示 10.x.x 或更高版本`); console.log('\n' + colors.gray + '安装完成后,请重新运行此部署脚本。' + colors.reset); } /** * Linux 系统自动安装 Node.js * * 根据检测到的发行版,使用对应的方式自动安装 Node.js 20.x * * @returns {Promise} 安装是否成功 */ async function autoInstallNodeLinux() { const distro = detectLinuxDistro(); log.step('自动安装 Node.js'); log.info(`检测到 Linux 发行版: ${distro}`); try { switch (distro) { case 'ubuntu': case 'debian': log.info('使用 NodeSource 源安装 Node.js 20.x...'); log.info('执行: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -'); const debianSetup = runCommand('curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -'); if (!debianSetup.success) { log.error('NodeSource 源配置失败'); return false; } log.info('执行: sudo apt-get install -y nodejs'); const debianInstall = runCommand('sudo apt-get install -y nodejs'); if (!debianInstall.success) { log.error('Node.js 安装失败'); return false; } break; case 'centos': case 'rhel': case 'fedora': log.info('使用 NodeSource 源安装 Node.js 20.x...'); log.info('执行: curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -'); const rpmSetup = runCommand('curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -'); if (!rpmSetup.success) { log.error('NodeSource 源配置失败'); return false; } log.info('执行: sudo yum install -y nodejs'); const rpmInstall = runCommand('sudo yum install -y nodejs'); if (!rpmInstall.success) { log.error('Node.js 安装失败'); return false; } break; case 'arch': log.info('执行: sudo pacman -S nodejs npm'); const archInstall = runCommand('sudo pacman -S --noconfirm nodejs npm'); if (!archInstall.success) { log.error('Node.js 安装失败'); return false; } break; default: // 未知发行版,尝试使用 nvm 安装 log.info('未知发行版,尝试使用 nvm 安装...'); return await installNodeViaNvm(); } // 验证安装 const checkResult = runCommand('node -v', { silent: true }); if (checkResult.success) { log.success(`Node.js 安装成功: ${checkResult.output.trim()}`); const npmCheck = runCommand('npm -v', { silent: true }); if (npmCheck.success) { log.success(`npm 安装成功: ${npmCheck.output.trim()}`); } return true; } else { log.error('Node.js 安装后验证失败'); return false; } } catch (error) { log.error(`自动安装失败: ${error.message}`); return false; } } /** * 使用 nvm 安装 Node.js * * 适用于所有 Linux 发行版,作为通用安装方式 * * @returns {Promise} 安装是否成功 */ async function installNodeViaNvm() { log.info('使用 nvm 安装 Node.js...'); try { // 1. 检查 nvm 是否已安装 const nvmCheck = runCommand('source ~/.nvm/nvm.sh && nvm --version', { silent: true }); if (!nvmCheck.success) { // 安装 nvm log.info('安装 nvm...'); const nvmInstall = runCommand('curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash'); if (!nvmInstall.success) { log.error('nvm 安装失败'); return false; } } // 2. 使用 nvm 安装 Node.js 20 log.info('使用 nvm 安装 Node.js 20...'); const nodeInstall = runCommand('export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && nvm install 20'); if (!nodeInstall.success) { log.error('Node.js 安装失败'); return false; } // 3. 设置默认版本 log.info('设置 Node.js 20 为默认版本...'); runCommand('export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && nvm alias default 20', { silent: true }); log.success('Node.js 安装成功'); return true; } catch (error) { log.error(`nvm 安装失败: ${error.message}`); return false; } } /** * 处理 Node.js 未安装的情况(Linux 交互式) * * @returns {Promise} 是否继续部署 */ async function handleNodeNotInstalledLinux() { log.error('未检测到 Node.js'); console.log('\n' + colors.yellow + '您可以选择:' + colors.reset); console.log(` ${colors.cyan}1.${colors.reset} 自动安装 Node.js 20.x(推荐,需要 sudo 权限)`); console.log(` ${colors.cyan}2.${colors.reset} 显示手动安装指引,退出脚本`); const choice = await ask('请选择', '1'); if (choice === '1') { const installed = await autoInstallNodeLinux(); if (installed) { log.success('Node.js 和 npm 安装完成!'); log.info('请重新运行此脚本以继续部署'); console.log(colors.cyan + ' node install.js' + colors.reset); // 询问是否立即重新运行 const rerun = await ask('\n是否立即重新运行部署脚本? (Y/n)', 'Y'); if (rerun.toLowerCase() === 'y') { // 重新检查环境 return true; } } else { log.error('自动安装失败,请尝试手动安装'); showNodeInstallGuide(); } process.exit(1); } else { showNodeInstallGuide(); process.exit(1); } } /** * 环境检测主函数 * * 检测内容: * - Node.js 是否安装及版本(要求 >= 14.0.0) * - npm 是否安装 * - PM2 是否安装(未安装则自动安装) * - Nginx 是否安装(可选) * * Linux 系统支持自动安装 Node.js */ async function checkEnvironment() { log.step('环境检测'); // 1. 检测 Node.js 和 npm const { nodeInstalled, npmInstalled, version } = checkNodeAndNpm(); const isLinux = process.platform === 'linux'; if (!nodeInstalled) { if (isLinux) { // Linux 系统:交互式询问是否自动安装 const shouldContinue = await handleNodeNotInstalledLinux(); if (shouldContinue) { // 重新检测 return await checkEnvironment(); } } else { // 非 Linux 系统:显示安装指引 log.error('未检测到 Node.js,请先安装'); showNodeInstallGuide(); process.exit(1); } } if (!npmInstalled) { if (isLinux && nodeInstalled) { // Linux 下 npm 未安装但 node 已安装,尝试重新安装 node log.warning('未检测到 npm,尝试修复...'); const fixed = await autoInstallNodeLinux(); if (!fixed) { log.error('npm 修复失败,请手动安装'); showNodeInstallGuide(); process.exit(1); } } else { log.error('未检测到 npm,请重新安装 Node.js(npm 会随 Node.js 一起安装)'); showNodeInstallGuide(); process.exit(1); } } // 2. 检查 Node.js 版本 if (checkNodeVersion()) { log.success(`Node.js ${version}`); } else { log.error(`Node.js 版本过低 (${version}),需要 >= 14.0.0`); if (isLinux) { console.log('\n' + colors.yellow + '您可以选择:' + colors.reset); console.log(` ${colors.cyan}1.${colors.reset} 自动升级到 Node.js 20.x`); console.log(` ${colors.cyan}2.${colors.reset} 显示手动升级指引,退出脚本`); const upgradeChoice = await ask('请选择', '1'); if (upgradeChoice === '1') { const upgraded = await autoInstallNodeLinux(); if (upgraded) { log.success('Node.js 升级完成!请重新运行此脚本'); } else { log.error('自动升级失败'); showNodeInstallGuide(); } } else { console.log('\n' + colors.yellow + '建议升级到 Node.js 20 LTS 版本:' + colors.reset); showNodeInstallGuide(); } } else { console.log('\n' + colors.yellow + '建议升级到 Node.js 20 LTS 版本:' + colors.reset); showNodeInstallGuide(); } process.exit(1); } // 3. 检测 npm 版本 const npmResult = runCommand('npm --version', { silent: true }); if (npmResult.success) { log.success(`npm ${npmResult.output.trim()}`); } // 4. 检测 PM2(进程管理器) if (commandExists('pm2')) { log.success('PM2 已安装'); } else { log.warning('未检测到 PM2,将自动安装'); const result = runCommand('npm install -g pm2'); if (result.success) { log.success('PM2 安装完成'); } else { log.error('PM2 安装失败,请手动执行: npm install -g pm2'); } } // 5. 检测 Nginx(可选,仅在使用 Nginx 部署时需要) if (commandExists('nginx') || commandExists('nginx.exe')) { log.success('Nginx 已安装'); } else { log.warning('未检测到 Nginx(可选,仅在使用Nginx部署时需要)'); } log.divider(); } // ============================================================================= // 配置函数 // ============================================================================= /** * 数据库配置函数 * * 交互流程: * 1. 选择数据库类型(SQLite / MySQL) * 2. 如选择 MySQL,提示输入连接参数或自动安装 */ async function configureDatabase(cmdArgs) { log.step('数据库配置'); 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' } ]); } if (config.dbType === 'mysql') { 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 { const mysqlOption = await select('MySQL 配置方式:', [ { label: '已有数据库(填写现有 MySQL 连接信息)', value: 'existing' }, { label: '自动安装 MySQL(Linux 系统支持自动安装配置)', value: 'install' } ]); if (mysqlOption === 'install') { const installResult = await installMySQL(); if (installResult.success) { config.dbConfig = { ...installResult.config }; } else { log.error('MySQL 自动安装失败'); const fallback = await ask('是否改为手动配置现有 MySQL? (Y/n)', 'Y'); if (fallback.toLowerCase() === 'y') { await configureExistingMySQL(); } else { log.info('已取消 MySQL 配置,将使用 SQLite'); config.dbType = 'sqlite'; } } } else { await configureExistingMySQL(); } } if (config.dbType === 'mysql') { 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 configureExistingMySQL() { 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'); 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'); } async function installMySQL() { log.subStep('检测系统环境...'); const platform = process.platform; if (platform === 'win32') { log.warning('Windows 系统暂不支持自动安装 MySQL'); console.log('\n' + colors.cyan + '请手动安装 MySQL:' + colors.reset); console.log(' 1. 下载 MySQL: https://dev.mysql.com/downloads/mysql/'); console.log(' 2. 或使用 XAMPP/WAMP 等集成环境'); console.log(' 3. 安装后创建数据库:'); console.log(colors.gray + ' CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset); return { success: false }; } if (platform !== 'linux') { log.warning('当前系统暂不支持自动安装 MySQL'); return { success: false }; } const distro = detectLinuxDistro(); log.info(`检测到 Linux 发行版: ${distro}`); const mysqlRootPassword = await askPassword('设置 MySQL root 密码(用于新安装的 MySQL)'); if (!mysqlRootPassword) { log.error('密码不能为空'); return { success: false }; } log.subStep('开始安装 MySQL...'); try { switch (distro) { case 'ubuntu': case 'debian': await installMySQLDebian(mysqlRootPassword); break; case 'centos': case 'rhel': case 'rocky': case 'almalinux': await installMySQLRHEL(mysqlRootPassword); break; default: log.warning(`暂不支持 ${distro} 自动安装 MySQL`); return { success: false }; } config.dbConfig.host = 'localhost'; config.dbConfig.port = '3306'; config.dbConfig.username = 'root'; config.dbConfig.password = mysqlRootPassword; config.dbConfig.database = 'idc_management'; log.subStep('创建数据库...'); await createMySQLDatabase(mysqlRootPassword); log.success('MySQL 安装配置完成'); return { success: true, config: { host: 'localhost', port: '3306', username: 'root', password: mysqlRootPassword, database: 'idc_management' } }; } catch (error) { log.error(`MySQL 安装失败: ${error.message}`); return { success: false, error: error.message }; } } function detectLinuxDistro() { try { if (fs.existsSync('/etc/os-release')) { const content = fs.readFileSync('/etc/os-release', 'utf8'); const idMatch = content.match(/^ID=(.*)$/m); if (idMatch) { const id = idMatch[1].replace(/"/g, '').toLowerCase(); if (['ubuntu', 'debian'].includes(id)) return 'ubuntu'; if (['centos', 'rhel', 'rocky', 'almalinux', 'fedora'].includes(id)) return 'centos'; return id; } } if (fs.existsSync('/etc/redhat-release')) return 'centos'; if (fs.existsSync('/etc/debian_version')) return 'ubuntu'; } catch {} return 'unknown'; } async function installMySQLDebian(rootPassword) { log.info('安装 MySQL (Ubuntu/Debian)...'); execSync('export DEBIAN_FRONTEND=noninteractive', { shell: '/bin/bash' }); execSync('apt-get update -qq', { shell: '/bin/bash', stdio: 'inherit' }); execSync(`debconf-set-selections <<< "mysql-server mysql-server/root_password password ${rootPassword}"`, { shell: '/bin/bash' }); execSync(`debconf-set-selections <<< "mysql-server mysql-server/root_password_again password ${rootPassword}"`, { shell: '/bin/bash' }); execSync('apt-get install -y -qq mysql-server', { shell: '/bin/bash', stdio: 'inherit' }); execSync('systemctl start mysql', { shell: '/bin/bash' }); execSync('systemctl enable mysql', { shell: '/bin/bash' }); log.success('MySQL 安装完成'); } async function installMySQLRHEL(rootPassword) { log.info('安装 MySQL (CentOS/RHEL)...'); execSync('yum install -y -q epel-release', { shell: '/bin/bash', stdio: 'inherit' }); execSync('yum install -y -q mysql-server', { shell: '/bin/bash', stdio: 'inherit' }); execSync('systemctl start mysqld', { shell: '/bin/bash' }); execSync('systemctl enable mysqld', { shell: '/bin/bash' }); try { execSync(`mysqladmin -u root password "${rootPassword}"`, { shell: '/bin/bash' }); } catch { log.info('root 密码可能已设置,跳过'); } log.success('MySQL 安装完成'); } async function createMySQLDatabase(rootPassword) { log.subStep('创建数据库...'); const createDbSql = 'CREATE DATABASE IF NOT EXISTS idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'; try { execSync(`mysql -u root -p'${rootPassword}' -e "${createDbSql}"`, { shell: '/bin/bash', stdio: ['pipe', 'pipe', 'pipe'] }); log.success('数据库 idc_management 创建成功'); return; } catch (error) { const errorMsg = error.stderr ? error.stderr.toString() : error.message; if (errorMsg.includes('Access denied') || errorMsg.includes('ERROR 1045')) { log.warning('MySQL root 密码验证失败,尝试无密码连接...'); try { execSync(`mysql -u root -e "${createDbSql}"`, { shell: '/bin/bash', stdio: ['pipe', 'pipe', 'pipe'] }); log.success('数据库 idc_management 创建成功'); return; } catch (e2) { log.warning(`无密码连接也失败: ${e2.message}`); } } log.warning(`数据库自动创建失败: ${errorMsg.trim()}`); log.info('请手动创建数据库:'); 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); } } async function testMySQLConnection() { log.subStep('测试 MySQL 连接...'); try { execSync(`mysql -h ${config.dbConfig.host} -P ${config.dbConfig.port} -u ${config.dbConfig.username} -p'${config.dbConfig.password}' -e "SELECT 1" ${config.dbConfig.database}`, { shell: '/bin/bash', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }); log.success('MySQL 连接测试成功'); return { success: true }; } catch (error) { const errorMsg = error.stderr ? error.stderr.toString() : error.message; if (errorMsg.includes('Access denied') || errorMsg.includes('ERROR 1045')) { return { success: false, error: '用户名或密码错误' }; } if (errorMsg.includes('Unknown database')) { return { success: false, error: `数据库 ${config.dbConfig.database} 不存在` }; } if (errorMsg.includes('Connection refused') || errorMsg.includes("Can't connect")) { return { success: false, error: `无法连接到 ${config.dbConfig.host}:${config.dbConfig.port}` }; } return { success: false, error: errorMsg.trim() || '连接失败' }; } } /** * 服务配置函数 * * 交互流程: * 1. 配置后端端口 * 2. 选择运行环境(development / production) * 3. 选择前端部署方式(Nginx / PM2 serve) * 4. 如选择 Nginx 但未安装,提供安装选项 * 5. 配置前端端口和域名 */ async function configureServices(cmdArgs) { log.step('服务配置'); 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'); } if (cmdArgs?.nonInteractive) { config.nodeEnv = 'production'; log.info('使用默认配置: 运行环境 = production'); } else { config.nodeEnv = await select('选择运行环境:', [ { label: 'production(生产模式,性能优化,推荐正式部署)', value: 'production' }, { label: 'development(开发模式,详细日志,便于调试)', value: 'development' } ]); } 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' } ]); } if (config.frontendDeploy === 'nginx') { const nginxInstalled = isNginxInstalled(); if (!nginxInstalled) { log.warning('未检测到 Nginx 安装'); if (process.platform === 'win32') { 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') { 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(); const continueDeploy = await ask('是否继续部署(后端将启动,前端需手动配置 Nginx)? (Y/n)', 'Y'); if (continueDeploy.toLowerCase() !== 'y') { log.info('已取消部署,请安装 Nginx 后重新运行'); process.exit(0); } } } } else { log.info('请使用 Homebrew 安装 Nginx:'); console.log(` ${colors.cyan}brew install nginx${colors.reset}`); } } if (!cmdArgs?.nonInteractive) { config.frontendPort = await ask('Nginx 监听端口', '80'); config.domain = await ask('域名(没有则填localhost)', 'localhost'); } else { config.frontendPort = '80'; config.domain = 'localhost'; } } else { if (!cmdArgs?.nonInteractive) { config.frontendPort = await ask('前端服务端口', '3000'); } } log.divider(); } /** * 配置确认函数 * * 显示所有配置参数,等待用户确认后开始部署 */ async function confirmConfiguration() { log.step('配置确认'); // 显示配置摘要 console.log('\n' + colors.bright + '部署配置摘要:' + colors.reset); console.log(` 数据库类型: ${colors.cyan}${config.dbType}${colors.reset}`); if (config.dbType === 'mysql') { console.log(` MySQL主机: ${colors.cyan}${config.dbConfig.host}:${config.dbConfig.port}${colors.reset}`); console.log(` 数据库名: ${colors.cyan}${config.dbConfig.database}${colors.reset}`); console.log(` 用户名: ${colors.cyan}${config.dbConfig.username}${colors.reset}`); } console.log(` 后端端口: ${colors.cyan}${config.backendPort}${colors.reset}`); console.log(` 运行环境: ${colors.cyan}${config.nodeEnv}${colors.reset}`); console.log(` 前端部署: ${colors.cyan}${config.frontendDeploy}${colors.reset}`); console.log(` 前端端口: ${colors.cyan}${config.frontendPort}${colors.reset}`); if (config.frontendDeploy === 'nginx') { console.log(` 域名: ${colors.cyan}${config.domain}${colors.reset}`); } // 等待用户确认 const confirm = await ask('\n确认以上配置并开始部署? (Y/n)', 'Y'); if (confirm.toLowerCase() !== 'y') { log.info('已取消部署'); process.exit(0); } log.divider(); } // ============================================================================= // Nginx 安装指引函数(Linux) // ============================================================================= /** * 显示 Linux Nginx 安装指引 * 根据是否 root 用户显示相应的命令 */ function showNginxInstallGuideLinux() { const distro = detectLinuxDistro(); const root = isRootUser(); const sudoPrefix = root ? '' : 'sudo '; console.log('\n' + colors.bright + 'Nginx 安装命令:' + colors.reset); if (root) { console.log(colors.gray + '(当前以 root 用户运行,无需 sudo)' + colors.reset); } switch (distro) { case 'ubuntu': case 'debian': console.log(` ${colors.cyan}${sudoPrefix}apt update${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}apt install -y nginx${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}systemctl start nginx${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}systemctl enable nginx${colors.reset}`); break; case 'centos': case 'rhel': case 'fedora': console.log(` ${colors.cyan}${sudoPrefix}yum install -y epel-release${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}yum install -y nginx${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}systemctl start nginx${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}systemctl enable nginx${colors.reset}`); break; case 'arch': console.log(` ${colors.cyan}${sudoPrefix}pacman -S --noconfirm nginx${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}systemctl start nginx${colors.reset}`); console.log(` ${colors.cyan}${sudoPrefix}systemctl enable nginx${colors.reset}`); break; default: console.log(` ${colors.cyan}# Ubuntu/Debian${colors.reset}`); console.log(` ${sudoPrefix}apt update && ${sudoPrefix}apt install -y nginx`); console.log(`\n ${colors.cyan}# CentOS/RHEL/Fedora${colors.reset}`); console.log(` ${sudoPrefix}yum install -y epel-release && ${sudoPrefix}yum install -y nginx`); console.log(`\n ${colors.cyan}# Arch Linux${colors.reset}`); console.log(` ${sudoPrefix}pacman -S nginx`); } console.log('\n' + colors.gray + '安装完成后,请重新运行此部署脚本。' + colors.reset); } /** * 检测是否以 root 用户运行 * * @returns {boolean} 是否 root 用户 */ function isRootUser() { return process.getuid && process.getuid() === 0; } /** * Linux 系统自动安装 Nginx * * 根据检测到的发行版,使用包管理器自动安装 Nginx * 自动检测是否以 root 用户运行,决定是否使用 sudo * * @returns {Promise} 安装是否成功 */ async function autoInstallNginxLinux() { const distro = detectLinuxDistro(); const root = isRootUser(); const sudoPrefix = root ? '' : 'sudo '; log.step('自动安装 Nginx'); log.info(`检测到 Linux 发行版: ${distro}`); if (root) { log.info('当前以 root 用户运行,无需 sudo'); } try { let installCommand = ''; let startCommand = `${sudoPrefix}systemctl start nginx`; let enableCommand = `${sudoPrefix}systemctl enable nginx`; switch (distro) { case 'ubuntu': case 'debian': log.info('使用 apt 安装 Nginx...'); log.info(`执行: ${sudoPrefix}apt update`); const updateResult = runCommand(`${sudoPrefix}apt update`); if (!updateResult.success) { log.warning('apt update 失败,尝试继续安装...'); } log.info(`执行: ${sudoPrefix}apt install -y nginx`); installCommand = `${sudoPrefix}apt install -y nginx`; break; case 'centos': case 'rhel': case 'fedora': log.info('使用 yum 安装 Nginx...'); log.info(`执行: ${sudoPrefix}yum install -y epel-release`); const epelResult = runCommand(`${sudoPrefix}yum install -y epel-release`); if (!epelResult.success) { log.warning('epel-release 安装失败,尝试继续...'); } log.info(`执行: ${sudoPrefix}yum install -y nginx`); installCommand = `${sudoPrefix}yum install -y nginx`; break; case 'arch': log.info('使用 pacman 安装 Nginx...'); log.info(`执行: ${sudoPrefix}pacman -S --noconfirm nginx`); installCommand = `${sudoPrefix}pacman -S --noconfirm nginx`; break; default: log.error('未知的 Linux 发行版,无法自动安装'); return false; } // 执行安装 const installResult = runCommand(installCommand); if (!installResult.success) { log.error('Nginx 安装失败'); return false; } log.success('Nginx 安装完成'); // 启动 Nginx log.info('启动 Nginx 服务...'); const startResult = runCommand(startCommand); if (!startResult.success) { log.warning('Nginx 启动失败,可能需要手动启动'); } else { log.success('Nginx 服务已启动'); } // 设置开机自启 log.info('设置开机自启...'); runCommand(enableCommand, { silent: true }); // 验证安装 const checkResult = runCommand('nginx -v', { silent: true }); if (checkResult.success) { log.success(`Nginx 安装成功: ${checkResult.output.trim()}`); return true; } else { log.error('Nginx 安装验证失败'); return false; } } catch (error) { log.error(`自动安装失败: ${error.message}`); return false; } } // ============================================================================= // Nginx 自动安装函数(Windows) // ============================================================================= /** * Windows 系统自动下载并安装 Nginx * * 安装流程: * 1. 从 nginx.org 下载稳定版压缩包 * 2. 解压到 C:\nginx * 3. 输出后续配置指引 */ async function autoInstallNginx() { log.step('自动安装 Nginx'); log.info('正在下载 Nginx...'); // 引入所需模块 const https = require('https'); // 配置参数 const nginxUrl = 'https://nginx.org/download/nginx-1.24.0.zip'; // Nginx 稳定版下载地址 const downloadPath = path.join(__dirname, 'nginx-1.24.0.zip'); // 临时下载路径 const installPath = 'C:\\nginx'; // 安装路径 try { // 1. 下载 Nginx 压缩包 await new Promise((resolve, reject) => { const file = fs.createWriteStream(downloadPath); https.get(nginxUrl, (response) => { response.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', reject); }); log.success('Nginx 下载完成'); // 2. 解压到 C:\ log.info('正在解压...'); execSync(`powershell -Command "Expand-Archive -Path '${downloadPath}' -DestinationPath 'C:\\\\' -Force"`, { stdio: 'inherit' }); // 3. 重命名文件夹为 nginx if (fs.existsSync('C:\\nginx-1.24.0')) { if (fs.existsSync(installPath)) { fs.rmSync(installPath, { recursive: true }); // 删除旧版本 } fs.renameSync('C:\\nginx-1.24.0', installPath); } // 4. 清理临时文件 fs.unlinkSync(downloadPath); // 5. 输出安装成功信息和后续步骤 log.success(`Nginx 已安装到 ${installPath}`); log.info('安装后操作:'); console.log(` 1. 将配置文件复制到: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`); console.log(` 2. 修改主配置: 在 C:/nginx/conf/nginx.conf 的 http 段添加: ${colors.cyan}include conf.d/*.conf;${colors.reset}`); console.log(` 3. 启动 Nginx: ${colors.cyan}C:/nginx/nginx.exe${colors.reset}`); } catch (error) { log.error('Nginx 自动安装失败: ' + error.message); log.info('请手动下载安装: https://nginx.org/en/download.html'); throw error; } } // ============================================================================= // 配置文件生成函数 // ============================================================================= /** * 生成后端环境变量文件 (.env) * * 根据用户选择的配置生成后端所需的 .env 文件 */ function generateBackendEnv() { const envPath = path.join(__dirname, 'backend', '.env'); const jwtSecret = generateSecretKey(64); let envContent = `# IDC设备管理系统 - 环境配置 # 生成时间: ${new Date().toISOString()} # ============================================== # 服务器配置 # ============================================== # 服务器端口 PORT=${config.backendPort} # 运行环境: development 或 production NODE_ENV=${config.nodeEnv} # ============================================== # 安全配置 # ============================================== # JWT 密钥(自动生成,请妥善保管) JWT_SECRET=${jwtSecret} # ============================================== # 数据库配置 # ============================================== # 数据库类型: sqlite 或 mysql DB_TYPE=${config.dbType} `; if (config.dbType === 'sqlite') { envContent += ` # SQLite 配置 DB_PATH=./idc_management.db `; } else { envContent += ` # MySQL 配置 MYSQL_HOST=${config.dbConfig.host} MYSQL_PORT=${config.dbConfig.port} MYSQL_USERNAME=${config.dbConfig.username} MYSQL_PASSWORD=${config.dbConfig.password} MYSQL_DATABASE=${config.dbConfig.database} `; } fs.writeFileSync(envPath, envContent); log.success('后端环境变量文件已生成 (.env)'); log.info(`JWT_SECRET 已自动生成 (${jwtSecret.length}位)`); } /** * 生成 PM2 配置文件 * * 生成 ecosystem.config.js 用于 PM2 进程管理 */ function generatePM2Config() { // 创建 deploy 目录 const deployDir = path.join(__dirname, 'deploy'); if (!fs.existsSync(deployDir)) { fs.mkdirSync(deployDir, { recursive: true }); } // 构建 PM2 配置对象 const pm2Config = { apps: [{ name: 'idc-backend', // 应用名称 script: './server.js', // 启动脚本 cwd: path.join(__dirname, 'backend'), // 工作目录 instances: 1, // 实例数 exec_mode: 'fork', // 执行模式 env: { NODE_ENV: 'production', PORT: config.backendPort }, max_memory_restart: '1G', // 内存限制 log_file: path.join(__dirname, 'backend', 'logs', 'combined.log'), out_file: path.join(__dirname, 'backend', 'logs', 'out.log'), error_file: path.join(__dirname, 'backend', 'logs', 'error.log'), log_date_format: 'YYYY-MM-DD HH:mm:ss Z', merge_logs: true, autorestart: true // 自动重启 }] }; // 如果使用 PM2 serve 部署前端,添加前端应用配置 if (config.frontendDeploy === 'pm2') { pm2Config.apps.push({ name: 'idc-frontend', script: 'serve', cwd: path.join(__dirname, 'frontend'), args: `-s dist -l ${config.frontendPort}`, // -s: 静态文件目录, -l: 监听端口 instances: 1, exec_mode: 'fork', env: { NODE_ENV: 'production' }, max_memory_restart: '500M', autorestart: true }); } // 写入配置文件 fs.writeFileSync( path.join(deployDir, 'ecosystem.config.js'), `module.exports = ${JSON.stringify(pm2Config, null, 2)};` ); log.success('PM2 配置文件已生成 (deploy/ecosystem.config.js)'); } /** * 生成 Nginx 配置文件 * * 生成 nginx-idc.conf 用于 Nginx 反向代理和静态文件服务 */ function generateNginxConfig() { if (config.frontendDeploy !== 'nginx') return; // 创建 deploy 目录 const deployDir = path.join(__dirname, 'deploy'); if (!fs.existsSync(deployDir)) { fs.mkdirSync(deployDir, { recursive: true }); } const isWindows = process.platform === 'win32'; // 转换路径分隔符(Windows 反斜杠转斜杠) const frontendPath = path.join(__dirname, 'frontend', 'dist').replace(/\\/g, '/'); // 构建 Nginx 配置内容 const nginxConfig = `# IDC设备管理系统 - Nginx配置 # 生成时间: ${new Date().toISOString()} server { listen ${config.frontendPort}; server_name ${config.domain}; # 前端静态文件目录 root "${frontendPath}"; index index.html; # Gzip压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; # 静态资源缓存(1年) location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # 前端路由支持(SPA单页应用) location / { try_files $uri $uri/ /index.html; } # API代理到后端 location /api { proxy_pass http://127.0.0.1:${config.backendPort}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 文件上传代理 location /uploads { proxy_pass http://127.0.0.1:${config.backendPort}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; client_max_body_size 50M; # 最大上传文件大小 } } `; // 写入配置文件 fs.writeFileSync(path.join(deployDir, 'nginx-idc.conf'), nginxConfig); log.success('Nginx 配置文件已生成 (deploy/nginx-idc.conf)'); // 自动配置 Nginx(Linux 系统) if (!isWindows && config.frontendDeploy === 'nginx') { autoConfigureNginx(); } // 输出平台特定的配置指引 if (isWindows) { log.info('Windows Nginx 配置路径示例:'); console.log(` 将配置文件复制到: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`); console.log(` 或修改主配置 include: ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset}`); } } /** * 自动配置 Nginx(Linux) * * 自动复制配置文件到系统目录并启用 */ function autoConfigureNginx() { log.step('自动配置 Nginx'); const root = isRootUser(); const sudoPrefix = root ? '' : 'sudo '; const configSource = path.join(__dirname, 'deploy', 'nginx-idc.conf'); try { // 检测 Nginx 配置目录结构 let configDir = ''; let useSitesAvailable = false; if (fs.existsSync('/etc/nginx/sites-available')) { configDir = '/etc/nginx/sites-available'; useSitesAvailable = true; } else if (fs.existsSync('/etc/nginx/conf.d')) { configDir = '/etc/nginx/conf.d'; useSitesAvailable = false; } else { log.warning('未找到标准 Nginx 配置目录,请手动配置'); return; } // 复制配置文件 const configDest = path.join(configDir, 'idc'); log.info(`复制配置到 ${configDest}...`); const copyResult = runCommand(`${sudoPrefix}cp "${configSource}" "${configDest}"`); if (!copyResult.success) { log.error('配置文件复制失败'); return; } // 如果使用 sites-available,创建软链接 if (useSitesAvailable) { log.info('创建站点软链接...'); const linkResult = runCommand(`${sudoPrefix}ln -sf "${configDest}" /etc/nginx/sites-enabled/idc`); if (!linkResult.success) { log.warning('软链接创建失败'); } // 删除默认配置(如果存在) if (fs.existsSync('/etc/nginx/sites-enabled/default')) { log.info('删除默认站点配置...'); runCommand(`${sudoPrefix}rm -f /etc/nginx/sites-enabled/default`, { silent: true }); } } // 测试配置 log.info('测试 Nginx 配置...'); const testResult = runCommand(`${sudoPrefix}nginx -t`); if (!testResult.success) { log.error('Nginx 配置测试失败'); return; } // 重载 Nginx log.info('重载 Nginx 服务...'); const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`); if (reloadResult.success) { log.success('Nginx 配置完成并已生效'); } else { log.warning('Nginx 重载失败,尝试启动服务...'); runCommand(`${sudoPrefix}systemctl start nginx`, { silent: true }); } } catch (error) { log.error(`自动配置失败: ${error.message}`); log.info('请手动配置 Nginx'); } } // ============================================================================= // 部署执行函数 // ============================================================================= /** * 安装项目依赖 * * 依次安装后端和前端的 npm 依赖 */ async function installDependencies() { log.step('安装依赖'); // 安装后端依赖 log.info('安装后端依赖...'); const backendResult = runCommand('npm install', { cwd: path.join(__dirname, 'backend') }); if (backendResult.success) { log.success('后端依赖安装完成'); } else { log.error('后端依赖安装失败'); throw new Error('Backend install failed'); } // 安装前端依赖 log.info('安装前端依赖...'); const frontendResult = runCommand('npm install', { cwd: path.join(__dirname, 'frontend') }); if (frontendResult.success) { log.success('前端依赖安装完成'); } else { log.error('前端依赖安装失败'); throw new Error('Frontend install failed'); } } /** * 初始化数据库 * * 执行 backend/scripts/init-database.js 创建数据库表结构 */ async function initDatabase() { log.step('数据库初始化'); const initScript = path.join(__dirname, 'backend', 'scripts', 'init-database.js'); // 检查初始化脚本是否存在 if (!fs.existsSync(initScript)) { log.warning('未找到数据库初始化脚本,跳过'); return; } log.info('正在初始化数据库...'); const result = runCommand('node scripts/init-database.js', { cwd: path.join(__dirname, 'backend') }); if (result.success) { log.success('数据库初始化完成'); } else { log.error('数据库初始化失败'); log.info('请检查数据库配置并手动运行: node backend/scripts/init-database.js'); } } /** * 构建前端项目 * * 执行 npm run build 生成生产环境静态文件 */ async function buildFrontend() { log.step('构建前端'); log.info('执行 npm run build...'); const result = runCommand('npm run build', { cwd: path.join(__dirname, 'frontend') }); if (result.success) { log.success('前端构建完成'); } else { log.error('前端构建失败'); throw new Error('Frontend build failed'); } } /** * 启动服务 * * 使用 PM2 启动后端和前端(PM2方式)服务 */ async function startServices() { log.step('启动服务'); // 确保日志目录存在 const logDir = path.join(__dirname, 'backend', 'logs'); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } // 停止已有服务(避免冲突) log.info('停止已有服务...'); const stopCmd = process.platform === 'win32' ? 'pm2 stop idc-backend idc-frontend 2>nul || exit 0' : 'pm2 stop idc-backend idc-frontend 2>/dev/null || true'; runCommand(stopCmd, { silent: true }); // 启动后端服务 log.info('启动后端服务...'); const backendResult = runCommand( `pm2 start server.js --name "idc-backend" --env ${config.nodeEnv}`, { cwd: path.join(__dirname, 'backend') } ); if (backendResult.success) { log.success(`后端服务已启动 (端口: ${config.backendPort})`); } else { log.error('后端服务启动失败'); } // 如果使用 PM2 serve,启动前端服务 if (config.frontendDeploy === 'pm2') { log.info('安装 serve 包...'); runCommand('npm install -g serve', { silent: true }); log.info('启动前端服务...'); const frontendResult = runCommand( `pm2 start serve --name "idc-frontend" -- -s dist -l ${config.frontendPort}`, { cwd: path.join(__dirname, 'frontend') } ); if (frontendResult.success) { log.success(`前端服务已启动 (端口: ${config.frontendPort})`); } else { log.error('前端服务启动失败'); } } // 保存 PM2 配置(开机自启) runCommand('pm2 save', { silent: true }); log.divider(); } // ============================================================================= // 输出函数 // ============================================================================= /** * 输出部署完成后的管理命令和访问地址 */ function printManagementCommands() { log.step('部署完成!管理命令:'); // PM2 管理命令 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 logs idc-frontend${colors.reset} 查看前端日志(PM2方式)`); console.log(` ${colors.cyan}pm2 restart idc-backend${colors.reset} 重启后端`); console.log(` ${colors.cyan}pm2 stop idc-backend${colors.reset} 停止后端`); console.log(` ${colors.cyan}pm2 delete idc-backend${colors.reset} 删除后端服务`); // Nginx 管理命令(如适用) if (config.frontendDeploy === 'nginx') { console.log('\n' + colors.bright + 'Nginx 管理:' + colors.reset); if (process.platform === 'win32') { console.log(` ${colors.cyan}nginx -t${colors.reset} 测试配置`); console.log(` ${colors.cyan}nginx -s reload${colors.reset} 重载配置`); console.log(` ${colors.cyan}nginx -s stop${colors.reset} 停止Nginx`); } else { console.log(` ${colors.cyan}sudo nginx -t${colors.reset} 测试配置`); console.log(` ${colors.cyan}sudo systemctl restart nginx${colors.reset} 重启Nginx`); } } // 访问地址 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}npm run update${colors.reset} 一键更新(拉取代码+重建+重启)`); log.divider(); log.success('安装部署完成!'); } // ============================================================================= // 主函数 // ============================================================================= /** * 主函数 - 脚本入口 * * 执行流程: * 1. 显示欢迎信息 * 2. 环境检测 * 3. 交互式配置(数据库、服务) * 4. 配置确认 * 5. 生成配置文件 * 6. 安装依赖 * 7. 初始化数据库 * 8. 构建前端 * 9. 启动服务 * 10. 输出管理命令 */ async function main() { installStartTime = Date.now(); const cmdArgs = parseArgs(); if (cmdArgs.help) { showHelp(); return; } initLogFile(); console.log(` ${colors.bright}${colors.cyan} ╔══════════════════════════════════════════════════════════╗ ║ IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION} ║ ║ Interactive Installation & Deployment Script ║ ╚══════════════════════════════════════════════════════════╝ ${colors.reset}`); if (cmdArgs.nonInteractive) { log.info('运行模式: 非交互式(使用默认配置)'); } try { await checkEnvironment(); await configureDatabase(cmdArgs); await configureServices(cmdArgs); if (!cmdArgs.nonInteractive) { await confirmConfiguration(); } generateBackendEnv(); generatePM2Config(); generateNginxConfig(); await installDependencies(); await initDatabase(); if (!cmdArgs.skipBuild) { await buildFrontend(); } else { log.info('已跳过前端构建'); } await startServices(); await healthCheck(); printSummary(); } catch (error) { log.error(`部署失败: ${error.message}`); console.error(error); process.exit(1); } finally { 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();