diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3a2849..a641f92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,10 @@ jobs: run: | PYTHON_BIN=python bash scripts/dev-verify.sh + - name: Run PDF runtime smoke + run: | + python -m pytest -q tests/test_pdf_runtime_smoke.py + - name: Upload coverage artifact if: always() && matrix.python-version == '3.11' && hashFiles('coverage.xml') != '' uses: actions/upload-artifact@v4 diff --git a/Dockerfile b/Dockerfile index 022eb2e..0a72a07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,17 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ no_proxy= \ NO_PROXY= +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libpango-1.0-0 \ + libpangoft2-1.0-0 \ + libharfbuzz-subset0 \ + libjpeg62-turbo \ + libopenjp2-7 \ + libffi8 \ + shared-mime-info \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY constraints.txt requirements-admin.txt /tmp/ diff --git a/INSTALL.md b/INSTALL.md index f3dfed4..4311b34 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -115,7 +115,17 @@ export QQ_EMAIL_PASSWORD="your-password" echo 'export QQ_EMAIL_PASSWORD="your-password"' >> ~/.bashrc ``` -### 3. 配置浏览器(可选) +### 3. PDF 运行时依赖(必看) + +当前仓库验证过的 PDF 生成口径: + +- 本地:激活 `.venv` 后执行 `python -m pytest -q tests/test_pdf_runtime_smoke.py` +- CI:workflow 会额外跑同一条 smoke +- Docker:`Dockerfile` 已安装 WeasyPrint 所需系统库 + +如果你的机器缺少系统库,WeasyPrint 可能安装成功但生成 PDF 失败。 + +### 4. 配置浏览器(可选) 用于生成PDF: @@ -202,16 +212,14 @@ hermes skills reload ### Q4: PDF 生成失败 -**原因**: weasyprint 依赖缺失 +**原因**: WeasyPrint 系统库缺失,或未跑过 PDF smoke **解决**: ```bash -# 安装系统依赖 -sudo apt-get install libpango-1.0-0 libpangoft2-1.0-0 - -# 重新安装 weasyprint -pip3 install --user --break-system-packages --force-reinstall weasyprint +sudo apt-get install libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 +source .venv/bin/activate +python -m pytest -q tests/test_pdf_runtime_smoke.py ``` ### Q5: Git 提交失败 diff --git a/README.md b/README.md index e6e2b0b..0c7a104 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,16 @@ bash scripts/dev-verify.sh GAOKAO_SKIP_INSTALL=1 bash scripts/dev-verify.sh ``` +### PDF 运行时证明 + +当前仓库的 PDF 生成证明口径: + +- 本地 smoke:`./.venv/bin/python -m pytest -q tests/test_pdf_runtime_smoke.py` +- CI smoke:GitHub Actions 额外执行 `tests/test_pdf_runtime_smoke.py` +- Docker 运行时:`Dockerfile` 已显式安装 WeasyPrint 所需系统库(`libpango-1.0-0`、`libpangoft2-1.0-0`、`libharfbuzz-subset0` 等) + +这才表示“PDF 运行时已被交付门禁覆盖”;不是只靠开发机本地可跑。 + ### T12 交付事件最小执行链 当前 `report_ready` 不再只停留在事件表: diff --git a/admin/app.py b/admin/app.py index 137b46a..ca635ff 100644 --- a/admin/app.py +++ b/admin/app.py @@ -32,6 +32,7 @@ from admin.config import ( ) from admin.db import bootstrap_admin, ensure_schema from admin.errors import register_exception_handler +from data.orders.intake_store import SCHEMA_SQL as INTAKE_SCHEMA_SQL from admin.logging_utils import ( bind_request_context, clear_request_context, @@ -98,6 +99,8 @@ def _setup_database(settings: Settings) -> None: cases_conn = apply_cases_schema(settings.db_path) cases_conn.close() orders_conn = apply_orders_schema(settings.orders_db_path) + orders_conn.executescript(INTAKE_SCHEMA_SQL) + orders_conn.commit() orders_conn.close() created, msg = bootstrap_admin(settings) if created: diff --git a/admin/routes/notifications.py b/admin/routes/notifications.py index 51dcfa3..4a0f2fa 100644 --- a/admin/routes/notifications.py +++ b/admin/routes/notifications.py @@ -9,7 +9,7 @@ from fastapi import APIRouter, Depends, Query from fastapi.responses import HTMLResponse from pydantic import BaseModel -from admin.auth import get_current_user, require_role +from admin.auth import require_role from admin.config import Settings, get_settings_dep from admin.db import AdminUser diff --git a/admin/routes/orders.py b/admin/routes/orders.py index 5ebfaff..a45c97b 100644 --- a/admin/routes/orders.py +++ b/admin/routes/orders.py @@ -20,7 +20,7 @@ from typing import Any, Literal, Optional, cast from fastapi import APIRouter, Depends, Path as ApiPath, Query, Response, status from pydantic import BaseModel, Field -from admin.auth import get_current_user, require_role +from admin.auth import require_role from admin.config import Settings, get_settings_dep from admin.db import AdminUser from admin.errors import ( diff --git a/admin/routes/users.py b/admin/routes/users.py index 4a8389a..225c09f 100644 --- a/admin/routes/users.py +++ b/admin/routes/users.py @@ -12,7 +12,7 @@ from typing import Any, Optional from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel -from admin.auth import get_current_user, require_role +from admin.auth import require_role from admin.config import Settings, get_settings_dep from admin.db import AdminUser from admin.errors import DATA_NOT_FOUND diff --git a/admin/tests/test_order_status_page.py b/admin/tests/test_order_status_page.py index 3148806..8b5f29a 100644 --- a/admin/tests/test_order_status_page.py +++ b/admin/tests/test_order_status_page.py @@ -58,8 +58,10 @@ def test_order_status_page_and_report_download(client, settings, tmp_path: Path) submit=True, ) - report_path = tmp_path / "report.html" - pdf_path = tmp_path / "report.pdf" + report_root = Path(settings.share_report_dir) + report_root.mkdir(parents=True, exist_ok=True) + report_path = report_root / "report.html" + pdf_path = report_root / "report.pdf" report_path.write_text("
已生成。
", encoding="utf-8") pdf_path.write_bytes(b"%PDF-1.4\nportal-report\n") @@ -99,8 +101,10 @@ def test_portal_status_page_shows_sent_station_notification( order_id=order.id, payload={"candidate_score": 578}, submit=True ) - report_path = tmp_path / "status-notice-report.html" - pdf_path = tmp_path / "status-notice-report.pdf" + report_root = Path(settings.share_report_dir) + report_root.mkdir(parents=True, exist_ok=True) + report_path = report_root / "status-notice-report.html" + pdf_path = report_root / "status-notice-report.pdf" report_path.write_text("已生成。
", encoding="utf-8") pdf_path.write_bytes(b"%PDF-1.4\nportal-notice\n") @@ -192,7 +196,9 @@ def test_partial_artifacts_do_not_expose_delivery_links_before_report_ready( submit=True, ) - report_path = tmp_path / "partial-report.html" + report_root = Path(settings.share_report_dir) + report_root.mkdir(parents=True, exist_ok=True) + report_path = report_root / "partial-report.html" report_path.write_text("