From eb44dc34911f976ea1dd558e9bb9152e2277a20f Mon Sep 17 00:00:00 2001 From: zhangqing Date: Mon, 15 Jun 2026 15:38:06 +0800 Subject: [PATCH] update --- backend/.env.example | 2 + backend/Dockerfile | 86 ++++++++++++++++++++++++++++++++++--- backend/core/config.py | 7 ++- backend/core/main.py | 9 ++-- backend/docker-compose.yaml | 2 +- backend/logger/logging.py | 8 ++-- 6 files changed, 99 insertions(+), 15 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 5abdea1..31ae530 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,3 +1,5 @@ +ENTERPRISE_BACKEND_VERSION=prod-v1.1.0 + # ==================== 服务器配置 ==================== # API 服务器配置 API.HOST=0.0.0.0 diff --git a/backend/Dockerfile b/backend/Dockerfile index 4c0c977..d00bbae 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,16 +1,92 @@ -# Python 3.11 + uv(与 pyproject 中 requires-python 一致) -FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim +# ============================================================================= +# 阶段一:编译阶段 +# 将核心业务目录编译为原生二进制,源码不进入最终镜像 +# ============================================================================= +FROM python:3.11-slim-bookworm AS builder -WORKDIR /app +WORKDIR /build + +# 替换为阿里云 apt 镜像源,解决国内网络访问 Debian 官方源超时问题 +RUN sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources \ + || sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list + +# 系统依赖:gcc/g++ 用于 Nuitka 编译;patchelf 用于修正共享库路径 +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc g++ patchelf ccache \ + && rm -rf /var/lib/apt/lists/* + +# 配置镜像源 +ENV PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +ENV UV_HTTP_TIMEOUT=120 +ENV UV_HTTP_RETRIES=5 + +RUN pip install --no-cache-dir uv COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev +# nuitka 安装到 venv 中,确保与 .venv/bin/python 一致 +RUN uv pip install --no-cache-dir nuitka + +# 复制所有源码 COPY . . +# 编译需要保护的目录(admin api core logger models services tools utils) +# --module:编译为可 import 的扩展模块(.so),而不是独立可执行文件 +# --follow-import-to:只跟踪项目自身的包,不拉入标准库和第三方包 +# --output-dir:编译产物输出目录(每个包单独编译) +RUN for pkg in admin api core logger models services tools utils; do \ + uv run python -m nuitka \ + --module \ + --follow-import-to=admin \ + --follow-import-to=api \ + --follow-import-to=core \ + --follow-import-to=logger \ + --follow-import-to=models \ + --follow-import-to=services \ + --follow-import-to=tools \ + --follow-import-to=utils \ + --include-package=${pkg} \ + --output-dir=/compiled \ + --remove-output \ + ${pkg} ; \ + done + +# ============================================================================= +# 阶段二:运行阶段 +# 只包含编译产物和必要运行时,不含任何 .py 源码 +# ============================================================================= +FROM python:3.11-slim-bookworm + +WORKDIR /app + +ENV PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +ENV UV_HTTP_TIMEOUT=120 +ENV UV_HTTP_RETRIES=5 + +RUN pip install --no-cache-dir uv + +# 复制依赖声明文件并安装生产依赖 +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev + +# 从编译阶段复制二进制模块(.so 文件) +COPY --from=builder /compiled/ ./ + +# 保留必要的明文文件 +# main.py:uvicorn 入口,只有两行 import,不含业务逻辑 +COPY main.py ./ +# prompt/:提示词模板,运行时动态读取 +COPY prompt/ ./prompt/ +# services/fonts/:字体资源文件 +COPY services/fonts/ ./services/fonts/ + +# 工作目录固定为 /app,所有路径相关代码基于 cwd 解析 ENV PYTHONUNBUFFERED=1 +ENV APP_DIR=/app -EXPOSE 7861 7862 +EXPOSE 7862 -# 端口以环境变量 API.PORT / API_PORT 为准(见 core.config) CMD ["uv", "run", "python", "-m", "main"] diff --git a/backend/core/config.py b/backend/core/config.py index 1ec048c..827362a 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -11,8 +11,11 @@ from urllib.parse import quote_plus from pydantic import AliasChoices, Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict -# backend/ 目录(与 uvicorn CWD 无关,始终读取该目录下的 .env) -_BACKEND_DIR = Path(__file__).resolve().parent.parent +# backend/ 目录 +# 编译为二进制后 __file__ 路径不可靠,优先使用环境变量 APP_DIR, +# 其次使用当前工作目录(Docker WORKDIR /app 固定为 /app)。 +import os as _os +_BACKEND_DIR = Path(_os.environ.get("APP_DIR", "")).resolve() if _os.environ.get("APP_DIR") else Path(_os.getcwd()) class Settings(BaseSettings): diff --git a/backend/core/main.py b/backend/core/main.py index 003fcb1..61cbef6 100644 --- a/backend/core/main.py +++ b/backend/core/main.py @@ -14,11 +14,14 @@ FastAPI 应用主模块 也可使用 ``uvicorn core.main:app``(与 ``main:app`` 等价)。 默认 host/port 来自配置项 api_host / api_port(可通过环境变量覆盖)。 """ +import os from pathlib import Path from dotenv import load_dotenv -load_dotenv(Path(__file__).resolve().parent.parent / ".env") +# 编译为二进制后 __file__ 路径不可靠,使用工作目录(Docker WORKDIR /app) +_env_path = Path(os.environ.get("APP_DIR", os.getcwd())) / ".env" +load_dotenv(_env_path) import os from contextlib import asynccontextmanager @@ -155,8 +158,8 @@ def create_app() -> FastAPI: # 注册全局异常处理器 register_exception_handlers(app) - # 静态文件服务(backend/core/main.py -> backend/static) - static_path = Path(__file__).resolve().parent.parent / "static" + # 静态文件服务(相对工作目录 /app/static) + static_path = Path(os.getcwd()) / "static" if static_path.exists(): app.mount("/static", StaticFiles(directory=str(static_path)), name="static") logger.info(f"静态文件目录已挂载: {static_path}") diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml index deb2cf4..ed754f2 100644 --- a/backend/docker-compose.yaml +++ b/backend/docker-compose.yaml @@ -3,7 +3,7 @@ services: build: context: . dockerfile: Dockerfile - image: registry.cn-hangzhou.aliyuncs.com/qing358/zlsj-huoyan-enterprise-backend-viersin:${ENTERPRISE_BACKEND_VERSION:-latest} + image: registry.cn-hangzhou.aliyuncs.com/qing358/zlsj-huoyan-enterprise-backend:${ENTERPRISE_BACKEND_VERSION:-latest} env_file: - .env ports: diff --git a/backend/logger/logging.py b/backend/logger/logging.py index ebeb30f..3863ae2 100644 --- a/backend/logger/logging.py +++ b/backend/logger/logging.py @@ -74,10 +74,10 @@ def setup_logger( if Path(log_dir_name).is_absolute(): log_dir = Path(log_dir_name) else: - # 相对路径:使用项目根目录 - # __file__ 是 backend/logger/logging.py,需要向上两级到项目根目录 - project_root = Path(__file__).parent.parent - log_dir = project_root / log_dir_name + # 相对路径:使用工作目录(编译为二进制后 __file__ 不可靠) + # Docker WORKDIR /app,直接相对于 cwd 解析 + import os + log_dir = Path(os.getcwd()) / log_dir_name else: log_dir = Path(log_dir)