Files
2026-moving-helper/docs/repository-brief.md
T
tliu93 8b8bd9f38f
test / pytest (push) Successful in 1m34s
Add Alembic migration foundation
2026-06-01 16:02:43 +02:00

18 KiB
Raw Blame History

仓库简报 / Repository Brief — 2026 搬家助手 (Moving Helper)

面向「下一轮改动」前的快速理解文档。中英双语对照。 A bilingual orientation doc to read before the next round of changes.

对应版本 / Snapshot: main @ b9b6583(撰写时 / at time of writing


1. 一句话定位 / In One Sentence

中文: 一个轻量的、面向可信家庭内网的搬家装箱记录工具:记录「有哪些箱子、每个箱子里有什么物品、容器型物品里又装了什么」,支持单图、全局搜索,并以 Docker 长期运行。

EN: A lightweight, trusted-home-LAN moving inventory tool: track which boxes exist, what items each box holds, and what sub-items sit inside container-type items. Supports one image per record, global search, and runs long-term via Docker.

设计取向 / Design stance:小而稳、易于几个月后回来继续扩展;不是企业平台、追求复杂运维。 Small, stable, easy to pick back up months later; not an enterprise platform.


2. 技术栈 / Tech Stack

层 / Layer 选型 / Choice 版本 / Version
Web 框架 / Framework FastAPI 0.116.1
ASGI 服务器 / Server Uvicorn ([standard]) 0.35.0
模板 / Templating Jinja2(服务端渲染 SSR 3.1.6
ORM SQLAlchemy 2.xMapped / mapped_column 风格) 2.0.43
数据库 / DB SQLite(文件库 / file-based
表单 / Forms python-multipart 0.0.20
图片处理 / Images PillowHEIC 时可选 pillow_heif / sips 兜底) 11.2.1
HTTP 客户端 / Client requests(仅 Notion 导入用) 2.32.3
测试 / Tests pytest + Starlette TestClient(httpx) 8.4.1 / 0.28.1
部署 / Deploy Docker / Docker Compose + nginx 反代

没有前端构建链 / No frontend build chain:纯 SSR + 一个 style.css + base.html 里的少量原生 JS。无 npm / Node / 打包器。 Pure SSR + one style.css + a little vanilla JS inline in base.html. No npm/Node/bundler.


3. 目录结构 / Project Layout

.
├── app/
│   ├── __init__.py
│   ├── config.py            # 环境变量配置 (Settings dataclass)
│   ├── db.py                # SQLAlchemy engine/session、init_db、SQLite 轻量迁移
│   ├── images.py            # 图片处理管线 (Pillow + HEIC 兜底)
│   ├── main.py              # 所有路由 + 应用工厂 create_app()  ← 核心
│   ├── models.py            # Box / Item / SubItem 三个 ORM 模型
│   ├── notion_import.py     # 一次性 Notion 导入的解析/写入逻辑
│   ├── static/              # style.css, manifest, service-worker.js, PWA 图标
│   └── templates/           # Jinja2 模板 (boxes/ items/ subitems/ search/ + base.html)
├── scripts/
│   ├── install.sh           # 一键安装:渲染 nginx/backup/compose + cron
│   ├── deploy.sh            # 仓库内的轻量更新脚本 (git pull + compose up)
│   ├── backup_db.sh         # 备份脚本模板 (占位符由 install.sh 渲染)
│   ├── import_notion.py     # Notion 导入 CLI 入口
│   └── nginx/moving-helper.nginx.template
├── tests/                   # pytesttest_app.py (~74) + test_notion_import.py (9)
├── data/                    # 运行期 SQLite (data/app.db),已 gitignore
├── .github/workflows/       # test.yml (CI) + docker-image.yml (CD)
├── Dockerfile, docker-compose.yml, .dockerignore
├── .env.example             # 部署配置示例(被 shell 脚本 source
├── pytest.ini, requirements.txt, README.md
└── docs/                    # ← 本文档所在

关键入口 / Key entry point app/main.py 里的 create_app() 注册了全部路由。整个后端逻辑几乎都在这一个文件里。 Almost all backend logic lives in the single create_app() factory in app/main.py.


4. 数据模型 / Data Model

固定三级层次,不是无限树 / A fixed 3-level hierarchy, not an arbitrary tree

Box  (顶层容器 / top container:纸箱、行李箱…)
└── Item  (箱子里的物品 / an item in the box)
    └── SubItem  (仅容器型 Item 才有 / only under container items)

定义于 app/models.py

模型 / Model 表 / Table 关键字段 / Key fields 关系 / Relations
Box boxes name, note, room, status + 图片字段 + 时间戳 items → 多个 Itemcascade="all, delete-orphan"
Item items box_id(FK), name, note, quantity, is_container + 图片字段 + 时间戳 属于一个 Boxsubitems → 多个 SubItem(级联删除)
SubItem subitems parent_item_id(FK), name, note, quantity + 图片字段 + 时间戳 属于一个 Item

核心规则 / Core rules

  • 只有 Item.is_container == True 的物品才允许拥有 SubItem;非容器去建子物品会返回 400_require_container_item)。 Only container items may hold sub-items; otherwise the API returns 400.
  • 在更新 Item 时,若取消勾选 is_container,会清空其所有 SubItemitem.subitems.clear()main.py:454)。 Un-checking is_container on update clears all sub-items.
  • 删除级联:删 Box → 删其 Item → 删其 SubItem,由 ORM cascade + 外键 ondelete="CASCADE" 双重保障(SQLite 还启用了 PRAGMA foreign_keys=ON)。 Delete cascades top-down, enforced both by ORM cascade and FK ondelete.

