diff --git a/install/index.js b/install/index.js index a818afe..f0c8ac9 100644 --- a/install/index.js +++ b/install/index.js @@ -21,13 +21,14 @@ function printSummary() { null ); - console.log(` ${ICONS.pipe} ${colors.dim}安装耗时${colors.reset} ${colors.cyan}${duration} 秒${colors.reset}`); - console.log(` ${ICONS.pipe} ${colors.dim}数据库类型${colors.reset} ${colors.cyan}${config.dbType}${colors.reset}`); - console.log(` ${ICONS.pipe} ${colors.dim}后端端口${colors.reset} ${colors.cyan}${config.backendPort}${colors.reset}`); - console.log(` ${ICONS.pipe} ${colors.dim}前端部署${colors.reset} ${colors.cyan}${config.frontendDeploy}${colors.reset}`); + log.keyValue('安装耗时', `${duration} 秒`); + log.keyValue('数据库类型', config.dbType); + log.keyValue('后端端口', String(config.backendPort)); + log.keyValue('运行环境', config.nodeEnv); + log.keyValue('前端部署', config.frontendDeploy); + log.keyValue('前端端口', String(config.frontendPort)); if (config.frontendDeploy === 'nginx') { - console.log(` ${ICONS.pipe} ${colors.dim}前端端口${colors.reset} ${colors.cyan}${config.frontendPort}${colors.reset}`); - console.log(` ${ICONS.pipe} ${colors.dim}域名${colors.reset} ${colors.cyan}${config.domain}${colors.reset}`); + log.keyValue('域名', config.domain); } log.divider(); diff --git a/install/logger.js b/install/logger.js index 9c0e9e6..9bd0793 100644 --- a/install/logger.js +++ b/install/logger.js @@ -2,21 +2,26 @@ const { INSTALL_STEPS } = require('./constants'); let currentStepIndex = 0; let logFileStream = null; +const NO_COLOR = process.env.NO_COLOR === '1' || process.env.NO_COLOR === 'true'; +const IS_TTY = process.stdout.isTTY !== undefined && process.stdout.isTTY !== null; +const FORCE_COLOR = process.env.FORCE_COLOR !== undefined; +const USE_COLOR = !NO_COLOR && (IS_TTY || FORCE_COLOR); +const ESC = USE_COLOR ? '\x1b' : ''; const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - dim: '\x1b[2m', - green: '\x1b[32m', - yellow: '\x1b[33m', - red: '\x1b[31m', - cyan: '\x1b[36m', - gray: '\x1b[90m', - magenta: '\x1b[35m', - blue: '\x1b[34m', - bgCyan: '\x1b[46m', - bgBlue: '\x1b[44m', - white: '\x1b[37m', + reset: `${ESC}[0m`, + bright: `${ESC}[1m`, + dim: `${ESC}[2m`, + green: `${ESC}[32m`, + yellow: `${ESC}[33m`, + red: `${ESC}[31m`, + cyan: `${ESC}[36m`, + gray: `${ESC}[90m`, + magenta: `${ESC}[35m`, + blue: `${ESC}[34m`, + bgCyan: `${ESC}[46m`, + bgBlue: `${ESC}[44m`, + white: `${ESC}[37m`, }; const ICONS = { @@ -59,10 +64,14 @@ const log = { warning: (msg) => console.log(` ${colors.yellow}${ICONS.warning}${colors.reset} ${msg}`), error: (msg) => console.log(` ${colors.red}${ICONS.error}${colors.reset} ${msg}`), - step: (msg) => { - const idx = INSTALL_STEPS.indexOf(msg); - if (idx >= 0) { - currentStepIndex = idx; + step: (msg, stepIndex) => { + if (stepIndex !== undefined) { + currentStepIndex = stepIndex; + } else { + const idx = INSTALL_STEPS.findIndex(step => msg.includes(step) || step.includes(msg)); + if (idx >= 0) { + currentStepIndex = idx; + } } const current = currentStepIndex + 1; const total = INSTALL_STEPS.length; diff --git a/uninstall.js b/uninstall.js index 8403e46..a9df827 100644 --- a/uninstall.js +++ b/uninstall.js @@ -28,6 +28,16 @@ const { SCRIPT_VERSION } = require('./install/constants'); const BACKUP_DIR = path.join(__dirname, 'backup'); const LOG_DIR = path.join(__dirname, 'logs'); +const UNINSTALL_STEPS = [ + '停止服务', + '清理 Nginx', + '清理配置', + '数据库清理', + '日志清理', + '上传清理', + '依赖清理', +]; + let logFileStream = null; let uninstallStartTime = null; let deletedItems = []; @@ -53,20 +63,20 @@ function showHelp() { ${colors.bright}用法:${colors.reset} node uninstall.js [选项] ${colors.bright}选项:${colors.reset} - ${ICONS.pointer} ${colors.cyan}-f, --force${colors.reset} 强制卸载(无需确认) - ${ICONS.pointer} ${colors.cyan}-y, --yes${colors.reset} 强制卸载别名(同 --force) - ${ICONS.pointer} ${colors.cyan}-b, --backup${colors.reset} 卸载前自动备份数据库 - ${colors.dim}--dry-run${colors.reset} 预览模式:仅显示将要删除的内容 - ${colors.dim}--skip-db${colors.reset} 跳过数据库删除 - ${colors.dim}--skip-deps${colors.reset} 跳过依赖删除 - ${colors.dim}--skip-uploads${colors.reset} 跳过上传文件目录删除 - ${colors.dim}-h, --help${colors.reset} 显示帮助信息 + ${ICONS.pointer} ${colors.cyan}-f, --force${colors.reset} 强制卸载(无需确认) + ${ICONS.pointer} ${colors.cyan}-y, --yes${colors.reset} 强制卸载别名(同 --force) + ${ICONS.pointer} ${colors.cyan}-b, --backup${colors.reset} 卸载前自动备份数据库 + ${colors.dim}--dry-run${colors.reset} 预览模式:仅显示将要删除的内容 + ${colors.dim}--skip-db${colors.reset} 跳过数据库删除 + ${colors.dim}--skip-deps${colors.reset} 跳过依赖删除 + ${colors.dim}--skip-uploads${colors.reset} 跳过上传文件目录删除 + ${colors.dim}-h, --help${colors.reset} 显示帮助信息 ${colors.bright}示例:${colors.reset} - ${colors.cyan}node uninstall.js${colors.reset} # 交互式卸载 - ${colors.cyan}node uninstall.js --force${colors.reset} # 强制卸载 - ${colors.cyan}node uninstall.js --backup${colors.reset} # 卸载前备份 - ${colors.cyan}node uninstall.js --dry-run${colors.reset} # 预览将删除的内容 + ${colors.cyan}node uninstall.js${colors.reset} # 交互式卸载 + ${colors.cyan}node uninstall.js --force${colors.reset} # 强制卸载 + ${colors.cyan}node uninstall.js --backup${colors.reset} # 卸载前备份 + ${colors.cyan}node uninstall.js --dry-run${colors.reset} # 预览将删除的内容 `); process.exit(0); } @@ -165,7 +175,7 @@ function isRootUser() { } async function stopAndDeleteServices() { - log.step('停止并删除服务'); + log.step('停止服务'); if (!commandExists('pm2')) { log.warning('未检测到 PM2,跳过服务停止步骤'); @@ -243,7 +253,7 @@ async function stopAndDeleteServices() { } async function cleanupNginxConfig(cmdArgs) { - log.step('清理 Nginx 配置'); + log.step('清理 Nginx'); const isWindows = process.platform === 'win32'; const isLinux = process.platform === 'linux'; @@ -501,7 +511,7 @@ async function cleanupNginxConfig(cmdArgs) { } async function cleanupConfigFiles() { - log.step('清理生成的配置文件'); + log.step('清理配置'); const filesToDelete = [ { path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)', type: '配置文件' }, @@ -687,7 +697,7 @@ async function cleanupDependencies(cmdArgs) { } async function cleanupLogs(cmdArgs) { - log.step('日志文件清理'); + log.step('日志清理'); const logsDir = path.join(__dirname, 'backend', 'logs'); if (fs.existsSync(logsDir)) { @@ -733,7 +743,7 @@ async function cleanupLogs(cmdArgs) { } async function cleanupUploads(cmdArgs) { - log.step('上传文件清理'); + log.step('上传清理'); const uploadsDir = path.join(__dirname, 'backend', 'uploads'); if (fs.existsSync(uploadsDir)) { @@ -837,7 +847,9 @@ function showDryRunPreview(cmdArgs) { }); console.log(`\n ${ICONS.pointer} ${colors.bright}4. 数据库${colors.reset}`); - if (dbConfig.dbType === 'sqlite' && fs.existsSync(dbConfig.sqliteDbPath)) { + if (cmdArgs?.skipDb) { + console.log(` ${ICONS.pipe} ${colors.dim}[跳过] 数据库清理${colors.reset}`); + } else if (dbConfig.dbType === 'sqlite' && fs.existsSync(dbConfig.sqliteDbPath)) { console.log(` ${ICONS.pipe} 删除 SQLite 数据库: ${path.relative(__dirname, dbConfig.sqliteDbPath)}`); if (cmdArgs?.backup) { console.log(` ${ICONS.pipe} 备份数据库到: backup/`); @@ -857,23 +869,31 @@ function showDryRunPreview(cmdArgs) { } console.log(`\n ${ICONS.pointer} ${colors.bright}6. 上传文件${colors.reset}`); - if (fs.existsSync(path.join(__dirname, 'backend', 'uploads'))) { + if (cmdArgs?.skipUploads) { + console.log(` ${ICONS.pipe} ${colors.dim}[跳过] 上传文件目录${colors.reset}`); + } else if (fs.existsSync(path.join(__dirname, 'backend', 'uploads'))) { console.log(` ${ICONS.pipe} 上传文件目录 (backend/uploads/)`); + } else { + console.log(` ${ICONS.pipe} ${colors.dim}未检测到上传文件目录${colors.reset}`); } console.log(`\n ${ICONS.pointer} ${colors.bright}7. 依赖和构建产物${colors.reset}`); - const depDirs = [ - { path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules' }, - { path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules' }, - { path: path.join(__dirname, 'frontend', 'node_modules', '.vite'), name: '前端 Vite 缓存' }, - { path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物' }, - { path: path.join(__dirname, 'backend', 'temp'), name: '后端临时文件' } - ]; - depDirs.forEach(d => { - if (fs.existsSync(d.path)) { - console.log(` ${ICONS.pipe} 删除: ${d.name}`); - } - }); + if (cmdArgs?.skipDeps) { + console.log(` ${ICONS.pipe} ${colors.dim}[跳过] 依赖和构建产物清理${colors.reset}`); + } else { + const depDirs = [ + { path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules' }, + { path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules' }, + { path: path.join(__dirname, 'frontend', 'node_modules', '.vite'), name: '前端 Vite 缓存' }, + { path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物' }, + { path: path.join(__dirname, 'backend', 'temp'), name: '后端临时文件' } + ]; + depDirs.forEach(d => { + if (fs.existsSync(d.path)) { + console.log(` ${ICONS.pipe} 删除: ${d.name}`); + } + }); + } log.divider(); log.warning('移除 --dry-run 参数以实际执行卸载'); @@ -903,8 +923,19 @@ function printSummary() { } log.section('保留的文件'); - console.log(` ${ICONS.pipe} 项目源代码 (backend/, frontend/)`); - console.log(` ${ICONS.pipe} 上传的文件 (backend/uploads/)`); + const backendExists = fs.existsSync(path.join(__dirname, 'backend')); + const frontendExists = fs.existsSync(path.join(__dirname, 'frontend')); + const uploadsExist = fs.existsSync(path.join(__dirname, 'backend', 'uploads')); + + if (backendExists || frontendExists) { + console.log(` ${ICONS.pipe} 项目源代码 (backend/, frontend/)`); + } + if (uploadsExist) { + console.log(` ${ICONS.pipe} 上传的文件 (backend/uploads/)`); + } + if (!backendExists && !frontendExists && !uploadsExist) { + console.log(` ${ICONS.pipe} 无`); + } log.section('重新部署'); console.log(` ${ICONS.pipe} ${colors.cyan}node install.js${colors.reset}`); diff --git a/update.js b/update.js index db221af..3236f33 100644 --- a/update.js +++ b/update.js @@ -9,6 +9,8 @@ const { execSync, spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); +const { colors, ICONS, log, padCenter, initLogFile, closeLogFile } = require('./install/logger'); +const { SCRIPT_VERSION: INSTALL_SCRIPT_VERSION, LOG_DIR: INSTALL_LOG_DIR } = require('./install/constants'); const SCRIPT_VERSION = '2.0.0'; const MIN_NODE_VERSION = 16; @@ -17,28 +19,16 @@ const LOG_DIR = path.join(__dirname, 'logs'); const BACKUP_DIR = path.join(__dirname, 'backup'); const MAX_BACKUP_FILES = 10; -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 UPDATE_STEPS = [ + '备份数据', + '拉取代码', + '安装依赖', + '数据库迁移', + '构建前端', + '重启服务', + '健康检查', +]; -const log = { - info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`), - success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`), - warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), - error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`), - step: (msg) => console.log(`\n${colors.bright}${colors.cyan}▶ ${msg}${colors.reset}`), - divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`), - subStep: (msg) => console.log(` ${colors.gray}└${colors.reset} ${msg}`) -}; - -let logFileStream = null; let updateStartTime = null; let rollbackInfo = { backupDbPath: null, @@ -62,54 +52,29 @@ function parseArgs() { function showHelp() { console.log(` -${colors.bright}IDC设备管理系统 - 一键更新脚本 v${SCRIPT_VERSION}${colors.reset} + ${colors.bright}IDC设备管理系统 - 一键更新脚本 v${SCRIPT_VERSION}${colors.reset} -用法: node update.js [选项] + ${colors.bright}用法:${colors.reset} node update.js [选项] -选项: - --skip-git 跳过 Git 拉取 - --skip-backup 跳过数据库备份 - --skip-deps 跳过依赖安装 - --skip-migrate 跳过数据库迁移 - --skip-build 跳过前端构建 - --skip-restart 跳过服务重启 - --dry-run 模拟运行,不执行实际操作 - --force 强制执行,忽略锁文件 - -h, --help 显示帮助信息 + ${colors.bright}选项:${colors.reset} + ${ICONS.pointer} ${colors.cyan}--skip-git${colors.reset} 跳过 Git 拉取 + ${ICONS.pointer} ${colors.cyan}--skip-backup${colors.reset} 跳过数据库备份 + ${ICONS.pointer} ${colors.cyan}--skip-deps${colors.reset} 跳过依赖安装 + ${ICONS.pointer} ${colors.cyan}--skip-migrate${colors.reset} 跳过数据库迁移 + ${ICONS.pointer} ${colors.cyan}--skip-build${colors.reset} 跳过前端构建 + ${ICONS.pointer} ${colors.cyan}--skip-restart${colors.reset} 跳过服务重启 + ${colors.dim}--dry-run${colors.reset} 模拟运行,不执行实际操作 + ${colors.dim}--force${colors.reset} 强制执行,忽略锁文件 + ${colors.dim}-h, --help${colors.reset} 显示帮助信息 -示例: - node update.js # 完整更新流程 - node update.js --skip-deps # 跳过依赖安装 - node update.js --dry-run # 模拟运行 + ${colors.bright}示例:${colors.reset} + ${colors.cyan}node update.js${colors.reset} # 完整更新流程 + ${colors.cyan}node update.js --skip-deps${colors.reset} # 跳过依赖安装 + ${colors.cyan}node update.js --dry-run${colors.reset} # 模拟运行 `); process.exit(0); } -function initLogFile() { - if (!fs.existsSync(LOG_DIR)) { - fs.mkdirSync(LOG_DIR, { recursive: true }); - } - const logFileName = `update_${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(); - } -} - function checkLock(options) { if (options.force) { if (fs.existsSync(LOCK_FILE)) { @@ -144,9 +109,9 @@ function releaseLock() { function checkNodeVersion() { const nodeVersion = process.versions.node; const majorVersion = parseInt(nodeVersion.split('.')[0], 10); - + log.info(`Node.js 版本: ${nodeVersion}`); - + if (majorVersion < MIN_NODE_VERSION) { log.error(`Node.js 版本过低,需要 v${MIN_NODE_VERSION} 或更高版本`); return false; @@ -176,16 +141,16 @@ function runCommand(command, options = {}) { function getGitInfo() { try { - const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'] + const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] }).trim(); - - const currentCommit = execSync('git rev-parse HEAD', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'] + + const currentCommit = execSync('git rev-parse HEAD', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] }).trim().substring(0, 7); - + return { branch: currentBranch, commit: currentCommit }; } catch { return { branch: 'unknown', commit: 'unknown' }; @@ -330,7 +295,7 @@ function pullCode(options) { if (gitStatus.output && gitStatus.output.trim()) { log.warning('工作区有未提交的更改'); log.subStep(gitStatus.output.trim().split('\n').slice(0, 5).join('\n ')); - + const stashResult = runCommand('git stash'); if (!stashResult.success) { log.error('暂存更改失败,请手动处理'); @@ -357,7 +322,6 @@ function pullCode(options) { function checkDepsNeedInstall(dirPath, dirName) { const nodeModulesPath = path.join(dirPath, 'node_modules'); const packageJsonPath = path.join(dirPath, 'package.json'); - const packageLockPath = path.join(dirPath, 'package-lock.json'); if (!fs.existsSync(packageJsonPath)) { return { needInstall: false, reason: '未找到 package.json' }; @@ -377,16 +341,14 @@ function checkDepsNeedInstall(dirPath, dirName) { if (gitDiff) { return { needInstall: true, reason: 'package.json 或 package-lock.json 有更新' }; } - } catch { - // Git 检查失败,继续其他检测 - } + } catch {} try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - const depsCount = Object.keys(packageJson.dependencies || {}).length + + const depsCount = Object.keys(packageJson.dependencies || {}).length + Object.keys(packageJson.devDependencies || {}).length; - - const installedCount = fs.readdirSync(nodeModulesPath).filter(f => + + const installedCount = fs.readdirSync(nodeModulesPath).filter(f => !f.startsWith('.') && fs.statSync(path.join(nodeModulesPath, f)).isDirectory() ).length; @@ -394,7 +356,6 @@ function checkDepsNeedInstall(dirPath, dirName) { return { needInstall: true, reason: `已安装依赖数量不足 (${installedCount}/${depsCount})` }; } } catch { - // 检测失败,保守起见执行安装 return { needInstall: true, reason: '依赖状态检测失败' }; } @@ -470,10 +431,10 @@ function runMigrations(options) { } log.subStep('执行数据库迁移...'); - const migrateResult = runCommand('node scripts/migrate-all.js', { + const migrateResult = runCommand('node scripts/migrate-all.js', { cwd: path.join(__dirname, 'backend') }); - + if (migrateResult.success) { log.success('数据库迁移完成'); return { success: true }; @@ -501,14 +462,12 @@ function checkFrontendNeedsBuild() { if (gitDiff) { const changedFiles = gitDiff.split('\n').filter(f => f).slice(0, 3); - return { - needBuild: true, + return { + needBuild: true, reason: `源码或配置有更新: ${changedFiles.join(', ')}${gitDiff.split('\n').length > 3 ? '...' : ''}` }; } - } catch { - // Git 检查失败,继续其他检测 - } + } catch {} try { let latestSrcTime = 0; @@ -540,7 +499,6 @@ function checkFrontendNeedsBuild() { return { needBuild: true, reason: '源码修改时间晚于构建产物' }; } } catch { - // 时间检测失败,保守起见执行构建 return { needBuild: true, reason: '构建状态检测失败' }; } @@ -562,11 +520,11 @@ function buildFrontend(options) { log.subStep('检测前端构建状态...'); const buildCheck = checkFrontendNeedsBuild(); - + if (buildCheck.needBuild) { log.info(`前端需要构建: ${buildCheck.reason}`); const buildResult = runCommand('npm run build', { cwd: frontendPath }); - + if (buildResult.success) { log.success('前端构建完成'); return { success: true }; @@ -594,7 +552,7 @@ function restartServices(options) { log.subStep('检查 PM2 服务状态...'); const listResult = runCommand('pm2 list', { silent: true }); - + if (!listResult.success) { log.warning('PM2 未安装或未运行,尝试直接启动'); return startDirectly(); @@ -612,7 +570,7 @@ function restartServices(options) { log.subStep(`重启后端服务 (${backendServiceName})...`); const backendRestart = runCommand(`pm2 restart ${backendServiceName}`); - + if (backendRestart.success) { log.success('后端服务已重启'); } else { @@ -635,10 +593,10 @@ function restartServices(options) { function startDirectly() { log.subStep('尝试直接启动后端服务...'); - + const backendPath = path.join(__dirname, 'backend'); const serverPath = path.join(backendPath, 'server.js'); - + if (!fs.existsSync(serverPath)) { log.error('未找到 server.js'); return { success: false }; @@ -660,27 +618,50 @@ function startDirectly() { } } +function httpHealthCheck(url) { + return new Promise((resolve) => { + try { + const http = require('http'); + const urlObj = new URL(url); + const req = http.request({ + hostname: urlObj.hostname, + port: urlObj.port || 80, + path: urlObj.pathname, + method: 'GET', + timeout: 5000 + }, (res) => { + resolve({ success: res.statusCode === 200, statusCode: res.statusCode }); + }); + req.on('error', () => resolve({ success: false, statusCode: 0 })); + req.on('timeout', () => { + req.destroy(); + resolve({ success: false, statusCode: 0 }); + }); + req.end(); + } catch { + resolve({ success: false, statusCode: 0 }); + } + }); +} + async function healthCheck() { log.subStep('检查服务健康状态...'); - + const maxRetries = 5; const retryDelay = 3000; - + const healthUrl = `http://localhost:${getBackendPort()}/health`; + for (let i = 1; i <= maxRetries; i++) { try { - const result = execSync('curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>nul || echo "000"', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], - timeout: 5000 - }).trim(); - - if (result === '200') { + const result = await httpHealthCheck(healthUrl); + + if (result.success) { log.success('服务健康检查通过'); return { success: true }; } - + if (i < maxRetries) { - log.subStep(`第 ${i} 次检查失败 (HTTP ${result}),${retryDelay/1000}秒后重试...`); + log.subStep(`第 ${i} 次检查失败 (HTTP ${result.statusCode}),${retryDelay/1000}秒后重试...`); await new Promise(resolve => setTimeout(resolve, retryDelay)); } } catch (error) { @@ -695,6 +676,20 @@ async function healthCheck() { return { success: false, warning: true }; } +function getBackendPort() { + const envPath = path.join(__dirname, 'backend', '.env'); + if (!fs.existsSync(envPath)) { + return 8000; + } + try { + const envContent = fs.readFileSync(envPath, 'utf8'); + const portMatch = envContent.match(/PORT=(\d+)/); + return portMatch ? parseInt(portMatch[1], 10) : 8000; + } catch { + return 8000; + } +} + function rollback(reason) { log.divider(); log.error(`更新失败: ${reason}`); @@ -715,32 +710,56 @@ function rollback(reason) { function printSummary(options, results) { const duration = ((Date.now() - updateStartTime) / 1000).toFixed(1); - - console.log(`\n${colors.bright}${colors.magenta} -╔══════════════════════════════════════════════════════════╗ -║ 更新摘要 ║ -╚══════════════════════════════════════════════════════════╝${colors.reset}`); - console.log(`\n ${colors.cyan}执行模式:${colors.reset} ${options.dryRun ? '模拟运行' : '实际执行'}`); - console.log(` ${colors.cyan}耗时:${colors.reset} ${duration} 秒`); - - console.log(`\n ${colors.cyan}步骤状态:${colors.reset}`); - console.log(` 数据库备份: ${results.backup?.skipped ? '已跳过' : results.backup?.success ? '成功' : '失败'}`); - console.log(` 代码更新: ${results.git?.skipped ? '已跳过' : results.git?.success ? '成功' : '失败'}`); - console.log(` 依赖安装: ${results.deps?.skipped ? '已跳过' : results.deps?.success ? '成功' : '失败'}`); - console.log(` 数据库迁移: ${results.migrate?.skipped ? '已跳过' : results.migrate?.success ? '成功' : '失败'}`); - console.log(` 前端构建: ${results.build?.skipped ? '已跳过' : results.build?.success ? '成功' : '失败'}`); - console.log(` 服务重启: ${results.restart?.skipped ? '已跳过' : results.restart?.success ? '成功' : '失败'}`); - console.log(` 健康检查: ${results.health?.success ? '通过' : '未通过'}`); + log.banner('更新摘要', 'Update Summary', null); - console.log(`\n ${colors.cyan}访问地址:${colors.reset}`); - console.log(` 后端API: http://localhost:8000/api`); - console.log(` 前端页面: http://localhost`); - - console.log(`\n ${colors.cyan}日志文件:${colors.reset}`); - console.log(` ${path.join(LOG_DIR, `update_*.log`)}`); - - console.log(''); + log.keyValue('执行模式', options.dryRun ? '模拟运行' : '实际执行'); + log.keyValue('执行耗时', `${duration} 秒`); + + log.divider(); + + const steps = [ + { name: '数据库备份', result: results.backup }, + { name: '代码更新', result: results.git }, + { name: '依赖安装', result: results.deps }, + { name: '数据库迁移', result: results.migrate }, + { name: '前端构建', result: results.build }, + { name: '服务重启', result: results.restart }, + { name: '健康检查', result: results.health }, + ]; + + steps.forEach((step, idx) => { + let status = ''; + if (step.result?.skipped) { + status = `${colors.dim}已跳过${colors.reset}`; + } else if (step.result?.success) { + status = `${colors.green}${ICONS.success}${colors.reset} ${colors.green}成功${colors.reset}`; + } else if (step.result?.warning) { + status = `${colors.yellow}${ICONS.warning}${colors.reset} ${colors.yellow}警告${colors.reset}`; + } else { + status = `${colors.red}${ICONS.error}${colors.reset} ${colors.red}失败${colors.reset}`; + } + console.log(` ${ICONS.pipe} ${colors.dim}${String(idx + 1)}.${colors.reset} ${step.name.padEnd(12)} ${status}`); + }); + + log.divider(); + + log.section('访问地址'); + const backendPort = getBackendPort(); + console.log(` ${ICONS.pipe} ${colors.dim}后端 API${colors.reset} ${colors.cyan}http://localhost:${backendPort}/api${colors.reset}`); + console.log(` ${ICONS.pipe} ${colors.dim}前端页面${colors.reset} ${colors.cyan}http://localhost${colors.reset}`); + + log.divider(); + + log.divider(); + if (!options.dryRun && results.health?.success) { + log.success('更新完成!系统已成功更新并正常运行'); + } else if (options.dryRun) { + log.success('模拟运行完成!使用不带 --dry-run 参数执行实际更新'); + } else { + log.warning('更新完成,但健康检查未通过,请手动验证服务状态'); + } + log.divider(); } async function main() { @@ -752,18 +771,16 @@ async function main() { return; } - initLogFile(); + initLogFile(LOG_DIR); - console.log(` -${colors.bright}${colors.cyan} -╔══════════════════════════════════════════════════════════╗ -║ IDC设备管理系统 - 一键更新脚本 v${SCRIPT_VERSION} ║ -║ One-Click Update Script ║ -╚══════════════════════════════════════════════════════════╝ -${colors.reset}`); + log.banner( + 'IDC 设备管理系统', + '一键更新脚本', + SCRIPT_VERSION + ); if (options.dryRun) { - log.info('运行模式: 模拟运行 (不会执行实际操作)'); + log.info('运行模式: 模拟运行(不会执行实际操作)'); } const results = {}; @@ -784,41 +801,41 @@ ${colors.reset}`); process.exit(130); }); - log.step('1. 备份数据'); + log.step('备份数据', 0); results.backup = backupDatabase(options); if (!results.backup.success && !options.dryRun) { throw new Error('数据库备份失败'); } - log.step('2. 拉取最新代码'); + log.step('拉取代码', 1); results.git = pullCode(options); - log.step('3. 安装依赖'); + log.step('安装依赖', 2); results.deps = installDependencies(options); if (!results.deps.success && !options.dryRun) { throw new Error('依赖安装失败'); } - log.step('4. 数据库迁移'); + log.step('数据库迁移', 3); results.migrate = runMigrations(options); - log.step('5. 构建前端'); + log.step('构建前端', 4); results.build = buildFrontend(options); if (!results.build.success && !options.dryRun) { throw new Error('前端构建失败'); } - log.step('6. 重启服务'); + log.step('重启服务', 5); results.restart = restartServices(options); if (!results.restart.success && !options.dryRun) { throw new Error('服务重启失败'); } - log.step('7. 健康检查'); + log.step('健康检查', 6); results.health = await healthCheck(); log.divider(); - + if (!options.dryRun && results.health.success) { log.success('更新完成!系统已成功更新并正常运行'); } else if (options.dryRun) {