#!/bin/bash # opsx-licence 启动脚本 # 一键启动前后端服务,支持日志轮转与单二进制部署 # 用法: ./run.sh [start|stop|restart|status|frontend|backend|build|binary|docker|help] set -e # ============================================================ # 配置区 # ============================================================ PROJECT_NAME="opsx-licence" BACKEND_PORT=9001 FRONTEND_PORT=5181 BACKEND_DIR="backend" FRONTEND_DIR="frontend" BACKEND_BIN="backend" STANDALONE_BIN="opsx-licence" STANDALONE_PORT="${PORT:-9001}" BACKEND_LAUNCH_LABEL="com.opsx.licence.backend" FRONTEND_LAUNCH_LABEL="com.opsx.licence.frontend" # 日志配置 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 文件 BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid" FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid" STANDALONE_PID_FILE="$SCRIPT_DIR/.opsx-licence.pid" STANDALONE_LOG_FILE="$SCRIPT_DIR/logs/opsx-licence.log" # 检查端口是否被占用 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 } # macOS 使用 launchctl 脱离当前终端,避免开发服务在脚本退出后被系统回收。 // Logan.Li start_detached() { local label="$1" local log_file="$2" local command="$3" if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then launchctl remove "$label" 2>/dev/null || true launchctl submit -l "$label" -o "$log_file" -e "$log_file" -- /bin/sh -c "$command" else nohup /bin/sh -c "$command" >> "$log_file" 2>&1 < /dev/null & echo $! fi } # macOS 下卸载 launchctl 任务,其他系统沿用 PID 与端口清理。 // Logan.Li stop_detached() { local label="$1" if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then launchctl remove "$label" 2>/dev/null || true fi } # ============================================================ # 后端服务 # ============================================================ start_backend() { info "正在启动后端服务..." cd "$SCRIPT_DIR/$BACKEND_DIR" mkdir -p logs rotate_log "logs/server.log" # 释放被占用的端口 kill_port $BACKEND_PORT export PORT=$BACKEND_PORT export GIN_MODE=debug # 编译并运行 info "编译 Go 程序..." go build -o "$BACKEND_BIN" . start_detached "$BACKEND_LAUNCH_LABEL" "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" \ "cd \"$SCRIPT_DIR/$BACKEND_DIR\" && exec env PORT=\"$BACKEND_PORT\" GIN_MODE=\"debug\" ./\"$BACKEND_BIN\"" # 等待服务启动 local retries=0 while ! curl -s "http://localhost:$BACKEND_PORT/health" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then error "后端服务启动超时,请检查日志: $BACKEND_DIR/logs/server.log" return 1 fi sleep 1 done echo "$(check_port $BACKEND_PORT)" > "$BACKEND_PID_FILE" success "后端服务已启动 (PID: $(cat "$BACKEND_PID_FILE"), 端口: $BACKEND_PORT)" return 0 } stop_backend() { stop_detached "$BACKEND_LAUNCH_LABEL" if [ -f "$BACKEND_PID_FILE" ]; then local pid=$(cat "$BACKEND_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 "$BACKEND_PID_FILE" fi kill_port $BACKEND_PORT } # ============================================================ # 前端服务 # ============================================================ start_frontend() { info "正在启动前端服务..." cd "$SCRIPT_DIR/$FRONTEND_DIR" mkdir -p logs rotate_log "logs/dev.log" kill_port $FRONTEND_PORT if [ ! -d "node_modules" ]; then warning "node_modules 不存在,正在安装依赖..." npm install fi local npm_bin npm_bin=$(command -v npm) start_detached "$FRONTEND_LAUNCH_LABEL" "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" \ "cd \"$SCRIPT_DIR/$FRONTEND_DIR\" && exec env PATH=\"$PATH\" \"$npm_bin\" run dev" # 等待服务启动 local retries=0 while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then error "前端服务启动超时,请检查日志: $FRONTEND_DIR/logs/dev.log" return 1 fi sleep 1 done echo "$(check_port $FRONTEND_PORT)" > "$FRONTEND_PID_FILE" success "前端服务已启动 (PID: $(cat "$FRONTEND_PID_FILE"), 端口: $FRONTEND_PORT)" info "前端地址: http://localhost:$FRONTEND_PORT" return 0 } stop_frontend() { stop_detached "$FRONTEND_LAUNCH_LABEL" if [ -f "$FRONTEND_PID_FILE" ]; then local pid=$(cat "$FRONTEND_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 "$FRONTEND_PID_FILE" fi kill_port $FRONTEND_PORT } # ============================================================ # 单二进制部署模式 # ============================================================ build_binary() { info "构建前端生产资源..." cd "$SCRIPT_DIR/$FRONTEND_DIR" npm run build info "嵌入前端资源并编译 $STANDALONE_BIN..." rm -rf "$SCRIPT_DIR/$BACKEND_DIR/static" mkdir -p "$SCRIPT_DIR/$BACKEND_DIR/static" cp -R dist/. "$SCRIPT_DIR/$BACKEND_DIR/static/" cd "$SCRIPT_DIR/$BACKEND_DIR" CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o "$SCRIPT_DIR/$STANDALONE_BIN" . success "单二进制已生成: $SCRIPT_DIR/$STANDALONE_BIN" } start_binary() { if [ ! -x "$SCRIPT_DIR/$STANDALONE_BIN" ]; then error "未找到 $STANDALONE_BIN,请先执行 ./run.sh build" return 1 fi mkdir -p "$SCRIPT_DIR/logs" rotate_log "$STANDALONE_LOG_FILE" kill_port "$STANDALONE_PORT" info "启动单二进制服务..." PORT="$STANDALONE_PORT" GIN_MODE="${GIN_MODE:-debug}" nohup "$SCRIPT_DIR/$STANDALONE_BIN" >> "$STANDALONE_LOG_FILE" 2>&1 & echo $! > "$STANDALONE_PID_FILE" local retries=0 while ! curl -s "http://localhost:$STANDALONE_PORT/health" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then error "单二进制启动超时,请检查日志: $STANDALONE_LOG_FILE" return 1 fi sleep 1 done success "单二进制服务已启动: http://localhost:$STANDALONE_PORT (PID: $(cat "$STANDALONE_PID_FILE"))" } stop_binary() { if [ -f "$STANDALONE_PID_FILE" ]; then local pid=$(cat "$STANDALONE_PID_FILE" 2>/dev/null || echo "") if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true fi rm -f "$STANDALONE_PID_FILE" fi kill_port "$STANDALONE_PORT" } binary_status() { if [ -n "$(check_port $STANDALONE_PORT)" ]; then success "单二进制服务: 运行中 (端口 $STANDALONE_PORT)" else warning "单二进制服务: 未运行" fi } binary_command() { case "${1:-start}" in start) start_binary ;; stop) stop_binary ;; restart) stop_binary; sleep 1; start_binary ;; status) binary_status ;; upgrade) stop_binary; build_binary; start_binary ;; *) error "未知 binary 命令: $1" echo "用法: ./run.sh binary [start|stop|restart|status|upgrade]" return 1 ;; esac } # ============================================================ # Docker 部署(生产推荐) # ============================================================ compose_cmd() { if docker compose version >/dev/null 2>&1; then echo "docker compose" elif command -v docker-compose >/dev/null 2>&1; then echo "docker-compose" else error "未找到 docker compose / docker-compose" return 1 fi } docker_command() { local sub=${1:-up} local dc dc=$(compose_cmd) || return 1 if [ ! -f "$SCRIPT_DIR/.env" ]; then warning "缺少 .env,正在从 .env.example 复制" cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env" warning "请编辑 .env 修改 JWT_SECRET / Waffo 配置后再部署" fi case "$sub" in up|start) info "Docker 构建并启动..." $dc -f "$SCRIPT_DIR/docker-compose.yml" up -d --build success "容器已启动: http://localhost:${HOST_PORT:-9001}" ;; down|stop) info "停止 Docker 容器..." $dc -f "$SCRIPT_DIR/docker-compose.yml" down ;; restart) $dc -f "$SCRIPT_DIR/docker-compose.yml" restart ;; rebuild|upgrade) info "重新构建并滚动升级..." $dc -f "$SCRIPT_DIR/docker-compose.yml" up -d --build --force-recreate ;; logs) $dc -f "$SCRIPT_DIR/docker-compose.yml" logs -f --tail=200 ;; status|ps) $dc -f "$SCRIPT_DIR/docker-compose.yml" ps ;; *) error "未知 docker 命令: $sub" echo "用法: ./run.sh docker [up|down|restart|rebuild|logs|status]" return 1 ;; esac } # ============================================================ # 命令 # ============================================================ stop() { info "正在停止服务..." stop_backend stop_frontend info "所有服务已停止" } show_status() { info "服务状态:" if [ -n "$(check_port $BACKEND_PORT)" ]; then success "后端服务: 运行中 (PID: $(check_port $BACKEND_PORT), 端口: $BACKEND_PORT)" else warning "后端服务: 未运行" fi if [ -n "$(check_port $FRONTEND_PORT)" ]; then success "前端服务: 运行中 (端口: $FRONTEND_PORT)" else warning "前端服务: 未运行" fi } show_help() { echo "$PROJECT_NAME 启动脚本" echo "" echo "用法: ./run.sh [命令]" echo "" echo "命令:" echo " start 启动前后端服务 (默认)" echo " stop 停止前后端服务" echo " restart 重启前后端服务" echo " backend 仅启动后端服务" echo " frontend 仅启动前端服务" echo " build 构建嵌入前端的单二进制 $STANDALONE_BIN" echo " binary 管理单二进制服务(start|stop|restart|status|upgrade)" echo " docker Docker 部署(up|down|restart|rebuild|logs|status)" echo " status 查看服务状态" echo " help 显示帮助信息" echo "" echo "示例:" echo " ./run.sh # 启动所有服务" echo " ./run.sh stop # 停止服务" echo " ./run.sh restart # 重启服务" echo " ./run.sh backend # 仅启动后端" echo " ./run.sh build # 生成 ./opsx-licence" echo " ./run.sh binary start # 启动单二进制服务" echo " ./run.sh docker up # Docker 构建并启动(生产推荐)" } # ============================================================ # 主入口 # ============================================================ main() { local command=${1:-start} case "$command" in start) info "========== $PROJECT_NAME 启动脚本 ==========" # 检查依赖 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 if start_backend && start_frontend; then echo "" success "========== 所有服务已启动 ==========" info "前端地址: http://localhost:$FRONTEND_PORT" info "后端地址: http://localhost:$BACKEND_PORT" info "默认账号: admin / admin" echo "" info "使用 './run.sh stop' 停止服务" else error "服务启动失败" exit 1 fi ;; stop) stop ;; restart) stop sleep 2 main start ;; backend) info "========== $PROJECT_NAME (仅后端) ==========" start_backend show_status ;; frontend) info "========== $PROJECT_NAME (仅前端) ==========" start_frontend show_status ;; build) build_binary ;; binary) binary_command "${2:-start}" ;; docker) docker_command "${2:-up}" ;; status) show_status ;; help|--help|-h) show_help ;; *) error "未知命令: $command" show_help exit 1 ;; esac } main "$@"