图片字段(每个模型都有同一组)/ Image fields (same set on every model) image_blob (BLOB)、image_mime_typeimage_widthimage_height。 → 图片直接以二进制存进 SQLite,不落地为文件。Images are stored inline as BLOBs in SQLite, not as files.

时间戳 / Timestampscreated_atupdated_atUTCupdated_atonupdate)。


5. 路由总览 / Route Map

全部在 app/main.py。POST 后统一 303 See Other 重定向(避免重复提交)。 All in app/main.py. POSTs redirect with 303 See Other.

方法 路径 / Method Path 作用 / Purpose
GET / 302 跳转到 /boxes
GET /manifest.webmanifest, GET /service-worker.js PWA 资源(从根路径返回)
GET /search?q= 全局搜索页
GET /boxes 箱子列表 + 概览统计
GET /boxes/new · POST /boxes 新建箱子表单 / 提交
GET /boxes/{id} 箱子详情(含其 Item 列表)
GET /boxes/{id}/edit · POST /boxes/{id}/update 编辑 / 保存
POST /boxes/{id}/delete 删除箱子
GET /boxes/{id}/image · POST /boxes/{id}/image/delete 取图 / 删图
GET /boxes/{id}/items/new · POST /boxes/{id}/items 在箱子下新建物品
GET /items/{id} 物品详情(容器型则含 SubItem 列表)
GET /items/{id}/edit · POST /items/{id}/update · POST /items/{id}/delete 编辑 / 保存 / 删除
GET /items/{id}/image · POST /items/{id}/image/delete 取图 / 删图
GET /items/{id}/subitems/new · POST /items/{id}/subitems 在容器型物品下新建子物品
GET /subitems/{id}/edit · POST /subitems/{id}/update · POST /subitems/{id}/delete 编辑 / 保存 / 删除
GET /subitems/{id}/image · POST /subitems/{id}/image/delete 取图 / 删图

重定向行为细节 / Redirect nuancesmain.py 创建逻辑):

  • 创建物品时,若点「保存并添加下一个」(submit_action=save_and_add_next) → 回到新建表单;若是容器型 → 跳到物品详情;否则 → 回箱子详情。 On create: save & add next → back to new form; container → item detail; else → box detail.
  • 子物品的「保存并添加下一个」同理回到子物品新建表单。 Sub-item save & add next returns to its new-form too.

没有 JSON API / no JSON API:全部返回 HTML(图片路由返回二进制)。FastAPI 的自动 /docs 仍可用,但业务路由均是表单驱动的 SSR。


6. 图片处理管线 / Image Pipeline (app/images.py)

每次上传都会经过 process_upload()_prepare_image() 统一处理: Every upload is normalized through process_upload()_prepare_image():

  1. 读取字节,空内容 → 400;非法图片 → 400。Read bytes; empty/invalid → 400.
  2. 按 EXIF 方向矫正ImageOps.exif_transpose),再处理。Apply EXIF orientation first.
  3. 去元数据并转 RGBRGBA/LA 贴白底、P 转 RGB)。Strip metadata, flatten to RGB.
  4. 最长边缩放到 ≤ 1600pxthumbnail)。Downscale longest side to ≤ 1600px.
  5. 存为 JPEG,质量 80optimize=True。Save as JPEG q80.
  6. 写入 image_blob + 记录 mime / 宽 / 高。Store blob + dimensions.

HEIC/HEIF 兜底 / fallback 先尝试 pillow_heif(若已安装);否则在 macOS 上用 sips 转 JPEG;都不行则返回中文错误提示让用户先转格式。 Tries pillow_heif, then macOS sips, else a clear error asking to convert first.

