# M1 — 单库化地基(DB Consolidation) > 阅读前提:先读 [`README.md`](./README.md)(协作模型、任务卡格式、校验闸门、数据安全红线)。本文档只展开 M1 的现状、目标与原子任务。 ## 1. 目标 把 location、poo 两个独立 SQLite 库合并进 `app.db`,收敛成**单库 + 单 engine + 单 DeclarativeBase + 单 Alembic 链**,清理项目早期散落的数据层代码,并移除 Grafana。历史数据零丢失。 ## 2. 现状(实现者可据此工作,不必重新通读全仓库) **三套数据层(散落点)** - `app/db.py`:`Base` + `engine`/`SessionLocal`/`get_db_session` —— 实际绑定 `settings.location_database_url`(即 location 库,命名有误导性)。 - `app/models/base.py`:仅 `from app.db import Base` 转出。 - `app/poo_db.py`:`PooBase` + `poo_engine`/`PooSessionLocal`/`get_poo_db_session` —— 绑定 `poo_database_url`。 - `app/auth_db.py`:`AuthBase` + 带 `lru_cache` 的 `_get_auth_engine` / `get_auth_session_local` / `reset_auth_db_caches` / `get_auth_db_session` —— 绑定 `app_database_url`(真正的 app 库)。 **模型与归属** - `app/models/auth.py`:`AuthUser`、`AuthSession`(`AuthBase` → app 库) - `app/models/config.py`:`AppConfigEntry`(`AuthBase` → app 库) - `app/models/public_ip.py`:`PublicIPState`、`PublicIPHistory`(`AuthBase` → app 库) - `app/models/location.py`:`Location`(`Base` → location 库),表 `location`,PK(`person`,`datetime`),`latitude`/`longitude` NOT NULL,`altitude` nullable - `app/models/poo.py`:`PooRecord`(`PooBase` → poo 库),表 `poo_records`,PK(`timestamp`),`status`/`latitude`/`longitude` NOT NULL - `app/models/__init__.py`:导出除 `PooRecord` 外的模型(`PooRecord` 单独存在) **三条 Alembic 链** - `alembic_app.ini` + `alembic_app/`(`env.py` 用 `AuthBase.metadata`),head = `20260429_05_public_ip_monitor` - `alembic_location.ini` + `alembic_location/`,head = `20260419_01_location_baseline` - `alembic_poo.ini` + `alembic_poo/`,head = `20260420_01_poo_baseline` **adoption / 启动链路** - `scripts/app_db_adopt.py`(常量 `APP_BASELINE_REVISION = "20260429_05_public_ip_monitor"`) - `scripts/location_db_adopt.py`、`scripts/poo_db_adopt.py`(含 legacy 校验:`EXPECTED_USER_VERSION`、表结构断言) - `scripts/run_migrations.py`:依次调用三个 adopt 函数,返回 `{"app","location","poo"}` - `app/main.py` lifespan:`ensure_runtime_dirs`(app/location/poo 三路径)、`ensure_auth_db_ready`、`ensure_location_db_ready`、`ensure_poo_db_ready`,再起 APScheduler 每 4h 检查 public IP **依赖与路由** - `app/dependencies.py`:`get_auth_db`(app session)、`get_db`(location session)、`get_poo_db`(poo session)、`get_app_settings`、`get_current_auth_session`、`get_homeassistant_client`、`get_ticktick_client` - `app/api/routes/location.py`:`POST /location/record`,依赖 `get_db`,**无鉴权** - `app/api/routes/poo.py`:`POST /poo/record`、`GET /poo/latest`,依赖 `get_poo_db`,**无鉴权** - `app/api/routes/homeassistant.py`:同时用 `get_db`(location)和 `get_poo_db` **config** - `app/config.py`:`app_database_url` / `location_database_url` / `poo_database_url` 三字段 + computed `app_sqlite_path` / `location_sqlite_path` / `poo_sqlite_path` - `app/services/config_page.py`:`build_runtime_settings` 用到 `reset_auth_db_caches`;配置页 sections 暴露 `location_database_url` / `poo_database_url`(约 263–264 行) **测试耦合点(M1 必然要改)** - `tests/conftest.py`:`test_database_urls` 设三套环境变量;`ready_location_database` / `ready_poo_database` / `auth_database` / `location_client`(monkeypatch `app_db.engine`/`SessionLocal`)/ `poo_client`(monkeypatch `poo_db.poo_engine`/`PooSessionLocal`) - `tests/test_location.py` / `tests/test_poo.py`:用上述 client + 各自 adopt 脚本的 adoption 测试 - `tests/test_deployment.py`:断言 `run_all_migrations()` 返回 `{app,location,poo}` 三库各自 revision;断言 entrypoint 不含 `*_db_adopt` - `tests/test_homeassistant_inbound.py`:monkeypatch `app.poo_db` - `tests/test_config.py` / `tests/test_public_ip.py` / `tests/test_smtp.py`:硬编码三套 URL / 路径 - `reset_auth_db_caches` 被 `conftest`、`test_app`、`test_auth`、`test_deployment`、`test_ticktick` 引用 ## 3. 目标架构(M1 完成态) **单数据层 `app/db.py`** ```python class Base(DeclarativeBase): ... # 绑定 settings.app_database_url 的 cached engine;建连时启用 WAL(PRAGMA journal_mode=WAL) def get_engine() -> Engine: ... def get_session_local() -> sessionmaker: ... def reset_db_caches() -> None: ... def get_db_session() -> Generator[Session, None, None]: ... ``` - 所有模型(auth / config / public_ip / location / poo)都继承这一个 `Base`。 - 删除 `app/auth_db.py`、`app/poo_db.py`、`app/models/base.py`。 - 依赖收敛为**单一** `get_db`(app session);移除 `get_poo_db`、旧 `get_auth_db`。 - 一条 Alembic 链(`alembic_app`),`location` / `poo_records` 成为其管理对象;删除 `alembic_location*` / `alembic_poo*`。 - `config.py` 只保留 `app_database_url`;移除 location/poo 的 url 与 path。 - `docker-compose.yml` 去掉 grafana service;删除 `grafana/`。 - 数据搬迁由 `scripts/migrate_legacy_data.py` 一次性完成(不进 Alembic 链)。 ## 4. 任务依赖图 ``` T01 (app 链建 location+poo 空表) ├─► T02 (数据搬迁脚本) # 逻辑上需要新表存在 └─► T03 [structural] (统一数据层/模型/依赖/路由) └─► T04 (lifespan + run_migrations 收敛, 删 adopt 脚本) └─► T05 (config 去 location/poo url + 配置页 + 测试硬编码) T06 (删 Grafana) # 独立, 可并行 T07 (文档 + OpenAPI 重导出) # 收尾, 依赖 T03/T04/T05 ``` `T01`、`T06` 无前置可先开;`T02` 依赖 `T01`;`T03` 依赖 `T01`;`T04`/`T05` 依赖 `T03`;`T07` 最后。 --- ## 5. 原子任务 ### M1-T01 — app 链新增 revision:建 `location` + `poo_records` 空表 `[schema]` - **Status**: `todo` - **Depends**: none - **Context**: 让 app 库的 Alembic 链能建出这两张表,schema 与旧库**完全一致**。本任务只动 schema,不搬数据、不移模型。 **Files** - `create alembic_app/versions/20260611_06_merge_location_poo_tables.py` - `modify scripts/app_db_adopt.py`(更新 `APP_BASELINE_REVISION`) **Steps** 1. 新 revision:`revision = "20260611_06_merge_location_poo_tables"`,`down_revision = "20260429_05_public_ip_monitor"`。 2. `upgrade()` 用 `op.create_table` 手写建 `location` 与 `poo_records`,列/约束严格照抄现有 baseline(`location`: person TEXT, datetime TEXT, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL nullable, PK(person,datetime);`poo_records`: timestamp TEXT, status TEXT, latitude REAL NOT NULL, longitude REAL NOT NULL, PK(timestamp))。 3. `downgrade()`:`op.drop_table("poo_records")` + `op.drop_table("location")`。 4. 把 `scripts/app_db_adopt.py` 的 `APP_BASELINE_REVISION` 更新为新 head。 **Out of scope / 不要碰** - 不要把 `Location` / `PooRecord` 模型改到 app Base(那是 T03)。 - 不要触碰 `alembic_location*` / `alembic_poo*`(T03/T04 删)。 - 不要在本 revision 里写任何数据拷贝。 **Acceptance criteria** - [ ] 在一个全新临时 app 库上 `command.upgrade(alembic_app head)` 后,`sqlite_master` 含 `location`、`poo_records`、且与旧 baseline 表结构一致(`PRAGMA table_info` 对齐)。 - [ ] `downgrade -1` 能干净回滚这两张表。 - [ ] `APP_BASELINE_REVISION == "20260611_06_merge_location_poo_tables"`。 - [ ] 校验闸门全绿(`pytest` 中 `test_deployment` 对 app head 的断言仍通过,因为它用的是常量)。 **Reviewer checklist** - 表结构与旧 baseline **逐列逐约束**一致(类型 TEXT/REAL、nullable、PK 顺序)。 - `down_revision` 正确指向旧 head,链上只有一个 head。 --- ### M1-T02 — 数据搬迁脚本 `scripts/migrate_legacy_data.py` - **Status**: `todo` - **Depends**: M1-T01 - **Context**: 把旧 `locationRecorder.db` / `pooRecorder.db` 的行幂等拷进 app 库的新表,搬完对账。**不进 Alembic 链**,人工运行一次。 **Files** - `create scripts/migrate_legacy_data.py` - `create tests/test_migrate_legacy_data.py` **Steps** 1. 入口 `migrate_legacy_data(app_url, location_url, poo_url, *, dry_run=False) -> dict`,CLI 默认从 env 读三个 url(即便 location/poo url 已从 `Settings` 移除,本脚本可直接读环境变量或接受 `--location-db`/`--poo-db` 参数,保持自包含)。 2. 对每个旧库:若文件不存在 → 该表 `skipped`(**不报错**,保证 CI / 全新部署可安全 no-op)。 3. 拷贝用 SQLite `ATTACH DATABASE '' AS legacy` + `INSERT OR IGNORE INTO main. SELECT <显式列> FROM legacy.
`(显式列名,禁用 `SELECT *`)。`INSERT OR IGNORE` 保证幂等(PK 冲突跳过)。 4. 搬完对账:对每张表比对 `源行数` 与 `目标行数中来自源的部分`;目标行数 < 源行数则 `raise` 并以非零码退出。 5. `dry_run` 模式只读统计、不写入。 6. 打印每表结果:`{location: {source, copied, skipped, final}, poo_records: {...}}`。 **Out of scope / 不要碰** - **绝不** `os.remove` / 覆盖任何旧文件(数据安全红线)。 - 不修改 Alembic 链,不在 app 启动链路里调用本脚本。 - 不改 `config.py`。 **Acceptance criteria** - [ ] 单测:给定含 N 行的临时旧库 + 已 upgrade 的临时 app 库,运行后 app 库对应表有 N 行;**再运行一次**仍是 N 行(幂等)。 - [ ] 单测:旧库文件不存在时该表返回 `skipped`,不抛异常,app 库该表保持为空。 - [ ] 单测:构造"目标缺行"场景,断言对账失败抛错且退出码非零。 - [ ] 脚本中不出现任何文件删除/覆盖调用(`grep -nE "os\.remove|unlink|shutil|truncate|DROP TABLE" scripts/migrate_legacy_data.py` 为空)。 - [ ] 校验闸门全绿。 **Reviewer checklist** - 幂等机制确实是 PK 冲突安全(`INSERT OR IGNORE` 或等价 upsert),不是靠"先清空目标"。 - 对账逻辑会在丢行时**真的中止**(非零退出),不是只打印 warning。 - 列名显式,与两表 schema 完全对应。 --- ### M1-T03 — 统一数据层、模型、依赖、路由到单库 `[structural]` - **Status**: `todo` - **Depends**: M1-T01 - **Context**: M1 的核心 sweep。把三套 engine/Base/session 收敛成 `app/db.py` 一套(绑 app 库、开 WAL),所有模型挂到同一个 `Base`,依赖收敛为单一 `get_db`,所有路由改用它。**本任务必须原子落地**——删除旧模块会同时打断所有 importer,无法分多次保持绿色。Orchestrator 可按下方 Steps 的自然分段派给较强 implementer。 **Files** - `modify app/db.py`(改写为统一数据层:`Base` + 绑 `app_database_url` 的 cached engine + WAL + `get_session_local` + `reset_db_caches` + `get_db_session`) - `delete app/auth_db.py` - `delete app/poo_db.py` - `delete app/models/base.py` - `modify app/models/location.py`(`from app.db import Base`) - `modify app/models/poo.py`(改继承统一 `Base`,import 改 `app.db`) - `modify app/models/auth.py`、`app/models/config.py`、`app/models/public_ip.py`(`AuthBase` → 统一 `Base`) - `modify app/models/__init__.py`(补导出 `PooRecord`,保证 `from app import models` 注册所有表到同一 metadata) - `modify app/dependencies.py`(单一 `get_db`;删 `get_poo_db`;`get_app_settings`/`get_current_auth_session` 改用 `get_db`) - `modify app/api/routes/auth.py`、`pages.py`、`public_ip.py`、`ticktick.py`(`get_auth_db` → `get_db`) - `modify app/api/routes/location.py`、`poo.py`、`homeassistant.py`(location/poo session 改用 `get_db`;删 `get_poo_db` 引用) - `modify app/services/config_page.py`(`reset_auth_db_caches` → `reset_db_caches`) - `modify app/main.py`(`import app.auth_db as auth_db` → 统一层;`get_auth_session_local` → `get_session_local`) - `modify tests/conftest.py`、`tests/test_app.py`、`tests/test_auth.py`、`tests/test_ticktick.py`、`tests/test_homeassistant_inbound.py`、`tests/test_location.py`、`tests/test_poo.py`(import sweep + 把 location/poo client 改成写 app 库的统一 session;移除对 `app.poo_db`/`app.db`(location) monkeypatch 的依赖) **Steps** 1. 改写 `app/db.py`:`Base(DeclarativeBase)`;沿用 `auth_db.py` 的 cached-engine + reset 模式但绑 `app_database_url`;为 sqlite 连接注册 `PRAGMA journal_mode=WAL`(用 `event.listens_for(engine, "connect")` 或建连后执行)。导出 `get_engine`/`get_session_local`/`reset_db_caches`/`get_db_session`。 2. 模型 sweep:所有 `from app.auth_db import AuthBase` / `from app.poo_db import PooBase` / `from app.db import Base` 统一成 `from app.db import Base`;类继承统一 `Base`。`app/models/__init__.py` 增加 `from app.models.poo import PooRecord` 并补进 `__all__`。 3. 删 `app/auth_db.py`、`app/poo_db.py`、`app/models/base.py`。 4. 依赖 sweep:`app/dependencies.py` 留单一 `get_db`(yield 统一 session),删 `get_poo_db`;`get_app_settings`、`get_current_auth_session` 的 `Depends(get_auth_db)` → `Depends(get_db)`。 5. 路由 sweep:所有 `Depends(get_auth_db)`、`Depends(get_poo_db)`、`Depends(get_db)` 统一为 `Depends(get_db)`(变量名 `auth_db_session`/`poo_db`/`db` 可保留,不强制改)。 6. `app/services/config_page.py`:`reset_auth_db_caches` → `reset_db_caches`。 7. `app/main.py`:把 `_run_scheduled_public_ip_check` / `ensure_auth_db_ready` 里的 `auth_db.get_auth_session_local()` 换成统一 `get_session_local()`。(lifespan 里 location/poo 的 ready 检查留到 T04 删。) 8. 测试 sweep:`reset_auth_db_caches` → `reset_db_caches`(6 个文件);conftest 的 `location_client`/`poo_client` 改成"写入统一 app session 即可"的形式(不再 monkeypatch 已删除的 `app.poo_db`/location `app.db`);`test_homeassistant_inbound` 同理。 **Out of scope / 不要碰** - 不删 `scripts/location_db_adopt.py` / `scripts/poo_db_adopt.py`,不改 lifespan 的 location/poo ready 调用(那是 T04,避免与本任务交叉冲突)。 - 不动 `config.py` 的字段(T05)。 - 不改业务逻辑(service 内部算法、HA 集成行为保持不变)。 **Acceptance criteria** - [ ] `grep -rnE "auth_db|poo_db|PooBase|AuthBase|get_auth_db|get_poo_db|reset_auth_db_caches|app\.models\.base" app | grep -v __pycache__` 结果为空。 - [ ] `app/db.py` 的 engine 绑定 `app_database_url`,sqlite 下 `PRAGMA journal_mode` 实测为 `wal`。 - [ ] 所有模型 `Base.metadata.tables` 同时包含 auth/config/public_ip/location/poo_records 五类表。 - [ ] `pytest` 全绿(含 location/poo/homeassistant_inbound 测试在单库下通过)。 - [ ] `ruff check .` 无新增告警。 **Reviewer checklist** - WAL 真的生效(实际连接 `PRAGMA journal_mode` 返回 `wal`),不是只写了注释。 - location/poo 的读写在单库下行为不变(端点仍返回 200、行落库)。 - 没有遗留指向已删模块的死 import;没有把业务逻辑顺手改了。 - `get_db` 现在产出的是 app 库 session(不是旧 location 库)。 --- ### M1-T04 — 收敛启动链路:lifespan + run_migrations,删除 location/poo adopt 脚本 - **Status**: `todo` - **Depends**: M1-T03 - **Context**: 单库后只需保证 app 库就绪;location/poo 的 adoption 链路整条退役。 **Files** - `modify app/main.py`(`ensure_runtime_dirs` 只建 app 路径;删 `ensure_location_db_ready`/`ensure_poo_db_ready` 及其调用与 import) - `modify scripts/run_migrations.py`(只 `adopt_or_initialize_app_db`,返回 `{"app": ...}`) - `delete scripts/location_db_adopt.py` - `delete scripts/poo_db_adopt.py` - `delete alembic_location.ini`、`alembic_location/`(含 env.py、versions) - `delete alembic_poo.ini`、`alembic_poo/` - `modify tests/test_deployment.py`(`run_all_migrations` 期望值改为单 `{"app": ...}`;删/改 legacy location/poo 迁移断言;保留"app DB 不存在则 fail-closed"用例) - `modify tests/test_location.py`、`tests/test_poo.py`(删除针对已删 adopt 脚本的 adoption 测试;保留端点行为测试) - `modify tests/conftest.py`(删 `_make_alembic_config`/`_make_poo_alembic_config`/`ready_location_database`/`ready_poo_database` 等已无意义的 fixture) **Steps** 1. `app/main.py`:移除 `from scripts.location_db_adopt ...` / `poo_db_adopt` import;删两个 `ensure_*_db_ready` 函数及 lifespan 中调用;`ensure_runtime_dirs` 只处理 `settings.app_sqlite_path`。 2. `scripts/run_migrations.py`:`run_all_migrations` 只返回 app 一项。 3. 删除两套 adopt 脚本与两套 alembic 环境/ini。 4. 测试:把 `test_migration_runner_*` 改成单库口径;删掉引用已删脚本常量(`LOCATION_BASELINE_REVISION` 等)的用例。 **Out of scope / 不要碰** - 不动 `scripts/app_db_adopt.py` 的核心逻辑(仅 T01 已更新其常量)。 - 不动数据搬迁脚本(T02)。 **Acceptance criteria** - [ ] `grep -rnE "location_db_adopt|poo_db_adopt|alembic_location|alembic_poo" app scripts tests | grep -v __pycache__` 为空。 - [ ] 仓库不再有 `alembic_location*` / `alembic_poo*` 文件。 - [ ] `python -m scripts.run_migrations` 在全新临时 app 库上成功初始化(含 location/poo_records 表)。 - [ ] 校验闸门全绿。 **Reviewer checklist** - lifespan 仍对 app 库 fail-closed(缺库时明确报错),未弱化启动安全。 - 没有残留对已删 alembic 环境的引用(包括 `.ini` 路径字符串)。 --- ### M1-T05 — config 去除 location/poo URL 与路径,清理配置页与测试硬编码 - **Status**: `todo` - **Depends**: M1-T03 - **Context**: 配置层只剩 `app_database_url`,运行时不再有 location/poo 库概念。 **Files** - `modify app/config.py`(删 `location_database_url`/`poo_database_url` 字段与 `location_sqlite_path`/`poo_sqlite_path` computed 属性) - `modify app/services/config_page.py`(配置页 sections 移除 `location_database_url`/`poo_database_url` 展示项) - `modify .env.example`(移除两行 legacy DB URL;保留 `APP_DATABASE_URL`) - `modify tests/test_config.py`(删对两个 URL/路径的断言) - `modify tests/test_public_ip.py`、`tests/test_smtp.py`(构造 `Settings` 时去掉 location/poo url 入参) - `modify tests/conftest.py`(`test_database_urls` 不再 set `LOCATION_DATABASE_URL`/`POO_DATABASE_URL`) **Out of scope / 不要碰** - 不动 `migrate_legacy_data.py`(它自带读旧库路径的能力,与 `Settings` 解耦)。 - 不改其它配置项(SMTP / TickTick / HA 等)。 **Acceptance criteria** - [ ] `grep -rnE "location_database_url|poo_database_url|location_sqlite_path|poo_sqlite_path" app tests | grep -v __pycache__` 为空。 - [ ] 配置页渲染不再出现 location/poo DB URL 字段。 - [ ] 校验闸门全绿。 **Reviewer checklist** - 没有别的代码还假设 `Settings` 上存在这两个属性(运行期不会 AttributeError)。 --- ### M1-T06 — 移除 Grafana - **Status**: `todo` - **Depends**: none(可与 T01 并行) - **Context**: 可视化将由 M2 的 React 承担;Grafana 直接删除,不再 re-point。 **Files** - `modify docker-compose.yml`(删 `grafana` service 及其 `depends_on`/挂载;删顶层 `volumes.homeautomation_grafana_storage`) - `delete grafana/`(`provisioning/`、`dashboards/` 全部) - `modify tests/test_deployment.py`(若有针对 grafana service 的断言则同步移除) - `modify README.md`(删"Grafana Provisioning"整节——也可并入 T07,二选一,避免重复改同段) **Out of scope / 不要碰** - **不在脚本里删除** named volume `homeautomation_grafana_storage` 的实际数据卷——这是人工 ops 步骤(见 §6),compose 里移除声明即可。 - 不动 app/migration service。 **Acceptance criteria** - [ ] `docker-compose.yml` 不再含 `grafana` 与 `homeautomation_grafana_storage`。 - [ ] 仓库不再有 `grafana/` 目录。 - [ ] `docker compose config` 能成功解析(语法有效)。 - [ ] 校验闸门全绿。 **Reviewer checklist** - 没有遗留对 `./grafana/...` 挂载路径的引用。 - 没有顺手删 `./data` 卷或改动 app service 端口/卷。 --- ### M1-T07 — 文档与 OpenAPI 收尾 - **Status**: `todo` - **Depends**: M1-T03, M1-T04, M1-T05 - **Context**: 让文档反映单库现实,并把"前后端不分离 / 三库不合并 / Grafana"约束在 architecture 文档中正式退役。 **Files** - `modify README.md`(三库 → 单库;删 location/poo DB 初始化与 adopt 说明;更新"运行测试"段落使其与实际测试一致) - `modify docs/architecture-overview.md`(退役"三库不合并";location/poo Alembic 链合并说明) - `modify docs/roadmap.md`(勾掉 M1 范围项) - `run python scripts/export_openapi.py` 并提交 `openapi/` 变更(location/poo 路由依赖在 T03 改过,schema 可能变化) **Acceptance criteria** - [ ] README / architecture 不再描述 location/poo 独立库与 adopt 脚本。 - [ ] `python scripts/export_openapi.py` 后 `git diff --exit-code openapi/` 无未提交差异。 - [ ] 校验闸门全绿。 **Reviewer checklist** - 文档无残留的旧命令(`location_db_adopt.py` 等)。 - OpenAPI 已重导出且入库。 --- ## 6. 人工操作 runbook(生产切换,不进自动化任务) 按数据安全红线,下列步骤由人执行,**不**写进 implementer 任务: 1. **备份**:停服前复制 `data/app.db`、`data/locationRecorder.db`、`data/pooRecorder.db` 到带时间戳的归档目录。 2. **演练**:把上述备份恢复到 scratch 目录,先在副本上跑完整流程(升级 + `migrate_legacy_data.py --dry-run` 再实跑),核对行数。 3. **部署新镜像**:新镜像的 migration job 会把 app 库升级到新 head,建出空的 `location` / `poo_records`。 4. **搬数据**:在生产机运行 `python scripts/migrate_legacy_data.py`(指向归档前的旧库),核对对账输出。 5. **验证**:app 起来后确认 location/poo 端点与历史查询正常、行数与旧库一致。 6. **(事后,确认无误再做)撤旧库**:归档旧 `.db` 文件、删除 `homeautomation_grafana_storage` 卷。**这一步人工、可回退地保留归档,永不在脚本中自动执行。** ## 7. 里程碑完成定义(Definition of Done) - 运行期只存在 `app.db` 一个库、一个 engine、一个 `Base`、一条 Alembic 链。 - `grep` 不到任何 `auth_db` / `poo_db` / location 独立库 / adopt 脚本 / grafana 的残留引用。 - 旧库历史数据已通过 `migrate_legacy_data.py` 搬入且对账通过。 - `pytest`、`ruff check .`、`export_openapi` 全绿且 `openapi/` 已入库。 - README / architecture / roadmap 反映单库现实。