start.sh 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. #!/bin/bash
  2. # ============================================
  3. # 安建医院纪检反馈系统 Backend 启动脚本
  4. # 用法: ./start.sh [选项]
  5. # ============================================
  6. set -euo pipefail
  7. RED='\033[0;31m'
  8. GREEN='\033[0;32m'
  9. YELLOW='\033[1;33m'
  10. BLUE='\033[0;34m'
  11. NC='\033[0m'
  12. HOST="${HOST:-0.0.0.0}"
  13. PORT="${PORT:-8000}"
  14. WORKERS="${WORKERS:-1}"
  15. RELOAD=false
  16. LOG_LEVEL="${LOG_LEVEL:-info}"
  17. FOREGROUND=false
  18. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  19. BACKEND_DIR="$SCRIPT_DIR"
  20. VENV_DIR="$BACKEND_DIR/venv"
  21. PYTHON="$VENV_DIR/bin/python"
  22. PIP="$VENV_DIR/bin/pip"
  23. UVICORN="$VENV_DIR/bin/uvicorn"
  24. LOG_DIR="$BACKEND_DIR/logs"
  25. LOG_FILE="$LOG_DIR/app.log"
  26. PID_FILE="$BACKEND_DIR/app.pid"
  27. log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
  28. log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
  29. log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
  30. log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
  31. show_help() {
  32. cat << EOF
  33. 安建医院纪检反馈系统 Backend 启动脚本
  34. 用法: $0 [选项]
  35. 选项:
  36. -h, --host <ip> 监听地址 (默认: 0.0.0.0)
  37. -p, --port <port> 监听端口 (默认: 8000)
  38. -w, --workers <n> 工作进程数 (默认: 1;与 --reload 互斥)
  39. -r, --reload 开发模式,代码变更自动重载(前台运行)
  40. -f, --foreground 前台运行(不加则后台 + PID/日志)
  41. -l, --log-level <lv> 日志级别: debug/info/warning/error (默认: info)
  42. --install 创建 venv 并安装 requirements.txt
  43. --init 检查 Python / venv / .env
  44. --stop 停止后台服务
  45. --restart 重启后台服务
  46. --status 查看服务状态
  47. --logs [-f] 查看日志(-f 跟踪)
  48. --help 显示帮助
  49. 示例:
  50. $0 --reload # 本地开发(前台 + 热重载)
  51. $0 # 后台启动
  52. $0 --port 8001 --workers 2 # 指定端口与进程数
  53. $0 --stop
  54. $0 --logs -f
  55. EOF
  56. }
  57. # 代码使用 PEP604 (X | Y) 等语法,创建 venv 时优先 3.10+
  58. resolve_python() {
  59. local cand ver major minor
  60. for cand in \
  61. "${PYTHON_BIN:-}" \
  62. python3.12 python3.11 python3.10 \
  63. "${HOME}/.pyenv/versions/3.12.13/bin/python3" \
  64. "${HOME}/.pyenv/versions/3.12.13/bin/python" \
  65. python3
  66. do
  67. [ -n "$cand" ] || continue
  68. if ! command -v "$cand" >/dev/null 2>&1 && [ ! -x "$cand" ]; then
  69. continue
  70. fi
  71. ver="$("$cand" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null)" || continue
  72. major="${ver%%.*}"
  73. minor="${ver#*.}"
  74. if [ "$major" -gt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -ge 10 ]; }; then
  75. echo "$cand"
  76. return 0
  77. fi
  78. done
  79. return 1
  80. }
  81. check_python() {
  82. local py
  83. if ! py="$(resolve_python)"; then
  84. log_error "未找到 Python 3.10+(当前系统 python3 可能过旧)。可用: pyenv install 3.12 && pyenv local 3.12"
  85. exit 1
  86. fi
  87. HOST_PYTHON="$py"
  88. local ver
  89. ver="$("$HOST_PYTHON" -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')"
  90. log_info "Python 版本: $ver ($HOST_PYTHON)"
  91. }
  92. create_venv() {
  93. check_python
  94. log_info "创建虚拟环境: $VENV_DIR"
  95. "$HOST_PYTHON" -m venv "$VENV_DIR"
  96. log_success "虚拟环境已创建"
  97. }
  98. install_dependencies() {
  99. check_python
  100. if [ ! -d "$VENV_DIR" ]; then
  101. create_venv
  102. fi
  103. # shellcheck disable=SC1091
  104. source "$VENV_DIR/bin/activate"
  105. log_info "升级 pip..."
  106. "$PIP" install --upgrade pip
  107. log_info "安装依赖..."
  108. "$PIP" install -r "$BACKEND_DIR/requirements.txt"
  109. log_success "依赖安装完成"
  110. }
  111. ensure_env_file() {
  112. if [ -f "$BACKEND_DIR/.env" ]; then
  113. return 0
  114. fi
  115. if [ -f "$BACKEND_DIR/.env.example" ]; then
  116. cp "$BACKEND_DIR/.env.example" "$BACKEND_DIR/.env"
  117. log_warn "已从 .env.example 生成 .env,请按环境修改数据库与 MinIO 配置"
  118. else
  119. log_error "缺少 .env 与 .env.example"
  120. exit 1
  121. fi
  122. }
  123. venv_usable() {
  124. # 路径迁移/换机后 shebang 可能仍指向旧解释器
  125. [ -x "$PYTHON" ] && "$PYTHON" -c "import sys" >/dev/null 2>&1
  126. }
  127. check_env() {
  128. if [ ! -d "$VENV_DIR" ]; then
  129. log_warn "虚拟环境不存在,正在创建并安装依赖..."
  130. install_dependencies
  131. elif ! venv_usable; then
  132. log_warn "虚拟环境不可用(解释器路径失效),正在重建..."
  133. rm -rf "$VENV_DIR"
  134. install_dependencies
  135. elif ! "$PYTHON" -c "import fastapi" >/dev/null 2>&1; then
  136. log_warn "依赖不完整,正在安装..."
  137. install_dependencies
  138. fi
  139. if [ ! -x "$UVICORN" ]; then
  140. log_error "未找到 uvicorn: $UVICORN"
  141. exit 1
  142. fi
  143. ensure_env_file
  144. }
  145. setup_logs() {
  146. mkdir -p "$LOG_DIR"
  147. }
  148. is_running() {
  149. if [ ! -f "$PID_FILE" ]; then
  150. return 1
  151. fi
  152. local pid
  153. pid="$(cat "$PID_FILE")"
  154. if ps -p "$pid" >/dev/null 2>&1; then
  155. return 0
  156. fi
  157. rm -f "$PID_FILE"
  158. return 1
  159. }
  160. start_server() {
  161. check_python
  162. check_env
  163. cd "$BACKEND_DIR"
  164. # shellcheck disable=SC1091
  165. source "$VENV_DIR/bin/activate"
  166. if [ "$RELOAD" = true ]; then
  167. FOREGROUND=true
  168. fi
  169. if [ "$FOREGROUND" != true ] && is_running; then
  170. log_error "服务已在运行 (PID: $(cat "$PID_FILE")),请先 ./start.sh --stop"
  171. exit 1
  172. fi
  173. local cmd=("$UVICORN" "app.main:app" "--host" "$HOST" "--port" "$PORT" "--log-level" "$LOG_LEVEL")
  174. if [ "$RELOAD" = true ]; then
  175. cmd+=(--reload)
  176. log_info "启动模式: 开发(热重载,前台)"
  177. else
  178. cmd+=(--workers "$WORKERS")
  179. log_info "启动模式: 后台(workers=${WORKERS})"
  180. fi
  181. log_info "=========================================="
  182. log_info " ajyy Backend 启动中"
  183. log_info " 地址: http://${HOST}:${PORT}"
  184. log_info " 文档: http://127.0.0.1:${PORT}/docs"
  185. log_info " 健康: http://127.0.0.1:${PORT}/api/health"
  186. log_info "=========================================="
  187. if [ "$FOREGROUND" = true ]; then
  188. exec "${cmd[@]}"
  189. fi
  190. setup_logs
  191. {
  192. echo "========================================"
  193. echo "启动时间: $(date '+%Y-%m-%d %H:%M:%S')"
  194. echo "命令: ${cmd[*]}"
  195. echo "========================================"
  196. } >>"$LOG_FILE"
  197. nohup "${cmd[@]}" >>"$LOG_FILE" 2>&1 &
  198. echo $! >"$PID_FILE"
  199. sleep 2
  200. if is_running; then
  201. log_success "服务启动成功 (PID: $(cat "$PID_FILE"))"
  202. log_info "日志: $LOG_FILE"
  203. else
  204. log_error "启动失败,请查看日志: $LOG_FILE"
  205. rm -f "$PID_FILE"
  206. exit 1
  207. fi
  208. }
  209. stop_server() {
  210. if [ ! -f "$PID_FILE" ]; then
  211. log_warn "PID 文件不存在,服务可能未运行"
  212. exit 0
  213. fi
  214. local pid
  215. pid="$(cat "$PID_FILE")"
  216. if ! ps -p "$pid" >/dev/null 2>&1; then
  217. log_warn "进程不存在 (PID: $pid),清理 PID 文件"
  218. rm -f "$PID_FILE"
  219. exit 0
  220. fi
  221. log_info "停止服务 (PID: $pid)..."
  222. kill -TERM "$pid" 2>/dev/null || true
  223. for _ in $(seq 1 10); do
  224. if ! ps -p "$pid" >/dev/null 2>&1; then
  225. break
  226. fi
  227. sleep 1
  228. done
  229. if ps -p "$pid" >/dev/null 2>&1; then
  230. log_warn "优雅退出超时,强制结束"
  231. kill -9 "$pid" 2>/dev/null || true
  232. fi
  233. rm -f "$PID_FILE"
  234. log_success "服务已停止"
  235. }
  236. show_status() {
  237. if is_running; then
  238. echo -e "${GREEN}[RUNNING]${NC} PID=$(cat "$PID_FILE") port=$PORT"
  239. if command -v lsof >/dev/null 2>&1; then
  240. lsof -i ":$PORT" 2>/dev/null | grep LISTEN || true
  241. fi
  242. else
  243. echo -e "${YELLOW}[STOPPED]${NC} 服务未运行"
  244. fi
  245. }
  246. show_logs() {
  247. if [ ! -f "$LOG_FILE" ]; then
  248. log_error "日志不存在: $LOG_FILE"
  249. exit 1
  250. fi
  251. if [ "${1:-}" = "-f" ] || [ "${1:-}" = "--follow" ]; then
  252. log_info "跟踪日志 (Ctrl+C 退出)..."
  253. tail -f "$LOG_FILE"
  254. else
  255. echo -e "${BLUE}=== 最近 100 行 ===${NC}"
  256. tail -100 "$LOG_FILE"
  257. fi
  258. }
  259. restart_server() {
  260. if is_running || [ -f "$PID_FILE" ]; then
  261. stop_server || true
  262. sleep 1
  263. fi
  264. start_server
  265. }
  266. ACTION="start"
  267. LOG_FOLLOW=""
  268. while [[ $# -gt 0 ]]; do
  269. case "$1" in
  270. -h|--host) HOST="$2"; shift 2 ;;
  271. -p|--port) PORT="$2"; shift 2 ;;
  272. -w|--workers) WORKERS="$2"; shift 2 ;;
  273. -r|--reload) RELOAD=true; shift ;;
  274. -f|--foreground) FOREGROUND=true; shift ;;
  275. -l|--log-level) LOG_LEVEL="$2"; shift 2 ;;
  276. --install) ACTION="install"; shift ;;
  277. --init) ACTION="init"; shift ;;
  278. --stop) ACTION="stop"; shift ;;
  279. --restart) ACTION="restart"; shift ;;
  280. --status) ACTION="status"; shift ;;
  281. --logs)
  282. ACTION="logs"
  283. shift
  284. if [[ "${1:-}" == "-f" || "${1:-}" == "--follow" ]]; then
  285. LOG_FOLLOW="-f"
  286. shift
  287. fi
  288. ;;
  289. --help) show_help; exit 0 ;;
  290. *)
  291. log_error "未知参数: $1"
  292. show_help
  293. exit 1
  294. ;;
  295. esac
  296. done
  297. case "$ACTION" in
  298. install) install_dependencies ;;
  299. init)
  300. check_python
  301. check_env
  302. log_success "环境检查完成"
  303. ;;
  304. stop) stop_server ;;
  305. restart) restart_server ;;
  306. status) show_status ;;
  307. logs) show_logs "$LOG_FOLLOW" ;;
  308. start) start_server ;;
  309. *)
  310. log_error "未知动作: $ACTION"
  311. exit 1
  312. ;;
  313. esac