From 8e7c181b2713a624f56bf66c6ff6c05e2fb6dbb9 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 7 Jun 2026 10:06:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=AF=E5=8A=A8=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/dev.sh | 220 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 211 insertions(+), 9 deletions(-) diff --git a/scripts/dev.sh b/scripts/dev.sh index 30ceaba..97695ca 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -5,6 +5,11 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.dev.yml" BACKEND_ENV="${ROOT_DIR}/backend/.env" BACKEND_ENV_EXAMPLE="${ROOT_DIR}/backend/.env.example" +FRONTEND_DIR="${ROOT_DIR}/frontend" +FRONTEND_PACKAGE_JSON="${FRONTEND_DIR}/package.json" +FRONTEND_PACKAGE_LOCK="${FRONTEND_DIR}/package-lock.json" +FRONTEND_NODE_MODULES="${FRONTEND_DIR}/node_modules" +FRONTEND_NODE_MODULES_LOCK="${FRONTEND_NODE_MODULES}/.package-lock.json" MIGRATIONS_DIR="${ROOT_DIR}/backend/migrations" MYSQL_DATABASE="${DEV_MYSQL_DATABASE:-hfb_sys}" BACKEND_PORT="${DEV_BACKEND_PORT:-8080}" @@ -12,6 +17,8 @@ FRONTEND_PORT="${DEV_FRONTEND_PORT:-5173}" BACKEND_HEALTH_URL="${DEV_BACKEND_HEALTH_URL:-http://127.0.0.1:${BACKEND_PORT}/health}" FRONTEND_HEALTH_URL="${DEV_FRONTEND_HEALTH_URL:-http://127.0.0.1:${FRONTEND_PORT}/}" HTTP_READY_TIMEOUT="${DEV_HTTP_READY_TIMEOUT:-120}" +AUTO_KILL_PORTS="${DEV_AUTO_KILL_PORTS:-1}" +FORCE_NPM_CI="${DEV_FORCE_NPM_CI:-0}" BACKEND_PID="" FRONTEND_PID="" @@ -64,15 +71,19 @@ show_help() { DEV_MYSQL_DATABASE=hfb_sys 开发数据库名 DEV_BACKEND_PORT=8080 - 后端端口占用检查端口 + 后端启动端口 DEV_FRONTEND_PORT=5173 - 前端端口占用检查端口 + 前端启动端口 DEV_BACKEND_HEALTH_URL=http://127.0.0.1:8080/health 后端健康检查地址 DEV_FRONTEND_HEALTH_URL=http://127.0.0.1:5173/ 前端健康检查地址 DEV_HTTP_READY_TIMEOUT=120 HTTP 服务就绪等待秒数 + DEV_AUTO_KILL_PORTS=1 + 前后端端口被占用时自动停止占用的 Docker 容器或本机进程 + DEV_FORCE_NPM_CI=0 + 强制重新执行前端 npm ci 示例: ./scripts/dev.sh # 正常启动 @@ -186,21 +197,175 @@ wait_http_ready() { exit 1 } +port_in_use() { + local port="$1" + + if command -v lsof >/dev/null 2>&1; then + lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 + return $? + fi + + if command -v nc >/dev/null 2>&1; then + nc -z 127.0.0.1 "${port}" >/dev/null 2>&1 + return $? + fi + + return 1 +} + +wait_port_released() { + local port="$1" + local timeout="${2:-5}" + local attempt + + for ((attempt = 1; attempt <= timeout; attempt++)); do + if ! port_in_use "${port}"; then + return 0 + fi + sleep 1 + done + + return 1 +} + +is_allowed_container() { + local container_name="$1" + shift + + local allowed_name + for allowed_name in "$@"; do + if [[ "${container_name}" == "${allowed_name}" ]]; then + return 0 + fi + done + + return 1 +} + +docker_containers_on_host_port() { + local port="$1" + + docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}' 2>/dev/null | + awk -F '\t' -v port="${port}" 'index($3, ":" port "->") || $3 ~ ("(^|[ ,])" port "->") { print $1 "\t" $2 }' +} + +stop_docker_containers_on_port() { + local port="$1" + local label="$2" + shift 2 + + local containers container_id container_name allowed_found=0 stopped_found=0 + containers="$(docker_containers_on_host_port "${port}" || true)" + if [[ -z "${containers}" ]]; then + return 1 + fi + + while IFS=$'\t' read -r container_id container_name; do + [[ -z "${container_id}" ]] && continue + + if is_allowed_container "${container_name}" "$@"; then + allowed_found=1 + log "${label}端口 ${port} 已由本项目容器 ${container_name} 使用,保留运行" + continue + fi + + if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then + log_error "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用" + exit 1 + fi + + log_warn "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用,正在停止..." + docker stop "${container_id}" >/dev/null + stopped_found=1 + log_success "已停止容器 ${container_name}" + done <<< "${containers}" + + if [[ "${allowed_found}" == "1" ]]; then + return 0 + fi + + if [[ "${stopped_found}" == "1" ]]; then + wait_port_released "${port}" 5 || true + fi + + return 1 +} + +process_args() { + local pid="$1" + ps -p "${pid}" -o args= 2>/dev/null || true +} + +is_docker_proxy_process() { + local args="$1" + + [[ "${args}" == *"com.docker"* || "${args}" == *"Docker Desktop"* || "${args}" == *"vpnkit"* || "${args}" == *"OrbStack"* ]] +} + ensure_port_free() { local port="$1" local label="$2" + shift 2 + + if stop_docker_containers_on_port "${port}" "${label}" "$@"; then + return 0 + fi if command -v lsof >/dev/null 2>&1; then - if lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then + local pids pid args + pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)" + if [[ -z "${pids}" ]]; then + return 0 + fi + + if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then log_error "${label}端口 ${port} 已被占用,请先停止旧进程或调整端口" exit 1 fi + + log_warn "${label}端口 ${port} 已被占用,正在停止占用进程..." + while IFS= read -r pid; do + [[ -z "${pid}" ]] && continue + args="$(process_args "${pid}")" + + if is_docker_proxy_process "${args}"; then + log_error "${label}端口 ${port} 仍由 Docker 代理进程占用,但未找到可自动停止的容器,请执行 docker ps 检查" + exit 1 + fi + + log_warn "停止 PID ${pid}: ${args:-未知进程}" + kill "${pid}" >/dev/null 2>&1 || true + done <<< "${pids}" + + if ! wait_port_released "${port}" 5; then + log_warn "${label}端口 ${port} 未及时释放,尝试强制停止..." + pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)" + while IFS= read -r pid; do + [[ -z "${pid}" ]] && continue + args="$(process_args "${pid}")" + + if is_docker_proxy_process "${args}"; then + log_error "${label}端口 ${port} 仍由 Docker 代理进程占用,但未找到可自动停止的容器,请执行 docker ps 检查" + exit 1 + fi + + log_warn "强制停止 PID ${pid}: ${args:-未知进程}" + kill -9 "${pid}" >/dev/null 2>&1 || true + done <<< "${pids}" + + if ! wait_port_released "${port}" 5; then + log_error "${label}端口 ${port} 释放失败,请手动检查占用进程" + exit 1 + fi + fi + + log_success "${label}端口 ${port} 已释放" return 0 fi if command -v nc >/dev/null 2>&1; then if nc -z 127.0.0.1 "${port}" >/dev/null 2>&1; then - log_error "${label}端口 ${port} 已被占用,请先停止旧进程或调整端口" + log_error "${label}端口 ${port} 已被占用,但未找到 lsof,无法自动定位并停止占用进程" exit 1 fi return 0 @@ -345,18 +510,55 @@ start_backend() { BACKEND_PID="$!" } -start_frontend() { - if [[ ! -d "${ROOT_DIR}/frontend/node_modules" ]]; then - log "安装前端依赖..." +frontend_dependencies_need_ci() { + if [[ "${FORCE_NPM_CI}" == "1" ]]; then + return 0 + fi + + if [[ ! -d "${FRONTEND_NODE_MODULES}" ]]; then + return 0 + fi + + if [[ ! -f "${FRONTEND_NODE_MODULES_LOCK}" ]]; then + return 0 + fi + + if [[ "${FRONTEND_PACKAGE_JSON}" -nt "${FRONTEND_NODE_MODULES_LOCK}" || "${FRONTEND_PACKAGE_LOCK}" -nt "${FRONTEND_NODE_MODULES_LOCK}" ]]; then + return 0 + fi + + ( + cd "${FRONTEND_DIR}" + npm ls --depth=0 >/dev/null 2>&1 + ) || return 0 + + return 1 +} + +ensure_frontend_dependencies() { + if [[ ! -f "${FRONTEND_PACKAGE_LOCK}" ]]; then + log_error "缺少 frontend/package-lock.json,无法执行 npm ci" + exit 1 + fi + + if frontend_dependencies_need_ci; then + log "同步前端依赖(npm ci)..." ( - cd "${ROOT_DIR}/frontend" + cd "${FRONTEND_DIR}" npm ci ) + log_success "前端依赖已同步" + else + log "前端依赖已同步,跳过 npm ci" fi +} + +start_frontend() { + ensure_frontend_dependencies log "启动前端:http://localhost:5173" ( - cd "${ROOT_DIR}/frontend" + cd "${FRONTEND_DIR}" npm run dev ) & FRONTEND_PID="$!"