#!/usr/bin/env bash set -euo pipefail # 一键启动 FastAPI 后端: # - 自动创建/复用虚拟环境(.venv) # - 自动安装 requirements.txt 依赖 # - 自动启动 uvicorn(默认开启 --reload) # # 用法示例: # ./run.sh # 默认一键启动:API + Celery Worker + Celery Beat # ./run.sh --env prod # 使用 .env.prod(若存在且可被 source) # ./run.sh --port 9000 # 改端口 # ./run.sh --no-reload # 关闭热更新 # ./run.sh --with-worker --with-beat # 同时启动 Celery Worker + Beat(用于定时推送) # ./run.sh --all # 等价于 --with-worker --with-beat # START_ALL=1 ./run.sh # 用环境变量一键启动(适合写到脚本/别名里) # ./run.sh --api-only # 只启动 API(不启动 Worker/Beat) # ./run.sh --install-only # 只安装依赖,不启动 usage() { cat <<'EOF' 用法: ./run.sh [--env dev|prod] [--host 0.0.0.0] [--port 8000] [--no-reload] [--api-only] [--with-worker] [--with-beat] [--all] [--skip-install] [--install-only] 参数: --env dev|prod 优先尝试加载 .env.dev 或 .env.prod(如果存在)。 --host uvicorn host(默认 0.0.0.0) --port uvicorn port(默认 8000) --no-reload 关闭 uvicorn --reload --api-only 只启动 API(不启动 Worker/Beat) --with-worker 同时启动 Celery Worker(处理异步/ETA 任务) --with-beat 同时启动 Celery Beat(定时调度,例如每日生成推送排程) --all 同时启动 Worker + Beat(等价于 --with-worker --with-beat) --skip-install 跳过依赖安装(默认会安装/更新 requirements.txt) --install-only 只安装依赖,不启动服务 -h, --help 显示帮助 说明: - 若你的 .env.* 不是 shell 可 source 的格式(例如包含空格/特殊字符未加引号),建议改成 KEY=value 形式。 - 仅启动 API 并不会生成 `push_send_log`;要测试“定时推送”,需要 Beat 调度 `tasks.push.generate_daily_schedule`,并由 Worker 执行后续 ETA 任务。 - 也可以用环境变量一键启动:START_ALL=1 ./run.sh - 启动后访问: /healthz 健康检查 /docs OpenAPI 文档 EOF } # 始终从脚本所在目录运行(避免在别处执行导致路径错) SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" ENV_NAME="dev" HOST="0.0.0.0" PORT="8000" RELOAD="1" SKIP_INSTALL="0" INSTALL_ONLY="0" API_ONLY="0" # 默认:一键启动(满足“bash run.sh 就全部启动”) WITH_WORKER="1" WITH_BEAT="1" while [[ $# -gt 0 ]]; do case "$1" in --env) ENV_NAME="${2:-}" shift 2 ;; --host) HOST="${2:-}" shift 2 ;; --port) PORT="${2:-}" shift 2 ;; --no-reload) RELOAD="0" shift 1 ;; --api-only) API_ONLY="1" WITH_WORKER="0" WITH_BEAT="0" shift 1 ;; --with-worker) WITH_WORKER="1" shift 1 ;; --with-beat) WITH_BEAT="1" shift 1 ;; --all) WITH_WORKER="1" WITH_BEAT="1" shift 1 ;; --skip-install) SKIP_INSTALL="1" shift 1 ;; --install-only) INSTALL_ONLY="1" shift 1 ;; -h|--help) usage exit 0 ;; *) echo "未知参数:$1" >&2 echo "" >&2 usage >&2 exit 2 ;; esac done if [[ "$ENV_NAME" != "dev" && "$ENV_NAME" != "prod" ]]; then echo "--env 仅支持 dev 或 prod,当前:$ENV_NAME" >&2 exit 2 fi ENV_FILE=".env.${ENV_NAME}" if [[ -f "$ENV_FILE" ]]; then # 让 source 进来的变量自动 export(供 pydantic-settings/应用读取) set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a fi # 允许通过环境变量一键开启(适合写到别名/CI 脚本里) if [[ "${START_ALL:-0}" == "1" ]]; then WITH_WORKER="1" WITH_BEAT="1" fi # 允许通过环境变量强制只启动 API if [[ "${START_API_ONLY:-0}" == "1" ]]; then API_ONLY="1" WITH_WORKER="0" WITH_BEAT="0" fi # 让 API/Celery 统一使用同一个 APP_ENV(影响 Redis key 前缀、定时任务配置等) export APP_ENV="${APP_ENV:-$ENV_NAME}" # Beat 只负责“投递任务到队列”,真正执行仍需要 Worker;这里自动补齐,避免误用。 if [[ "$WITH_BEAT" == "1" && "$WITH_WORKER" == "0" ]]; then echo "提示:已启用 --with-beat,自动同时启用 --with-worker(否则队列无人执行)。" WITH_WORKER="1" fi # 选择 python 命令(优先 python3) PY_BIN="" if command -v python3 >/dev/null 2>&1; then PY_BIN="python3" elif command -v python >/dev/null 2>&1; then PY_BIN="python" else echo "未找到 python/python3,请先安装 Python 3.11+。" >&2 exit 1 fi VENV_DIR=".venv" if [[ ! -d "$VENV_DIR" ]]; then echo "创建虚拟环境:$VENV_DIR" "$PY_BIN" -m venv "$VENV_DIR" fi # 激活虚拟环境 # shellcheck disable=SC1091 source "$VENV_DIR/bin/activate" if [[ "$SKIP_INSTALL" == "0" ]]; then if [[ -f "requirements.txt" ]]; then echo "升级 pip 并安装依赖(requirements.txt)" python -m pip install -U pip python -m pip install -r requirements.txt else echo "未找到 requirements.txt,跳过依赖安装。" >&2 fi fi if [[ "$INSTALL_ONLY" == "1" ]]; then echo "依赖安装完成(install-only),退出。" exit 0 fi UVICORN_ARGS=(app.main:app --host "$HOST" --port "$PORT") if [[ "$RELOAD" == "1" ]]; then UVICORN_ARGS+=(--reload) fi if [[ "$WITH_WORKER" == "0" && "$WITH_BEAT" == "0" ]]; then echo "启动服务:uvicorn ${UVICORN_ARGS[*]}" exec uvicorn "${UVICORN_ARGS[@]}" fi PIDS=() cleanup() { # 避免重复清理导致脚本退出码被覆盖 set +e if [[ ${#PIDS[@]} -gt 0 ]]; then echo "" echo "正在停止后台进程..." # 先尝试温和退出 for pid in "${PIDS[@]}"; do kill -TERM "$pid" >/dev/null 2>&1 || true done # 等待一点时间,再强制杀掉仍存活的(防止残留) sleep 1 for pid in "${PIDS[@]}"; do kill -KILL "$pid" >/dev/null 2>&1 || true done fi } trap cleanup EXIT INT TERM if [[ "$WITH_WORKER" == "1" ]]; then echo "启动 Celery Worker:celery -A app.worker:celery_app worker -l info" celery -A app.worker:celery_app worker -l info & PIDS+=("$!") fi if [[ "$WITH_BEAT" == "1" ]]; then echo "启动 Celery Beat:celery -A app.worker:celery_app beat -l info" celery -A app.worker:celery_app beat -l info & PIDS+=("$!") fi echo "启动服务:uvicorn ${UVICORN_ARGS[*]}" uvicorn "${UVICORN_ARGS[@]}"