# Moving Helper 这是一个面向可信家庭内网环境的小型搬家记录工具,当前采用轻量技术栈: - FastAPI - Jinja2 服务端渲染 - SQLAlchemy - SQLite - Pillow - pytest / FastAPI TestClient - Docker / Docker Compose 项目目标是小而稳、容易继续扩展。它不是企业平台,也不是复杂运维系统,重点是本地开发简单、容器部署稳定、数据持久化清楚、后续几个月后自己回来看也能快速接上。 ## 当前已支持 - 固定 3 级结构:`Box -> Item -> SubItem` - Box / Item / SubItem 基础 CRUD - Box / Item / SubItem 单图上传、替换、删除、展示 - Box / Item / SubItem 全局搜索 - Docker / Compose 长期运行 - SQLite 数据持久化 - 基础自动化测试 ## 当前数据模型 这个项目不是无限树结构,而是固定最多 3 级: - `Box` - `Item` - `SubItem` 关系如下: - 一个 `Box` 包含多个 `Item` - 一个 `Item` 属于一个 `Box` - `Item` 通过 `is_container` 区分是否为“小容器” - 只有 `is_container = true` 的 `Item` 才允许拥有 `SubItem` - `SubItem` 是最后一级,不允许继续向下嵌套 结构固定为: ```text Box └── Item └── SubItem ``` ## 图片能力说明 图片系统保持简单直接: - `Box` 最多支持 1 张图片 - `Item` 最多支持 1 张图片 - `SubItem` 最多支持 1 张图片 - 支持上传、替换、删除 - 不支持多图 图片主要用于帮助识别物品、提高浏览效率、方便手机拍照后直接附加到记录中。它不是原图归档系统。 上传图片后,系统会使用 Pillow 做统一处理: - 读取上传图片 - 去除 EXIF 元数据 - 转换为 JPEG - 按最长边缩放到不超过 `1600px` - 使用约 `80` 质量保存 - 将处理后的 JPEG 二进制直接写入 SQLite `BLOB` 同时还会记录: - `image_mime_type` - `image_width` - `image_height` 图片访问路由例如: - `/boxes/{id}/image` - `/items/{id}/image` - `/subitems/{id}/image` ## 全局搜索 当前支持一个轻量的全局搜索页: - 路由:`/search` - 方式:`GET /search?q=关键词` 搜索范围包括: - `Box.name` - `Box.note` - `Item.name` - `Item.note` - `SubItem.name` - `SubItem.note` 当前使用 SQLite 上的简单模糊匹配,不引入外部搜索引擎或复杂全文系统。 搜索结果会显示: - 对象类型:`Box / Item / SubItem` - 名称和备注 - 归属路径 - 对 `Item` 展示所属 `Box` - 对 `SubItem` 展示所属 `Item` 和 `Box` - 如果对象已有图片,会显示一个小缩略图 ## 当前未实现 这一阶段仍然没有实现以下内容: - 多图上传 - OCR - AI 识别物品 - 图片标签 - 图片分类 - 登录 / 鉴权 - 标签系统 - 前后端分离 - 复杂 UI ## 项目结构 ```text . ├── app │ ├── __init__.py │ ├── config.py │ ├── db.py │ ├── images.py │ ├── main.py │ ├── models.py │ ├── static │ │ └── style.css │ └── templates ├── data ├── scripts │ ├── backup_db.sh │ └── deploy.sh ├── tests ├── .dockerignore ├── .env.example ├── docker-compose.yml ├── Dockerfile ├── pytest.ini ├── README.md └── requirements.txt ``` ## 轻量配置 项目通过环境变量支持以下部署时真正需要关心的配置: - `HOST` - `PORT` - `DATABASE_URL` - `DATA_DIR` - `UID` - `GID` 推荐从示例文件开始: ```bash cp .env.example .env ``` 默认值如下: ```env HOST=0.0.0.0 PORT=10000 DATABASE_URL=sqlite:////app/data/app.db DATA_DIR=./data UID=1000 GID=1000 ``` 说明: - 本地开发默认数据库仍然是 `./data/app.db` - Docker 内建议继续使用 `sqlite:////app/data/app.db` - `DATA_DIR` 控制宿主机上的持久化目录 - `UID/GID` 用来让容器内文件权限更贴近宿主机用户 ## 本地开发模式 推荐使用本地 Python `venv` 开发和调试。 ### 1. 创建虚拟环境 ```bash python3 -m venv .venv source .venv/bin/activate ``` ### 2. 安装依赖 ```bash pip install -r requirements.txt ``` ### 3. 启动开发服务器 ```bash uvicorn app.main:app --reload --host 0.0.0.0 --port 10000 ``` 访问: ```text http://localhost:10000 ``` 本地默认数据库位置: ```text ./data/app.db ``` ## Docker 运行方式 Docker / Compose 是这个项目面向长期运行环境的方式。 当前 compose 同时保留了: - `image`:固定指向 `code.wanderingbadger.dev/tliu93/2026-moving-helper:latest` - `build`:用于本地开发时从当前代码构建镜像 ### 首次准备 ```bash cp .env.example .env mkdir -p data ``` ### 启动 / 更新:本地代码构建 ```bash docker compose up -d --build ``` 这个模式会使用当前仓库代码重新构建镜像,适合本地开发、调试或尚未发布 tag 的阶段。 ### 启动 / 更新:直接拉取已发布镜像 ```bash docker compose pull docker compose up -d ``` ### 查看状态 ```bash docker compose ps ``` ### 查看日志 ```bash docker compose logs -f web ``` 访问: ```text http://localhost:10000 ``` ### Compose 配置说明 当前 `docker-compose.yml` 保持尽量简单: - 默认镜像地址来自 `REGISTRY_HOST / IMAGE_NAME / IMAGE_TAG` - 默认暴露 `10000` 端口 - `restart: unless-stopped` - 容器用户来自 `UID:GID` - 宿主机 `DATA_DIR` 挂载到容器内 `/app/data` - SQLite 默认写入 `/app/data/app.db` 因此同一个 compose 文件可以覆盖两种使用方式: - 本地开发容器:`docker compose up -d --build` - 远端部署发布镜像:`docker compose pull && docker compose up -d` ## 自动化部署 这个项目没有复杂 CI/CD,只提供一个适合家用项目的轻量部署脚本: ```bash ./scripts/deploy.sh ``` 它会按顺序执行: 1. 检查 `.env` 2. 准备数据目录 3. 如果当前目录是 git 仓库,执行 `git pull --ff-only` 4. 执行 `docker compose up -d --build` 5. 输出容器状态 6. 输出最近日志 这个脚本的目标不是做平台化发布,而是让“更新代码并刷新容器”变成一个稳定、可重复执行的动作。 如果你不想自动拉代码,也可以直接手动运行: ```bash docker compose up -d --build ``` ## 数据持久化 当前 SQLite 文件默认会保存在: - 宿主机:`./data/app.db` - 容器内:`/app/data/app.db` 这是因为 `docker-compose.yml` 把: ```text ${DATA_DIR:-./data} ``` 挂载到了容器内: ```text /app/data ``` 因此: - 容器重建不会删除宿主机上的数据库文件 - 更新镜像不会导致 SQLite 数据丢失 - 只要保留 `DATA_DIR` 目录,数据就还在 ## 备份与恢复 ### 最简单的备份方式 直接复制 SQLite 文件即可: ```bash cp data/app.db backups/app.db ``` 或者使用附带脚本: ```bash ./scripts/backup_db.sh ``` 它会在 `backups/` 目录下生成一个带时间戳的副本。 ### 最简单的恢复方式 停止容器后,把备份文件覆盖回去: ```bash docker compose stop cp backups/app-YYYYMMDD-HHMMSS.db data/app.db docker compose up -d ``` ## 常见排查 ### 1. 查看容器日志 ```bash docker compose logs -f web ``` ### 2. 确认服务是否已启动 ```bash docker compose ps ``` 如果状态是 `Up`,通常说明容器已经跑起来了。 ### 3. 确认端口映射 ```bash docker compose port web 10000 ``` ### 4. 确认数据库文件还在 ```bash ls -lh data ``` 如果看到 `app.db`,说明宿主机持久化文件还在。 ### 5. 容器更新后数据为什么没丢 因为数据库不放在镜像里,而是放在宿主机挂载目录 `DATA_DIR` 中。 镜像更新只会替换应用代码和运行环境,不会覆盖这个宿主机目录。 ## 测试 运行测试: ```bash python -m pytest ``` 测试使用独立测试数据库,不会污染真实开发数据。 当前测试覆盖包括: - Box / Item / SubItem 基础 CRUD - 图片上传、替换、删除与错误路径 - 全局搜索 name / note - 创建后的重定向行为 - 关键页面结构和 UX 文案 ## CI / CD 仓库现在包含两条基础自动化流程,文件位于: - `.github/workflows/test.yml` - `.github/workflows/docker-image.yml` ### CI:branch push 自动跑 pytest `test.yml` 会在任意 branch 的 `push` 上执行: 1. checkout 代码 2. 使用 Python `3.12` 3. 安装 `requirements.txt` 4. 运行 `pytest` 当前测试不依赖外部数据库服务。 测试使用 `tmp_path` 创建独立 SQLite 文件,并通过 `configure_database(...)` 切换到临时数据库,因此: - 不会污染 `./data/app.db` - 不要求额外启动 Docker 或 Compose - 不要求额外配置测试环境变量 ### CD:tag 发布 Docker image `docker-image.yml` 会在推送符合 `v*` 格式的 tag 时触发,例如: - `v1.0.0` - `v1.2.3` workflow 会先校验该 tag 指向的提交是否可从 `origin/main` 到达;只有满足这个条件的 tag 才会继续构建并推送镜像。 镜像发布目标: - Registry Host: `code.wanderingbadger.dev` - Image Name: `${{ github.repository }}` - Platforms: - `linux/amd64` - `linux/arm64` 推送的 tag 策略: - `${tag}` - `latest` 例如仓库名为 `tliu93/2026-moving-helper`,打出 `v1.0.0` 后会推送: ```text code.wanderingbadger.dev/tliu93/2026-moving-helper:v1.0.0 code.wanderingbadger.dev/tliu93/2026-moving-helper:latest ``` ### Actions / Gitea Secrets 需要在仓库的 Actions secrets 中配置: - `REGISTRY_USERNAME` - `REGISTRY_TOKEN` 推荐含义: - `REGISTRY_USERNAME`: Gitea 用户名 - `REGISTRY_TOKEN`: 具备 Container Registry 推送权限的 Access Token 如果你的 Gitea 实例对 package / registry 权限做了单独控制,确保这个 token 至少具备对应仓库的镜像推送权限。 ### 如何触发镜像发布 建议流程: ```bash git checkout main git pull --ff-only origin main git tag v1.0.0 git push origin main --tags ``` 如果只想推送单个 tag: ```bash git push origin v1.0.0 ``` ### 本地手动构建镜像 单架构本地构建: ```bash docker build -t moving-helper:local . ``` 本地运行: ```bash docker run --rm -p 10000:10000 \ -e DATABASE_URL=sqlite:////app/data/app.db \ moving-helper:local ``` 如果要模拟发布时的多架构构建,可以使用 buildx: ```bash docker buildx build \ --platform linux/amd64,linux/arm64 \ -t code.wanderingbadger.dev/${USER}/2026-moving-helper:test \ --load \ . ``` ## 一次性 Notion 导入 项目内附带了一个一次性迁移脚本: ```bash python scripts/import_notion.py --dry-run python scripts/import_notion.py --apply ``` 说明: - 这是一次性 migration / import 工具,不是长期同步功能 - 运行时会交互要求输入: - Notion API token - Notion 页面完整 URL - `--dry-run` 只读取和解析,不写数据库 - `--apply` 会真正写入当前 SQLite 数据库 - 建议导入前先备份 `data/app.db` ### 当前支持的 Notion 结构映射 - `heading_2` -> `Box` - 某个 `heading_2` 下的一级 bullet -> `Item` - 如果一级 bullet 下还有二级 bullet: - 一级 bullet -> 容器型 `Item` - 二级 bullet -> `SubItem` 当前最大只处理到这个层级: ```text heading_2 └── 一级 bullet └── 二级 bullet ``` 更深层级会在日志中提示,但不会继续扩展成无限树。 ### 这一版不导入图片 这一版导入脚本: - 不下载图片 - 不导入图片 - 遇到图片或其他媒体 block 时会提示已跳过 图片后续可以在应用里手动补录。