240 lines
6.5 KiB
Bash
Executable File
240 lines
6.5 KiB
Bash
Executable File
#!/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 <host> uvicorn host(默认 0.0.0.0)
|
||
--port <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[@]}"
|
||
|