Files
2026-moving-helper/README.md
T
tliu93 ed1e3311a5
test / pytest (push) Successful in 37s
Add minimal installable PWA support
- serve manifest and service worker from the app root for install compatibility
- add manifest metadata, service worker registration, and Apple touch icon links to the base template
- add install icon assets for Android, iOS, and desktop install flows
- document deployment and validation notes for the new PWA support
- cover the new endpoints and template output with tests
2026-04-23 15:23:20 +02:00

708 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 全局搜索
- 最小 PWA 安装支持(主屏幕 / 桌面安装)
- Docker / Compose 长期运行
- SQLite 数据持久化
- 基础自动化测试
## PWA 安装支持
当前版本在不改变 FastAPI + Jinja2 SSR 结构的前提下,补充了最小可维护的 PWA 能力:
- 提供根路径 `manifest.webmanifest`
- 提供根路径 `service-worker.js`
- 在基础模板中注入 `manifest``theme-color``apple-touch-icon` 和安装相关 meta
- 支持 Android Chrome 添加到主屏幕
- 支持 iPhone Safari 添加到主屏幕
- 支持桌面 Chrome / Edge 安装为独立 app 窗口
当前新增的安装图标尺寸:
- `180x180`Apple touch icon
- `192x192`Android / Chromium 安装图标
- `512x512`:高分辨率安装图标
- `512x512`maskable 图标
## 当前数据模型
这个项目不是无限树结构,而是固定最多 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
│ ├── install.sh
│ └── nginx
│ └── moving-helper.nginx.template
├── tests
├── .dockerignore
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── pytest.ini
├── README.md
└── requirements.txt
```
## 轻量配置
项目通过环境变量支持以下部署时真正需要关心的配置:
- `HOST_DOMAIN`
- `SSL_PATH`
- `APP_DIR`
- `BACKUP_DIR`
- `BACKUP_REMOTE`
- `APP_PORT`
- `DATA_DIR`
- `DATABASE_URL`
- `COMPOSE_PROJECT_NAME`
推荐从示例文件开始:
```bash
cp .env.example .env
```
默认值如下:
```env
HOST_DOMAIN=moving-helper.lan
SSL_PATH=/etc/acme.sh/$HOST_DOMAIN
APP_DIR=$HOME/.local/share/moving-helper
BACKUP_DIR=$HOME/.local/backup/moving-helper
BACKUP_REMOTE=
APP_PORT=10000
DATABASE_URL=sqlite:////app/data/app.db
DATA_DIR=./data
COMPOSE_PROJECT_NAME=moving-helper
```
说明:
- 容器内应用固定监听 `0.0.0.0:10000`
- `APP_PORT` 只控制宿主机暴露端口,nginx 默认反代到这个端口
- `APP_DIR` 是安装脚本复制 compose、`.env`、备份脚本等运行资产的目标目录
- `DATA_DIR` 默认为相对路径 `./data`,安装后会相对于 `APP_DIR` 解析
- `SSL_PATH` 由用户自行准备证书目录,安装脚本不会签发证书
- `.env` 会被 shell 脚本直接 `source`,请保持 shell 兼容写法
## 本地开发模式
推荐使用本地 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
```
本地开发验证 PWA 时,页面与安装元数据可以直接检查;如果要完整验证桌面安装体验,优先在 HTTPS 或受信任反向代理环境下测试。
## PWA 部署注意事项
- 生产环境应使用 HTTPSAndroid Chrome 和桌面 Chrome / Edge 的安装能力通常要求安全上下文
- `manifest.webmanifest` 需要返回 `application/manifest+json`
- `service-worker.js` 需要从站点根路径返回,保证作用域覆盖整个应用
- 如果前面有 nginx 或其他反向代理,不要拦截或改写这两个根路径资源
- iPhone Safari 的“添加到主屏幕”主要依赖 meta 和 `apple-touch-icon`,不包含离线能力
## PWA 简单验收
1. Android Chrome:打开站点,确认浏览器菜单或地址栏出现“添加到主屏幕”或“安装应用”。
2. iPhone Safari:打开站点,点击分享菜单,确认可见“添加到主屏幕”。
3. Desktop Chrome / Edge:打开站点,确认地址栏或菜单中出现“安装应用”。
本地默认数据库位置:
```text
./data/app.db
```
## Docker 运行方式
Docker / Compose 是这个项目面向长期运行环境的方式。
当前 compose 同时保留了:
- `image`:固定指向 `code.wanderingbadger.dev/tliu93/2026-moving-helper:latest`
- `build`:用于本地开发时从当前代码构建镜像
当前部署约定已经收敛为:
- 容器内应用固定监听 `0.0.0.0:10000`
- compose 固定使用 `user: 1000:1000`
- 宿主机仅在 `127.0.0.1:${APP_PORT}` 暴露后端端口
- SQLite 固定写入容器内 `/app/data/app.db`
### 首次准备
```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` 保持尽量简单:
- 固定镜像地址为 `code.wanderingbadger.dev/tliu93/2026-moving-helper:latest`
- 宿主机默认仅在 `127.0.0.1:10000` 暴露容器 `10000`
- `restart: unless-stopped`
- 容器固定使用 `1000:1000`
- 宿主机 `DATA_DIR` 挂载到容器内 `/app/data`
- SQLite 默认写入 `/app/data/app.db`
因此同一个 compose 文件可以覆盖两种使用方式:
- 本地开发容器:`docker compose up -d --build`
- 远端部署发布镜像:`docker compose pull && docker compose up -d`
## 自动化部署
这个项目现在额外提供一个面向本地网络环境的最小安装脚本:
```bash
sh scripts/install.sh
```
安装脚本会执行:
1. 检查项目根目录下是否存在 `.env`
2. 读取 `.env`
3.`docker-compose.yml``.env` 和渲染后的 `backup_db.sh` 复制到 `APP_DIR`
4.`HOST_DOMAIN``SSL_PATH``APP_PORT` 渲染 nginx 配置
5. 写入 `/etc/nginx/sites-available/moving-helper-nginx`
6. 创建到 `/etc/nginx/sites-enabled/` 的符号链接
7. 执行 `nginx -t` 并 reload nginx
8.`APP_DIR` 下执行 `docker compose pull``docker compose up -d`
9. 为当前用户写入每日 `02:10` 的 backup cron
其中以下步骤需要 root 或 sudo:
- 写入 nginx 配置
- 执行 `nginx -t`
- reload nginx
如果 `.env` 不存在,脚本会直接退出,不会继续做任何安装动作。
如果你只想在仓库目录里做一次手动更新,也保留了一个轻量部署脚本:
```bash
./scripts/deploy.sh
```
它会按顺序执行:
1. 检查 `.env`
2. 准备数据目录
3. 如果当前目录是 git 仓库,执行 `git pull --ff-only`
4. 执行 `docker compose pull web`
5. 执行 `docker compose up -d`
5. 输出容器状态
6. 输出最近日志
这个脚本的目标不是做平台化发布,而是让“更新代码并刷新容器”变成一个稳定、可重复执行的动作。
如果你不想自动拉代码,也可以直接手动运行:
```bash
docker compose pull
docker compose up -d
```
## 数据持久化
当前 SQLite 文件默认会保存在:
- 宿主机:`./data/app.db`
- 容器内:`/app/data/app.db`
这是因为 `docker-compose.yml` 把:
```text
${DATA_DIR:-./data}
```
挂载到了容器内:
```text
/app/data
```
因此:
- 容器重建不会删除宿主机上的数据库文件
- 更新镜像不会导致 SQLite 数据丢失
- 只要保留 `DATA_DIR` 目录,数据就还在
## 备份与恢复
### 备份机制
安装脚本会把渲染后的备份脚本安装到:
- `APP_DIR/backup_db.sh`
并为当前用户创建一条 cron
- `10 2 * * *`
备份行为如下:
- 目标目录是 `BACKUP_DIR`
- 备份文件名带时间戳,例如 `app-20260421-021000.db`
- 最多保留 5 个本地备份
- 如果 `BACKUP_REMOTE` 非空,会在本地备份完成后调用 `rclone copyto`
SQLite 一致性策略:
- 备份脚本优先使用 `sqlite3``.backup`
- 不停容器
- 不直接 `cp` 正在写入的数据库文件
这样可以在应用仍然运行时生成事务一致的快照,避免简单文件复制带来的损坏风险。
### 手动执行备份
如果你想手动触发一次备份:
```bash
sh "$APP_DIR/backup_db.sh"
```
如果当前还没有执行安装脚本,也可以在仓库内手动准备 `.env` 后运行:
```bash
./scripts/backup_db.sh
```
前提是先通过安装脚本把它渲染并部署到 `APP_DIR`,因为仓库内版本本身是带占位符的模板。
### 恢复大致步骤
停止容器后,把备份文件覆盖回去:
```bash
cd "$APP_DIR"
docker compose stop
cp "$BACKUP_DIR/app-YYYYMMDD-HHMMSS.db" "${DATA_DIR:-./data}/app.db"
docker compose up -d
```
如果 `DATA_DIR` 是相对路径,记得在 `APP_DIR` 下执行这些命令。
### nginx 与证书约定
仓库提供的 nginx 模板位于:
- `scripts/nginx/moving-helper.nginx.template`
安装脚本会把它渲染成 Debian / Ubuntu 风格的站点配置:
- `/etc/nginx/sites-available/moving-helper-nginx`
- `/etc/nginx/sites-enabled/moving-helper-nginx`
模板约定:
- 80 端口强制跳转到 443
- 443 默认启用 SSL
- 反代到仅绑定在本机回环地址上的 `127.0.0.1:${APP_PORT}`
- `client_max_body_size 0`
证书文件需要由用户自己准备在 `SSL_PATH` 下,当前模板默认引用:
- `fullchain.pem`
- `privkey.key`
## 常见排查
### 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`
### CIbranch 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
- 不要求额外配置测试环境变量
### CDtag 发布 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 时会提示已跳过
图片后续可以在应用里手动补录。