fix: P4T1审查修复(platform.db入data卷/tzdata注释/corepack钉版/compose版本注记)

- main.py: PLATFORM_DB 挪到 <repo>/data/platform.db——data/ 是 compose
  bind mount,容器/镜像重建不再丢租户注册表;lifespan 对父目录兜底
  mkdir(冷机 data/ 不存在,sqlite 不建父目录)。bootstrap 脚本 --db
  默认值同步锚定 data/。
- 新测试 test_platform_db_defaults_into_data_dir: 常量形状钉死(回退
  仓库根即红)+ 冷机形态下 lifespan 建库于 data/ 内(全量 603)。
- Dockerfile: corepack 钉实测通过的具体版本 0.36.0(不追 latest,构建
  可复现);tzdata 注释照实改写(trixie slim 实测已自带,安装留作基
  镜像漂移保险)。
- docker-compose: 头注释改准确(tenants/=租户库,data/=platform.db+
  uploads);env_file 注记对象式写法需 Docker Compose >= 2.24。
- 实证: compose down/up 重建容器后宿主 ./data/platform.db 仍在,
  healthcheck healthy,/admin/ 200;docker compose build 复验通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-14 08:42:50 +08:00
parent 4f38522759
commit 6100b4eb15
5 changed files with 51 additions and 17 deletions
+6 -4
View File
@@ -9,8 +9,9 @@ WORKDIR /build
# 先拷清单再拷源码:依赖层缓存与源码改动解耦
COPY admin/package.json admin/pnpm-lock.yaml admin/pnpm-workspace.yaml ./
# corepack 按 package.json 的 packageManager 字段钉 pnpm 版本(与本地
# 一致的单一事实源);升级 corepack 规避 node20 镜像内置旧版的签名钥问题
RUN npm install -g corepack@latest && corepack enable
# 一致的单一事实源);显式装 0.36.0(实测通过)规避 node20 镜像内置旧版
# 的签名钥问题——钉具体版本,不追 latest(构建可复现)
RUN npm install -g corepack@0.36.0 && corepack enable
RUN pnpm install --frozen-lockfile
COPY admin/ ./
# VITE_BASE_URL 取 admin/.env.production(=/admin/):产物资源引用与后端
@@ -21,8 +22,9 @@ RUN pnpm build
# ---- 阶段二: 生产镜像 --------------------------------------------------------
FROM python:3.13-slim
WORKDIR /app
# tzdata: compose 设 TZ=Asia/Shanghai,推送窗口等业务时间依赖本地时区,
# slim 基镜像不带 tzdata 时 TZ 会被静默忽略(退 UTC)
# tzdata: trixie slim 实测已自带(容器内 TZ=Asia/Shanghai 生效,UTC+8);
# 显式安装留作基镜像漂移保险——将来换基/slim 进一步瘦身时,缺 tzdata 的
# TZ 会被静默忽略(退 UTC),推送窗口等业务时间随之漂移
RUN apt-get update \
&& apt-get install -y --no-install-recommends tzdata \
&& rm -rf /var/lib/apt/lists/*
+12 -6
View File
@@ -28,7 +28,10 @@ logger = logging.getLogger(__name__)
BASE_DIR = Path(__file__).resolve().parents[1]
SEED_DIR = BASE_DIR / "seed_profiles"
TENANTS_DIR = BASE_DIR / "tenants" # .gitignore 已忽略 tenants/*.db
PLATFORM_DB = BASE_DIR / "platform.db" # .gitignore 已忽略 platform.db*
# 平台库落 data/ 子目录(P4T1 审查修复):data/ 是 compose 的 bind mount
# ——容器/镜像重建不丢租户注册表(租户库本就在挂载的 tenants/ 内);
# .gitignore 的 data/ 规则覆盖其运行产物。
PLATFORM_DB = BASE_DIR / "data" / "platform.db"
# ---- admin 前端生产服务(Phase 4 Task 1)---------------------------------------
@@ -116,13 +119,16 @@ async def lifespan(app: FastAPI):
f"TENANT_SOURCE 只允许 db|file(实际 {tenant_source!r})")
# 平台库测试缝(Phase 2 Task 1,同 registry 缝模式):预置过
# app.state.platform_engine(临时库)则沿用——不预置则每个 TestClient
# 测试都会在仓库根建出真实 platform.db。schema+bootstrap 无条件跑
# (两者幂等):预置引擎同样被建到可用,Task 2 起认证测试直接可用。
# 装配次序(Task 11 起)engine 先于 registry:db 源 registry 构造即需
# 持有 platform_engine。
# app.state.platform_engine(临时库)则沿用——不预置则会在 <repo>/data/
# 下建出真实 platform.db(生产;测试必须预置或锚到 tmp)。schema+
# bootstrap 无条件跑(两者幂等):预置引擎同样被建到可用,Task 2 起
# 认证测试直接可用。装配次序(Task 11 起)engine 先于 registry:db 源
# registry 构造即需持有 platform_engine。
platform_engine = getattr(app.state, "platform_engine", None)
if platform_engine is None:
# sqlite 不建父目录——冷机 data/ 可能尚不存在(全新检出/新容器),
# 连接前兜底建目录(registry._ensure_ready 同纪律)
PLATFORM_DB.parent.mkdir(parents=True, exist_ok=True)
platform_engine = make_platform_engine(PLATFORM_DB)
app.state.platform_engine = platform_engine
+5 -2
View File
@@ -1,5 +1,7 @@
# 生产编排(Phase 4 Task 1)——单容器全栈(uvicorn 直出,无反代层时
# 8018 对外;有反代时由反代转发)。数据全在 bind mount,容器可随手重建。
# 8018 对外;有反代时由反代转发)。持久化全在 bind mount,容器可随手
# 重建:./tenants 挂每租户 SQLite 库;./data 挂 platform.db(租户注册表,
# P4T1 审查修复起落在 data/ 内)与 data/uploads 头像上传。
name: all-kefu
services:
@@ -10,7 +12,8 @@ services:
env_file:
# 生产服务器放 .env(ADMIN_PASSWORD/ZHIPU_API_KEY/SMTP 等);本机
# 没有 .env 也能起(出厂默认+启动日志警示)——required: false 只是
# 容忍缺省,不改变变量优先级
# 容忍缺省,不改变变量优先级。注意对象式写法(path/required)需
# Docker Compose >= 2.24
- path: .env
required: false
environment:
+7 -5
View File
@@ -91,8 +91,9 @@ async def _import_profile(engine, seed_dir: Path, slug: str) -> str:
async def run(args) -> None:
db_path = Path(args.db)
# sqlite 不建父目录(仓库根默认存在,--db 指新目录时会炸——conftest
# build_app 同坑注记);照 registry._ensure_ready 纪律先建目录
# sqlite 不建父目录(默认的 data/ 在冷机可能尚不存在,--db 指新目录
# 同炸——conftest build_app 同坑注记);照 registry._ensure_ready 纪律
# 先建目录(app/main.py 的 lifespan 对 PLATFORM_DB 同兜底)
db_path.parent.mkdir(parents=True, exist_ok=True)
engine = make_platform_engine(db_path)
try:
@@ -122,9 +123,10 @@ async def run(args) -> None:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--db", default=str(REPO_ROOT / "platform.db"),
help="platform.db 路径(默认仓库根,与 app/main.py 的 PLATFORM_DB "
"同一锚定;测试/分环境可覆盖)")
"--db", default=str(REPO_ROOT / "data" / "platform.db"),
help="platform.db 路径(默认 <repo>/data/,与 app/main.py 的 "
"PLATFORM_DB 同一锚定——data/ 是 compose bind mount,重建"
"容器不丢;测试/分环境可覆盖)")
parser.add_argument(
"--profiles", nargs="+", default=list(DEFAULT_PROFILES),
help=f"导入的出厂档案 slug 列表(默认 {' '.join(DEFAULT_PROFILES)})")
+21
View File
@@ -200,6 +200,27 @@ def test_main_default_db_source_empty_platform_boots(tmp_path, monkeypatch):
assert app.state.registry.runtimes() == () # 空注册表不炸
def test_platform_db_defaults_into_data_dir(tmp_path, monkeypatch):
"""PLATFORM_DB 落 <repo>/data/(P4T1 审查修复):data/ 是 compose 的
bind mount——容器/镜像重建不再丢租户注册表。两层断言:
1. 常量形状钉死(回退仓库根即红,compose 持久化前提);
2. 功能:锚到 tmp 的 data/ 子目录(目录刻意不预建=冷机形态),
lifespan 启动后平台库文件确实建在 data/ 内(父目录兜底 mkdir)。"""
import app.main as main_mod
assert main_mod.PLATFORM_DB == main_mod.BASE_DIR / "data" / "platform.db"
target = tmp_path / "data" / "platform.db"
assert not target.parent.exists() # 冷机:data/ 尚不存在
monkeypatch.setattr(main_mod, "PLATFORM_DB", target)
monkeypatch.setattr(main_mod, "TENANTS_DIR", tmp_path / "tenants")
monkeypatch.delenv("TENANT_SOURCE", raising=False)
with app_test_client(main_mod.app, "http://testserver", tmp_path) as c:
assert c.get("/health").status_code == 200
# 断言须在 client 存活期内:app_test_client 退出时会清掉整个 tmp
# (含本文件)——文件在应用运行中确实存在即为所证
assert target.exists() # lifespan 建库于 data/ 内
def test_main_default_db_source_bootstrapped_platform(tmp_path, monkeypatch):
"""默认 db + bootstrap 导入后的 platform.db:启动即装配两租户(registry
经 async load_all),jiancai 域名真实请求走通(Host 中间件→懒建租户库