From 7f4602d46505a9d653077b51913f98fad4543e02 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Sat, 9 May 2026 15:54:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(install):=20=E6=96=B0=E5=A2=9E=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E8=84=9A=E6=9C=AC=E6=A8=A1=E5=9D=97=E5=8C=96=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + install.js | 2496 +----------------------------------------- install/config.js | 110 ++ install/constants.js | 35 + install/database.js | 261 +++++ install/env-check.js | 426 +++++++ install/generator.js | 257 +++++ install/health.js | 53 + install/index.js | 134 +++ install/installer.js | 242 ++++ install/logger.js | 68 ++ install/nginx.js | 404 +++++++ install/rollback.js | 60 + install/ui.js | 102 ++ install/utils.js | 35 + 15 files changed, 2192 insertions(+), 2492 deletions(-) create mode 100644 install/config.js create mode 100644 install/constants.js create mode 100644 install/database.js create mode 100644 install/env-check.js create mode 100644 install/generator.js create mode 100644 install/health.js create mode 100644 install/index.js create mode 100644 install/installer.js create mode 100644 install/logger.js create mode 100644 install/nginx.js create mode 100644 install/rollback.js create mode 100644 install/ui.js create mode 100644 install/utils.js diff --git a/.gitignore b/.gitignore index 215affa..7c0934d 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ backend/config/remote-backup-configs.json .gitignore .vercelignore docs/install-improvement-plan.md +docs/install-split-plan.md diff --git a/install.js b/install.js index ffc6d5d..8b69366 100644 --- a/install.js +++ b/install.js @@ -1,2498 +1,10 @@ #!/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 INSTALL_STEPS = [ - '环境检测', - '数据库配置', - '服务配置', - '生成配置文件', - '安装依赖', - '数据库初始化', - '构建前端', - '启动服务', - '配置 Nginx', - '健康检查', -]; - -let currentStepIndex = 0; - -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) => { - const idx = INSTALL_STEPS.indexOf(msg); - if (idx >= 0) { - currentStepIndex = idx; - } - const progress = currentStepIndex < INSTALL_STEPS.length - ? `${colors.gray}[${currentStepIndex + 1}/${INSTALL_STEPS.length}]${colors.reset} ` - : ''; - console.log(`\n${progress}${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; -const rollbackSteps = []; - -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(); - } -} - -// ============================================================================= -// 配置存储对象 -// ============================================================================= - -/** - * 部署配置对象 - * 存储用户交互过程中设置的所有配置参数 + * IDC设备管理系统 - 安装部署脚本入口 * - * @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 使用) + * 实际逻辑已拆分到 install/ 目录下的各个模块 + * 此文件仅作为入口,加载并执行 install/index.js */ -const config = { - dbType: 'sqlite', - dbConfig: {}, - backendPort: 8000, - nodeEnv: 'production', - frontendDeploy: 'nginx', - frontendPort: 80, - domain: 'localhost' -}; -const SAVED_CONFIG_PATH = path.join(__dirname, 'deploy', 'install-config.json'); - -/** - * 保存当前配置到文件 - * - * 将配置写入 deploy/install-config.json,供下次安装时读取 - * 不保存敏感信息(MySQL 密码) - */ -function saveConfig() { - const deployDir = path.dirname(SAVED_CONFIG_PATH); - if (!fs.existsSync(deployDir)) { - fs.mkdirSync(deployDir, { recursive: true }); - } - - const savedConfig = { - dbType: config.dbType, - dbConfig: { - host: config.dbConfig.host, - port: config.dbConfig.port, - username: config.dbConfig.username, - database: config.dbConfig.database, - }, - backendPort: config.backendPort, - nodeEnv: config.nodeEnv, - frontendDeploy: config.frontendDeploy, - frontendPort: config.frontendPort, - domain: config.domain, - savedAt: new Date().toISOString(), - }; - - fs.writeFileSync(SAVED_CONFIG_PATH, JSON.stringify(savedConfig, null, 2)); - log.subStep('配置已保存到 deploy/install-config.json'); -} - -/** - * 读取上次保存的配置 - * - * @returns {Object|null} 上次保存的配置,无则返回 null - */ -function loadSavedConfig() { - try { - if (!fs.existsSync(SAVED_CONFIG_PATH)) { - return null; - } - const content = fs.readFileSync(SAVED_CONFIG_PATH, 'utf8'); - return JSON.parse(content); - } catch { - return null; - } -} - -/** - * 将保存的配置应用到当前 config 对象 - * - * @param {Object} saved - 上次保存的配置 - */ -function applySavedConfig(saved) { - if (!saved) return; - - if (saved.dbType) config.dbType = saved.dbType; - if (saved.dbConfig) { - config.dbConfig = { ...config.dbConfig, ...saved.dbConfig }; - } - if (saved.backendPort) config.backendPort = saved.backendPort; - if (saved.nodeEnv) config.nodeEnv = saved.nodeEnv; - if (saved.frontendDeploy) config.frontendDeploy = saved.frontendDeploy; - if (saved.frontendPort) config.frontendPort = saved.frontendPort; - if (saved.domain) config.domain = saved.domain; -} - -// ============================================================================= -// 交互式输入函数 -// ============================================================================= - -/** - * 创建 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 - 提示问题文本 - * @returns {Promise} 用户输入的密码 - * - * 使用示例: - * const password = await askPassword('请输入密码'); - */ -function askPassword(question) { - return new Promise((resolve) => { - const prompt = `${question}: `; - process.stdout.write(prompt); - - let password = ''; - const stdin = process.stdin; - const stdout = process.stdout; - - const cleanup = () => { - if (stdin.isTTY) { - try { - stdin.setRawMode(false); - } catch {} - } - stdin.removeListener('data', onData); - }; - - const onData = (data) => { - const char = data.toString(); - const code = char.charCodeAt(0); - - if (code === 10 || code === 13) { - cleanup(); - stdout.write('\n'); - resolve(password); - } else if (code === 3) { - cleanup(); - stdout.write('\n已取消\n'); - process.exit(0); - } else if (code === 127 || code === 8) { - if (password.length > 0) { - password = password.slice(0, -1); - stdout.write('\b \b'); - } - } else if (code >= 32) { - password += char; - stdout.write('*'); - } - }; - - if (stdin.isTTY) { - stdin.setRawMode(true); - stdin.resume(); - stdin.on('data', onData); - } else { - rl.question('', (answer) => { - resolve(answer.trim()); - }); - } - }); -} - -/** - * 选择函数 - 显示选项列表并获取用户选择 - * - * @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 }; -} - -/** - * 显示 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'); - - try { - 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); - - if (process.platform !== 'win32') { - try { - fs.chmodSync(envPath, 0o600); - log.success('.env 文件权限已设为 600(仅所有者可读写)'); - } catch { - log.warning('.env 文件权限设置失败,建议手动执行: chmod 600 backend/.env'); - } - } - - log.success('后端环境变量文件已生成 (.env)'); - log.info(`JWT_SECRET 已自动生成 (${jwtSecret.length}位)`); - log.warning('请妥善保管 .env 文件,切勿提交到版本控制系统'); - } catch (error) { - log.error(`后端环境变量文件生成失败: ${error.message}`); - throw new Error('Backend env generation failed'); - } -} - -/** - * 生成 PM2 配置文件 - * - * 生成 ecosystem.config.js 用于 PM2 进程管理 - */ -function generatePM2Config() { - const deployDir = path.join(__dirname, 'deploy'); - - try { - 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)'); - } catch (error) { - log.error(`PM2 配置文件生成失败: ${error.message}`); - throw new Error('PM2 config generation failed'); - } -} - -/** - * 生成 Nginx 配置文件 - * - * 生成 nginx-idc.conf 用于 Nginx 反向代理和静态文件服务 - */ -function generateNginxConfig() { - if (config.frontendDeploy !== 'nginx') return; - - const deployDir = path.join(__dirname, 'deploy'); - - try { - if (!fs.existsSync(deployDir)) { - fs.mkdirSync(deployDir, { recursive: true }); - } - - const isWindows = process.platform === 'win32'; - - let frontendPath; - if (isWindows) { - frontendPath = path.join(__dirname, 'frontend', 'dist').replace(/\\/g, '/'); - } else { - frontendPath = '/var/www/idc'; - } - - const nginxConfig = `# IDC设备管理系统 - Nginx配置 -# 生成时间: ${new Date().toISOString()} - -server { - listen ${config.frontendPort}; - server_name ${config.domain}; - - client_max_body_size 100M; - - # 前端静态文件目录 - root "${frontendPath}"; - index index.html; - - # Gzip压缩 - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_proxied any; - gzip_comp_level 6; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/x-javascript image/svg+xml; - - # 静态资源缓存(1年) - location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|hdr)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - try_files $uri =404; - } - - # API代理到后端 - location /api/ { - proxy_pass http://127.0.0.1:${config.backendPort}/api/; - 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_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 300s; - proxy_buffering off; - } - - # 文件上传代理 - location /uploads/ { - proxy_pass http://127.0.0.1:${config.backendPort}/uploads/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - client_max_body_size 100M; - } - - # 前端路由支持(SPA单页应用,必须放在最后) - location / { - try_files $uri $uri/ /index.html =404; - } -} -`; - - // 写入配置文件 - fs.writeFileSync(path.join(deployDir, 'nginx-idc.conf'), nginxConfig); - log.success('Nginx 配置文件已生成 (deploy/nginx-idc.conf)'); - - 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}`); - } - } catch (error) { - log.error(`Nginx 配置文件生成失败: ${error.message}`); - throw new Error('Nginx config generation failed'); - } -} - -/** - * 自动配置 Nginx(Linux) - * - * 自动复制配置文件到系统目录并启用 - */ -async function autoConfigureNginx() { - log.step('配置 Nginx'); - - if (!isNginxInstalled()) { - log.warning('Nginx 未安装,尝试自动安装...'); - if (process.platform === 'linux') { - const installed = await autoInstallNginxLinux(); - if (!installed) { - log.error('Nginx 自动安装失败'); - log.info('请手动安装 Nginx 后执行:'); - console.log(` ${colors.cyan}sudo cp deploy/nginx-idc.conf /etc/nginx/sites-available/idc${colors.reset}`); - console.log(` ${colors.cyan}sudo ln -sf /etc/nginx/sites-available/idc /etc/nginx/sites-enabled/idc${colors.reset}`); - console.log(` ${colors.cyan}sudo nginx -t && sudo systemctl restart nginx${colors.reset}`); - return; - } - } else { - log.error('当前平台不支持自动安装 Nginx,请手动安装'); - return; - } - } - - const root = isRootUser(); - const sudoPrefix = root ? '' : 'sudo '; - - // 复制前端构建产物到 /var/www/idc(Nginx 可访问的目录) - const wwwDir = '/var/www/idc'; - const distDir = path.join(__dirname, 'frontend', 'dist'); - - if (fs.existsSync(distDir)) { - log.info(`复制前端文件到 ${wwwDir}...`); - runCommand(`${sudoPrefix}mkdir -p ${wwwDir}`, { silent: true }); - const copyResult = runCommand(`${sudoPrefix}cp -r ${distDir}/* ${wwwDir}/`, { silent: true }); - if (copyResult.success) { - runCommand(`${sudoPrefix}chmod -R 755 ${wwwDir}`, { silent: true }); - log.success('前端文件部署完成'); - } else { - log.warning('前端文件复制失败,Nginx 可能无法访问前端页面'); - } - } else { - log.warning(`前端构建产物不存在: ${distDir}`); - log.info('请先完成前端构建后再配置 Nginx'); - } - - const configSource = path.join(__dirname, 'deploy', 'nginx-idc.conf'); - - if (!fs.existsSync(configSource)) { - log.error('Nginx 配置文件不存在,请先运行安装脚本'); - return; - } - - try { - 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; - } - - if (useSitesAvailable) { - log.info('创建站点软链接...'); - runCommand(`${sudoPrefix}ln -sf "${configDest}" /etc/nginx/sites-enabled/idc`); - - 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 配置测试失败'); - log.info('请检查配置文件: ' + configDest); - return; - } - - log.info('重启 Nginx 服务...'); - const restartResult = runCommand(`${sudoPrefix}systemctl restart nginx`); - if (restartResult.success) { - log.success('Nginx 配置完成并已生效'); - } else { - log.warning('systemctl 重启失败,尝试直接重载...'); - const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`); - if (reloadResult.success) { - log.success('Nginx 配置完成并已生效'); - } else { - log.warning('Nginx 重载失败,请手动重启: sudo systemctl restart nginx'); - } - } - - } catch (error) { - log.error(`自动配置失败: ${error.message}`); - log.info('请手动配置 Nginx'); - } -} - -// ============================================================================= -// 回滚机制 -// ============================================================================= - -/** - * 执行回滚操作 - * - * 按照安装步骤的逆序执行清理操作 - */ -async function rollback() { - if (rollbackSteps.length === 0) { - log.info('无需回滚'); - return; - } - - log.step('回滚'); - log.warning('安装失败,正在清理已安装的内容...'); - - const totalSteps = rollbackSteps.length; - let completedSteps = 0; - - for (const step of rollbackSteps.reverse()) { - try { - await step(); - completedSteps++; - } catch { - // 回滚步骤失败时继续执行后续步骤 - } - } - - // 清理生成的配置文件 - const configFiles = [ - path.join(__dirname, 'backend', '.env'), - path.join(__dirname, 'ecosystem.config.js'), - path.join(__dirname, 'deploy', 'nginx-idc.conf'), - ]; - - for (const file of configFiles) { - try { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - } catch { - // 忽略清理失败 - } - } - - // 保存 PM2 状态(清理已删除的服务) - try { - runCommand('pm2 save', { silent: true }); - } catch { - // 忽略 - } - - log.info(`已回滚 ${completedSteps}/${totalSteps} 个步骤`); - log.info('如需重新安装,请再次运行: node install.js'); -} - -// ============================================================================= -// 部署执行函数 -// ============================================================================= - -/** - * npm 镜像源列表 - * 当默认 registry 不可用时依次尝试 - */ -const NPM_MIRRORS = [ - { name: 'npm 官方', registry: 'https://registry.npmjs.org' }, - { name: '淘宝镜像', registry: 'https://registry.npmmirror.com' }, - { name: '腾讯镜像', registry: 'https://mirrors.tencent.com/npm' }, -]; - -/** - * 检测 npm registry 连通性 - * - * @param {string} registry - registry URL - * @param {number} timeout - 超时时间(毫秒) - * @returns {Promise} 是否可达 - */ -function checkRegistryReachable(registry, timeout = 10000) { - return new Promise((resolve) => { - const url = new URL(registry); - const http = url.protocol === 'https:' ? require('https') : require('http'); - const req = http.get(`${registry}/-/ping`, { timeout }, (res) => { - res.resume(); - resolve(res.statusCode >= 200 && res.statusCode < 500); - }); - req.on('error', () => resolve(false)); - req.on('timeout', () => { req.destroy(); resolve(false); }); - }); -} - -/** - * 检测可用的 npm 镜像源 - * - * @returns {Promise} 可用的 registry URL - */ -async function detectAvailableRegistry() { - for (const mirror of NPM_MIRRORS) { - log.subStep(`检测 ${mirror.name} (${mirror.registry})...`); - const reachable = await checkRegistryReachable(mirror.registry); - if (reachable) { - log.success(`${mirror.name} 可用`); - return mirror.registry; - } - log.subStep(`${mirror.name} 不可达`); - } - return NPM_MIRRORS[0].registry; -} - -/** - * 带重试和镜像源切换的 npm install - * - * @param {string} cwd - 工作目录 - * @param {string} label - 显示标签(如"后端"/"前端") - * @returns {Promise<{success: boolean, error?: string}>} - */ -async function npmInstallWithRetry(cwd, label) { - const maxRetries = 3; - const baseDelay = 3000; - - const availableRegistry = await detectAvailableRegistry(); - const isDefaultRegistry = availableRegistry === NPM_MIRRORS[0].registry; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - if (attempt > 1) { - const delay = baseDelay * Math.pow(2, attempt - 2); - log.subStep(`第 ${attempt}/${maxRetries} 次重试,${delay / 1000}秒后...`); - await new Promise(resolve => setTimeout(resolve, delay)); - } - - let command = 'npm install'; - if (!isDefaultRegistry) { - command += ` --registry=${availableRegistry}`; - } - - log.info(`安装${label}依赖${attempt > 1 ? ` (第${attempt}次尝试)` : ''}...`); - const result = runCommand(command, { cwd }); - - if (result.success) { - const nodeModulesPath = path.join(cwd, 'node_modules'); - if (fs.existsSync(nodeModulesPath)) { - return { success: true }; - } - log.subStep(`${label} node_modules 不存在,安装可能未完成`); - } else { - log.subStep(`${label}依赖安装失败: ${result.error || '未知错误'}`); - } - } - - return { success: false, error: `${label}依赖安装失败(已重试${maxRetries}次)` }; -} - -/** - * 安装项目依赖 - * - * 依次安装后端和前端的 npm 依赖 - * 支持镜像源自动切换和指数退避重试 - */ -async function installDependencies() { - log.step('安装依赖'); - - const backendDir = path.join(__dirname, 'backend'); - const frontendDir = path.join(__dirname, 'frontend'); - - log.info('安装后端依赖...'); - const backendResult = await npmInstallWithRetry(backendDir, '后端'); - if (backendResult.success) { - log.success('后端依赖安装完成'); - rollbackSteps.push(() => { - const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules'; - runCommand(rmCmd, { cwd: backendDir, silent: true }); - }); - } else { - log.error(backendResult.error); - throw new Error('Backend install failed'); - } - - log.info('安装前端依赖...'); - const frontendResult = await npmInstallWithRetry(frontendDir, '前端'); - if (frontendResult.success) { - log.success('前端依赖安装完成'); - rollbackSteps.push(() => { - const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules'; - runCommand(rmCmd, { cwd: frontendDir, silent: true }); - }); - } else { - log.error(frontendResult.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'); - throw new Error('Database initialization failed'); - } -} - -/** - * 获取系统可用内存(MB) - * - * @returns {number|null} 可用内存(MB),获取失败返回 null - */ -function getAvailableMemoryMB() { - try { - const os = require('os'); - return Math.round(os.freemem() / 1024 / 1024); - } catch { - return null; - } -} - -/** - * 构建前端项目 - * - * 执行 npm run build 生成生产环境静态文件 - * 自动检测系统内存,小内存环境下限制 Node 堆栈大小 - */ -async function buildFrontend() { - log.step('构建前端'); - - const frontendDir = path.join(__dirname, 'frontend'); - - const freeMemMB = getAvailableMemoryMB(); - let buildCommand = 'npm run build'; - - if (freeMemMB !== null) { - if (freeMemMB < 512) { - log.warning(`系统可用内存较低 (${freeMemMB}MB),前端构建可能失败`); - log.info('建议增加内存或创建 swap 分区后再试'); - } else if (freeMemMB < 1024) { - log.info(`可用内存 ${freeMemMB}MB,限制 Node 堆栈为 512MB...`); - buildCommand = 'node --max_old_space_size=512 node_modules/vite/bin/vite.js build'; - } - } - - log.info('执行前端构建...'); - const result = runCommand(buildCommand, { cwd: frontendDir }); - - if (result.success) { - log.success('前端构建完成'); - rollbackSteps.push(() => { - const distDir = path.join(frontendDir, 'dist'); - if (fs.existsSync(distDir)) { - const rmCmd = process.platform === 'win32' ? 'rmdir /s /q dist' : 'rm -rf dist'; - runCommand(rmCmd, { cwd: frontendDir, silent: true }); - } - }); - } 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})`); - rollbackSteps.push(() => { - runCommand('pm2 stop idc-backend', { silent: true }); - runCommand('pm2 delete idc-backend', { silent: true }); - }); - } else { - log.error('后端服务启动失败'); - log.info('请检查后端日志: pm2 logs idc-backend'); - throw new Error('Backend service start failed'); - } - - 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})`); - rollbackSteps.push(() => { - runCommand('pm2 stop idc-frontend', { silent: true }); - runCommand('pm2 delete idc-frontend', { silent: true }); - }); - } else { - log.error('前端服务启动失败'); - log.info('请检查前端日志: pm2 logs idc-frontend'); - throw new Error('Frontend service start failed'); - } - } - - 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('运行模式: 非交互式(使用默认配置)'); - } - - const savedConfig = loadSavedConfig(); - if (savedConfig) { - log.info(`检测到上次安装配置 (${savedConfig.savedAt || '未知时间'})`); - if (!cmdArgs.nonInteractive) { - const useSaved = await ask('是否使用上次配置作为默认值? (Y/n)', 'Y'); - if (useSaved.toLowerCase() === 'y') { - applySavedConfig(savedConfig); - log.success('已加载上次配置'); - } - } else { - applySavedConfig(savedConfig); - log.success('已加载上次配置'); - } - } - - try { - await checkEnvironment(); - await configureDatabase(cmdArgs); - await configureServices(cmdArgs); - - if (!cmdArgs.nonInteractive) { - await confirmConfiguration(); - } - - saveConfig(); - - generateBackendEnv(); - generatePM2Config(); - generateNginxConfig(); - log.step('生成配置文件'); - log.success('所有配置文件已生成'); - - await installDependencies(); - await initDatabase(); - - if (!cmdArgs.skipBuild) { - await buildFrontend(); - } else { - log.info('已跳过前端构建'); - } - - await startServices(); - - if (process.platform !== 'win32' && config.frontendDeploy === 'nginx') { - await autoConfigureNginx(); - } - - await healthCheck(); - printSummary(); - - } catch (error) { - log.error(`部署失败: ${error.message}`); - console.error(error); - await rollback(); - process.exit(1); - } finally { - rl.close(); - closeLogFile(); - } -} - -/** - * 使用 Node.js http 模块进行健康检查 - * - * @param {string} host - 主机地址 - * @param {number} port - 端口号 - * @param {string} path - 请求路径 - * @param {number} timeout - 超时时间(毫秒) - * @returns {Promise<{success: boolean, statusCode?: number, error?: string}>} - */ -function httpHealthCheck(host, port, path = '/health', timeout = 5000) { - return new Promise((resolve) => { - const http = require('http'); - const req = http.get({ hostname: host, port, path, timeout }, (res) => { - let body = ''; - res.on('data', (chunk) => { body += chunk; }); - res.on('end', () => { - resolve({ success: res.statusCode >= 200 && res.statusCode < 400, statusCode: res.statusCode }); - }); - }); - req.on('error', (err) => { - resolve({ success: false, error: err.message }); - }); - req.on('timeout', () => { - req.destroy(); - resolve({ success: false, error: 'timeout' }); - }); - }); -} - -async function healthCheck() { - log.step('健康检查'); - log.subStep('检查后端服务状态...'); - - const maxRetries = 5; - const retryDelay = 3000; - - for (let i = 1; i <= maxRetries; i++) { - const result = await httpHealthCheck('localhost', parseInt(config.backendPort)); - - if (result.success) { - log.success('后端服务健康检查通过'); - return { success: true }; - } - - const detail = result.statusCode ? `HTTP ${result.statusCode}` : (result.error || '无响应'); - if (i < maxRetries) { - log.subStep(`第 ${i} 次检查失败 (${detail}),${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(); +require('./install/index.js'); diff --git a/install/config.js b/install/config.js new file mode 100644 index 0000000..0b31421 --- /dev/null +++ b/install/config.js @@ -0,0 +1,110 @@ +const fs = require('fs'); +const path = require('path'); +const { SAVED_CONFIG_PATH, SCRIPT_VERSION } = require('./constants'); +const { colors, log } = require('./logger'); + +const config = { + dbType: 'sqlite', + dbConfig: {}, + backendPort: 8000, + nodeEnv: 'production', + frontendDeploy: 'nginx', + frontendPort: 80, + domain: 'localhost' +}; + +function saveConfig() { + const deployDir = path.dirname(SAVED_CONFIG_PATH); + if (!fs.existsSync(deployDir)) { + fs.mkdirSync(deployDir, { recursive: true }); + } + + const savedConfig = { + dbType: config.dbType, + dbConfig: { + host: config.dbConfig.host, + port: config.dbConfig.port, + username: config.dbConfig.username, + database: config.dbConfig.database, + }, + backendPort: config.backendPort, + nodeEnv: config.nodeEnv, + frontendDeploy: config.frontendDeploy, + frontendPort: config.frontendPort, + domain: config.domain, + savedAt: new Date().toISOString(), + }; + + fs.writeFileSync(SAVED_CONFIG_PATH, JSON.stringify(savedConfig, null, 2)); + log.subStep('配置已保存到 deploy/install-config.json'); +} + +function loadSavedConfig() { + try { + if (!fs.existsSync(SAVED_CONFIG_PATH)) { + return null; + } + const content = fs.readFileSync(SAVED_CONFIG_PATH, 'utf8'); + return JSON.parse(content); + } catch { + return null; + } +} + +function applySavedConfig(saved) { + if (!saved) return; + + if (saved.dbType) config.dbType = saved.dbType; + if (saved.dbConfig) { + config.dbConfig = { ...config.dbConfig, ...saved.dbConfig }; + } + if (saved.backendPort) config.backendPort = saved.backendPort; + if (saved.nodeEnv) config.nodeEnv = saved.nodeEnv; + if (saved.frontendDeploy) config.frontendDeploy = saved.frontendDeploy; + if (saved.frontendPort) config.frontendPort = saved.frontendPort; + if (saved.domain) config.domain = saved.domain; +} + +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); +} + +module.exports = { + config, + saveConfig, + loadSavedConfig, + applySavedConfig, + parseArgs, + showHelp, +}; diff --git a/install/constants.js b/install/constants.js new file mode 100644 index 0000000..da19b2c --- /dev/null +++ b/install/constants.js @@ -0,0 +1,35 @@ +const path = require('path'); + +const SCRIPT_VERSION = '2.0.0'; +const MIN_NODE_VERSION = 16; +const LOG_DIR = path.join(__dirname, '..', 'logs'); + +const INSTALL_STEPS = [ + '环境检测', + '数据库配置', + '服务配置', + '生成配置文件', + '安装依赖', + '数据库初始化', + '构建前端', + '启动服务', + '配置 Nginx', + '健康检查', +]; + +const NPM_MIRRORS = [ + { name: 'npm 官方', registry: 'https://registry.npmjs.org' }, + { name: '淘宝镜像', registry: 'https://registry.npmmirror.com' }, + { name: '腾讯镜像', registry: 'https://mirrors.tencent.com/npm' }, +]; + +const SAVED_CONFIG_PATH = path.join(__dirname, '..', 'deploy', 'install-config.json'); + +module.exports = { + SCRIPT_VERSION, + MIN_NODE_VERSION, + LOG_DIR, + INSTALL_STEPS, + NPM_MIRRORS, + SAVED_CONFIG_PATH, +}; diff --git a/install/database.js b/install/database.js new file mode 100644 index 0000000..d975282 --- /dev/null +++ b/install/database.js @@ -0,0 +1,261 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); +const { colors, log } = require('./logger'); +const { ask, askPassword, select } = require('./ui'); +const { config } = require('./config'); +const { detectLinuxDistro } = require('./env-check'); + +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 }; + } +} + +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() || '连接失败' }; + } +} + +module.exports = { + configureDatabase, + configureExistingMySQL, + installMySQL, + installMySQLDebian, + installMySQLRHEL, + createMySQLDatabase, + testMySQLConnection, +}; diff --git a/install/env-check.js b/install/env-check.js new file mode 100644 index 0000000..2ea8852 --- /dev/null +++ b/install/env-check.js @@ -0,0 +1,426 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); +const { MIN_NODE_VERSION } = require('./constants'); +const { colors, log } = require('./logger'); +const { runCommand, commandExists } = require('./utils'); +const { ask } = require('./ui'); + +function checkNodeVersion() { + const version = process.version; + const major = parseInt(version.slice(1).split('.')[0]); + return major >= MIN_NODE_VERSION; +} + +function isNginxInstalled() { + try { + execSync('nginx -v', { stdio: 'pipe', shell: true }); + return true; + } catch { + 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; + } +} + +function checkNodeAndNpm() { + let nodeInstalled = false; + let npmInstalled = false; + let version = ''; + + try { + version = process.version; + nodeInstalled = true; + } catch { + nodeInstalled = false; + } + + npmInstalled = commandExists('npm'); + + return { nodeInstalled, npmInstalled, version }; +} + +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'; +} + +function isRootUser() { + return process.getuid && process.getuid() === 0; +} + +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) { + 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`); + } + + 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`); + + 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) { + 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) { + 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); +} + +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: + 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; + } +} + +async function installNodeViaNvm() { + log.info('使用 nvm 安装 Node.js...'); + + try { + const nvmCheck = runCommand('source ~/.nvm/nvm.sh && nvm --version', { silent: true }); + + if (!nvmCheck.success) { + 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; + } + } + + 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; + } + + 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; + } +} + +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); + } +} + +async function checkEnvironment() { + log.step('环境检测'); + + const { nodeInstalled, npmInstalled, version } = checkNodeAndNpm(); + const isLinux = process.platform === 'linux'; + + if (!nodeInstalled) { + if (isLinux) { + const shouldContinue = await handleNodeNotInstalledLinux(); + if (shouldContinue) { + return await checkEnvironment(); + } + } else { + log.error('未检测到 Node.js,请先安装'); + showNodeInstallGuide(); + process.exit(1); + } + } + + if (!npmInstalled) { + if (isLinux && nodeInstalled) { + 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); + } + } + + 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); + } + + const npmResult = runCommand('npm --version', { silent: true }); + if (npmResult.success) { + log.success(`npm ${npmResult.output.trim()}`); + } + + 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'); + } + } + + if (commandExists('nginx') || commandExists('nginx.exe')) { + log.success('Nginx 已安装'); + } else { + log.warning('未检测到 Nginx(可选,仅在使用Nginx部署时需要)'); + } + + log.divider(); +} + +module.exports = { + checkNodeVersion, + isNginxInstalled, + checkNodeAndNpm, + detectLinuxDistro, + isRootUser, + showNodeInstallGuide, + autoInstallNodeLinux, + installNodeViaNvm, + handleNodeNotInstalledLinux, + checkEnvironment, +}; diff --git a/install/generator.js b/install/generator.js new file mode 100644 index 0000000..c4684cf --- /dev/null +++ b/install/generator.js @@ -0,0 +1,257 @@ +const fs = require('fs'); +const path = require('path'); +const { colors, log } = require('./logger'); +const { generateSecretKey } = require('./utils'); +const { config } = require('./config'); +const { ask } = require('./ui'); + +function generateBackendEnv() { + const envPath = path.join(__dirname, '..', 'backend', '.env'); + + try { + 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); + + if (process.platform !== 'win32') { + try { + fs.chmodSync(envPath, 0o600); + log.success('.env 文件权限已设为 600(仅所有者可读写)'); + } catch { + log.warning('.env 文件权限设置失败,建议手动执行: chmod 600 backend/.env'); + } + } + + log.success('后端环境变量文件已生成 (.env)'); + log.info(`JWT_SECRET 已自动生成 (${jwtSecret.length}位)`); + log.warning('请妥善保管 .env 文件,切勿提交到版本控制系统'); + } catch (error) { + log.error(`后端环境变量文件生成失败: ${error.message}`); + throw new Error('Backend env generation failed'); + } +} + +function generatePM2Config() { + const deployDir = path.join(__dirname, '..', 'deploy'); + + try { + if (!fs.existsSync(deployDir)) { + fs.mkdirSync(deployDir, { recursive: true }); + } + + 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 + }] + }; + + if (config.frontendDeploy === 'pm2') { + pm2Config.apps.push({ + name: 'idc-frontend', + script: 'serve', + cwd: path.join(__dirname, '..', 'frontend'), + args: `-s dist -l ${config.frontendPort}`, + 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)'); + } catch (error) { + log.error(`PM2 配置文件生成失败: ${error.message}`); + throw new Error('PM2 config generation failed'); + } +} + +function generateNginxConfig() { + if (config.frontendDeploy !== 'nginx') return; + + const deployDir = path.join(__dirname, '..', 'deploy'); + + try { + if (!fs.existsSync(deployDir)) { + fs.mkdirSync(deployDir, { recursive: true }); + } + + const isWindows = process.platform === 'win32'; + + let frontendPath; + if (isWindows) { + frontendPath = path.join(__dirname, '..', 'frontend', 'dist').replace(/\\/g, '/'); + } else { + frontendPath = '/var/www/idc'; + } + + const nginxConfig = `# IDC设备管理系统 - Nginx配置 +# 生成时间: ${new Date().toISOString()} + +server { + listen ${config.frontendPort}; + server_name ${config.domain}; + + client_max_body_size 100M; + + # 前端静态文件目录 + root "${frontendPath}"; + index index.html; + + # Gzip压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/x-javascript image/svg+xml; + + # 静态资源缓存(1年) + location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|hdr)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # API代理到后端 + location /api/ { + proxy_pass http://127.0.0.1:${config.backendPort}/api/; + 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_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 300s; + proxy_buffering off; + } + + # 文件上传代理 + location /uploads/ { + proxy_pass http://127.0.0.1:${config.backendPort}/uploads/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + client_max_body_size 100M; + } + + # 前端路由支持(SPA单页应用,必须放在最后) + location / { + try_files $uri $uri/ /index.html =404; + } +} +`; + + fs.writeFileSync(path.join(deployDir, 'nginx-idc.conf'), nginxConfig); + log.success('Nginx 配置文件已生成 (deploy/nginx-idc.conf)'); + + 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}`); + } + } catch (error) { + log.error(`Nginx 配置文件生成失败: ${error.message}`); + throw new Error('Nginx config generation failed'); + } +} + +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(); +} + +module.exports = { + generateBackendEnv, + generatePM2Config, + generateNginxConfig, + confirmConfiguration, +}; diff --git a/install/health.js b/install/health.js new file mode 100644 index 0000000..4fdd65f --- /dev/null +++ b/install/health.js @@ -0,0 +1,53 @@ +const { log } = require('./logger'); +const { config } = require('./config'); + +function httpHealthCheck(host, port, path = '/health', timeout = 5000) { + return new Promise((resolve) => { + const http = require('http'); + const req = http.get({ hostname: host, port, path, timeout }, (res) => { + let body = ''; + res.on('data', (chunk) => { body += chunk; }); + res.on('end', () => { + resolve({ success: res.statusCode >= 200 && res.statusCode < 400, statusCode: res.statusCode }); + }); + }); + req.on('error', (err) => { + resolve({ success: false, error: err.message }); + }); + req.on('timeout', () => { + req.destroy(); + resolve({ success: false, error: 'timeout' }); + }); + }); +} + +async function healthCheck() { + log.step('健康检查'); + log.subStep('检查后端服务状态...'); + + const maxRetries = 5; + const retryDelay = 3000; + + for (let i = 1; i <= maxRetries; i++) { + const result = await httpHealthCheck('localhost', parseInt(config.backendPort)); + + if (result.success) { + log.success('后端服务健康检查通过'); + return { success: true }; + } + + const detail = result.statusCode ? `HTTP ${result.statusCode}` : (result.error || '无响应'); + if (i < maxRetries) { + log.subStep(`第 ${i} 次检查失败 (${detail}),${retryDelay/1000}秒后重试...`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + } + + log.warning('健康检查未通过,请手动验证服务状态'); + return { success: false }; +} + +module.exports = { + httpHealthCheck, + healthCheck, +}; diff --git a/install/index.js b/install/index.js new file mode 100644 index 0000000..5ccd2db --- /dev/null +++ b/install/index.js @@ -0,0 +1,134 @@ +const { SCRIPT_VERSION, LOG_DIR } = require('./constants'); +const { colors, log, initLogFile, closeLogFile } = require('./logger'); +const { ask, closeReadline } = require('./ui'); +const { config, saveConfig, loadSavedConfig, applySavedConfig, parseArgs, showHelp } = require('./config'); +const { checkEnvironment } = require('./env-check'); +const { configureDatabase } = require('./database'); +const { configureServices, autoConfigureNginx } = require('./nginx'); +const { generateBackendEnv, generatePM2Config, generateNginxConfig, confirmConfiguration } = require('./generator'); +const { installDependencies, initDatabase, buildFrontend, startServices } = require('./installer'); +const { healthCheck } = require('./health'); +const { rollback } = require('./rollback'); + +let installStartTime = Date.now(); + +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('安装部署完成!'); +} + +async function main() { + installStartTime = Date.now(); + const cmdArgs = parseArgs(); + + if (cmdArgs.help) { + showHelp(); + return; + } + + initLogFile(LOG_DIR); + + console.log(` +${colors.bright}${colors.cyan} +╔══════════════════════════════════════════════════════════╗ +║ IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION} ║ +║ Interactive Installation & Deployment Script ║ +╚══════════════════════════════════════════════════════════╝ +${colors.reset}`); + + if (cmdArgs.nonInteractive) { + log.info('运行模式: 非交互式(使用默认配置)'); + } + + const savedConfig = loadSavedConfig(); + if (savedConfig) { + log.info(`检测到上次安装配置 (${savedConfig.savedAt || '未知时间'})`); + if (!cmdArgs.nonInteractive) { + const useSaved = await ask('是否使用上次配置作为默认值? (Y/n)', 'Y'); + if (useSaved.toLowerCase() === 'y') { + applySavedConfig(savedConfig); + log.success('已加载上次配置'); + } + } else { + applySavedConfig(savedConfig); + log.success('已加载上次配置'); + } + } + + try { + await checkEnvironment(); + await configureDatabase(cmdArgs); + await configureServices(cmdArgs); + + if (!cmdArgs.nonInteractive) { + await confirmConfiguration(); + } + + saveConfig(); + + generateBackendEnv(); + generatePM2Config(); + generateNginxConfig(); + log.step('生成配置文件'); + log.success('所有配置文件已生成'); + + await installDependencies(); + await initDatabase(); + + if (!cmdArgs.skipBuild) { + await buildFrontend(); + } else { + log.info('已跳过前端构建'); + } + + await startServices(); + + if (process.platform !== 'win32' && config.frontendDeploy === 'nginx') { + await autoConfigureNginx(); + } + + await healthCheck(); + printSummary(); + + } catch (error) { + log.error(`部署失败: ${error.message}`); + console.error(error); + await rollback(); + process.exit(1); + } finally { + closeReadline(); + closeLogFile(); + } +} + +main(); diff --git a/install/installer.js b/install/installer.js new file mode 100644 index 0000000..bfb2b87 --- /dev/null +++ b/install/installer.js @@ -0,0 +1,242 @@ +const fs = require('fs'); +const path = require('path'); +const { log } = require('./logger'); +const { runCommand } = require('./utils'); +const { config } = require('./config'); +const { rollbackSteps } = require('./rollback'); +const { NPM_MIRRORS } = require('./constants'); + +function checkRegistryReachable(registry, timeout = 10000) { + return new Promise((resolve) => { + const url = new URL(registry); + const http = url.protocol === 'https:' ? require('https') : require('http'); + const req = http.get(`${registry}/-/ping`, { timeout }, (res) => { + res.resume(); + resolve(res.statusCode >= 200 && res.statusCode < 500); + }); + req.on('error', () => resolve(false)); + req.on('timeout', () => { req.destroy(); resolve(false); }); + }); +} + +async function detectAvailableRegistry() { + for (const mirror of NPM_MIRRORS) { + log.subStep(`检测 ${mirror.name} (${mirror.registry})...`); + const reachable = await checkRegistryReachable(mirror.registry); + if (reachable) { + log.success(`${mirror.name} 可用`); + return mirror.registry; + } + log.subStep(`${mirror.name} 不可达`); + } + return NPM_MIRRORS[0].registry; +} + +async function npmInstallWithRetry(cwd, label) { + const maxRetries = 3; + const baseDelay = 3000; + + const availableRegistry = await detectAvailableRegistry(); + const isDefaultRegistry = availableRegistry === NPM_MIRRORS[0].registry; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + if (attempt > 1) { + const delay = baseDelay * Math.pow(2, attempt - 2); + log.subStep(`第 ${attempt}/${maxRetries} 次重试,${delay / 1000}秒后...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + + let command = 'npm install'; + if (!isDefaultRegistry) { + command += ` --registry=${availableRegistry}`; + } + + log.info(`安装${label}依赖${attempt > 1 ? ` (第${attempt}次尝试)` : ''}...`); + const result = runCommand(command, { cwd }); + + if (result.success) { + const nodeModulesPath = path.join(cwd, 'node_modules'); + if (fs.existsSync(nodeModulesPath)) { + return { success: true }; + } + log.subStep(`${label} node_modules 不存在,安装可能未完成`); + } else { + log.subStep(`${label}依赖安装失败: ${result.error || '未知错误'}`); + } + } + + return { success: false, error: `${label}依赖安装失败(已重试${maxRetries}次)` }; +} + +async function installDependencies() { + log.step('安装依赖'); + + const backendDir = path.join(__dirname, '..', 'backend'); + const frontendDir = path.join(__dirname, '..', 'frontend'); + + log.info('安装后端依赖...'); + const backendResult = await npmInstallWithRetry(backendDir, '后端'); + if (backendResult.success) { + log.success('后端依赖安装完成'); + rollbackSteps.push(() => { + const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules'; + runCommand(rmCmd, { cwd: backendDir, silent: true }); + }); + } else { + log.error(backendResult.error); + throw new Error('Backend install failed'); + } + + log.info('安装前端依赖...'); + const frontendResult = await npmInstallWithRetry(frontendDir, '前端'); + if (frontendResult.success) { + log.success('前端依赖安装完成'); + rollbackSteps.push(() => { + const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules'; + runCommand(rmCmd, { cwd: frontendDir, silent: true }); + }); + } else { + log.error(frontendResult.error); + throw new Error('Frontend install failed'); + } +} + +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'); + throw new Error('Database initialization failed'); + } +} + +function getAvailableMemoryMB() { + try { + const os = require('os'); + return Math.round(os.freemem() / 1024 / 1024); + } catch { + return null; + } +} + +async function buildFrontend() { + log.step('构建前端'); + + const frontendDir = path.join(__dirname, '..', 'frontend'); + + const freeMemMB = getAvailableMemoryMB(); + let buildCommand = 'npm run build'; + + if (freeMemMB !== null) { + if (freeMemMB < 512) { + log.warning(`系统可用内存较低 (${freeMemMB}MB),前端构建可能失败`); + log.info('建议增加内存或创建 swap 分区后再试'); + } else if (freeMemMB < 1024) { + log.info(`可用内存 ${freeMemMB}MB,限制 Node 堆栈为 512MB...`); + buildCommand = 'node --max_old_space_size=512 node_modules/vite/bin/vite.js build'; + } + } + + log.info('执行前端构建...'); + const result = runCommand(buildCommand, { cwd: frontendDir }); + + if (result.success) { + log.success('前端构建完成'); + rollbackSteps.push(() => { + const distDir = path.join(frontendDir, 'dist'); + if (fs.existsSync(distDir)) { + const rmCmd = process.platform === 'win32' ? 'rmdir /s /q dist' : 'rm -rf dist'; + runCommand(rmCmd, { cwd: frontendDir, silent: true }); + } + }); + } else { + log.error('前端构建失败'); + throw new Error('Frontend build failed'); + } +} + +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})`); + rollbackSteps.push(() => { + runCommand('pm2 stop idc-backend', { silent: true }); + runCommand('pm2 delete idc-backend', { silent: true }); + }); + } else { + log.error('后端服务启动失败'); + log.info('请检查后端日志: pm2 logs idc-backend'); + throw new Error('Backend service start failed'); + } + + 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})`); + rollbackSteps.push(() => { + runCommand('pm2 stop idc-frontend', { silent: true }); + runCommand('pm2 delete idc-frontend', { silent: true }); + }); + } else { + log.error('前端服务启动失败'); + log.info('请检查前端日志: pm2 logs idc-frontend'); + throw new Error('Frontend service start failed'); + } + } + + runCommand('pm2 save', { silent: true }); + + log.divider(); +} + +module.exports = { + checkRegistryReachable, + detectAvailableRegistry, + npmInstallWithRetry, + installDependencies, + initDatabase, + getAvailableMemoryMB, + buildFrontend, + startServices, +}; diff --git a/install/logger.js b/install/logger.js new file mode 100644 index 0000000..10e4810 --- /dev/null +++ b/install/logger.js @@ -0,0 +1,68 @@ +const { INSTALL_STEPS } = require('./constants'); + +let currentStepIndex = 0; +let logFileStream = null; + +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) => { + const idx = INSTALL_STEPS.indexOf(msg); + if (idx >= 0) { + currentStepIndex = idx; + } + const progress = currentStepIndex < INSTALL_STEPS.length + ? `${colors.gray}[${currentStepIndex + 1}/${INSTALL_STEPS.length}]${colors.reset} ` + : ''; + console.log(`\n${progress}${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}`) +}; + +function initLogFile(LOG_DIR) { + const fs = require('fs'); + const path = require('path'); + 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(); + } +} + +module.exports = { + colors, + log, + initLogFile, + closeLogFile, +}; diff --git a/install/nginx.js b/install/nginx.js new file mode 100644 index 0000000..fb4db2b --- /dev/null +++ b/install/nginx.js @@ -0,0 +1,404 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { colors, log } = require('./logger'); +const { ask, select } = require('./ui'); +const { config } = require('./config'); +const { runCommand } = require('./utils'); +const { isNginxInstalled, detectLinuxDistro, isRootUser } = require('./env-check'); + +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); +} + +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 安装完成'); + + 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; + } +} + +async function autoInstallNginx() { + log.step('自动安装 Nginx'); + log.info('正在下载 Nginx...'); + + const https = require('https'); + + const nginxUrl = 'https://nginx.org/download/nginx-1.24.0.zip'; + const downloadPath = path.join(__dirname, '..', 'nginx-1.24.0.zip'); + const installPath = 'C:\\nginx'; + + try { + 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 下载完成'); + + log.info('正在解压...'); + execSync(`powershell -Command "Expand-Archive -Path '${downloadPath}' -DestinationPath 'C:\\\\' -Force"`, { stdio: 'inherit' }); + + 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); + } + + fs.unlinkSync(downloadPath); + + 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; + } +} + +async function autoConfigureNginx() { + log.step('配置 Nginx'); + + if (!isNginxInstalled()) { + log.warning('Nginx 未安装,尝试自动安装...'); + if (process.platform === 'linux') { + const installed = await autoInstallNginxLinux(); + if (!installed) { + log.error('Nginx 自动安装失败'); + log.info('请手动安装 Nginx 后执行:'); + console.log(` ${colors.cyan}sudo cp deploy/nginx-idc.conf /etc/nginx/sites-available/idc${colors.reset}`); + console.log(` ${colors.cyan}sudo ln -sf /etc/nginx/sites-available/idc /etc/nginx/sites-enabled/idc${colors.reset}`); + console.log(` ${colors.cyan}sudo nginx -t && sudo systemctl restart nginx${colors.reset}`); + return; + } + } else { + log.error('当前平台不支持自动安装 Nginx,请手动安装'); + return; + } + } + + const root = isRootUser(); + const sudoPrefix = root ? '' : 'sudo '; + + const wwwDir = '/var/www/idc'; + const distDir = path.join(__dirname, '..', 'frontend', 'dist'); + + if (fs.existsSync(distDir)) { + log.info(`复制前端文件到 ${wwwDir}...`); + runCommand(`${sudoPrefix}mkdir -p ${wwwDir}`, { silent: true }); + const copyResult = runCommand(`${sudoPrefix}cp -r ${distDir}/* ${wwwDir}/`, { silent: true }); + if (copyResult.success) { + runCommand(`${sudoPrefix}chmod -R 755 ${wwwDir}`, { silent: true }); + log.success('前端文件部署完成'); + } else { + log.warning('前端文件复制失败,Nginx 可能无法访问前端页面'); + } + } else { + log.warning(`前端构建产物不存在: ${distDir}`); + log.info('请先完成前端构建后再配置 Nginx'); + } + + const configSource = path.join(__dirname, '..', 'deploy', 'nginx-idc.conf'); + + if (!fs.existsSync(configSource)) { + log.error('Nginx 配置文件不存在,请先运行安装脚本'); + return; + } + + try { + 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; + } + + if (useSitesAvailable) { + log.info('创建站点软链接...'); + runCommand(`${sudoPrefix}ln -sf "${configDest}" /etc/nginx/sites-enabled/idc`); + + 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 配置测试失败'); + log.info('请检查配置文件: ' + configDest); + return; + } + + log.info('重启 Nginx 服务...'); + const restartResult = runCommand(`${sudoPrefix}systemctl restart nginx`); + if (restartResult.success) { + log.success('Nginx 配置完成并已生效'); + } else { + log.warning('systemctl 重启失败,尝试直接重载...'); + const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`); + if (reloadResult.success) { + log.success('Nginx 配置完成并已生效'); + } else { + log.warning('Nginx 重载失败,请手动重启: sudo systemctl restart nginx'); + } + } + + } catch (error) { + log.error(`自动配置失败: ${error.message}`); + log.info('请手动配置 Nginx'); + } +} + +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(); +} + +module.exports = { + showNginxInstallGuideLinux, + autoInstallNginxLinux, + autoInstallNginx, + autoConfigureNginx, + configureServices, +}; diff --git a/install/rollback.js b/install/rollback.js new file mode 100644 index 0000000..9bc736d --- /dev/null +++ b/install/rollback.js @@ -0,0 +1,60 @@ +const { log } = require('./logger'); +const { runCommand } = require('./utils'); + +const rollbackSteps = []; + +function addRollbackStep(fn) { + rollbackSteps.push(fn); +} + +async function rollback() { + if (rollbackSteps.length === 0) { + log.info('无需回滚'); + return; + } + + log.step('回滚'); + log.warning('安装失败,正在清理已安装的内容...'); + + const totalSteps = rollbackSteps.length; + let completedSteps = 0; + + for (const step of rollbackSteps.reverse()) { + try { + await step(); + completedSteps++; + } catch { + } + } + + const fs = require('fs'); + const path = require('path'); + const configFiles = [ + path.join(__dirname, '..', 'backend', '.env'), + path.join(__dirname, '..', 'ecosystem.config.js'), + path.join(__dirname, '..', 'deploy', 'nginx-idc.conf'), + ]; + + for (const file of configFiles) { + try { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + } catch { + } + } + + try { + runCommand('pm2 save', { silent: true }); + } catch { + } + + log.info(`已回滚 ${completedSteps}/${totalSteps} 个步骤`); + log.info('如需重新安装,请再次运行: node install.js'); +} + +module.exports = { + rollbackSteps, + addRollbackStep, + rollback, +}; diff --git a/install/ui.js b/install/ui.js new file mode 100644 index 0000000..7c009bb --- /dev/null +++ b/install/ui.js @@ -0,0 +1,102 @@ +const readline = require('readline'); +const { log } = require('./logger'); + +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); +}); + +function ask(question, defaultValue = '') { + return new Promise((resolve) => { + const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `; + rl.question(prompt, (answer) => { + resolve(answer.trim() || defaultValue); + }); + }); +} + +function askPassword(question) { + return new Promise((resolve) => { + const prompt = `${question}: `; + process.stdout.write(prompt); + + let password = ''; + const stdin = process.stdin; + const stdout = process.stdout; + + const cleanup = () => { + if (stdin.isTTY) { + try { + stdin.setRawMode(false); + } catch {} + } + stdin.removeListener('data', onData); + }; + + const onData = (data) => { + const char = data.toString(); + const code = char.charCodeAt(0); + + if (code === 10 || code === 13) { + cleanup(); + stdout.write('\n'); + resolve(password); + } else if (code === 3) { + cleanup(); + stdout.write('\n已取消\n'); + process.exit(0); + } else if (code === 127 || code === 8) { + if (password.length > 0) { + password = password.slice(0, -1); + stdout.write('\b \b'); + } + } else if (code >= 32) { + password += char; + stdout.write('*'); + } + }; + + if (stdin.isTTY) { + stdin.setRawMode(true); + stdin.resume(); + stdin.on('data', onData); + } else { + rl.question('', (answer) => { + resolve(answer.trim()); + }); + } + }); +} + +async function select(question, options) { + const { colors } = require('./logger'); + 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; +} + +function closeReadline() { + rl.close(); +} + +module.exports = { + ask, + askPassword, + select, + closeReadline, +}; diff --git a/install/utils.js b/install/utils.js new file mode 100644 index 0000000..eacdfc9 --- /dev/null +++ b/install/utils.js @@ -0,0 +1,35 @@ +const { execSync } = require('child_process'); +const crypto = require('crypto'); + +function runCommand(command, options = {}) { + try { + const result = execSync(command, { + encoding: 'utf8', + stdio: options.silent ? 'pipe' : 'inherit', + cwd: options.cwd || process.cwd(), + shell: true + }); + return { success: true, output: result }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +function commandExists(command) { + try { + execSync(`${command} --version`, { stdio: 'pipe', shell: true }); + return true; + } catch { + return false; + } +} + +function generateSecretKey(length = 64) { + return crypto.randomBytes(length).toString('hex'); +} + +module.exports = { + runCommand, + commandExists, + generateSecretKey, +};