| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- #!/bin/bash
- # ============================================
- # 安建医院纪检反馈系统 Backend 启动脚本
- # 用法: ./start.sh [选项]
- # ============================================
- set -euo pipefail
- RED='\033[0;31m'
- GREEN='\033[0;32m'
- YELLOW='\033[1;33m'
- BLUE='\033[0;34m'
- NC='\033[0m'
- HOST="${HOST:-0.0.0.0}"
- PORT="${PORT:-8000}"
- WORKERS="${WORKERS:-1}"
- RELOAD=false
- LOG_LEVEL="${LOG_LEVEL:-info}"
- FOREGROUND=false
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- BACKEND_DIR="$SCRIPT_DIR"
- VENV_DIR="$BACKEND_DIR/venv"
- PYTHON="$VENV_DIR/bin/python"
- PIP="$VENV_DIR/bin/pip"
- UVICORN="$VENV_DIR/bin/uvicorn"
- LOG_DIR="$BACKEND_DIR/logs"
- LOG_FILE="$LOG_DIR/app.log"
- PID_FILE="$BACKEND_DIR/app.pid"
- log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
- log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
- log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
- log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
- show_help() {
- cat << EOF
- 安建医院纪检反馈系统 Backend 启动脚本
- 用法: $0 [选项]
- 选项:
- -h, --host <ip> 监听地址 (默认: 0.0.0.0)
- -p, --port <port> 监听端口 (默认: 8000)
- -w, --workers <n> 工作进程数 (默认: 1;与 --reload 互斥)
- -r, --reload 开发模式,代码变更自动重载(前台运行)
- -f, --foreground 前台运行(不加则后台 + PID/日志)
- -l, --log-level <lv> 日志级别: debug/info/warning/error (默认: info)
- --install 创建 venv 并安装 requirements.txt
- --init 检查 Python / venv / .env
- --stop 停止后台服务
- --restart 重启后台服务
- --status 查看服务状态
- --logs [-f] 查看日志(-f 跟踪)
- --help 显示帮助
- 示例:
- $0 --reload # 本地开发(前台 + 热重载)
- $0 # 后台启动
- $0 --port 8001 --workers 2 # 指定端口与进程数
- $0 --stop
- $0 --logs -f
- EOF
- }
- # 代码使用 PEP604 (X | Y) 等语法,创建 venv 时优先 3.10+
- resolve_python() {
- local cand ver major minor
- for cand in \
- "${PYTHON_BIN:-}" \
- python3.12 python3.11 python3.10 \
- "${HOME}/.pyenv/versions/3.12.13/bin/python3" \
- "${HOME}/.pyenv/versions/3.12.13/bin/python" \
- python3
- do
- [ -n "$cand" ] || continue
- if ! command -v "$cand" >/dev/null 2>&1 && [ ! -x "$cand" ]; then
- continue
- fi
- ver="$("$cand" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null)" || continue
- major="${ver%%.*}"
- minor="${ver#*.}"
- if [ "$major" -gt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -ge 10 ]; }; then
- echo "$cand"
- return 0
- fi
- done
- return 1
- }
- check_python() {
- local py
- if ! py="$(resolve_python)"; then
- log_error "未找到 Python 3.10+(当前系统 python3 可能过旧)。可用: pyenv install 3.12 && pyenv local 3.12"
- exit 1
- fi
- HOST_PYTHON="$py"
- local ver
- ver="$("$HOST_PYTHON" -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')"
- log_info "Python 版本: $ver ($HOST_PYTHON)"
- }
- create_venv() {
- check_python
- log_info "创建虚拟环境: $VENV_DIR"
- "$HOST_PYTHON" -m venv "$VENV_DIR"
- log_success "虚拟环境已创建"
- }
- install_dependencies() {
- check_python
- if [ ! -d "$VENV_DIR" ]; then
- create_venv
- fi
- # shellcheck disable=SC1091
- source "$VENV_DIR/bin/activate"
- log_info "升级 pip..."
- "$PIP" install --upgrade pip
- log_info "安装依赖..."
- "$PIP" install -r "$BACKEND_DIR/requirements.txt"
- log_success "依赖安装完成"
- }
- ensure_env_file() {
- if [ -f "$BACKEND_DIR/.env" ]; then
- return 0
- fi
- if [ -f "$BACKEND_DIR/.env.example" ]; then
- cp "$BACKEND_DIR/.env.example" "$BACKEND_DIR/.env"
- log_warn "已从 .env.example 生成 .env,请按环境修改数据库与 MinIO 配置"
- else
- log_error "缺少 .env 与 .env.example"
- exit 1
- fi
- }
- venv_usable() {
- # 路径迁移/换机后 shebang 可能仍指向旧解释器
- [ -x "$PYTHON" ] && "$PYTHON" -c "import sys" >/dev/null 2>&1
- }
- check_env() {
- if [ ! -d "$VENV_DIR" ]; then
- log_warn "虚拟环境不存在,正在创建并安装依赖..."
- install_dependencies
- elif ! venv_usable; then
- log_warn "虚拟环境不可用(解释器路径失效),正在重建..."
- rm -rf "$VENV_DIR"
- install_dependencies
- elif ! "$PYTHON" -c "import fastapi" >/dev/null 2>&1; then
- log_warn "依赖不完整,正在安装..."
- install_dependencies
- fi
- if [ ! -x "$UVICORN" ]; then
- log_error "未找到 uvicorn: $UVICORN"
- exit 1
- fi
- ensure_env_file
- }
- setup_logs() {
- mkdir -p "$LOG_DIR"
- }
- is_running() {
- if [ ! -f "$PID_FILE" ]; then
- return 1
- fi
- local pid
- pid="$(cat "$PID_FILE")"
- if ps -p "$pid" >/dev/null 2>&1; then
- return 0
- fi
- rm -f "$PID_FILE"
- return 1
- }
- start_server() {
- check_python
- check_env
- cd "$BACKEND_DIR"
- # shellcheck disable=SC1091
- source "$VENV_DIR/bin/activate"
- if [ "$RELOAD" = true ]; then
- FOREGROUND=true
- fi
- if [ "$FOREGROUND" != true ] && is_running; then
- log_error "服务已在运行 (PID: $(cat "$PID_FILE")),请先 ./start.sh --stop"
- exit 1
- fi
- local cmd=("$UVICORN" "app.main:app" "--host" "$HOST" "--port" "$PORT" "--log-level" "$LOG_LEVEL")
- if [ "$RELOAD" = true ]; then
- cmd+=(--reload)
- log_info "启动模式: 开发(热重载,前台)"
- else
- cmd+=(--workers "$WORKERS")
- log_info "启动模式: 后台(workers=${WORKERS})"
- fi
- log_info "=========================================="
- log_info " ajyy Backend 启动中"
- log_info " 地址: http://${HOST}:${PORT}"
- log_info " 文档: http://127.0.0.1:${PORT}/docs"
- log_info " 健康: http://127.0.0.1:${PORT}/api/health"
- log_info "=========================================="
- if [ "$FOREGROUND" = true ]; then
- exec "${cmd[@]}"
- fi
- setup_logs
- {
- echo "========================================"
- echo "启动时间: $(date '+%Y-%m-%d %H:%M:%S')"
- echo "命令: ${cmd[*]}"
- echo "========================================"
- } >>"$LOG_FILE"
- nohup "${cmd[@]}" >>"$LOG_FILE" 2>&1 &
- echo $! >"$PID_FILE"
- sleep 2
- if is_running; then
- log_success "服务启动成功 (PID: $(cat "$PID_FILE"))"
- log_info "日志: $LOG_FILE"
- else
- log_error "启动失败,请查看日志: $LOG_FILE"
- rm -f "$PID_FILE"
- exit 1
- fi
- }
- stop_server() {
- if [ ! -f "$PID_FILE" ]; then
- log_warn "PID 文件不存在,服务可能未运行"
- exit 0
- fi
- local pid
- pid="$(cat "$PID_FILE")"
- if ! ps -p "$pid" >/dev/null 2>&1; then
- log_warn "进程不存在 (PID: $pid),清理 PID 文件"
- rm -f "$PID_FILE"
- exit 0
- fi
- log_info "停止服务 (PID: $pid)..."
- kill -TERM "$pid" 2>/dev/null || true
- for _ in $(seq 1 10); do
- if ! ps -p "$pid" >/dev/null 2>&1; then
- break
- fi
- sleep 1
- done
- if ps -p "$pid" >/dev/null 2>&1; then
- log_warn "优雅退出超时,强制结束"
- kill -9 "$pid" 2>/dev/null || true
- fi
- rm -f "$PID_FILE"
- log_success "服务已停止"
- }
- show_status() {
- if is_running; then
- echo -e "${GREEN}[RUNNING]${NC} PID=$(cat "$PID_FILE") port=$PORT"
- if command -v lsof >/dev/null 2>&1; then
- lsof -i ":$PORT" 2>/dev/null | grep LISTEN || true
- fi
- else
- echo -e "${YELLOW}[STOPPED]${NC} 服务未运行"
- fi
- }
- show_logs() {
- if [ ! -f "$LOG_FILE" ]; then
- log_error "日志不存在: $LOG_FILE"
- exit 1
- fi
- if [ "${1:-}" = "-f" ] || [ "${1:-}" = "--follow" ]; then
- log_info "跟踪日志 (Ctrl+C 退出)..."
- tail -f "$LOG_FILE"
- else
- echo -e "${BLUE}=== 最近 100 行 ===${NC}"
- tail -100 "$LOG_FILE"
- fi
- }
- restart_server() {
- if is_running || [ -f "$PID_FILE" ]; then
- stop_server || true
- sleep 1
- fi
- start_server
- }
- ACTION="start"
- LOG_FOLLOW=""
- while [[ $# -gt 0 ]]; do
- case "$1" in
- -h|--host) HOST="$2"; shift 2 ;;
- -p|--port) PORT="$2"; shift 2 ;;
- -w|--workers) WORKERS="$2"; shift 2 ;;
- -r|--reload) RELOAD=true; shift ;;
- -f|--foreground) FOREGROUND=true; shift ;;
- -l|--log-level) LOG_LEVEL="$2"; shift 2 ;;
- --install) ACTION="install"; shift ;;
- --init) ACTION="init"; shift ;;
- --stop) ACTION="stop"; shift ;;
- --restart) ACTION="restart"; shift ;;
- --status) ACTION="status"; shift ;;
- --logs)
- ACTION="logs"
- shift
- if [[ "${1:-}" == "-f" || "${1:-}" == "--follow" ]]; then
- LOG_FOLLOW="-f"
- shift
- fi
- ;;
- --help) show_help; exit 0 ;;
- *)
- log_error "未知参数: $1"
- show_help
- exit 1
- ;;
- esac
- done
- case "$ACTION" in
- install) install_dependencies ;;
- init)
- check_python
- check_env
- log_success "环境检查完成"
- ;;
- stop) stop_server ;;
- restart) restart_server ;;
- status) show_status ;;
- logs) show_logs "$LOG_FOLLOW" ;;
- start) start_server ;;
- *)
- log_error "未知动作: $ACTION"
- exit 1
- ;;
- esac
|