healthapp
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

374 lines
11 KiB

#!/usr/bin/env node
/**
* 前端开发服务启动脚本
*
* 用法:
* node scripts/dev.js # 同时启动 web + mall
* node scripts/dev.js web # 仅启动健康APP (web)
* node scripts/dev.js mall # 仅启动商城 (mall)
* node scripts/dev.js --kill # 仅关闭占用端口的进程
*
* 启动前会自动检查端口占用并清理。
*/
const { execSync, spawn } = require('child_process');
const path = require('path');
const os = require('os');
// ─── 配置 ────────────────────────────────────────────
const PROJECTS = {
web: {
name: '健康APP (web)',
dir: 'web',
port: 5173,
color: '\x1b[36m', // cyan
url: 'http://localhost:5173'
},
mall: {
name: '健康商城 (mall)',
dir: 'mall',
port: 5174,
color: '\x1b[32m', // green
url: 'http://localhost:5174'
}
};
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const GREEN = '\x1b[32m';
const CYAN = '\x1b[36m';
const DIM = '\x1b[2m';
// ─── 工具函数 ────────────────────────────────────────
function log(msg) { console.log(msg); }
function info(msg) { log(`${CYAN}${RESET} ${msg}`); }
function success(msg) { log(`${GREEN}${RESET} ${msg}`); }
function warn(msg) { log(`${YELLOW}${RESET} ${msg}`); }
function error(msg) { log(`${RED}${RESET} ${msg}`); }
function banner() {
log('');
log(`${BOLD}╔═══════════════════════════════════════════════╗${RESET}`);
log(`${BOLD}║ 健康AI助手 · 前端开发服务启动脚本 ║${RESET}`);
log(`${BOLD}╚═══════════════════════════════════════════════╝${RESET}`);
log('');
}
/**
* 获取占用指定端口的进程 PID
* 使用 netstat(Windows/Linux 通用)进行精确检测
* 返回 PID 或 null
*/
function getPortPid(port) {
try {
const isWin = os.platform() === 'win32';
if (isWin) {
// Windows: netstat -ano,查找 LISTENING 状态
const result = execSync(`netstat -ano`, { encoding: 'utf-8', timeout: 5000 });
const lines = result.split('\n');
for (const line of lines) {
// 匹配精确端口(避免 51730 匹配 5173)
const match = line.match(new RegExp(`:\\s*${port}\\s+.*LISTENING\\s+(\\d+)`));
if (match) {
const pid = parseInt(match[1], 10);
if (pid > 0) return pid;
}
}
// 兜底:用 findstr
try {
const result2 = execSync(`netstat -ano | findstr ":${port} "`, { encoding: 'utf-8', timeout: 5000 });
const lines2 = result2.trim().split('\n');
for (const line of lines2) {
if (line.includes('LISTENING')) {
const parts = line.trim().split(/\s+/);
const pid = parseInt(parts[parts.length - 1], 10);
if (pid > 0) return pid;
}
}
} catch { /* no match */ }
} else {
const result = execSync(`lsof -ti:${port}`, { encoding: 'utf-8', timeout: 5000 });
const pid = parseInt(result.trim().split('\n')[0], 10);
if (pid > 0) return pid;
}
} catch {
// 没有找到进程
}
return null;
}
/**
* 获取进程名称
*/
function getProcessName(pid) {
try {
const isWin = os.platform() === 'win32';
if (isWin) {
const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf-8', timeout: 5000 });
const match = result.match(/"([^"]+)"/);
return match ? match[1] : '未知进程';
} else {
return execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf-8', timeout: 5000 }).trim();
}
} catch {
return '未知进程';
}
}
/**
* 终止进程
*/
function killProcess(pid) {
try {
const isWin = os.platform() === 'win32';
if (isWin) {
// 先尝试终止进程树
execSync(`taskkill /PID ${pid} /T /F`, { encoding: 'utf-8', timeout: 5000, stdio: 'ignore' });
} else {
execSync(`kill -9 ${pid}`, { encoding: 'utf-8', timeout: 5000, stdio: 'ignore' });
}
return true;
} catch {
return false;
}
}
/**
* 检查并释放端口
*/
async function ensurePortFree(port, projectName) {
const pid = getPortPid(port);
if (!pid) {
success(`端口 ${BOLD}${port}${RESET} 空闲 ${DIM}(${projectName})${RESET}`);
return true;
}
const procName = getProcessName(pid);
warn(`端口 ${BOLD}${port}${RESET} 被占用 → PID: ${pid} (${procName}) ${DIM}[${projectName}]${RESET}`);
if (killProcess(pid)) {
// 等待端口释放(最多重试 3 次)
for (let i = 0; i < 3; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
if (!getPortPid(port)) {
success(`已终止进程 PID ${pid},端口 ${port} 已释放`);
return true;
}
}
error(`端口 ${port} 仍被占用,无法释放`);
return false;
} else {
error(`无法终止进程 PID ${pid}`);
return false;
}
}
/**
* 检查项目依赖是否已安装
*/
function checkNodeModules(projectDir) {
const nmPath = path.join(projectDir, 'node_modules');
try {
require('fs').accessSync(nmPath);
return true;
} catch {
return false;
}
}
/**
* 安装项目依赖
*/
function installDeps(projectDir, projectName) {
info(`${projectName}: 正在安装依赖 (npm install)...`);
try {
execSync('npm install', {
cwd: projectDir,
stdio: 'inherit',
timeout: 120000
});
success(`${projectName}: 依赖安装完成`);
return true;
} catch {
error(`${projectName}: 依赖安装失败`);
return false;
}
}
/**
* 启动项目开发服务
*/
function startProject(project, rootDir) {
const projectDir = path.join(rootDir, project.dir);
const label = `${project.color}[${project.name}]${RESET}`;
info(`${label} 启动中...`);
const child = spawn('npm', ['run', 'dev'], {
cwd: projectDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env },
shell: true
});
child.stdout.on('data', (data) => {
const lines = data.toString().split('\n').filter(l => l.trim());
for (const line of lines) {
log(` ${label} ${line}`);
}
});
child.stderr.on('data', (data) => {
const lines = data.toString().split('\n').filter(l => l.trim());
for (const line of lines) {
// Vite 的 deprecation warning 降级显示
if (line.includes('Deprecation Warning') || line.includes('More info:')) {
log(` ${label} ${DIM}${line}${RESET}`);
} else {
log(` ${label} ${YELLOW}${line}${RESET}`);
}
}
});
child.on('error', (err) => {
error(`${label} 启动失败: ${err.message}`);
});
child.on('close', (code) => {
if (code !== null && code !== 0) {
warn(`${label} 进程退出,退出码: ${code}`);
}
});
return child;
}
// ─── 主流程 ──────────────────────────────────────────
async function main() {
banner();
const args = process.argv.slice(2);
const killOnly = args.includes('--kill');
const targets = [];
if (args.includes('web')) targets.push('web');
if (args.includes('mall')) targets.push('mall');
if (targets.length === 0 && !killOnly) {
targets.push('web', 'mall');
}
const rootDir = path.resolve(__dirname, '..');
// ── 步骤 1:显示计划 ──
if (killOnly) {
info('模式: 仅关闭端口占用进程');
} else if (targets.length === 2) {
info('模式: 同时启动 健康APP + 商城');
} else {
info(`模式: 仅启动 ${PROJECTS[targets[0]].name}`);
}
log('');
// ── 步骤 2:检查端口 ──
log(`${BOLD}[1/3] 检查端口占用${RESET}`);
log('─'.repeat(48));
const portProjects = killOnly
? Object.values(PROJECTS)
: targets.map(t => PROJECTS[t]);
for (const proj of portProjects) {
const freed = await ensurePortFree(proj.port, proj.name);
if (!freed && !killOnly) {
error(`无法释放端口 ${proj.port},请手动处理后重试`);
process.exit(1);
}
}
log('');
if (killOnly) {
success('端口清理完成');
process.exit(0);
}
// ── 步骤 3:检查依赖 ──
log(`${BOLD}[2/3] 检查项目依赖${RESET}`);
log('─'.repeat(48));
for (const key of targets) {
const proj = PROJECTS[key];
const projDir = path.join(rootDir, proj.dir);
if (checkNodeModules(projDir)) {
success(`${proj.name}: node_modules 已就绪`);
} else {
warn(`${proj.name}: 未安装依赖`);
const ok = installDeps(projDir, proj.name);
if (!ok) {
error('依赖安装失败,退出');
process.exit(1);
}
}
}
log('');
// ── 步骤 4:启动服务 ──
log(`${BOLD}[3/3] 启动开发服务${RESET}`);
log('─'.repeat(48));
const children = [];
for (const key of targets) {
const child = startProject(PROJECTS[key], rootDir);
children.push(child);
}
// 等待所有服务就绪后显示汇总
await new Promise(resolve => setTimeout(resolve, 3000));
log('');
log(`${BOLD}╔═══════════════════════════════════════════════╗${RESET}`);
log(`${BOLD}║ 开发服务已启动 ║${RESET}`);
log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`);
for (const key of targets) {
const proj = PROJECTS[key];
log(`${BOLD}${RESET} ${proj.color}${proj.name}${RESET}: ${BOLD}${proj.url}${RESET}`);
}
log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`);
log(`${BOLD}${RESET} 后端 API: ${BOLD}http://localhost:8080${RESET}`);
log(`${BOLD}${RESET} 测试账号: ${DIM}13800138000 / 123456${RESET}`);
log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`);
log(`${BOLD}${RESET}${RED}Ctrl+C${RESET} 停止所有服务`);
log(`${BOLD}╚═══════════════════════════════════════════════╝${RESET}`);
log('');
// ── 优雅退出处理 ──
function cleanup() {
log('');
info('正在停止所有服务...');
for (const child of children) {
try {
const isWin = os.platform() === 'win32';
if (isWin) {
execSync(`taskkill /PID ${child.pid} /T /F`, { stdio: 'ignore', timeout: 5000 });
} else {
child.kill('SIGTERM');
}
} catch {
// 进程可能已退出
}
}
success('所有服务已停止');
process.exit(0);
}
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// 保持主进程运行
await new Promise(() => {});
}
main().catch(err => {
error(`脚本执行出错: ${err.message}`);
process.exit(1);
});