refactor(install,uninstall,update): 统一日志输出并优化安装流程

- 重构install模块的打印逻辑,使用统一的log工具替代原生console
- 新增运行环境、前端端口等安装信息展示
- 优化uninstall模块的步骤命名和预览逻辑,新增跳过提示
- 重构update模块,复用install的日志工具,统一输出格式
- 新增健康检查直接读取后端端口配置,不再硬编码8000
This commit is contained in:
zhang1106
2026-05-12 08:57:14 +08:00
parent 58a152e155
commit 2f8e95be49
4 changed files with 261 additions and 203 deletions
+164 -147
View File
@@ -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) {