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