From 53128a6f022b0c52c5c8fff5d64dae362da11000 Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Fri, 22 May 2026 00:51:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E6=B7=BB=E5=8A=A0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AE=A1=E7=90=86=E8=84=9A=E6=9C=AC=E5=B9=B6=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=BA=94=E7=94=A8=E5=9F=BA=E7=A1=80=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 .gitignore 忽略后端日志目录和 PID 文件 - 新增 app_name 配置项并替换主程序中硬编码的应用名称 - 为配置模型添加 extra: ignore 以兼容额外环境变量 - 新增完整的后端服务管理脚本 serv.sh,支持常用管理操作 --- .gitignore | 3 + backend/app/config.py | 4 +- backend/app/main.py | 6 +- backend/serv.sh | 393 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 402 insertions(+), 4 deletions(-) create mode 100755 backend/serv.sh diff --git a/.gitignore b/.gitignore index 3c2cb7a..b5b059c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ __pycache__/ dist/ build/ +backend/logs/ +backend/*.pid + # Uploads uploads/ diff --git a/backend/app/config.py b/backend/app/config.py index 8626fea..f75fc0e 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -2,6 +2,8 @@ from pydantic_settings import BaseSettings class Settings(BaseSettings): + app_name: str = "孕育无忧" + # Path Prefix api_prefix: str = "/api" uploads_prefix: str = "/uploads" @@ -37,7 +39,7 @@ class Settings(BaseSettings): port: int = 8000 debug: bool = True - model_config = {"env_file": ".env", "env_file_encoding": "utf-8"} + model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"} settings = Settings() diff --git a/backend/app/main.py b/backend/app/main.py index 221cb06..03ef9a0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -28,7 +28,7 @@ logging.basicConfig( format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) -logger = logging.getLogger("孕育无忧") +logger = logging.getLogger(settings.app_name) @asynccontextmanager @@ -42,7 +42,7 @@ async def lifespan(app: FastAPI): app = FastAPI( - title="孕育无忧 API", + title=f"{settings.app_name} API", description="孕期健康检查报告解读后端服务", version="1.0.0", lifespan=lifespan, @@ -111,7 +111,7 @@ app.include_router(share.router, prefix=f"{api_prefix}/share", tags=["分享"]) @app.get(f"{api_prefix}/health") async def health_check(): - return {"status": "ok", "service": "孕育无忧 API"} + return {"status": "ok", "service": f"{settings.app_name} API"} @app.get("/") diff --git a/backend/serv.sh b/backend/serv.sh new file mode 100755 index 0000000..d97102e --- /dev/null +++ b/backend/serv.sh @@ -0,0 +1,393 @@ +#!/bin/bash + +# Unicorn 服务管理脚本 +# 优化版本 - 支持完整的服务管理功能,配置完全从 .env 文件读取 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +is_true() { + local value="$1" + value=$(echo "$value" | tr '[:upper:]' '[:lower:]') + case "$value" in + true|1|yes|y|on) return 0 ;; + *) return 1 ;; + esac +} + +# 加载 .env 文件 +load_env() { + local env_file=".env" + + if [ -f "$env_file" ]; then + set -a + source "$env_file" + set +a + log_info "已加载 $env_file 文件中的环境变量" + else + log_warning "$env_file 文件不存在,使用默认值或环境变量" + fi +} + +# 从环境变量获取配置,提供默认值 +init_config() { + # 应用配置 + APP_NAME=${APP_NAME:-"app_name"} + APP_MODULE=${APP_MODULE:-"app.main:app"} + APP_DIR=${APP_DIR:-"$(pwd)"} + + # 服务器配置 + APP_HOST=${APP_HOST:-${HOST:-"0.0.0.0"}} + APP_PORT=${APP_PORT:-${PORT:-8000}} + APP_WORKERS=${APP_WORKERS:-1} + APP_LOG_LEVEL=${APP_LOG_LEVEL:-"info"} + + # PID 文件路径 + PID_FILE="${APP_DIR}/.${APP_NAME}.pid" + + # 日志文件路径 + LOG_DIR="${APP_DIR}/logs" + + # 获取当前日期用于日志文件分割 + CURRENT_DATE=$(date +%Y-%m-%d) + LOG_FILE="${LOG_DIR}/${APP_NAME}-${CURRENT_DATE}.log" + + log_info "应用名称: $APP_NAME" + log_info "应用模块: $APP_MODULE" + log_info "应用目录: $APP_DIR" + log_info "服务器地址: $APP_HOST" + log_info "服务器端口: $APP_PORT" + log_info "工作进程数: $APP_WORKERS" + log_info "日志级别: $APP_LOG_LEVEL" + log_info "日志文件: $LOG_FILE" +} + +# 检查是否在正确的目录 +check_directory() { + local target_dir="${APP_DIR}" + + if [ "$(pwd)" != "$target_dir" ]; then + log_warning "当前目录不是项目根目录,切换到 $target_dir" + cd "$target_dir" || { + log_error "无法切换到项目目录: $target_dir" + exit 1 + } + fi +} + +# 检查 uv 是否可用 +check_uv() { + if ! command -v uv &> /dev/null; then + log_error "uv 命令未找到,请确保 uv 已安装" + exit 1 + fi +} + +# 检查应用模块是否存在 +check_app_module() { + local module_path=$(echo "$APP_MODULE" | sed 's/:.*//' | sed 's/\./\//g').py + + if [ ! -f "$module_path" ]; then + log_error "应用模块不存在: $module_path" + exit 1 + fi +} + +# 创建日志目录 +setup_logs() { + if [ ! -d "$LOG_DIR" ]; then + mkdir -p "$LOG_DIR" + log_info "创建日志目录: $LOG_DIR" + fi +} + +# 启动 Unicorn 服务器 +start_server() { + local enable_reload=false + local reload_from_arg=false + + # 检查是否传递了 --reload 参数 + for arg in "$@"; do + if [ "$arg" = "--reload" ]; then + enable_reload=true + reload_from_arg=true + break + fi + done + + if [ "$reload_from_arg" = false ]; then + if is_true "${APP_DEBUG:-$DEBUG}"; then + enable_reload=true + fi + fi + + log_info "正在启动 $APP_NAME..." + + # 检查是否已经在运行 + if [ -f "$PID_FILE" ]; then + local pid=$(cat "$PID_FILE") + if kill -0 "$pid" 2>/dev/null; then + log_warning "$APP_NAME 已经在运行 (PID: $pid)" + return 0 + else + log_warning "发现旧的 PID 文件,清理中..." + rm -f "$PID_FILE" + fi + fi + + # 构建启动命令 + local uvicorn_cmd="uv run uvicorn \ + $APP_MODULE \ + --host $APP_HOST \ + --port $APP_PORT \ + --workers $APP_WORKERS \ + --log-level $APP_LOG_LEVEL \ + --access-log" + + # 根据参数添加 --reload + if [ "$enable_reload" = true ]; then + uvicorn_cmd="$uvicorn_cmd --reload" + log_info "启用热重载模式" + else + log_info "禁用热重载模式(生产环境推荐)" + fi + + # 启动服务器(使用 >> 追加日志,相同日期日志不会丢失) + nohup $uvicorn_cmd >> "$LOG_FILE" 2>&1 & + + local server_pid=$! + echo $server_pid > "$PID_FILE" + + sleep 2 + + if kill -0 "$server_pid" 2>/dev/null; then + log_success "$APP_NAME 已启动 (PID: $server_pid)" + log_info "日志文件: $LOG_FILE" + log_info "访问地址: http://$APP_HOST:$APP_PORT" + log_info "热重载: $([ "$enable_reload" = true ] && echo "已启用" || echo "已禁用")" + else + log_error "$APP_NAME 启动失败,请检查日志文件: $LOG_FILE" + rm -f "$PID_FILE" + exit 1 + fi +} + +# 停止 Unicorn 服务器 +stop_server() { + log_info "正在停止 $APP_NAME..." + + if [ ! -f "$PID_FILE" ]; then + log_warning "PID 文件不存在,$APP_NAME 可能未运行" + return 0 + fi + + local pid=$(cat "$PID_FILE") + + if kill -0 "$pid" 2>/dev/null; then + kill -TERM "$pid" + sleep 3 + + if kill -0 "$pid" 2>/dev/null; then + log_warning "正常停止失败,尝试强制停止..." + kill -KILL "$pid" + sleep 1 + fi + + if kill -0 "$pid" 2>/dev/null; then + log_error "无法停止进程 $pid" + return 1 + else + rm -f "$PID_FILE" + log_success "$APP_NAME 已停止" + fi + else + log_warning "进程 $pid 不存在,清理 PID 文件" + rm -f "$PID_FILE" + fi +} + +# 重启 Unicorn 服务器 +restart_server() { + log_info "正在重启 $APP_NAME..." + stop_server + sleep 2 + # 传递所有参数给 start_server(包括可能的 --reload) + start_server "$@" +} + +# 查看服务器状态 +status_server() { + if [ -f "$PID_FILE" ]; then + local pid=$(cat "$PID_FILE") + if kill -0 "$pid" 2>/dev/null; then + log_success "$APP_NAME 正在运行 (PID: $pid)" + log_info "端口: $APP_PORT" + log_info "工作进程: $APP_WORKERS" + return 0 + else + log_warning "PID 文件存在但进程未运行,清理中..." + rm -f "$PID_FILE" + fi + fi + + log_info "$APP_NAME 未运行" + return 1 +} + +# 查看服务器日志 +tail_logs() { + local target_log_file="$LOG_FILE" + + # 检查是否有日期参数 + if [ $# -gt 0 ]; then + local date_param="$1" + # 验证日期格式是否为 YYYY-MM-DD + if [[ "$date_param" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + target_log_file="${LOG_DIR}/${APP_NAME}-${date_param}.log" + log_info "查看指定日期日志: $date_param" + else + log_warning "日期参数格式错误,使用默认日期日志" + fi + fi + + if [ -f "$target_log_file" ]; then + log_info "显示日志文件最后 50 行: $target_log_file" + echo "==========================================" + tail -50 "$target_log_file" + echo "==========================================" + + # 显示文件基本信息 + local file_size=$(du -h "$target_log_file" | cut -f1) + local line_count=$(wc -l < "$target_log_file") + log_info "文件大小: $file_size, 行数: $line_count" + else + # 尝试查找最新的日志文件 + local latest_log=$(ls -t "${LOG_DIR}/${APP_NAME}-"*.log 2>/dev/null | head -1) + if [ -f "$latest_log" ]; then + log_warning "指定日志文件不存在,显示最新日志文件: $latest_log" + log_info "显示日志文件最后 50 行: $latest_log" + echo "==========================================" + tail -50 "$latest_log" + echo "==========================================" + else + log_warning "日志文件不存在: $target_log_file" + log_info "可用的日志文件:" + ls -la "${LOG_DIR}/"*.log 2>/dev/null || echo "无日志文件" + fi + fi +} + +# 显示帮助信息 +show_help() { + echo "用法: $0 {start|stop|restart|status|logs|help} [--reload] [日期]" + echo "" + echo "服务管理命令:" + echo " start [--reload] 启动 $APP_NAME 服务器(默认禁用热重载)" + echo " stop 停止 $APP_NAME 服务器" + echo " restart [--reload] 重启 $APP_NAME 服务器" + echo " status 查看 $APP_NAME 服务器状态" + echo " logs [YYYY-MM-DD] 查看 $APP_NAME 服务器日志(可指定日期)" + echo " help 显示此帮助信息" + echo "" + echo "可选参数:" + echo " --reload 启用热重载模式(开发环境使用)" + echo " 日期参数 格式: YYYY-MM-DD,用于查看特定日期日志" + echo "" + echo "环境变量配置:" + echo " 所有配置都从 .env 文件读取,包括:" + echo " APP_NAME, APP_MODULE, HOST, PORT, DEBUG, APP_HOST, APP_PORT, APP_WORKERS, APP_LOG_LEVEL 等" + echo "" + echo "日志功能:" + echo " • 日志按日期分割: ${APP_NAME}-YYYY-MM-DD.log" + echo " • 相同日期日志自动追加" + echo " • 支持查看特定日期日志" + echo "" + echo "示例:" + echo " $0 start # 启动服务器(生产环境推荐)" + echo " $0 start --reload # 启动服务器并启用热重载(开发环境)" + echo " $0 restart --reload # 重启服务器并启用热重载" + echo " $0 status # 查看服务器状态" + echo " $0 logs # 查看今天日志" + echo " $0 logs 2025-12-14 # 查看指定日期日志" +} + +# 主函数 +main() { + # 加载环境变量 + load_env + + # 初始化配置 + init_config + + # 执行检查 + check_directory + check_uv + check_app_module + setup_logs + + # 处理命令行参数 + case "$1" in + start) + # 传递剩余参数给 start_server + shift + start_server "$@" + ;; + stop) + stop_server + ;; + restart) + # 重启时也支持 --reload 参数 + shift + restart_server "$@" + ;; + status) + status_server + ;; + logs) + # 传递可能的日期参数给 tail_logs + shift + tail_logs "$@" + ;; + help|--help|-h) + show_help + ;; + *) + if [ -z "$1" ]; then + # 默认行为:启动服务器(向后兼容) + log_warning "未指定命令,默认执行启动操作" + log_warning "建议使用: $0 start" + start_server + else + log_error "未知命令: $1" + show_help + exit 1 + fi + ;; + esac +} + +# 脚本入口 +main "$@"