#!/bin/bash # VPN 专线管理平台 启动脚本 // Logan.Li # 单二进制内嵌前端交付:构建前端 → 编译 Go → 启动单二进制 # 用法: ./run.sh [build-frontend|build|run|dev|stop|restart|status|help] # # 与 opsx-web-lite 的区别: # - opsx-web-lite 是前后端分离 dev 模式(backend + frontend 独立进程) # - 本项目把前端构建产物 embed 进 Go 二进制,最终 ./opsx-vpn 单文件运行 set -e # ============================================================ # 配置区 # ============================================================ PROJECT_NAME="VPN 管理平台" BACKEND_PORT=8080 FRONTEND_PORT=5173 BINARY_NAME="opsx-vpn" FRONTEND_DIR="frontend" BACKEND_DIR="backend" # 日志配置 MAX_LOG_FILES=3 MAX_LOG_SIZE=$((50 * 1024 * 1024)) # 50MB # ============================================================ # 通用函数 # ============================================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $1"; } success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # PID 文件 PID_FILE="$SCRIPT_DIR/.vpn.pid" # 检查端口是否被占用 check_port() { lsof -ti:$1 2>/dev/null | head -1 } # 释放端口 kill_port() { local pids=$(lsof -ti:$1 2>/dev/null || echo "") if [ -n "$pids" ]; then info "释放端口 $1..." echo "$pids" | xargs kill -9 2>/dev/null || true sleep 1 fi } # 日志轮转(仅启动时调用) rotate_log() { local log_file=$1 [ ! -f "$log_file" ] && return 0 local file_size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null || echo "0") if [ "$file_size" -gt "$MAX_LOG_SIZE" ]; then [ -f "${log_file}.${MAX_LOG_FILES}" ] && rm -f "${log_file}.${MAX_LOG_FILES}" for i in $(seq $((MAX_LOG_FILES - 1)) -1 1); do [ -f "${log_file}.${i}" ] && mv "${log_file}.${i}" "${log_file}.$((i + 1))" done mv "$log_file" "${log_file}.1" info "日志已轮转: $log_file" fi } # ============================================================ # 构建前端 # ============================================================ build_frontend() { info "========== 构建前端 ==========" cd "$SCRIPT_DIR/$FRONTEND_DIR" if [ ! -d "node_modules" ]; then warning "node_modules 不存在,正在安装依赖..." npm install fi info "执行 vite build(产物输出到 ../$BACKEND_DIR/dist)..." npm run build cd "$SCRIPT_DIR" success "前端构建完成" } # ============================================================ # 编译单二进制 # ============================================================ build_binary() { info "========== 编译 Go 单二进制 ==========" cd "$SCRIPT_DIR" # dist 必须存在(embed 需要至少一个文件) if [ ! -d "$BACKEND_DIR/dist" ]; then mkdir -p "$BACKEND_DIR/dist" echo "请先构建前端" > "$BACKEND_DIR/dist/index.html" fi info "go build -ldflags='-s -w' -o $BINARY_NAME ./$BACKEND_DIR" go build -ldflags="-s -w" -o "$BINARY_NAME" "./$BACKEND_DIR" success "二进制编译完成: $BINARY_NAME" } # ============================================================ # 全量构建(前端 + 后端) # ============================================================ build_all() { build_frontend build_binary } # ============================================================ # 运行单二进制 # ============================================================ start_binary() { info "========== 启动 $PROJECT_NAME ==========" cd "$SCRIPT_DIR" mkdir -p logs data rotate_log "logs/vpn.log" # 每次启动前都将最新前端与后端合并为单一二进制 build_all kill_port $BACKEND_PORT export VPN_LISTEN_ADDR=":${BACKEND_PORT}" nohup ./"$BINARY_NAME" >> logs/vpn.log 2>&1 & echo $! > "$PID_FILE" # 等待服务启动 local retries=0 while ! curl -s "http://localhost:$BACKEND_PORT/api/health" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then error "服务启动超时,请检查日志: logs/vpn.log" return 1 fi sleep 1 done success "服务已启动 (PID: $(cat "$PID_FILE"), 端口: $BACKEND_PORT)" info "访问地址: http://localhost:$BACKEND_PORT" info "默认账号: admin / admin123" return 0 } # ============================================================ # 开发模式(前后端分离) # ============================================================ start_dev() { info "========== $PROJECT_NAME 开发模式 ==========" cd "$SCRIPT_DIR" if ! command -v go &> /dev/null; then error "Go 未安装,请先安装 Go" exit 1 fi if ! command -v npm &> /dev/null; then error "npm 未安装,请先安装 Node.js" exit 1 fi mkdir -p logs data # 后端:go run(前台 / 后台均可,这里后台以便同时跑前端) kill_port $BACKEND_PORT export VPN_LISTEN_ADDR=":${BACKEND_PORT}" export GIN_MODE=debug nohup go run "./$BACKEND_DIR" >> logs/vpn-dev.log 2>&1 & echo $! > "$PID_FILE.dev" info "后端已启动 (go run, 端口: $BACKEND_PORT)" # 前端:vite dev server cd "$FRONTEND_DIR" if [ ! -d "node_modules" ]; then warning "node_modules 不存在,正在安装依赖..." npm install fi kill_port $FRONTEND_PORT nohup npm run dev >> ../logs/frontend-dev.log 2>&1 & echo $! > "../.frontend.pid" cd "$SCRIPT_DIR" # 等待前端就绪 local retries=0 while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 40 ]; then error "前端启动超时,请检查日志: logs/frontend-dev.log" return 1 fi sleep 1 done success "开发模式已启动" info "前端地址: http://localhost:$FRONTEND_PORT(代理 /api 到 $BACKEND_PORT)" info "后端地址: http://localhost:$BACKEND_PORT" } # ============================================================ # 停止服务 # ============================================================ stop_all() { info "正在停止服务..." # 单二进制 if [ -f "$PID_FILE" ]; then local pid=$(cat "$PID_FILE" 2>/dev/null || echo "") if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true success "单二进制服务已停止" fi rm -f "$PID_FILE" fi kill_port $BACKEND_PORT # 开发模式后端 if [ -f "$PID_FILE.dev" ]; then local devpid=$(cat "$PID_FILE.dev" 2>/dev/null || echo "") if [ -n "$devpid" ] && kill -0 "$devpid" 2>/dev/null; then kill "$devpid" 2>/dev/null || true success "后端 dev 进程已停止" fi rm -f "$PID_FILE.dev" fi # 开发模式前端 if [ -f "$SCRIPT_DIR/.frontend.pid" ]; then local fpid=$(cat "$SCRIPT_DIR/.frontend.pid" 2>/dev/null || echo "") if [ -n "$fpid" ] && kill -0 "$fpid" 2>/dev/null; then kill "$fpid" 2>/dev/null || true success "前端 dev 进程已停止" fi rm -f "$SCRIPT_DIR/.frontend.pid" fi kill_port $FRONTEND_PORT info "所有服务已停止" } # ============================================================ # 状态查看 # ============================================================ show_status() { info "服务状态:" if [ -n "$(check_port $BACKEND_PORT)" ]; then local pid=$(check_port $BACKEND_PORT) success "后端服务: 运行中 (PID: $pid, 端口: $BACKEND_PORT)" else warning "后端服务: 未运行" fi if [ -n "$(check_port $FRONTEND_PORT)" ]; then local fpid=$(lsof -ti:$FRONTEND_PORT 2>/dev/null | grep -v Chrome | head -1) if [ -n "$fpid" ]; then success "前端 dev : 运行中 (PID: $fpid, 端口: $FRONTEND_PORT)" fi else info "前端 dev : 未运行(生产模式由后端内嵌提供)" fi } # ============================================================ # 帮助 # ============================================================ show_help() { echo "$PROJECT_NAME 启动脚本" echo "" echo "用法: ./run.sh [命令]" echo "" echo "命令:" echo " run 构建并后台运行单二进制(默认)" echo " build 全量构建(前端 + 后端二进制)" echo " build-frontend 仅构建前端(输出到 backend/dist)" echo " build-binary 仅编译 Go 二进制" echo " dev 开发模式(前后端分离,vite 代理到后端)" echo " stop 停止所有服务" echo " restart 重启服务" echo " status 查看服务状态" echo " help 显示帮助信息" echo "" echo "环境变量:" echo " VPN_LISTEN_ADDR 监听地址(默认 :8080)" echo " VPN_DATA_DIR 数据目录(默认 ./data)" echo "" echo "示例:" echo " ./run.sh # 构建并运行" echo " ./run.sh build # 仅构建" echo " ./run.sh dev # 开发模式" echo " ./run.sh stop # 停止服务" } # ============================================================ # 主入口 # ============================================================ main() { local command=${1:-run} case "$command" in run) start_binary ;; build) build_all ;; build-frontend) build_frontend ;; build-binary) build_binary ;; dev) start_dev ;; stop) stop_all ;; restart) stop_all sleep 2 start_binary ;; status) show_status ;; help|--help|-h) show_help ;; *) error "未知命令: $command" show_help exit 1 ;; esac } main "$@"