refactor(install,uninstall,update): 统一日志输出并优化安装流程
- 重构install模块的打印逻辑,使用统一的log工具替代原生console - 新增运行环境、前端端口等安装信息展示 - 优化uninstall模块的步骤命名和预览逻辑,新增跳过提示 - 重构update模块,复用install的日志工具,统一输出格式 - 新增健康检查直接读取后端端口配置,不再硬编码8000
This commit is contained in:
+7
-6
@@ -21,13 +21,14 @@ function printSummary() {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(` ${ICONS.pipe} ${colors.dim}安装耗时${colors.reset} ${colors.cyan}${duration} 秒${colors.reset}`);
|
log.keyValue('安装耗时', `${duration} 秒`);
|
||||||
console.log(` ${ICONS.pipe} ${colors.dim}数据库类型${colors.reset} ${colors.cyan}${config.dbType}${colors.reset}`);
|
log.keyValue('数据库类型', config.dbType);
|
||||||
console.log(` ${ICONS.pipe} ${colors.dim}后端端口${colors.reset} ${colors.cyan}${config.backendPort}${colors.reset}`);
|
log.keyValue('后端端口', String(config.backendPort));
|
||||||
console.log(` ${ICONS.pipe} ${colors.dim}前端部署${colors.reset} ${colors.cyan}${config.frontendDeploy}${colors.reset}`);
|
log.keyValue('运行环境', config.nodeEnv);
|
||||||
|
log.keyValue('前端部署', config.frontendDeploy);
|
||||||
|
log.keyValue('前端端口', String(config.frontendPort));
|
||||||
if (config.frontendDeploy === 'nginx') {
|
if (config.frontendDeploy === 'nginx') {
|
||||||
console.log(` ${ICONS.pipe} ${colors.dim}前端端口${colors.reset} ${colors.cyan}${config.frontendPort}${colors.reset}`);
|
log.keyValue('域名', config.domain);
|
||||||
console.log(` ${ICONS.pipe} ${colors.dim}域名${colors.reset} ${colors.cyan}${config.domain}${colors.reset}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.divider();
|
log.divider();
|
||||||
|
|||||||
+24
-15
@@ -2,21 +2,26 @@ const { INSTALL_STEPS } = require('./constants');
|
|||||||
|
|
||||||
let currentStepIndex = 0;
|
let currentStepIndex = 0;
|
||||||
let logFileStream = null;
|
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 = {
|
const colors = {
|
||||||
reset: '\x1b[0m',
|
reset: `${ESC}[0m`,
|
||||||
bright: '\x1b[1m',
|
bright: `${ESC}[1m`,
|
||||||
dim: '\x1b[2m',
|
dim: `${ESC}[2m`,
|
||||||
green: '\x1b[32m',
|
green: `${ESC}[32m`,
|
||||||
yellow: '\x1b[33m',
|
yellow: `${ESC}[33m`,
|
||||||
red: '\x1b[31m',
|
red: `${ESC}[31m`,
|
||||||
cyan: '\x1b[36m',
|
cyan: `${ESC}[36m`,
|
||||||
gray: '\x1b[90m',
|
gray: `${ESC}[90m`,
|
||||||
magenta: '\x1b[35m',
|
magenta: `${ESC}[35m`,
|
||||||
blue: '\x1b[34m',
|
blue: `${ESC}[34m`,
|
||||||
bgCyan: '\x1b[46m',
|
bgCyan: `${ESC}[46m`,
|
||||||
bgBlue: '\x1b[44m',
|
bgBlue: `${ESC}[44m`,
|
||||||
white: '\x1b[37m',
|
white: `${ESC}[37m`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -59,11 +64,15 @@ const log = {
|
|||||||
warning: (msg) => console.log(` ${colors.yellow}${ICONS.warning}${colors.reset} ${msg}`),
|
warning: (msg) => console.log(` ${colors.yellow}${ICONS.warning}${colors.reset} ${msg}`),
|
||||||
error: (msg) => console.log(` ${colors.red}${ICONS.error}${colors.reset} ${msg}`),
|
error: (msg) => console.log(` ${colors.red}${ICONS.error}${colors.reset} ${msg}`),
|
||||||
|
|
||||||
step: (msg) => {
|
step: (msg, stepIndex) => {
|
||||||
const idx = INSTALL_STEPS.indexOf(msg);
|
if (stepIndex !== undefined) {
|
||||||
|
currentStepIndex = stepIndex;
|
||||||
|
} else {
|
||||||
|
const idx = INSTALL_STEPS.findIndex(step => msg.includes(step) || step.includes(msg));
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
currentStepIndex = idx;
|
currentStepIndex = idx;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const current = currentStepIndex + 1;
|
const current = currentStepIndex + 1;
|
||||||
const total = INSTALL_STEPS.length;
|
const total = INSTALL_STEPS.length;
|
||||||
const bar = progressBar(current, total, 15);
|
const bar = progressBar(current, total, 15);
|
||||||
|
|||||||
+38
-7
@@ -28,6 +28,16 @@ const { SCRIPT_VERSION } = require('./install/constants');
|
|||||||
const BACKUP_DIR = path.join(__dirname, 'backup');
|
const BACKUP_DIR = path.join(__dirname, 'backup');
|
||||||
const LOG_DIR = path.join(__dirname, 'logs');
|
const LOG_DIR = path.join(__dirname, 'logs');
|
||||||
|
|
||||||
|
const UNINSTALL_STEPS = [
|
||||||
|
'停止服务',
|
||||||
|
'清理 Nginx',
|
||||||
|
'清理配置',
|
||||||
|
'数据库清理',
|
||||||
|
'日志清理',
|
||||||
|
'上传清理',
|
||||||
|
'依赖清理',
|
||||||
|
];
|
||||||
|
|
||||||
let logFileStream = null;
|
let logFileStream = null;
|
||||||
let uninstallStartTime = null;
|
let uninstallStartTime = null;
|
||||||
let deletedItems = [];
|
let deletedItems = [];
|
||||||
@@ -165,7 +175,7 @@ function isRootUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function stopAndDeleteServices() {
|
async function stopAndDeleteServices() {
|
||||||
log.step('停止并删除服务');
|
log.step('停止服务');
|
||||||
|
|
||||||
if (!commandExists('pm2')) {
|
if (!commandExists('pm2')) {
|
||||||
log.warning('未检测到 PM2,跳过服务停止步骤');
|
log.warning('未检测到 PM2,跳过服务停止步骤');
|
||||||
@@ -243,7 +253,7 @@ async function stopAndDeleteServices() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupNginxConfig(cmdArgs) {
|
async function cleanupNginxConfig(cmdArgs) {
|
||||||
log.step('清理 Nginx 配置');
|
log.step('清理 Nginx');
|
||||||
|
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const isLinux = process.platform === 'linux';
|
const isLinux = process.platform === 'linux';
|
||||||
@@ -501,7 +511,7 @@ async function cleanupNginxConfig(cmdArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupConfigFiles() {
|
async function cleanupConfigFiles() {
|
||||||
log.step('清理生成的配置文件');
|
log.step('清理配置');
|
||||||
|
|
||||||
const filesToDelete = [
|
const filesToDelete = [
|
||||||
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)', type: '配置文件' },
|
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)', type: '配置文件' },
|
||||||
@@ -687,7 +697,7 @@ async function cleanupDependencies(cmdArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupLogs(cmdArgs) {
|
async function cleanupLogs(cmdArgs) {
|
||||||
log.step('日志文件清理');
|
log.step('日志清理');
|
||||||
|
|
||||||
const logsDir = path.join(__dirname, 'backend', 'logs');
|
const logsDir = path.join(__dirname, 'backend', 'logs');
|
||||||
if (fs.existsSync(logsDir)) {
|
if (fs.existsSync(logsDir)) {
|
||||||
@@ -733,7 +743,7 @@ async function cleanupLogs(cmdArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupUploads(cmdArgs) {
|
async function cleanupUploads(cmdArgs) {
|
||||||
log.step('上传文件清理');
|
log.step('上传清理');
|
||||||
|
|
||||||
const uploadsDir = path.join(__dirname, 'backend', 'uploads');
|
const uploadsDir = path.join(__dirname, 'backend', 'uploads');
|
||||||
if (fs.existsSync(uploadsDir)) {
|
if (fs.existsSync(uploadsDir)) {
|
||||||
@@ -837,7 +847,9 @@ function showDryRunPreview(cmdArgs) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log(`\n ${ICONS.pointer} ${colors.bright}4. 数据库${colors.reset}`);
|
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)}`);
|
console.log(` ${ICONS.pipe} 删除 SQLite 数据库: ${path.relative(__dirname, dbConfig.sqliteDbPath)}`);
|
||||||
if (cmdArgs?.backup) {
|
if (cmdArgs?.backup) {
|
||||||
console.log(` ${ICONS.pipe} 备份数据库到: backup/`);
|
console.log(` ${ICONS.pipe} 备份数据库到: backup/`);
|
||||||
@@ -857,11 +869,18 @@ function showDryRunPreview(cmdArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n ${ICONS.pointer} ${colors.bright}6. 上传文件${colors.reset}`);
|
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/)`);
|
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}`);
|
console.log(`\n ${ICONS.pointer} ${colors.bright}7. 依赖和构建产物${colors.reset}`);
|
||||||
|
if (cmdArgs?.skipDeps) {
|
||||||
|
console.log(` ${ICONS.pipe} ${colors.dim}[跳过] 依赖和构建产物清理${colors.reset}`);
|
||||||
|
} else {
|
||||||
const depDirs = [
|
const depDirs = [
|
||||||
{ path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules' },
|
{ 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'), name: '前端 node_modules' },
|
||||||
@@ -874,6 +893,7 @@ function showDryRunPreview(cmdArgs) {
|
|||||||
console.log(` ${ICONS.pipe} 删除: ${d.name}`);
|
console.log(` ${ICONS.pipe} 删除: ${d.name}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
log.divider();
|
log.divider();
|
||||||
log.warning('移除 --dry-run 参数以实际执行卸载');
|
log.warning('移除 --dry-run 参数以实际执行卸载');
|
||||||
@@ -903,8 +923,19 @@ function printSummary() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.section('保留的文件');
|
log.section('保留的文件');
|
||||||
|
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/)`);
|
console.log(` ${ICONS.pipe} 项目源代码 (backend/, frontend/)`);
|
||||||
|
}
|
||||||
|
if (uploadsExist) {
|
||||||
console.log(` ${ICONS.pipe} 上传的文件 (backend/uploads/)`);
|
console.log(` ${ICONS.pipe} 上传的文件 (backend/uploads/)`);
|
||||||
|
}
|
||||||
|
if (!backendExists && !frontendExists && !uploadsExist) {
|
||||||
|
console.log(` ${ICONS.pipe} 无`);
|
||||||
|
}
|
||||||
|
|
||||||
log.section('重新部署');
|
log.section('重新部署');
|
||||||
console.log(` ${ICONS.pipe} ${colors.cyan}node install.js${colors.reset}`);
|
console.log(` ${ICONS.pipe} ${colors.cyan}node install.js${colors.reset}`);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
const { execSync, spawn } = require('child_process');
|
const { execSync, spawn } = require('child_process');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
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 SCRIPT_VERSION = '2.0.0';
|
||||||
const MIN_NODE_VERSION = 16;
|
const MIN_NODE_VERSION = 16;
|
||||||
@@ -17,28 +19,16 @@ const LOG_DIR = path.join(__dirname, 'logs');
|
|||||||
const BACKUP_DIR = path.join(__dirname, 'backup');
|
const BACKUP_DIR = path.join(__dirname, 'backup');
|
||||||
const MAX_BACKUP_FILES = 10;
|
const MAX_BACKUP_FILES = 10;
|
||||||
|
|
||||||
const colors = {
|
const UPDATE_STEPS = [
|
||||||
reset: '\x1b[0m',
|
'备份数据',
|
||||||
bright: '\x1b[1m',
|
'拉取代码',
|
||||||
green: '\x1b[32m',
|
'安装依赖',
|
||||||
yellow: '\x1b[33m',
|
'数据库迁移',
|
||||||
red: '\x1b[31m',
|
'构建前端',
|
||||||
cyan: '\x1b[36m',
|
'重启服务',
|
||||||
gray: '\x1b[90m',
|
'健康检查',
|
||||||
magenta: '\x1b[35m'
|
];
|
||||||
};
|
|
||||||
|
|
||||||
const log = {
|
|
||||||
info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`),
|
|
||||||
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
|
||||||
warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
|
|
||||||
error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
|
|
||||||
step: (msg) => console.log(`\n${colors.bright}${colors.cyan}▶ ${msg}${colors.reset}`),
|
|
||||||
divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`),
|
|
||||||
subStep: (msg) => console.log(` ${colors.gray}└${colors.reset} ${msg}`)
|
|
||||||
};
|
|
||||||
|
|
||||||
let logFileStream = null;
|
|
||||||
let updateStartTime = null;
|
let updateStartTime = null;
|
||||||
let rollbackInfo = {
|
let rollbackInfo = {
|
||||||
backupDbPath: null,
|
backupDbPath: null,
|
||||||
@@ -64,52 +54,27 @@ function showHelp() {
|
|||||||
console.log(`
|
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 [选项]
|
||||||
|
|
||||||
选项:
|
${colors.bright}选项:${colors.reset}
|
||||||
--skip-git 跳过 Git 拉取
|
${ICONS.pointer} ${colors.cyan}--skip-git${colors.reset} 跳过 Git 拉取
|
||||||
--skip-backup 跳过数据库备份
|
${ICONS.pointer} ${colors.cyan}--skip-backup${colors.reset} 跳过数据库备份
|
||||||
--skip-deps 跳过依赖安装
|
${ICONS.pointer} ${colors.cyan}--skip-deps${colors.reset} 跳过依赖安装
|
||||||
--skip-migrate 跳过数据库迁移
|
${ICONS.pointer} ${colors.cyan}--skip-migrate${colors.reset} 跳过数据库迁移
|
||||||
--skip-build 跳过前端构建
|
${ICONS.pointer} ${colors.cyan}--skip-build${colors.reset} 跳过前端构建
|
||||||
--skip-restart 跳过服务重启
|
${ICONS.pointer} ${colors.cyan}--skip-restart${colors.reset} 跳过服务重启
|
||||||
--dry-run 模拟运行,不执行实际操作
|
${colors.dim}--dry-run${colors.reset} 模拟运行,不执行实际操作
|
||||||
--force 强制执行,忽略锁文件
|
${colors.dim}--force${colors.reset} 强制执行,忽略锁文件
|
||||||
-h, --help 显示帮助信息
|
${colors.dim}-h, --help${colors.reset} 显示帮助信息
|
||||||
|
|
||||||
示例:
|
${colors.bright}示例:${colors.reset}
|
||||||
node update.js # 完整更新流程
|
${colors.cyan}node update.js${colors.reset} # 完整更新流程
|
||||||
node update.js --skip-deps # 跳过依赖安装
|
${colors.cyan}node update.js --skip-deps${colors.reset} # 跳过依赖安装
|
||||||
node update.js --dry-run # 模拟运行
|
${colors.cyan}node update.js --dry-run${colors.reset} # 模拟运行
|
||||||
`);
|
`);
|
||||||
process.exit(0);
|
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) {
|
function checkLock(options) {
|
||||||
if (options.force) {
|
if (options.force) {
|
||||||
if (fs.existsSync(LOCK_FILE)) {
|
if (fs.existsSync(LOCK_FILE)) {
|
||||||
@@ -357,7 +322,6 @@ function pullCode(options) {
|
|||||||
function checkDepsNeedInstall(dirPath, dirName) {
|
function checkDepsNeedInstall(dirPath, dirName) {
|
||||||
const nodeModulesPath = path.join(dirPath, 'node_modules');
|
const nodeModulesPath = path.join(dirPath, 'node_modules');
|
||||||
const packageJsonPath = path.join(dirPath, 'package.json');
|
const packageJsonPath = path.join(dirPath, 'package.json');
|
||||||
const packageLockPath = path.join(dirPath, 'package-lock.json');
|
|
||||||
|
|
||||||
if (!fs.existsSync(packageJsonPath)) {
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
return { needInstall: false, reason: '未找到 package.json' };
|
return { needInstall: false, reason: '未找到 package.json' };
|
||||||
@@ -377,9 +341,7 @@ function checkDepsNeedInstall(dirPath, dirName) {
|
|||||||
if (gitDiff) {
|
if (gitDiff) {
|
||||||
return { needInstall: true, reason: 'package.json 或 package-lock.json 有更新' };
|
return { needInstall: true, reason: 'package.json 或 package-lock.json 有更新' };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Git 检查失败,继续其他检测
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
@@ -394,7 +356,6 @@ function checkDepsNeedInstall(dirPath, dirName) {
|
|||||||
return { needInstall: true, reason: `已安装依赖数量不足 (${installedCount}/${depsCount})` };
|
return { needInstall: true, reason: `已安装依赖数量不足 (${installedCount}/${depsCount})` };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 检测失败,保守起见执行安装
|
|
||||||
return { needInstall: true, reason: '依赖状态检测失败' };
|
return { needInstall: true, reason: '依赖状态检测失败' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,9 +467,7 @@ function checkFrontendNeedsBuild() {
|
|||||||
reason: `源码或配置有更新: ${changedFiles.join(', ')}${gitDiff.split('\n').length > 3 ? '...' : ''}`
|
reason: `源码或配置有更新: ${changedFiles.join(', ')}${gitDiff.split('\n').length > 3 ? '...' : ''}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Git 检查失败,继续其他检测
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let latestSrcTime = 0;
|
let latestSrcTime = 0;
|
||||||
@@ -540,7 +499,6 @@ function checkFrontendNeedsBuild() {
|
|||||||
return { needBuild: true, reason: '源码修改时间晚于构建产物' };
|
return { needBuild: true, reason: '源码修改时间晚于构建产物' };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 时间检测失败,保守起见执行构建
|
|
||||||
return { needBuild: true, reason: '构建状态检测失败' };
|
return { needBuild: true, reason: '构建状态检测失败' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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() {
|
async function healthCheck() {
|
||||||
log.subStep('检查服务健康状态...');
|
log.subStep('检查服务健康状态...');
|
||||||
|
|
||||||
const maxRetries = 5;
|
const maxRetries = 5;
|
||||||
const retryDelay = 3000;
|
const retryDelay = 3000;
|
||||||
|
const healthUrl = `http://localhost:${getBackendPort()}/health`;
|
||||||
|
|
||||||
for (let i = 1; i <= maxRetries; i++) {
|
for (let i = 1; i <= maxRetries; i++) {
|
||||||
try {
|
try {
|
||||||
const result = execSync('curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>nul || echo "000"', {
|
const result = await httpHealthCheck(healthUrl);
|
||||||
encoding: 'utf8',
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
timeout: 5000
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
if (result === '200') {
|
if (result.success) {
|
||||||
log.success('服务健康检查通过');
|
log.success('服务健康检查通过');
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < maxRetries) {
|
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));
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -695,6 +676,20 @@ async function healthCheck() {
|
|||||||
return { success: false, warning: true };
|
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) {
|
function rollback(reason) {
|
||||||
log.divider();
|
log.divider();
|
||||||
log.error(`更新失败: ${reason}`);
|
log.error(`更新失败: ${reason}`);
|
||||||
@@ -716,31 +711,55 @@ function rollback(reason) {
|
|||||||
function printSummary(options, results) {
|
function printSummary(options, results) {
|
||||||
const duration = ((Date.now() - updateStartTime) / 1000).toFixed(1);
|
const duration = ((Date.now() - updateStartTime) / 1000).toFixed(1);
|
||||||
|
|
||||||
console.log(`\n${colors.bright}${colors.magenta}
|
log.banner('更新摘要', 'Update Summary', null);
|
||||||
╔══════════════════════════════════════════════════════════╗
|
|
||||||
║ 更新摘要 ║
|
|
||||||
╚══════════════════════════════════════════════════════════╝${colors.reset}`);
|
|
||||||
|
|
||||||
console.log(`\n ${colors.cyan}执行模式:${colors.reset} ${options.dryRun ? '模拟运行' : '实际执行'}`);
|
log.keyValue('执行模式', options.dryRun ? '模拟运行' : '实际执行');
|
||||||
console.log(` ${colors.cyan}耗时:${colors.reset} ${duration} 秒`);
|
log.keyValue('执行耗时', `${duration} 秒`);
|
||||||
|
|
||||||
console.log(`\n ${colors.cyan}步骤状态:${colors.reset}`);
|
log.divider();
|
||||||
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 ? '通过' : '未通过'}`);
|
|
||||||
|
|
||||||
console.log(`\n ${colors.cyan}访问地址:${colors.reset}`);
|
const steps = [
|
||||||
console.log(` 后端API: http://localhost:8000/api`);
|
{ name: '数据库备份', result: results.backup },
|
||||||
console.log(` 前端页面: http://localhost`);
|
{ name: '代码更新', result: results.git },
|
||||||
|
{ name: '依赖安装', result: results.deps },
|
||||||
|
{ name: '数据库迁移', result: results.migrate },
|
||||||
|
{ name: '前端构建', result: results.build },
|
||||||
|
{ name: '服务重启', result: results.restart },
|
||||||
|
{ name: '健康检查', result: results.health },
|
||||||
|
];
|
||||||
|
|
||||||
console.log(`\n ${colors.cyan}日志文件:${colors.reset}`);
|
steps.forEach((step, idx) => {
|
||||||
console.log(` ${path.join(LOG_DIR, `update_*.log`)}`);
|
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}`);
|
||||||
|
});
|
||||||
|
|
||||||
console.log('');
|
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() {
|
async function main() {
|
||||||
@@ -752,18 +771,16 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
initLogFile();
|
initLogFile(LOG_DIR);
|
||||||
|
|
||||||
console.log(`
|
log.banner(
|
||||||
${colors.bright}${colors.cyan}
|
'IDC 设备管理系统',
|
||||||
╔══════════════════════════════════════════════════════════╗
|
'一键更新脚本',
|
||||||
║ IDC设备管理系统 - 一键更新脚本 v${SCRIPT_VERSION} ║
|
SCRIPT_VERSION
|
||||||
║ One-Click Update Script ║
|
);
|
||||||
╚══════════════════════════════════════════════════════════╝
|
|
||||||
${colors.reset}`);
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
log.info('运行模式: 模拟运行 (不会执行实际操作)');
|
log.info('运行模式: 模拟运行(不会执行实际操作)');
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = {};
|
const results = {};
|
||||||
@@ -784,37 +801,37 @@ ${colors.reset}`);
|
|||||||
process.exit(130);
|
process.exit(130);
|
||||||
});
|
});
|
||||||
|
|
||||||
log.step('1. 备份数据');
|
log.step('备份数据', 0);
|
||||||
results.backup = backupDatabase(options);
|
results.backup = backupDatabase(options);
|
||||||
if (!results.backup.success && !options.dryRun) {
|
if (!results.backup.success && !options.dryRun) {
|
||||||
throw new Error('数据库备份失败');
|
throw new Error('数据库备份失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.step('2. 拉取最新代码');
|
log.step('拉取代码', 1);
|
||||||
results.git = pullCode(options);
|
results.git = pullCode(options);
|
||||||
|
|
||||||
log.step('3. 安装依赖');
|
log.step('安装依赖', 2);
|
||||||
results.deps = installDependencies(options);
|
results.deps = installDependencies(options);
|
||||||
if (!results.deps.success && !options.dryRun) {
|
if (!results.deps.success && !options.dryRun) {
|
||||||
throw new Error('依赖安装失败');
|
throw new Error('依赖安装失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.step('4. 数据库迁移');
|
log.step('数据库迁移', 3);
|
||||||
results.migrate = runMigrations(options);
|
results.migrate = runMigrations(options);
|
||||||
|
|
||||||
log.step('5. 构建前端');
|
log.step('构建前端', 4);
|
||||||
results.build = buildFrontend(options);
|
results.build = buildFrontend(options);
|
||||||
if (!results.build.success && !options.dryRun) {
|
if (!results.build.success && !options.dryRun) {
|
||||||
throw new Error('前端构建失败');
|
throw new Error('前端构建失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.step('6. 重启服务');
|
log.step('重启服务', 5);
|
||||||
results.restart = restartServices(options);
|
results.restart = restartServices(options);
|
||||||
if (!results.restart.success && !options.dryRun) {
|
if (!results.restart.success && !options.dryRun) {
|
||||||
throw new Error('服务重启失败');
|
throw new Error('服务重启失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.step('7. 健康检查');
|
log.step('健康检查', 6);
|
||||||
results.health = await healthCheck();
|
results.health = await healthCheck();
|
||||||
|
|
||||||
log.divider();
|
log.divider();
|
||||||
|
|||||||
Reference in New Issue
Block a user