49a5452141
This commit adds the first complete local-network deployment path for the project. It normalizes the runtime contract around a fixed container listener on 0.0.0.0:10000, binds the published compose port to 127.0.0.1, and keeps the image/build workflow aligned with the released container image. It also introduces an installation script, an nginx reverse-proxy template, and a safer SQLite backup flow based on sqlite3 .backup with retention and optional rclone upload support. Deployment-oriented configuration has been consolidated into .env.example, repository-local .env files are now ignored, and the deployment scripts are executable. In addition, the frontend mixed-content issue is fixed by switching the stylesheet reference to a root-relative static path, with tests updated to cover the regression. README guidance has been expanded to document the new install, nginx, backup, and restore conventions.
670 lines
14 KiB
Markdown
670 lines
14 KiB
Markdown
# 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
|
||
│ ├── 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
|
||
```
|
||
|
||
本地默认数据库位置:
|
||
|
||
```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`
|
||
|
||
### 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 时会提示已跳过
|
||
|
||
图片后续可以在应用里手动补录。
|