feat(install): 添加Linux安装引导脚本并优化安装/卸载流程

添加install.sh脚本支持Linux系统自动安装Node.js和依赖
优化install.js和uninstall.js脚本,增加非交互模式、健康检查、日志记录等功能
更新README.md和DEPLOYMENT.md文档,提供更详细的安装/卸载指南
在package.json中添加uninstall脚本命令
This commit is contained in:
zhang1106
2026-03-17 14:13:22 +08:00
parent 19bc1a32ef
commit 2905fb4fa3
6 changed files with 1159 additions and 202 deletions
+315 -123
View File
@@ -15,12 +15,12 @@
* - 使用 PM2 启动和管理后端服务
*
* 使用方法:
* node install.js
*
* npm run deploy
* node install.js # 交互式安装
* node install.js --help # 查看帮助
* node install.js --non-interactive # 非交互式(使用默认配置)
*
* 系统要求:
* - Node.js >= 14.0.0
* - Node.js >= 16.0.0
* - npm >= 6.0.0
* - Windows/Linux/macOS
*
@@ -29,54 +29,99 @@
* ============================================================================
*/
// Node.js 内置模块引入
const readline = require('readline'); // 读取命令行输入
const { execSync } = require('child_process'); // 执行系统命令
const fs = require('fs'); // 文件系统操作
const path = require('path'); // 路径处理
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');
/**
* ANSI 颜色代码配置
* 用于在终端输出彩色文字,提升可读性
*/
const colors = {
reset: '\x1b[0m', // 重置颜色
bright: '\x1b[1m', // 高亮
green: '\x1b[32m', // 绿色(成功)
yellow: '\x1b[33m', // 黄色(警告)
red: '\x1b[31m', // 红色(错误)
cyan: '\x1b[36m', // 青色(信息)
gray: '\x1b[90m' // 灰色(分隔线)
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}`)
divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`),
subStep: (msg) => console.log(` ${colors.gray}${colors.reset} ${msg}`)
};
let logFileStream = null;
let installStartTime = null;
function parseArgs() {
const args = process.argv.slice(2);
return {
nonInteractive: args.includes('--non-interactive') || args.includes('-y'),
skipNginx: args.includes('--skip-nginx'),
skipBuild: args.includes('--skip-build'),
dbType: args.find(a => a.startsWith('--db='))?.split('=')[1],
backendPort: args.find(a => a.startsWith('--port='))?.split('=')[1],
help: args.includes('--help') || args.includes('-h')
};
}
function showHelp() {
console.log(`
${colors.bright}IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION}${colors.reset}
用法: node install.js [选项]
选项:
-y, --non-interactive 非交互式安装(使用默认配置)
--skip-nginx 跳过 Nginx 配置
--skip-build 跳过前端构建
--db=<type> 数据库类型 (sqlite/mysql)
--port=<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();
}
}
// =============================================================================
// 配置存储对象
// =============================================================================
@@ -286,12 +331,12 @@ function commandExists(command) {
/**
* 检查 Node.js 版本是否满足要求
*
* @returns {boolean} 版本是否 >= 14.0.0
* @returns {boolean} 版本是否 >= MIN_NODE_VERSION
*/
function checkNodeVersion() {
const version = process.version; // 获取当前 Node 版本,如 "v20.10.0"
const major = parseInt(version.slice(1).split('.')[0]); // 提取主版本号
return major >= 14;
const version = process.version;
const major = parseInt(version.slice(1).split('.')[0]);
return major >= MIN_NODE_VERSION;
}
/**
@@ -785,31 +830,78 @@ async function checkEnvironment() {
* 1. 选择数据库类型(SQLite / MySQL
* 2. 如选择 MySQL,提示输入连接参数
*/
async function configureDatabase() {
async function configureDatabase(cmdArgs) {
log.step('数据库配置');
// 选择数据库类型
config.dbType = await select('选择数据库类型:', [
{ label: 'SQLite(零配置,适合开发/小规模)', value: 'sqlite' },
{ label: 'MySQL(生产环境推荐)', value: 'mysql' }
]);
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' }
]);
}
// MySQL 配置
if (config.dbType === 'mysql') {
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');
// 依次询问 MySQL 连接参数
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');
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 {
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');
}
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 testMySQLConnection() {
log.subStep('测试 MySQL 连接...');
try {
const mysql = require('mysql2/promise');
const connection = await mysql.createConnection({
host: config.dbConfig.host,
port: parseInt(config.dbConfig.port),
user: config.dbConfig.username,
password: config.dbConfig.password,
database: config.dbConfig.database,
connectTimeout: 5000
});
await connection.execute('SELECT 1');
await connection.end();
log.success('MySQL 连接测试成功');
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* 服务配置函数
*
@@ -820,25 +912,40 @@ async function configureDatabase() {
* 4. 如选择 Nginx 但未安装,提供安装选项
* 5. 配置前端端口和域名
*/
async function configureServices() {
async function configureServices(cmdArgs) {
log.step('服务配置');
// 1. 后端端口配置
config.backendPort = await ask('后端服务端口', '8000');
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');
}
// 2. 运行环境选择
config.nodeEnv = await select('选择运行环境:', [
{ label: 'production(生产模式,性能优化,推荐正式部署)', value: 'production' },
{ label: 'development(开发模式,详细日志,便于调试)', value: 'development' }
]);
if (cmdArgs?.nonInteractive) {
config.nodeEnv = 'production';
log.info('使用默认配置: 运行环境 = production');
} else {
config.nodeEnv = await select('选择运行环境:', [
{ label: 'production(生产模式,性能优化,推荐正式部署)', value: 'production' },
{ label: 'development(开发模式,详细日志,便于调试)', value: 'development' }
]);
}
// 3. 前端部署方式选择
config.frontendDeploy = await select('前端部署方式:', [
{ label: 'Nginx(性能最优,推荐生产环境)', value: 'nginx' },
{ label: 'PM2 serve(简单快捷,无需额外安装)', value: 'pm2' }
]);
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' }
]);
}
// 3. 如果选择 Nginx,检查是否已安装
if (config.frontendDeploy === 'nginx') {
const nginxInstalled = isNginxInstalled();
@@ -846,55 +953,54 @@ async function configureServices() {
log.warning('未检测到 Nginx 安装');
if (process.platform === 'win32') {
// Windows 下询问是否自动安装
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}`);
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') {
// Linux 下询问是否自动安装
const autoInstall = await ask('是否自动安装 Nginx? (Y/n)', 'Y');
if (autoInstall.toLowerCase() === 'y') {
const installed = await autoInstallNginxLinux();
if (!installed) {
log.error('Nginx 自动安装失败');
log.info('请手动安装后重新运行脚本');
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();
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);
const continueDeploy = await ask('是否继续部署(后端将启动,前端需手动配置 Nginx? (Y/n)', 'Y');
if (continueDeploy.toLowerCase() !== 'y') {
log.info('已取消部署,请安装 Nginx 后重新运行');
process.exit(0);
}
}
}
} else {
// macOS 下提供安装命令
log.info('请使用 Homebrew 安装 Nginx');
console.log(` ${colors.cyan}brew install nginx${colors.reset}`);
console.log(`\n安装完成后,请重新运行此脚本继续部署。\n`);
const continueDeploy = await ask('是否继续部署(后端将启动,前端需手动配置 Nginx? (Y/n)', 'Y');
if (continueDeploy.toLowerCase() !== 'y') {
log.info('已取消部署,请安装 Nginx 后重新运行');
process.exit(0);
}
}
}
// Nginx 端口和域名配置
config.frontendPort = await ask('Nginx 监听端口', '80');
config.domain = await ask('域名(没有则填localhost', 'localhost');
if (!cmdArgs?.nonInteractive) {
config.frontendPort = await ask('Nginx 监听端口', '80');
config.domain = await ask('域名(没有则填localhost', 'localhost');
} else {
config.frontendPort = '80';
config.domain = 'localhost';
}
} else {
// PM2 serve 端口配置
config.frontendPort = await ask('前端服务端口', '3000');
if (!cmdArgs?.nonInteractive) {
config.frontendPort = await ask('前端服务端口', '3000');
}
}
log.divider();
@@ -1658,47 +1764,133 @@ function printManagementCommands() {
* 10. 输出管理命令
*/
async function main() {
// 显示欢迎信息
installStartTime = Date.now();
const cmdArgs = parseArgs();
if (cmdArgs.help) {
showHelp();
return;
}
initLogFile();
console.log(`
${colors.bright}${colors.cyan}
╔══════════════════════════════════════════════════════════╗
║ IDC设备管理系统 - 交互式安装部署脚本
║ IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION}
║ Interactive Installation & Deployment Script ║
╚══════════════════════════════════════════════════════════╝
${colors.reset}`);
if (cmdArgs.nonInteractive) {
log.info('运行模式: 非交互式(使用默认配置)');
}
try {
// 步骤1:环境检测
await checkEnvironment();
await configureDatabase(cmdArgs);
await configureServices(cmdArgs);
if (!cmdArgs.nonInteractive) {
await confirmConfiguration();
}
// 步骤2:交互式配置
await configureDatabase();
await configureServices();
await confirmConfiguration();
// 步骤3:生成配置文件
generateBackendEnv();
generatePM2Config();
generateNginxConfig();
// 步骤4:安装和部署
await installDependencies();
await initDatabase();
await buildFrontend();
if (!cmdArgs.skipBuild) {
await buildFrontend();
} else {
log.info('已跳过前端构建');
}
await startServices();
// 步骤5:输出管理命令
printManagementCommands();
await healthCheck();
printSummary();
} catch (error) {
log.error(`部署失败: ${error.message}`);
console.error(error);
process.exit(1);
} finally {
// 关闭 readline 接口
rl.close();
closeLogFile();
}
}
// 运行主函数
async function healthCheck() {
log.step('健康检查');
log.subStep('检查后端服务状态...');
const maxRetries = 5;
const retryDelay = 3000;
for (let i = 1; i <= maxRetries; i++) {
try {
const result = execSync('curl -s -o /dev/null -w "%{http_code}" http://localhost:' + config.backendPort + '/health 2>nul || echo "000"', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 5000
}).trim();
if (result === '200') {
log.success('后端服务健康检查通过');
return { success: true };
}
if (i < maxRetries) {
log.subStep(`${i} 次检查失败 (HTTP ${result})${retryDelay/1000}秒后重试...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
} catch (error) {
if (i < maxRetries) {
log.subStep(`${i} 次检查失败,${retryDelay/1000}秒后重试...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
}
log.warning('健康检查未通过,请手动验证服务状态');
return { success: false };
}
function printSummary() {
const duration = ((Date.now() - installStartTime) / 1000).toFixed(1);
console.log(`
${colors.bright}${colors.magenta}
╔══════════════════════════════════════════════════════════╗
║ 安装摘要 ║
╚══════════════════════════════════════════════════════════╝${colors.reset}`);
console.log(`\n ${colors.cyan}安装耗时:${colors.reset} ${duration}`);
console.log(` ${colors.cyan}数据库类型:${colors.reset} ${config.dbType}`);
console.log(` ${colors.cyan}后端端口:${colors.reset} ${config.backendPort}`);
console.log(` ${colors.cyan}前端部署:${colors.reset} ${config.frontendDeploy}`);
console.log(`\n${colors.bright}服务管理命令:${colors.reset}`);
console.log(` ${colors.cyan}pm2 status${colors.reset} 查看服务状态`);
console.log(` ${colors.cyan}pm2 logs idc-backend${colors.reset} 查看后端日志`);
console.log(` ${colors.cyan}pm2 restart idc-backend${colors.reset} 重启后端`);
console.log(` ${colors.cyan}pm2 stop idc-backend${colors.reset} 停止后端`);
console.log(`\n${colors.bright}访问地址:${colors.reset}`);
console.log(` 后端API: ${colors.cyan}http://localhost:${config.backendPort}/api${colors.reset}`);
if (config.frontendDeploy === 'nginx') {
console.log(` 前端页面: ${colors.cyan}http://${config.domain}:${config.frontendPort}${colors.reset}`);
} else {
console.log(` 前端页面: ${colors.cyan}http://localhost:${config.frontendPort}${colors.reset}`);
}
console.log(`\n${colors.bright}更新升级:${colors.reset}`);
console.log(` ${colors.cyan}node update.js${colors.reset} 一键更新`);
log.divider();
log.success('安装部署完成!');
}
main();