Files
yunrui_asset/docs/install-improvement-plan.md
T

662 lines
17 KiB
Markdown
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.
# install.js 安装脚本改进计划
## 概述
本文档记录 `install.js` 安装脚本的改进计划,涵盖 **15 项改进**,按优先级分为高/中/低三档。
---
## 一、高优先级改进
### 1. npm 依赖安装网络问题处理
**问题描述**
- 直接执行 `npm install`,无重试机制
- 国内访问 npmjs.org 超时/速度慢
- 网络波动导致安装中断无反馈
**改进方案**
#### 1.1 自动检测并切换镜像源
```javascript
async function detectAndSetRegistry() {
const registries = [
{ name: '淘宝镜像', url: 'https://registry.npmmirror.com' },
{ name: '腾讯镜像', url: 'https://mirrors.cloud.tencent.com/npm/' },
{ name: '华为镜像', url: 'https://repo.huaweicloud.com/repository/npm/' },
{ name: '官方源', url: 'https://registry.npmjs.org' },
];
for (const registry of registries.slice(0, 3)) {
const pingResult = await pingRegistry(registry.url);
if (pingResult.success && pingResult.latency < 3000) {
log.success(`使用 ${registry.name} (${pingResult.latency}ms)`);
return registry.url;
}
}
return null;
}
async function pingRegistry(url) {
const start = Date.now();
try {
await new Promise((resolve, reject) => {
const req = require('https').get(`${url}/-/ping`, (res) => {
if (res.statusCode === 200) resolve();
else reject(new Error('Not 200'));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('Timeout')); });
});
return { success: true, latency: Date.now() - start };
} catch {
return { success: false, latency: Infinity };
}
}
```
#### 1.2 重试机制与指数退避
```javascript
async function installWithRetry(cwd, name, maxRetries = 3) {
const baseDelay = 2000;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
log.info(`安装 ${name} 依赖 (第 ${attempt}/${maxRetries} 次)...`);
const result = await runNpmInstall(cwd);
if (result.success) {
return { success: true };
}
const errorType = analyzeNpmError(result.error);
if (errorType === 'NETWORK_ERROR' && attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt - 1);
log.warning(`网络错误,${delay/1000} 秒后重试...`);
await new Promise(r => setTimeout(r, delay));
runCommand('npm cache clean --force', { cwd, silent: true });
} else if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, baseDelay));
}
}
return { success: false };
}
function analyzeNpmError(error) {
const errorLower = (error || '').toLowerCase();
if (errorLower.includes('econnrefused') ||
errorLower.includes('econnreset') ||
errorLower.includes('etimedout') ||
errorLower.includes('network')) {
return 'NETWORK_ERROR';
}
return 'UNKNOWN_ERROR';
}
```
#### 1.3 安装进度显示
```javascript
function runNpmInstall(cwd) {
return new Promise((resolve) => {
const spawn = require('child_process').spawn;
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const child = spawn(npm, ['install', '--loglevel=warn'], {
cwd, shell: true, stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let lastProgress = '';
child.stdout.on('data', (data) => {
stdout += data;
const progressMatch = data.toString().match(/(\d+\/\d+|\d+%)/);
if (progressMatch) {
const progress = progressMatch[1];
if (progress !== lastProgress) {
process.stdout.write(`\r 安装进度: ${progress} `);
lastProgress = progress;
}
}
});
child.on('close', (code) => {
if (lastProgress) process.stdout.write('\n');
resolve({ success: code === 0, output: stdout });
});
setTimeout(() => { child.kill(); resolve({ success: false, error: '安装超时' }); }, 10 * 60 * 1000);
});
}
```
#### 1.4 安装后验证
```javascript
async function verifyDependencies(cwd, name) {
log.subStep(`验证 ${name} 依赖完整性...`);
const lsResult = runCommand('npm ls --depth=0 --json', { cwd, silent: true });
if (!lsResult.success) {
return { success: false, error: '依赖检查失败' };
}
try {
const deps = JSON.parse(lsResult.output);
if (deps.problems && deps.problems.length > 0) {
log.warning(`发现 ${deps.problems.length} 个依赖问题`);
return { success: false, problems: deps.problems };
}
return { success: true };
} catch {
return { success: false, error: '验证解析失败' };
}
}
```
**实施位置**`install.js` 第 1782-1804 行 `installDependencies()` 函数
---
### 2. 健康检查跨平台兼容性
**问题描述**
```javascript
// 当前代码(Windows 无 curl
execSync('curl -s -o /dev/null -w "%{http_code}" ...');
```
**改进方案**
```javascript
async function healthCheck() {
const http = require('http');
return new Promise((resolve) => {
const req = http.get(`http://localhost:${config.backendPort}/health`, (res) => {
resolve({ success: res.statusCode === 200, statusCode: res.statusCode });
});
req.on('error', () => resolve({ success: false }));
req.setTimeout(5000, () => { req.destroy(); resolve({ success: false }); });
});
}
```
**实施位置**`install.js` 第 2039-2073 行 `healthCheck()` 函数
---
### 3. MySQL 密码安全传递
**问题描述**
```javascript
// 当前代码 - 密码暴露在命令行
execSync(`mysql -u root -p'${rootPassword}' -e "${sql}"`);
```
**改进方案**
```javascript
async function createMySQLDatabase(rootPassword) {
const createDbSql = 'CREATE DATABASE IF NOT EXISTS idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;';
try {
execSync(`mysql -u root -e "${createDbSql}"`, {
env: { ...process.env, MYSQL_PWD: rootPassword },
stdio: ['pipe', 'pipe', 'pipe']
});
log.success('数据库创建成功');
} catch (error) {
// 错误处理...
}
}
```
**实施位置**`install.js` 第 1052-1086 行 `createMySQLDatabase()` 函数
---
### 4. 错误处理完善
**问题描述**
- 错误后继续执行,导致级联失败
- 无重试选项
**改进方案**
```javascript
async function initDatabase() {
const initScript = path.join(__dirname, 'backend', 'scripts', 'init-database.js');
if (!fs.existsSync(initScript)) {
log.warning('未找到数据库初始化脚本,跳过');
return;
}
log.info('正在初始化数据库...');
const result = runCommand('node scripts/init-database.js', {
cwd: path.join(__dirname, 'backend')
});
if (!result.success) {
const retry = await ask('数据库初始化失败,是否重试? (Y/n)', 'Y');
if (retry.toLowerCase() === 'y') {
return await initDatabase();
}
throw new Error('Database initialization failed');
}
log.success('数据库初始化完成');
}
```
**实施位置**`install.js` 第 1811-1833 行 `initDatabase()` 函数
---
### 5. 回滚机制
**问题描述**
- 安装失败后无清理
- 残留半成品状态
**改进方案**
```javascript
const rollbackSteps = [];
async function installDependencies() {
try {
log.step('安装后端依赖...');
await installBackend();
rollbackSteps.push(() => runCommand('rmdir /s /q node_modules', { cwd: backendDir, shell: true }));
log.step('安装前端依赖...');
await installFrontend();
rollbackSteps.push(() => runCommand('rmdir /s /q node_modules', { cwd: frontendDir, shell: true }));
} catch (error) {
await rollback();
throw error;
}
}
async function rollback() {
log.warning('安装失败,正在回滚...');
for (const step of rollbackSteps.reverse()) {
try { await step(); } catch {}
}
}
```
**实施位置**:新增 `rollbackSteps` 数组和 `rollback()` 函数,在 `main()` 中调用
---
### 6. SQLite 编译依赖替换
**已完成 ✅**
| 文件 | 修改内容 |
|------|---------|
| `backend/package.json` | `sqlite3``better-sqlite3` |
| `backend/db.js` | 添加 `dialectModule` 配置 |
---
## 二、中优先级改进
### 7. 日志管理优化
**问题描述**
- 每次安装生成新日志,无清理
- Windows 文件名不兼容冒号
**改进方案**
```javascript
function initLogFile() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
// 清理 7 天前的旧日志
const oldLogs = fs.readdirSync(LOG_DIR)
.filter(f => f.startsWith('install_'))
.filter(f => {
const stat = fs.statSync(path.join(LOG_DIR, f));
return Date.now() - stat.mtimeMs > 7 * 24 * 60 * 60 * 1000;
});
oldLogs.forEach(f => fs.unlinkSync(path.join(LOG_DIR, f)));
// 使用安全的时间格式
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const logFileName = `install_${timestamp}.log`;
logFileStream = fs.createWriteStream(path.join(LOG_DIR, logFileName), { flags: 'a' });
// 重写 console.log 同时输出到文件和终端
const originalLog = console.log;
console.log = (...args) => {
const message = args.map(arg => String(arg)).join(' ');
logFileStream.write(`[${new Date().toISOString()}] ${message}\n`);
originalLog.apply(console, args);
};
}
```
**实施位置**`install.js` 第 100-123 行 `initLogFile()` 函数
---
### 8. 配置持久化与恢复
**问题描述**
- 每次安装需重新配置
**改进方案**
```javascript
const CONFIG_FILE = path.join(__dirname, '.install-config.json');
async function loadSavedConfig() {
if (!fs.existsSync(CONFIG_FILE)) return false;
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
const useSaved = await ask('检测到上次配置,是否使用? (Y/n)', 'Y');
if (useSaved.toLowerCase() === 'y') {
Object.assign(config, saved);
return true;
}
return false;
}
function saveConfig() {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
log.subStep('配置已保存');
}
// 在 confirmConfiguration() 成功后调用 saveConfig()
```
**实施位置**:新增 `CONFIG_FILE` 常量、`loadSavedConfig()``saveConfig()` 函数
---
### 9. 进度显示优化
**问题描述**
- 只有文字输出,无进度条
**改进方案**
```javascript
const steps = [
{ name: '环境检测', fn: checkEnvironment },
{ name: '数据库配置', fn: configureDatabase },
{ name: '服务配置', fn: configureServices },
{ name: '安装依赖', fn: installDependencies },
{ name: '初始化数据库', fn: initDatabase },
{ name: '构建前端', fn: buildFrontend },
{ name: '启动服务', fn: startServices },
];
async function runWithProgress() {
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const progress = `[${i + 1}/${steps.length}]`;
log.step(`${progress} ${step.name}`);
const startTime = Date.now();
await step.fn();
log.subStep(`耗时 ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
}
}
```
**实施位置**:新增 `steps` 数组和 `runWithProgress()` 函数
---
### 10. Nginx 版本动态获取
**问题描述**
```javascript
// 固定版本
const nginxUrl = 'https://nginx.org/download/nginx-1.24.0.zip';
```
**改进方案**
```javascript
async function getLatestNginxVersion() {
const https = require('https');
return new Promise((resolve) => {
https.get('https://nginx.org/en/download.html', (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const match = data.match(/nginx-(\d+\.\d+\.\d+)\.zip/);
resolve(match ? match[1] : '1.24.0');
});
}).on('error', () => resolve('1.24.0'));
});
}
async function autoInstallNginx() {
const version = await getLatestNginxVersion();
const nginxUrl = `https://nginx.org/download/nginx-${version}.zip`;
// 后续下载逻辑...
}
```
**实施位置**`install.js` 第 1430-1483 行 `autoInstallNginx()` 函数
---
### 11. PM2 serve 本地化
**问题描述**
```javascript
// 全局安装
runCommand('npm install -g serve');
```
**改进方案**
```javascript
// 方案1:使用 npx(推荐)
const frontendResult = runCommand(
`npx serve -s dist -l ${config.frontendPort}`,
{ cwd: path.join(__dirname, 'frontend') }
);
// 方案2:本地安装
runCommand('npm install serve', { cwd: path.join(__dirname, 'frontend') });
```
**实施位置**`install.js` 第 1889-1903 行 `startServices()` 函数
---
## 三、低优先级改进
### 12. 代码模块化重构
**问题描述**
- 2100+ 行单文件,难以维护
**改进方案**
```
scripts/
├── install.js # 入口
├── lib/
│ ├── logger.js # 日志模块
│ ├── prompt.js # 交互输入模块
│ ├── executor.js # 命令执行模块
│ ├── database.js # 数据库配置模块
│ ├── nginx.js # Nginx 配置模块
│ ├── pm2.js # PM2 配置模块
│ └── health-check.js # 健康检查模块
└── templates/
├── env.template # .env 模板
├── pm2.template.js # PM2 配置模板
└── nginx.template # Nginx 配置模板
```
---
### 13. 卸载脚本
**改进方案**
```javascript
// uninstall.js
async function uninstall() {
log.step('停止服务');
runCommand('pm2 stop idc-backend idc-frontend');
runCommand('pm2 delete idc-backend idc-frontend');
log.step('清理文件');
['node_modules', 'dist', 'logs'].forEach(dir => {
const cmd = process.platform === 'win32'
? `rmdir /s /q ${dir}`
: `rm -rf ${dir}`;
runCommand(cmd, { cwd: backendDir });
runCommand(cmd, { cwd: frontendDir });
});
log.step('清理配置');
['.env', '.install-config.json'].forEach(file => {
const path = `${backendDir}/${file}`;
if (fs.existsSync(path)) fs.unlinkSync(path);
});
log.success('卸载完成');
}
```
---
### 14. 配置验证
**改进方案**
```javascript
async function validateConfig() {
const errors = [];
// 端口冲突检查
const usedPorts = await checkUsedPorts([config.backendPort, config.frontendPort]);
if (usedPorts.length > 0) {
errors.push(`端口已被占用: ${usedPorts.join(', ')}`);
}
// 磁盘空间检查
const freeSpace = await checkDiskSpace(__dirname);
if (freeSpace < 500 * 1024 * 1024) {
errors.push('磁盘空间不足 500MB');
}
// 权限检查
if (config.frontendPort < 1024 && !isRootUser()) {
errors.push(`端口 ${config.frontendPort} 需要 root 权限`);
}
return errors;
}
```
---
### 15. 配置导入导出
**改进方案**
```javascript
// 导入配置
// node install.js --import=config.json
async function importConfig(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const loaded = JSON.parse(content);
Object.assign(config, loaded);
log.success(`配置已导入: ${filePath}`);
}
// 导出配置(在 saveConfig 时可选择)
// node install.js --export=config.json
```
---
## 四、实施优先级
| 优先级 | 编号 | 改进项 | 复杂度 | 状态 |
|--------|------|--------|--------|------|
| 🔴高 | 1 | npm 安装网络处理 | 中 | 待实施 |
| 🔴高 | 2 | 健康检查跨平台 | 低 | 待实施 |
| 🔴高 | 3 | MySQL 密码安全 | 低 | 待实施 |
| 🔴高 | 4 | 错误处理完善 | 中 | 待实施 |
| 🔴高 | 5 | 回滚机制 | 高 | 待实施 |
| 🔴高 | 6 | SQLite 替换 | 中 | ✅ 已完成 |
| 🟡中 | 7 | 日志管理 | 低 | 待实施 |
| 🟡中 | 8 | 配置持久化 | 中 | 待实施 |
| 🟡中 | 9 | 进度显示 | 低 | 待实施 |
| 🟡中 | 10 | Nginx 版本动态 | 低 | 待实施 |
| 🟡中 | 11 | PM2 serve 本地化 | 低 | 待实施 |
| 🟢低 | 12 | 代码模块化 | 高 | 待实施 |
| 🟢低 | 13 | 卸载脚本 | 中 | 待实施 |
| 🟢低 | 14 | 配置验证 | 中 | 待实施 |
| 🟢低 | 15 | 配置导入导出 | 低 | 待实施 |
---
## 五、建议实施顺序
### 第一阶段(快速见效)
1. **Nginx 版本动态** (10) - 改动小,立即生效
2. **日志管理优化** (7) - 改动小,提升调试体验
3. **PM2 serve 本地化** (11) - 改动小,避免全局污染
### 第二阶段(核心增强)
4. **npm 安装网络处理** (1) - 提升安装成功率
5. **健康检查跨平台** (2) - Windows 兼容性
6. **MySQL 密码安全** (3) - 安全性
### 第三阶段(健壮性)
7. **错误处理完善** (4) - 提升容错能力
8. **回滚机制** (5) - 失败恢复
9. **配置持久化** (8) - 复用配置
10. **进度显示** (9) - 用户体验
### 第四阶段(长期)
11. **代码模块化** (12) - 提升可维护性
12. **卸载脚本** (13) - 功能完整性
13. **配置验证** (14) - 提前发现问题
14. **配置导入导出** (15) - 扩展性
---
## 六、测试清单
每项改进实施后需验证:
- [ ] Windows 环境
- [ ] Linux 环境(Ubuntu/Debian
- [ ] Linux 环境(CentOS/RHEL
- [ ] macOS 环境
- [ ] 非交互模式 (`-y`)
- [ ] 配置恢复功能
- [ ] 日志文件生成
- [ ] 回滚功能(模拟失败)