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:
+6
-4
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)})")
|
||||
|
||||
@@ -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 中间件→懒建租户库
|
||||
|
||||
Reference in New Issue
Block a user