注意 / Notepillow_heif 不在 requirements.txt 里,所以默认环境 HEIC 依赖系统 sips(仅 macOS)。Linux 容器里上传 HEIC 会得到「请先转换」的提示。

每个对象最多一张图,支持上传 / 替换 / 删除,不支持多图。 One image per object; upload/replace/delete; no multi-image.


7. 全局搜索 / Global Search (_build_search_results, main.py)

  • GET /search?q=关键词,对 Box / Item / SubItemnamenoteSQLite LOWER(...) LIKE %q% 模糊匹配(大小写不敏感)。 Case-insensitive LIKE over name + note across all three types.
  • 结果带:类型标签、归属路径(Item 显示所属 BoxSubItem 显示所属 Item + Box)、若有图则带缩略图链接。 Results include type, location path, and a thumbnail link if an image exists.
  • 无外部搜索引擎、无全文索引。No external search engine / full-text index.

/boxes 概览页另有统计(_build_boxes_overview_summary):箱子数、物品数(含/不含子物品)、每箱平均物品数、每容器型 Item 平均子物品数。


8. PWA 支持 / PWA Support

最小可安装 PWA不改 SSR 结构 / minimal installable PWA without changing SSR

  • 根路径提供 manifest.webmanifest(正确 mime)和 service-worker.js
  • base.html 注入 theme-color、apple-touch-icon、安装相关 meta,并注册 service worker。
  • 图标:180apple-touch)、192、512、512-maskable,位于 app/static/icons/

当前 service worker 仅做 skipWaiting + clients.claim(),没有任何缓存/离线能力。 The service worker only claims clients — no caching, no offline yet.

base.html 里的原生 JS 还实现了:可点击卡片(.clickable-card[data-href],含键盘 Enter/Space 支持)、表单内回车跳到下一个字段。


9. 配置与环境变量 / Configuration

应用运行时 / App runtimeapp/config.pySettings dataclass):

变量 / Var 默认 / Default 说明
DATABASE_URL sqlite:///./data/app.db 数据库连接串
HOST 0.0.0.0 (定义了但 uvicorn 在 CMD 里写死)
PORT 10000 同上

部署时 / Deploy-time.env,被 shell 脚本 source应用直接读取): HOST_DOMAINSSL_PATHAPP_DIRBACKUP_DIRBACKUP_REMOTEAPP_PORTDATA_DIRDATABASE_URLCOMPOSE_PROJECT_NAME。详见 .env.example

约定 / Conventions:容器内固定监听 0.0.0.0:10000APP_PORT 只控制宿主机暴露端口;SQLite 固定写 /app/data/app.db(容器内)。


10. 数据库初始化与迁移 / DB Init & Migrations (app/migrate.py + app/db.py)

  • Alembic 接管 schema:迁移系统由 Alembic 管理(alembic.ini + migrations/),V1 baseline 等于当前三表 schema。 Alembic owns schema creation and changes (alembic.ini + migrations/); V1 baseline equals the current three-table schema.
  • 迁移与启动分离 / Migrations separated from startup
    • init_db()app/db.py)在 FastAPI lifespan 启动时调用 verify_schema_is_current(),只做只读校验——检查 DB 是否在 head,不一致则 fail-close(拒绝启动、不执行任何 DDL)。 init_db() calls verify_schema_is_current() at startup — read-only check, fails closed on mismatch, no DDL.
    • 实际迁移由独立幂等命令 python -m app.migrateapp/migrate.py)执行:空库建表、老库认领(stamp V1 → upgrade head)、已在 head 则空操作。老库 schema 不匹配则 fail-close 不改动。 Actual migration via standalone idempotent command python -m app.migrate: fresh DB → create, matching existing → adopt, already-at-head → no-op, mismatch → fail closed.
  • SQLite 连接开启 PRAGMA foreign_keys=ON
  • 手写列同步 _sync_sqlite_image_columns() 已退休删除。 The hand-rolled _sync_sqlite_image_columns() has been retired and removed.

11. 部署 / Deployment

DockerDockerfile):python:3.12-slim → 装依赖 → 拷贝 app/uvicorn app.main:app --host 0.0.0.0 --port 10000

Composedocker-compose.yml):

  • 镜像固定 code.wanderingbadger.dev/tliu93/2026-moving-helper:latest,同时保留 build: 用于本地构建。
  • user: "1000:1000",仅 127.0.0.1:${APP_PORT}:10000 暴露,${DATA_DIR}:/app/data 持久化,restart: unless-stopped

一键安装scripts/install.sh,需 root/sudo 写 nginx):读 .env → 拷 compose/.env/渲染后的 backup 脚本到 APP_DIR → 渲染并启用 nginx 站点 → nginx -t + reload → docker compose pull && up -d → 写每日 02:10 备份 cron。无 .env 直接退出。

