Files
yunrui_asset/uninstall.js
T
yi e9bff9beb3
Build and Push Docker Images / Build Backend Image (push) Has been cancelled
Build and Push Docker Images / Build Frontend Image (push) Has been cancelled
Build and Push Docker Images / Notify Build Complete (push) Has been cancelled
Update 2026-06-23 10:40:10
2026-06-23 10:40:10 +08:00

904 lines
31 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
/**
* 云睿资产管理系统 - 卸载脚本 v2.0.0
*
* 功能说明:
* - 停止并删除 PM2 管理的后端和前端服务
* - 清理 Nginx 配置文件(可选)
* - 删除生成的配置文件(.env、ecosystem.config.js、nginx-yunrui.conf
* - 可选删除数据库文件(SQLite)
* - 可选删除 node_modules 和构建产物
*
* 使用方法:
* node uninstall.js # 交互式卸载
* node uninstall.js --help # 查看帮助
* node uninstall.js --force # 强制卸载(无需确认)
* node uninstall.js --backup # 卸载前自动备份
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { colors, ICONS, log, padCenter, initLogFile, closeLogFile } = require('./install/logger');
const { ask, closeReadline } = require('./install/ui');
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 = [];
let backedUpItems = [];
function parseArgs() {
const args = process.argv.slice(2);
return {
force: args.includes('--force') || args.includes('-f') || args.includes('--yes') || args.includes('-y'),
backup: args.includes('--backup') || args.includes('-b'),
dryRun: args.includes('--dry-run'),
skipDb: args.includes('--skip-db'),
skipDeps: args.includes('--skip-deps'),
skipUploads: args.includes('--skip-uploads'),
help: args.includes('--help') || args.includes('-h')
};
}
function showHelp() {
console.log(`
${colors.bright}云睿资产管理系统 - 卸载脚本 v${SCRIPT_VERSION}${colors.reset}
${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} 显示帮助信息
${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} # 预览将删除的内容
`);
process.exit(0);
}
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;
}
}
function getPM2ServiceNames() {
const defaultNames = { backendName: 'yunrui_asset-backend', frontendName: 'yunrui_asset-frontend' };
const configPath = path.join(__dirname, 'deploy', 'ecosystem.config.js');
if (!fs.existsSync(configPath)) {
return defaultNames;
}
try {
const config = require(configPath);
if (!config || !Array.isArray(config.apps)) {
return defaultNames;
}
const names = { ...defaultNames };
config.apps.forEach(app => {
if (app.name && app.name.startsWith('idc-')) {
if (app.name.includes('backend') || (app.script && app.script.includes('server'))) {
names.backendName = app.name;
} else if (app.name.includes('frontend') || app.name === 'yunrui_asset-frontend') {
names.frontendName = app.name;
}
}
});
return names;
} catch {
return defaultNames;
}
}
function getDatabaseConfig() {
const defaultConfig = {
dbType: 'sqlite',
sqliteDbPath: path.join(__dirname, 'backend', 'yunrui_management.db')
};
const envPath = path.join(__dirname, 'backend', '.env');
if (!fs.existsSync(envPath)) {
return defaultConfig;
}
try {
const envContent = fs.readFileSync(envPath, 'utf8');
const config = { ...defaultConfig };
const dbTypeMatch = envContent.match(/^DB_TYPE\s*=\s*(\w+)/m);
if (dbTypeMatch) {
config.dbType = dbTypeMatch[1].toLowerCase();
}
const dbPathMatch = envContent.match(/^DB_PATH\s*=\s*(.+)$/m);
if (dbPathMatch && config.dbType === 'sqlite') {
const rawPath = dbPathMatch[1].trim().replace(/['"]/g, '');
if (path.isAbsolute(rawPath)) {
config.sqliteDbPath = rawPath;
} else {
config.sqliteDbPath = path.resolve(__dirname, 'backend', rawPath);
}
}
return config;
} catch {
return defaultConfig;
}
}
function isRootUser() {
return process.getuid && process.getuid() === 0;
}
async function stopAndDeleteServices() {
log.step('停止服务', 0, 7);
if (!commandExists('pm2')) {
log.warning('未检测到 PM2,跳过服务停止步骤');
return;
}
const { backendName, frontendName } = getPM2ServiceNames();
const isWindows = process.platform === 'win32';
const nullRedirect = isWindows ? '2>nul' : '2>/dev/null';
const orTrue = isWindows ? '|| exit 0' : '|| true';
function pm2ProcessExists(name) {
const result = runCommand(`pm2 describe ${name} ${nullRedirect}`, { silent: true });
return result.success;
}
const services = [
{ name: backendName, label: '后端服务' },
{ name: frontendName, label: '前端服务' }
];
for (const service of services) {
if (!pm2ProcessExists(service.name)) {
log.info(`${service.label} (${service.name}) 不存在于 PM2 中,跳过`);
continue;
}
log.info(`停止 ${service.label} (${service.name})...`);
const stopResult = runCommand(`pm2 stop ${service.name} ${nullRedirect} ${orTrue}`, { silent: true });
if (stopResult.success) {
log.success(`${service.label} 已停止`);
} else {
log.warning(`${service.label} 停止失败(可能已停止)`);
}
log.info(`删除 ${service.label} (${service.name})...`);
const deleteResult = runCommand(`pm2 delete ${service.name} ${nullRedirect} ${orTrue}`, { silent: true });
if (deleteResult.success) {
log.success(`${service.label} 已删除`);
} else {
log.warning(`${service.label} 删除失败`);
}
}
log.info('保存 PM2 配置...');
runCommand(`pm2 save ${nullRedirect} ${orTrue}`, { silent: true });
log.info('清理 PM2 开机自启配置...');
const startupResult = runCommand(`pm2 unstartup ${nullRedirect} ${orTrue}`, { silent: true });
if (startupResult.success) {
log.success('PM2 开机自启配置已清理');
}
log.info('清理 PM2 日志文件...');
const pm2LogDir = path.join(os.homedir(), '.pm2', 'logs');
if (fs.existsSync(pm2LogDir)) {
const pm2LogFiles = fs.readdirSync(pm2LogDir).filter(f =>
f.startsWith(backendName) || f.startsWith(frontendName)
);
if (pm2LogFiles.length > 0) {
pm2LogFiles.forEach(f => {
try {
fs.unlinkSync(path.join(pm2LogDir, f));
log.subStep(`PM2 日志已清理: ${f}`);
} catch (e) {
log.warning(`PM2 日志清理失败: ${f}`);
}
});
} else {
log.subStep('未找到 PM2 相关日志文件');
}
}
log.divider();
}
async function cleanupNginxConfig(cmdArgs) {
log.step('清理 Nginx', 1, 7);
const isWindows = process.platform === 'win32';
const isLinux = process.platform === 'linux';
const isMac = process.platform === 'darwin';
function nginxInstalled() {
return commandExists('nginx');
}
if (isWindows) {
log.info('Windows Nginx 清理:');
console.log(` ${ICONS.pipe} 请手动完成以下操作:`);
console.log(` ${ICONS.pipe} 删除配置文件: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`);
console.log(` ${ICONS.pipe} 编辑主配置: 从 ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset} 中移除 include 语句`);
console.log(` ${ICONS.pipe} 重载 Nginx: ${colors.cyan}nginx -s reload${colors.reset}`);
if (!cmdArgs?.force) {
const confirm = await ask('是否已完成以上清理操作? (Y/n)', 'Y');
if (confirm.toLowerCase() !== 'y') {
log.warning('请记得手动清理 Nginx 配置');
}
}
log.divider();
return;
}
if (isMac) {
log.info('macOS Nginx 清理:');
console.log(` ${ICONS.pipe} 请手动完成以下操作:`);
console.log(` ${ICONS.pipe} 删除配置文件: ${colors.cyan}/usr/local/etc/nginx/servers/idc${colors.reset}`);
console.log(` ${ICONS.pipe} 重载 Nginx: ${colors.cyan}nginx -s reload${colors.reset}`);
if (commandExists('brew')) {
console.log(` ${ICONS.pipe} 如需卸载 Nginx${colors.cyan}brew uninstall nginx${colors.reset}`);
}
if (!cmdArgs?.force) {
const confirm = await ask('是否已完成以上清理操作? (Y/n)', 'Y');
if (confirm.toLowerCase() !== 'y') {
log.warning('请记得手动清理 Nginx 配置');
}
}
log.divider();
return;
}
const root = isRootUser();
const sudoPrefix = root ? '' : 'sudo ';
const idcConfigPaths = [
{ path: '/etc/nginx/sites-available/yunrui_asset', label: 'sites-available/yunrui_asset' },
{ path: '/etc/nginx/sites-enabled/yunrui_asset', label: 'sites-enabled/yunrui_asset(软链接)' },
{ path: '/etc/nginx/conf.d/idc', label: 'conf.d/idc' },
{ path: '/etc/nginx/conf.d/idc.conf', label: 'conf.d/idc.conf' },
];
const wwwDir = '/var/www/yunrui_asset';
const defaultSitePath = '/etc/nginx/sites-enabled/default';
const defaultAvailablePath = '/etc/nginx/sites-available/default';
let configRemoved = false;
for (const config of idcConfigPaths) {
if (fs.existsSync(config.path)) {
log.info(`删除 ${config.label}...`);
const result = runCommand(`${sudoPrefix}rm -f "${config.path}"`, { silent: true });
if (result.success) {
log.success(`${config.label} 已删除`);
configRemoved = true;
deletedItems.push({ type: 'Nginx配置', name: config.path });
} else {
log.error(`${config.label} 删除失败`);
}
}
}
if (!configRemoved) {
log.info('未找到云睿资产 Nginx 站点配置文件');
}
if (fs.existsSync(wwwDir)) {
log.info('清理前端部署目录...');
const result = runCommand(`${sudoPrefix}rm -rf "${wwwDir}"`, { silent: true });
if (result.success) {
log.success('前端部署目录已删除 (/var/www/yunrui_asset)');
deletedItems.push({ type: '目录', name: '/var/www/yunrui_asset' });
} else {
log.warning('前端部署目录删除失败,请手动清理');
}
}
if (!fs.existsSync(defaultSitePath) && fs.existsSync(defaultAvailablePath)) {
log.info('恢复 Nginx 默认站点...');
const result = runCommand(
`${sudoPrefix}ln -sf "${defaultAvailablePath}" "${defaultSitePath}"`,
{ silent: true }
);
if (result.success) {
log.success('Nginx 默认站点已恢复');
} else {
log.warning('恢复默认站点失败,请手动执行:');
console.log(` ${ICONS.pipe} ${colors.cyan}${sudoPrefix}ln -sf ${defaultAvailablePath} ${defaultSitePath}${colors.reset}`);
}
}
if (nginxInstalled() && configRemoved) {
log.info('重载 Nginx 配置...');
const testResult = runCommand(`${sudoPrefix}nginx -t`, { silent: true });
if (testResult.success) {
const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`, { silent: true });
if (reloadResult.success) {
log.success('Nginx 配置已重载');
} else {
log.warning('Nginx 重载失败,请手动执行: sudo nginx -s reload');
}
} else {
log.warning('Nginx 配置测试未通过,跳过重载');
log.info('请手动检查并重载: sudo nginx -t && sudo nginx -s reload');
}
}
log.divider();
}
async function cleanupConfigFiles() {
log.step('清理配置', 2, 7);
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-yunrui.conf'), name: 'Nginx 配置 (nginx-yunrui.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(cmdArgs) {
log.step('数据库清理', 3, 7);
const dbConfig = getDatabaseConfig();
const sqliteDbPath = dbConfig.sqliteDbPath;
const sqliteExists = fs.existsSync(sqliteDbPath);
let dbSize = 0;
let dbCreateTime = '';
if (sqliteExists) {
const stats = fs.statSync(sqliteDbPath);
dbSize = (stats.size / 1024 / 1024).toFixed(2);
dbCreateTime = stats.birthtime.toLocaleString();
}
const dbType = dbConfig.dbType;
log.section('当前数据库配置');
log.keyValue('数据库类型', dbType === 'sqlite' ? 'SQLite' : 'MySQL');
if (dbType === 'sqlite') {
if (sqliteExists) {
log.keyValue('文件路径', sqliteDbPath);
log.keyValue('文件大小', `${dbSize} MB`);
log.keyValue('创建时间', dbCreateTime);
log.divider();
log.warning('删除数据库将永久丢失所有数据!');
log.divider();
const confirm = await ask('是否删除 SQLite 数据库文件? (y/N)', 'N');
if (confirm.toLowerCase() === 'y') {
if (!cmdArgs?.backup && !cmdArgs?.force) {
const backupConfirm = await ask('是否先备份数据库再删除? (Y/n)', 'Y');
if (backupConfirm.toLowerCase() === 'y') {
await backupDatabase();
}
} else if (cmdArgs?.backup) {
await backupDatabase();
}
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') || error.message.includes('used by another process')) {
log.warning('数据库文件被占用,尝试强制删除...');
try {
const isWindows = process.platform === 'win32';
let forceResult;
if (isWindows) {
forceResult = runCommand(`del /f /q "${sqliteDbPath}" 2>nul`, { silent: true });
if (!forceResult.success) {
forceResult = runCommand(`powershell -Command "Remove-Item -Path '${sqliteDbPath}' -Force -ErrorAction SilentlyContinue" 2>nul`, { silent: true });
}
} else {
forceResult = runCommand(`rm -f "${sqliteDbPath}"`, { silent: true });
}
if (forceResult.success && !fs.existsSync(sqliteDbPath)) {
log.success('数据库文件已强制删除');
deletedItems.push({ type: '数据库', name: `SQLite (${dbSize} MB)` });
} else {
log.error('强制删除失败,文件仍被占用');
log.warning('请手动停止占用数据库文件的进程后再删除');
console.log(` ${ICONS.pipe} 手动删除命令:`);
if (process.platform === 'win32') {
console.log(` ${ICONS.pipe} ${colors.cyan}del /f "${sqliteDbPath}"${colors.reset}`);
} else {
console.log(` ${ICONS.pipe} ${colors.cyan}rm -f "${sqliteDbPath}"${colors.reset}`);
}
}
} catch (forceError) {
log.error(`强制删除失败: ${forceError.message}`);
}
}
}
} else {
log.info('保留 SQLite 数据库文件');
log.keyValue('文件位置', path.relative(__dirname, sqliteDbPath));
}
} else {
log.info('未检测到 SQLite 数据库文件');
}
} else {
log.divider();
log.warning('当前使用 MySQL 数据库');
log.info('卸载脚本不会自动删除 MySQL 数据库,请手动清理:');
log.section('MySQL 删除方法');
console.log(` ${ICONS.pipe} 方法1 - MySQL 命令行:`);
console.log(` ${ICONS.pipe} ${colors.cyan}mysql -u root -p${colors.reset}`);
console.log(` ${ICONS.pipe} ${colors.cyan}DROP DATABASE yunrui_management;${colors.reset}`);
console.log(` ${ICONS.pipe} 方法2 - 使用数据库管理工具(如 Navicat、DBeaver、phpMyAdmin`);
console.log(` ${ICONS.pipe} 方法3 - 如果不再需要 MySQL 数据,可直接卸载 MySQL 服务`);
}
log.divider();
log.section('数据备份建议');
if (dbType === 'sqlite' && sqliteExists) {
console.log(` ${ICONS.pipe} 备份 SQLite:直接复制 ${colors.cyan}${path.relative(__dirname, sqliteDbPath)}${colors.reset} 文件`);
}
console.log(` ${ICONS.pipe} 如需保留数据,请在卸载前手动备份`);
log.divider();
}
async function cleanupDependencies(cmdArgs) {
log.step('依赖和构建产物清理', 6, 7);
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', 'node_modules', '.vite'), name: '前端 Vite 缓存 (.vite)', type: '构建产物' },
{ path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物 (dist)', type: '构建产物' },
{ path: path.join(__dirname, 'backend', 'temp'), name: '后端临时文件 (temp)', 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(cmdArgs) {
log.step('日志清理', 4, 7);
const logsDir = path.join(__dirname, 'backend', 'logs');
if (fs.existsSync(logsDir)) {
if (cmdArgs?.force) {
log.info('强制模式:跳过日志文件删除');
return;
}
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('未找到后端日志目录');
}
const rootLogsDir = path.join(__dirname, 'logs');
if (fs.existsSync(rootLogsDir)) {
if (cmdArgs?.force) {
log.info('强制模式:跳过脚本日志删除');
} else {
const rootLogConfirm = await ask('是否删除安装/卸载脚本日志? (y/N)', 'N');
if (rootLogConfirm.toLowerCase() === 'y') {
try {
fs.rmSync(rootLogsDir, { recursive: true, force: true });
log.success('脚本日志文件已删除');
} catch (error) {
log.error(`删除失败: ${error.message}`);
}
} else {
log.info('保留脚本日志文件');
}
}
}
log.divider();
}
async function cleanupUploads(cmdArgs) {
log.step('上传清理', 5, 7);
const uploadsDir = path.join(__dirname, 'backend', 'uploads');
if (fs.existsSync(uploadsDir)) {
if (cmdArgs?.force) {
log.info('强制模式:跳过上传文件目录删除');
return;
}
const confirm = await ask('是否删除上传文件目录? (y/N)', 'N');
if (confirm.toLowerCase() === 'y') {
try {
fs.rmSync(uploadsDir, { 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('备份数据库', 0, 7);
const dbConfig = getDatabaseConfig();
const sqliteDbPath = dbConfig.sqliteDbPath;
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 showDryRunPreview(cmdArgs) {
const { backendName, frontendName } = getPM2ServiceNames();
const dbConfig = getDatabaseConfig();
log.banner('卸载预览', 'Dry Run Mode', null);
console.log(` ${colors.bright}${colors.cyan}以下是将要执行的操作(预览模式):${colors.reset}\n`);
console.log(` ${ICONS.pointer} ${colors.bright}1. 停止并删除 PM2 服务${colors.reset}`);
console.log(` ${ICONS.pipe} ${backendName}`);
console.log(` ${ICONS.pipe} ${frontendName}`);
console.log(` ${ICONS.pipe} 清理 PM2 开机自启配置`);
console.log(` ${ICONS.pipe} 清理 PM2 相关日志文件`);
console.log(`\n ${ICONS.pointer} ${colors.bright}2. 清理 Nginx 配置${colors.reset}`);
if (process.platform === 'linux') {
const nginxConfigPaths = ['/etc/nginx/sites-available/yunrui_asset', '/etc/nginx/sites-enabled/yunrui_asset',
'/etc/nginx/conf.d/idc', '/etc/nginx/conf.d/idc.conf'];
nginxConfigPaths.forEach(p => {
if (fs.existsSync(p)) {
console.log(` ${ICONS.pipe} 删除云睿资产站点配置: ${p}`);
}
});
if (fs.existsSync('/var/www/yunrui_asset')) {
console.log(` ${ICONS.pipe} 删除前端部署目录 (/var/www/yunrui_asset)`);
}
if (!fs.existsSync('/etc/nginx/sites-enabled/default') && fs.existsSync('/etc/nginx/sites-available/default')) {
console.log(` ${ICONS.pipe} 恢复 Nginx 默认站点`);
}
console.log(` ${ICONS.pipe} 重载 Nginx 配置 (nginx -s reload)`);
} else if (process.platform === 'win32') {
console.log(` ${ICONS.pipe} 删除 Nginx 配置文件 (C:/nginx/conf/conf.d/idc.conf)`);
console.log(` ${ICONS.pipe} 重载 Nginx (nginx -s reload)`);
} else if (process.platform === 'darwin') {
console.log(` ${ICONS.pipe} 删除 Nginx 配置 (/usr/local/etc/nginx/servers/idc)`);
console.log(` ${ICONS.pipe} 重载 Nginx (nginx -s reload)`);
}
console.log(`\n ${ICONS.pointer} ${colors.bright}3. 清理生成的配置文件${colors.reset}`);
const configFiles = [
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)' },
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置' },
{ path: path.join(__dirname, 'deploy', 'nginx-yunrui.conf'), name: 'Nginx 配置' }
];
configFiles.forEach(f => {
if (fs.existsSync(f.path)) {
console.log(` ${ICONS.pipe} 删除: ${f.name}`);
}
});
console.log(`\n ${ICONS.pointer} ${colors.bright}4. 数据库${colors.reset}`);
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/`);
}
} else if (dbConfig.dbType === 'mysql') {
console.log(` ${ICONS.pipe} 提示手动删除 MySQL 数据库`);
} else {
console.log(` ${ICONS.pipe} 未检测到数据库文件`);
}
console.log(`\n ${ICONS.pointer} ${colors.bright}5. 日志文件${colors.reset}`);
if (fs.existsSync(path.join(__dirname, 'backend', 'logs'))) {
console.log(` ${ICONS.pipe} 后端日志目录 (backend/logs/)`);
}
if (fs.existsSync(path.join(__dirname, 'logs'))) {
console.log(` ${ICONS.pipe} 脚本日志目录 (logs/)`);
}
console.log(`\n ${ICONS.pointer} ${colors.bright}6. 上传文件${colors.reset}`);
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}`);
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 参数以实际执行卸载');
log.divider();
}
function printSummary() {
const duration = ((Date.now() - uninstallStartTime) / 1000).toFixed(1);
log.banner('卸载完成', 'Uninstallation Complete', null);
log.keyValue('卸载耗时', `${duration} 秒`);
if (deletedItems.length > 0) {
log.section('已删除项目');
deletedItems.forEach(item => {
console.log(` ${ICONS.pipe} ${colors.dim}${item.type}:${colors.reset} ${item.name}`);
});
}
if (backedUpItems.length > 0) {
log.section('已备份项目');
backedUpItems.forEach(item => {
console.log(` ${ICONS.pipe} ${colors.dim}${item.type}:${colors.reset} ${path.basename(item.path)}`);
});
log.keyValue('备份位置', BACKUP_DIR);
}
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/)`);
}
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}`);
log.divider();
log.success('卸载完成!');
}
async function main() {
uninstallStartTime = Date.now();
const cmdArgs = parseArgs();
if (cmdArgs.help) {
showHelp();
return;
}
initLogFile(LOG_DIR);
log.banner(
'云睿资产管理系统',
'卸载脚本',
SCRIPT_VERSION
);
log.warning('此脚本将卸载 云睿资产管理系统');
if (cmdArgs.force) {
log.info('运行模式: 强制卸载(无需确认)');
}
if (cmdArgs.dryRun) {
log.info('运行模式: 预览模式(不实际执行)');
}
log.divider();
if (!cmdArgs.force && !cmdArgs.dryRun) {
const confirm = await ask('确认要开始卸载? (y/N)', 'N');
if (confirm.toLowerCase() !== 'y') {
log.info('已取消卸载');
closeReadline();
closeLogFile();
return;
}
}
if (cmdArgs.dryRun) {
showDryRunPreview(cmdArgs);
closeReadline();
closeLogFile();
return;
}
try {
if (cmdArgs.backup) {
await backupDatabase();
}
await stopAndDeleteServices();
await cleanupNginxConfig(cmdArgs);
await cleanupConfigFiles();
if (!cmdArgs.skipDb) {
await cleanupDatabase(cmdArgs);
} else {
log.info('已跳过数据库删除');
}
await cleanupLogs(cmdArgs);
if (!cmdArgs.skipUploads) {
await cleanupUploads(cmdArgs);
} else {
log.info('已跳过上传文件目录删除');
}
if (!cmdArgs.skipDeps) {
await cleanupDependencies(cmdArgs);
} else {
log.info('已跳过依赖删除');
}
printSummary();
} catch (error) {
log.error(`卸载失败: ${error.message}`);
console.error(error);
process.exit(1);
} finally {
closeReadline();
closeLogFile();
}
}
main();