Files
yunrui_asset/uninstall.js
T
zhang1106 2905fb4fa3 feat(install): 添加Linux安装引导脚本并优化安装/卸载流程
添加install.sh脚本支持Linux系统自动安装Node.js和依赖
优化install.js和uninstall.js脚本,增加非交互模式、健康检查、日志记录等功能
更新README.md和DEPLOYMENT.md文档,提供更详细的安装/卸载指南
在package.json中添加uninstall脚本命令
2026-03-17 14:13:22 +08:00

664 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* ============================================================================
* IDC设备管理系统 - 卸载脚本 v2.0.0
* ============================================================================
*
* 功能说明:
* - 停止并删除 PM2 管理的后端和前端服务
* - 清理 Nginx 配置文件(可选)
* - 删除生成的配置文件(.env、ecosystem.config.js、nginx-idc.conf
* - 可选删除数据库文件(SQLite)
* - 可选删除 node_modules 和构建产物
*
* 使用方法:
* node uninstall.js # 交互式卸载
* node uninstall.js --help # 查看帮助
* node uninstall.js --force # 强制卸载(无需确认)
* node uninstall.js --backup # 卸载前自动备份
*/
const readline = require('readline');
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const SCRIPT_VERSION = '2.0.0';
const BACKUP_DIR = path.join(__dirname, 'backup');
const LOG_DIR = path.join(__dirname, 'logs');
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) => 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 uninstallStartTime = null;
let deletedItems = [];
let backedUpItems = [];
function parseArgs() {
const args = process.argv.slice(2);
return {
force: args.includes('--force') || args.includes('-f'),
backup: args.includes('--backup') || args.includes('-b'),
skipDb: args.includes('--skip-db'),
skipDeps: args.includes('--skip-deps'),
help: args.includes('--help') || args.includes('-h')
};
}
function showHelp() {
console.log(`
${colors.bright}IDC设备管理系统 - 卸载脚本 v${SCRIPT_VERSION}${colors.reset}
用法: node uninstall.js [选项]
选项:
-f, --force 强制卸载(无需确认)
-b, --backup 卸载前自动备份数据库
--skip-db 跳过数据库删除
--skip-deps 跳过依赖删除
-h, --help 显示帮助信息
示例:
node uninstall.js # 交互式卸载
node uninstall.js --force # 强制卸载
node uninstall.js --backup # 卸载前备份
`);
process.exit(0);
}
function initLogFile() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
const logFileName = `uninstall_${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();
}
}
// =============================================================================
// 交互式输入函数
// =============================================================================
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function ask(question, defaultValue = '') {
return new Promise((resolve) => {
const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
rl.question(prompt, (answer) => {
resolve(answer.trim() || defaultValue);
});
});
}
// =============================================================================
// 系统命令执行函数
// =============================================================================
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;
}
}
// =============================================================================
// 检测是否以 root 用户运行
// =============================================================================
function isRootUser() {
return process.getuid && process.getuid() === 0;
}
// =============================================================================
// 服务停止与删除
// =============================================================================
async function stopAndDeleteServices() {
log.step('停止并删除服务');
if (!commandExists('pm2')) {
log.warning('未检测到 PM2,跳过服务停止步骤');
return;
}
const isWindows = process.platform === 'win32';
const nullRedirect = isWindows ? '2>nul' : '2>/dev/null';
const orTrue = isWindows ? '|| exit 0' : '|| true';
log.info('停止后端服务 (idc-backend)...');
const backendStop = runCommand(`pm2 stop idc-backend ${nullRedirect} ${orTrue}`, { silent: true });
if (backendStop.success) {
log.success('后端服务已停止');
}
log.info('删除后端服务 (idc-backend)...');
const backendDelete = runCommand(`pm2 delete idc-backend ${nullRedirect} ${orTrue}`, { silent: true });
if (backendDelete.success) {
log.success('后端服务已删除');
}
log.info('停止前端服务 (idc-frontend)...');
const frontendStop = runCommand(`pm2 stop idc-frontend ${nullRedirect} ${orTrue}`, { silent: true });
if (frontendStop.success) {
log.success('前端服务已停止');
}
log.info('删除前端服务 (idc-frontend)...');
const frontendDelete = runCommand(`pm2 delete idc-frontend ${nullRedirect} ${orTrue}`, { silent: true });
if (frontendDelete.success) {
log.success('前端服务已删除');
}
log.info('保存 PM2 配置...');
runCommand(`pm2 save ${nullRedirect} ${orTrue}`, { silent: true });
log.divider();
}
// =============================================================================
// Nginx 配置清理
// =============================================================================
async function cleanupNginxConfig() {
log.step('清理 Nginx 配置');
const isWindows = process.platform === 'win32';
const isLinux = process.platform === 'linux';
if (isWindows) {
// Windows 下提示手动清理
log.info('Windows 系统请手动清理 Nginx 配置:');
console.log(` 1. 删除配置文件: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`);
console.log(` 2. 编辑主配置: 从 ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset} 中移除 include conf.d/*.conf;`);
console.log(` 3. 停止 Nginx: ${colors.cyan}nginx -s stop${colors.reset}`);
const confirm = await ask('是否已手动清理 Nginx 配置? (Y/n)', 'Y');
if (confirm.toLowerCase() !== 'y') {
log.warning('请记得手动清理 Nginx 配置');
}
} else if (isLinux) {
// Linux 下自动清理
const root = isRootUser();
const sudoPrefix = root ? '' : 'sudo ';
// 检测配置文件位置
const sitesAvailablePath = '/etc/nginx/sites-available/idc';
const sitesEnabledPath = '/etc/nginx/sites-enabled/idc';
const confDPath = '/etc/nginx/conf.d/idc';
let configExists = false;
// 删除 sites-available 中的配置
if (fs.existsSync(sitesAvailablePath)) {
configExists = true;
log.info('删除 sites-available 配置...');
const result = runCommand(`${sudoPrefix}rm -f "${sitesAvailablePath}"`, { silent: true });
if (result.success) {
log.success('sites-available/idc 已删除');
} else {
log.error('删除失败');
}
}
// 删除 sites-enabled 中的软链接
if (fs.existsSync(sitesEnabledPath)) {
configExists = true;
log.info('删除 sites-enabled 软链接...');
const result = runCommand(`${sudoPrefix}rm -f "${sitesEnabledPath}"`, { silent: true });
if (result.success) {
log.success('sites-enabled/idc 已删除');
} else {
log.error('删除失败');
}
}
// 删除 conf.d 中的配置
if (fs.existsSync(confDPath)) {
configExists = true;
log.info('删除 conf.d 配置...');
const result = runCommand(`${sudoPrefix}rm -f "${confDPath}"`, { silent: true });
if (result.success) {
log.success('conf.d/idc 已删除');
} else {
log.error('删除失败');
}
}
if (!configExists) {
log.info('未找到系统 Nginx 配置文件');
} else {
// 测试并重载 Nginx
log.info('测试 Nginx 配置...');
const testResult = runCommand(`${sudoPrefix}nginx -t`, { silent: true });
if (testResult.success) {
log.success('Nginx 配置测试通过');
log.info('重载 Nginx 服务...');
const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`, { silent: true });
if (reloadResult.success) {
log.success('Nginx 已重载');
} else {
log.warning('Nginx 重载失败,可能需要手动重启');
}
} else {
log.warning('Nginx 配置测试失败,请手动检查');
}
}
} else {
// macOS 或其他系统
log.info('请手动清理 Nginx 配置');
console.log(` 配置文件可能位于: ${colors.cyan}/usr/local/etc/nginx/servers/${colors.reset}`);
console.log(` 或使用: ${colors.cyan}brew services stop nginx${colors.reset}`);
}
log.divider();
}
// =============================================================================
// 配置文件清理
// =============================================================================
async function cleanupConfigFiles() {
log.step('清理生成的配置文件');
const filesToDelete = [
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)', type: '配置文件' },
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置 (ecosystem.config.js)', type: '配置文件' },
{ path: path.join(__dirname, 'deploy', 'nginx-idc.conf'), name: 'Nginx 配置 (nginx-idc.conf)', type: '配置文件' }
];
for (const file of filesToDelete) {
if (fs.existsSync(file.path)) {
try {
fs.unlinkSync(file.path);
log.success(`${file.name} 已删除`);
deletedItems.push({ type: file.type, name: file.name });
} catch (error) {
log.error(`删除 ${file.name} 失败: ${error.message}`);
}
} else {
log.info(`${file.name} 不存在,跳过`);
}
}
const deployDir = path.join(__dirname, 'deploy');
if (fs.existsSync(deployDir)) {
try {
const files = fs.readdirSync(deployDir);
if (files.length === 0) {
fs.rmdirSync(deployDir);
log.success('deploy 目录已删除');
deletedItems.push({ type: '目录', name: 'deploy/' });
} else {
log.info('deploy 目录不为空,保留');
}
} catch (error) {
log.error(`删除 deploy 目录失败: ${error.message}`);
}
}
log.divider();
}
// =============================================================================
// 数据库清理
// =============================================================================
async function cleanupDatabase() {
log.step('数据库清理');
// 检查 SQLite 数据库文件
const sqliteDbPath = path.join(__dirname, 'backend', 'idc_management.db');
const sqliteExists = fs.existsSync(sqliteDbPath);
// 获取数据库文件信息
let dbSize = 0;
let dbCreateTime = '';
if (sqliteExists) {
const stats = fs.statSync(sqliteDbPath);
dbSize = (stats.size / 1024 / 1024).toFixed(2); // MB
dbCreateTime = stats.birthtime.toLocaleString();
}
// 检测数据库类型(从 .env 文件读取)
const envPath = path.join(__dirname, 'backend', '.env');
let dbType = 'sqlite';
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
const dbTypeMatch = envContent.match(/DB_TYPE\s*=\s*(\w+)/);
if (dbTypeMatch) {
dbType = dbTypeMatch[1].toLowerCase();
}
}
console.log(`\n${colors.bright}当前数据库配置:${colors.reset}`);
console.log(` 数据库类型: ${colors.cyan}${dbType === 'sqlite' ? 'SQLite' : 'MySQL'}${colors.reset}`);
if (dbType === 'sqlite') {
// SQLite 数据库处理
console.log(`\n${colors.bright}SQLite 数据库信息:${colors.reset}`);
if (sqliteExists) {
console.log(` 文件路径: ${colors.cyan}${sqliteDbPath}${colors.reset}`);
console.log(` 文件大小: ${colors.cyan}${dbSize} MB${colors.reset}`);
console.log(` 创建时间: ${colors.cyan}${dbCreateTime}${colors.reset}`);
console.log(`\n${colors.yellow}⚠ 警告:删除数据库将永久丢失所有数据!${colors.reset}`);
console.log(`\n${colors.bright}删除方法:${colors.reset}`);
console.log(` 方法1 - 脚本自动删除:输入 Y 确认删除`);
console.log(` 方法2 - 手动删除文件:直接删除 ${colors.cyan}backend/idc_management.db${colors.reset}`);
console.log(` 方法3 - 命令行删除:`);
if (process.platform === 'win32') {
console.log(` ${colors.cyan}del "${sqliteDbPath}"${colors.reset}`);
} else {
console.log(` ${colors.cyan}rm "${sqliteDbPath}"${colors.reset}`);
}
const confirm = await ask('\n是否删除 SQLite 数据库文件? (y/N)', 'N');
if (confirm.toLowerCase() === 'y') {
try {
log.info('正在删除数据库文件...');
fs.unlinkSync(sqliteDbPath);
log.success('SQLite 数据库已删除');
deletedItems.push({ type: '数据库', name: `SQLite (${dbSize} MB)` });
} catch (error) {
log.error(`删除失败: ${error.message}`);
if (error.message.includes('EBUSY') || error.message.includes('resource busy')) {
log.warning('数据库文件被占用,请先停止服务后再手动删除');
console.log(` 手动删除命令:`);
if (process.platform === 'win32') {
console.log(` ${colors.cyan}del /f "${sqliteDbPath}"${colors.reset}`);
} else {
console.log(` ${colors.cyan}rm -f "${sqliteDbPath}"${colors.reset}`);
}
}
}
} else {
log.info('保留 SQLite 数据库文件');
console.log(` 文件位置: ${colors.cyan}${sqliteDbPath}${colors.reset}`);
}
} else {
log.info('未检测到 SQLite 数据库文件');
}
} else {
// MySQL 数据库提示
console.log(`\n${colors.yellow}⚠ 注意:当前使用 MySQL 数据库${colors.reset}`);
console.log(` 卸载脚本不会自动删除 MySQL 数据库,请手动清理:`);
console.log(`\n${colors.bright}MySQL 删除方法:${colors.reset}`);
console.log(` 方法1 - MySQL 命令行:`);
console.log(` ${colors.cyan}mysql -u root -p${colors.reset}`);
console.log(` ${colors.cyan}DROP DATABASE idc_management;${colors.reset}`);
console.log(` 方法2 - 使用数据库管理工具(如 Navicat、DBeaver、phpMyAdmin`);
console.log(` 方法3 - 如果不再需要 MySQL 数据,可直接卸载 MySQL 服务`);
}
// 备份提示
console.log(`\n${colors.bright}数据备份建议:${colors.reset}`);
if (dbType === 'sqlite' && sqliteExists) {
console.log(` 备份 SQLite:直接复制 ${colors.cyan}backend/idc_management.db${colors.reset} 文件`);
}
console.log(` 如需保留数据,请在卸载前手动备份`);
log.divider();
}
// =============================================================================
// 依赖和构建产物清理
// =============================================================================
async function cleanupDependencies(cmdArgs) {
log.step('依赖和构建产物清理');
log.warning('此操作将删除 node_modules 和构建产物,需要重新安装依赖才能再次运行');
const confirm = cmdArgs?.force ? 'y' : await ask('是否删除依赖和构建产物? (y/N)', 'N');
if (confirm.toLowerCase() !== 'y') {
log.info('跳过依赖清理');
return;
}
const dirsToDelete = [
{ path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules', type: '依赖' },
{ path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules', type: '依赖' },
{ path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物 (dist)', type: '构建产物' }
];
for (const dir of dirsToDelete) {
if (fs.existsSync(dir.path)) {
try {
fs.rmSync(dir.path, { recursive: true, force: true });
log.success(`${dir.name} 已删除`);
deletedItems.push({ type: dir.type, name: dir.name });
} catch (error) {
log.error(`删除 ${dir.name} 失败: ${error.message}`);
}
} else {
log.info(`${dir.name} 不存在,跳过`);
}
}
log.divider();
}
// =============================================================================
// 日志清理
// =============================================================================
async function cleanupLogs() {
log.step('日志文件清理');
const logsDir = path.join(__dirname, 'backend', 'logs');
if (fs.existsSync(logsDir)) {
const confirm = await ask('是否删除后端日志文件? (y/N)', 'N');
if (confirm.toLowerCase() === 'y') {
try {
fs.rmSync(logsDir, { recursive: true, force: true });
log.success('日志文件已删除');
} catch (error) {
log.error(`删除失败: ${error.message}`);
}
} else {
log.info('保留日志文件');
}
} else {
log.info('未找到日志目录');
}
log.divider();
}
// =============================================================================
// 主函数
// =============================================================================
async function backupDatabase() {
log.step('备份数据库');
const sqliteDbPath = path.join(__dirname, 'backend', 'idc_management.db');
if (!fs.existsSync(sqliteDbPath)) {
log.info('未找到 SQLite 数据库文件,跳过备份');
return false;
}
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
}
const backupPath = path.join(BACKUP_DIR, `database_backup_${Date.now()}.db`);
try {
fs.copyFileSync(sqliteDbPath, backupPath);
backedUpItems.push({ type: '数据库', path: backupPath });
log.success(`数据库已备份: ${path.basename(backupPath)}`);
return true;
} catch (error) {
log.error(`备份失败: ${error.message}`);
return false;
}
}
function printSummary() {
const duration = ((Date.now() - uninstallStartTime) / 1000).toFixed(1);
console.log(`
${colors.bright}${colors.magenta}
╔══════════════════════════════════════════════════════════╗
║ 卸载摘要 ║
╚══════════════════════════════════════════════════════════╝${colors.reset}`);
console.log(`\n ${colors.cyan}卸载耗时:${colors.reset} ${duration} 秒`);
if (deletedItems.length > 0) {
console.log(`\n ${colors.cyan}已删除项目:${colors.reset}`);
deletedItems.forEach(item => {
console.log(` - ${item.type}: ${item.name}`);
});
}
if (backedUpItems.length > 0) {
console.log(`\n ${colors.cyan}已备份项目:${colors.reset}`);
backedUpItems.forEach(item => {
console.log(` - ${item.type}: ${path.basename(item.path)}`);
});
console.log(`\n ${colors.yellow}备份位置:${colors.reset} ${BACKUP_DIR}`);
}
console.log(`\n ${colors.cyan}保留的文件:${colors.reset}`);
console.log(` - 项目源代码 (backend/, frontend/)`);
console.log(` - 上传的文件 (backend/uploads/)`);
console.log(`\n ${colors.cyan}重新部署:${colors.reset}`);
console.log(` node install.js`);
log.divider();
log.success('卸载完成!');
}
async function main() {
uninstallStartTime = Date.now();
const cmdArgs = parseArgs();
if (cmdArgs.help) {
showHelp();
return;
}
initLogFile();
console.log(`
${colors.bright}${colors.yellow}
╔══════════════════════════════════════════════════════════╗
║ IDC设备管理系统 - 卸载脚本 v${SCRIPT_VERSION}
║ Uninstallation Script ║
╚══════════════════════════════════════════════════════════╝
${colors.reset}`);
log.warning('此脚本将卸载 IDC设备管理系统');
if (cmdArgs.force) {
log.info('运行模式: 强制卸载(无需确认)');
}
log.divider();
if (!cmdArgs.force) {
const confirm = await ask('确认要开始卸载? (y/N)', 'N');
if (confirm.toLowerCase() !== 'y') {
log.info('已取消卸载');
rl.close();
closeLogFile();
return;
}
}
try {
if (cmdArgs.backup) {
await backupDatabase();
}
await stopAndDeleteServices();
await cleanupNginxConfig();
await cleanupConfigFiles();
if (!cmdArgs.skipDb) {
await cleanupDatabase();
} else {
log.info('已跳过数据库删除');
}
await cleanupLogs();
if (!cmdArgs.skipDeps) {
await cleanupDependencies();
} else {
log.info('已跳过依赖删除');
}
printSummary();
} catch (error) {
log.error(`卸载失败: ${error.message}`);
console.error(error);
process.exit(1);
} finally {
rl.close();
closeLogFile();
}
}
main();