feat(backend): 添加服务管理脚本并配置应用基础参数
- 更新 .gitignore 忽略后端日志目录和 PID 文件 - 新增 app_name 配置项并替换主程序中硬编码的应用名称 - 为配置模型添加 extra: ignore 以兼容额外环境变量 - 新增完整的后端服务管理脚本 serv.sh,支持常用管理操作
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,6 +11,9 @@ __pycache__/
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
backend/logs/
|
||||||
|
backend/*.pid
|
||||||
|
|
||||||
# Uploads
|
# Uploads
|
||||||
uploads/
|
uploads/
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from pydantic_settings import BaseSettings
|
|||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
app_name: str = "孕育无忧"
|
||||||
|
|
||||||
# Path Prefix
|
# Path Prefix
|
||||||
api_prefix: str = "/api"
|
api_prefix: str = "/api"
|
||||||
uploads_prefix: str = "/uploads"
|
uploads_prefix: str = "/uploads"
|
||||||
@@ -37,7 +39,7 @@ class Settings(BaseSettings):
|
|||||||
port: int = 8000
|
port: int = 8000
|
||||||
debug: bool = True
|
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()
|
settings = Settings()
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ logging.basicConfig(
|
|||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
datefmt="%Y-%m-%d %H:%M:%S",
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
)
|
)
|
||||||
logger = logging.getLogger("孕育无忧")
|
logger = logging.getLogger(settings.app_name)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -42,7 +42,7 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="孕育无忧 API",
|
title=f"{settings.app_name} API",
|
||||||
description="孕期健康检查报告解读后端服务",
|
description="孕期健康检查报告解读后端服务",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
@@ -111,7 +111,7 @@ app.include_router(share.router, prefix=f"{api_prefix}/share", tags=["分享"])
|
|||||||
|
|
||||||
@app.get(f"{api_prefix}/health")
|
@app.get(f"{api_prefix}/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
return {"status": "ok", "service": "孕育无忧 API"}
|
return {"status": "ok", "service": f"{settings.app_name} API"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
393
backend/serv.sh
Executable file
393
backend/serv.sh
Executable file
@@ -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 "$@"
|
||||||
Reference in New Issue
Block a user