轻量更新scripts/deploy.sh):git pull --ff-onlydocker compose pull webup -d → 打印状态/日志。

nginx 模板scripts/nginx/...):80→443 跳转、443 启用 SSL、反代到 127.0.0.1:${APP_PORT}client_max_body_size 0。证书由用户自备于 SSL_PATHfullchain.pem / privkey.key)。

备份scripts/backup_db.sh,模板带占位符由 install 渲染):用 sqlite3 .backup 取事务一致快照(不停容器),文件名带时间戳,最多保留 5 个BACKUP_REMOTE 非空时 rclone sync 到远端。


12. CI / CD.github/workflows/

  • test.ymlCI:任意分支 push 触发 → Python 3.12 → 装依赖 → pytest。无需外部服务/DB。
  • docker-image.ymlCDv* tag 触发;先校验该 tag 提交可从 origin/main 到达,再 buildx 构建 linux/amd64 + linux/arm64,推 :${tag}:latest
  • 需在仓库 Secrets 配 REGISTRY_USERNAME / REGISTRY_TOKENGitea container registry)。

发布流程 / Releasegit tag vX.Y.Z && git push origin main --tags


13. 测试 / Tests

  • tests/conftest.py:每个测试用 tmp_path 建独立 SQLiteconfigure_database(...) 切换,再 create_app() —— 不污染 data/app.db,无需 Docker。 Each test gets an isolated tmp SQLite; never touches dev data.
  • tests/test_app.py(约 74 个):Box/Item/SubItem CRUD、级联删除、404、图片上传/替换/删除/错误路径、EXIF 矫正、图片路由、搜索(name/note/路径/缩略图)、重定向行为、页面结构与 UX 文案、概览统计。
  • tests/test_notion_import.py9 个):page id 提取、heading/bullet 解析、容器判定、超层级警告、媒体跳过、dry-run 不写库、apply 写库结构。

运行 / Runpython -m pytest


14. Notion 一次性导入 / One-time Notion Import

app/notion_import.py + scripts/import_notion.py(交互式 CLI--dry-run / --apply)。

结构映射 / Mappingheading_2Box;其下一级 bullet → Item;二级 bullet → SubItem(此时父 bullet 自动判为容器型)。更深层级只警告不导入;不导入任何图片/媒体

定位 / Positioning一次性 migration 工具,非长期同步;建议导入前先备份 data/app.dbNOTION_VERSION = "2026-03-11"


15. 已知约束 & 下一轮改动建议 / Constraints & Notes for the Next Round

当前明确「未实现」/ Explicitly out of scope(见 README): 离线缓存/同步、多图、OCR、AI 识别、图片标签/分类、登录鉴权、标签系统、前后端分离、复杂 UI。

改动前值得注意的点 / Things to watch before changing things

  1. 无鉴权 / No auth. 设计前提是「可信内网 + nginx HTTPS」。任何要暴露到公网的改动都需先加访问控制。
  2. 迁移机制薄弱 / Weak migrations(§10. 加新字段到已有库不会自动建列。建议:要么扩展 _sync_sqlite_image_columns 思路(改成更通用的列同步),要么正式引入 Alembic。
  3. 图片存在 SQLite 里 / Images live in the DB. 好处是备份/迁移只需一个文件;代价是库体积随图增长、备份成本上升。若要支持多图或大图归档,应考虑改为对象存储/文件系统 + 路径引用。
  4. 逻辑高度集中在 main.py / Logic concentrated in main.py. 路由、表单解析、查询、统计、搜索都在一个文件。新增大功能时可考虑拆分 router/service 模块,但要保留 create_app() 工厂以维持测试隔离。
  5. Service Worker 是空壳 / SW is a stub. README 写的「PWA」目前不含离线能力;要做离线需真正实现缓存策略。
  6. 固定 3 级层次 / Fixed 3 levels. Box → Item → SubItem 写死在模型、路由、模板、Notion 解析多处;若要变成可嵌套树,是一次跨层改动。
  7. HEIC 在 Linux 容器里不可用 / HEIC fails in Linux containerspillow_heif 未列入依赖,sips 仅 macOS)。若用户多用 iPhone 原图,考虑把 pillow_heif 加进 requirements.txt
  8. UI 全中文、SSR、单 style.css. 前端改动直接编辑 app/templates/*app/static/style.css,无构建步骤。

16. 本地快速启动 / Quick Local Start

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0 --port 10000
# 打开 / open http://localhost:10000  (默认数据库 / default DB: ./data/app.db)
python -m pytest        # 跑测试 / run tests