PersonalWebApplication/docs/superpowers/specs/2026-06-24-photo-wall-server-storage-design.md
cc 42639856b2 docs(photo-wall): server-storage design spec + implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:55:03 +08:00

95 lines
5.5 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.

# 设计:照片墙服务器存图(按用户、最多 10 张)
> 把照片墙从「纯前端 localStorage 存图」升级为「图片存服务器、按登录用户隔离」。
> 排版与装饰仍存浏览器。前置阅读:[photo-wall.md](../../photo-wall.md)、
> [auth-login-gate.md](../../auth-login-gate.md)。
## 1. 目标与职责切分
| 存哪 | 内容 | 跨设备 |
|---|---|---|
| **服务器**`PHOTO_DIR/<用户>/`≤10 张/用户) | 图片字节本身 | ✅ 任意设备登录可见 |
| **浏览器 localStorage** | 背景、装饰(便签/胶带/贴纸/印章/手绘)、**每张图的排版覆盖**(位置/旋转/缩放/层级/标注/宽度,按图片 id 关联) | ❌ 各浏览器独立 |
**加载逻辑**:登录后拉取本用户图片列表 → 每张取字节显示 → 若 localStorage 有该 id
的排版记录则套用,否则**自动散落排版**(沿用软木板随机摆放风格,按数量铺开避免重叠)。
图片严格私有:读取必须带登录凭证,后端校验身份后才返回字节。
## 2. 鉴权JWT
现状:登录返回的 `token` 是时间戳 SHA256**无任何接口校验**。本设计赋予其真实含义。
- `auth::issue_token(username)` 改为签发 **HS256 JWT**`jsonwebtoken`,已是依赖):
claims `sub = 用户名`、`exp = 现在 + 30 天`,密钥取环境变量 `JWT_SECRET`
- 未设 `JWT_SECRET` → 用内置开发兜底密钥并打印警告(仅本地开发用;生产必须设)。
- 新增 `auth::verify_token(token) -> Option<String>`(返回用户名)。
- 新增 axum 提取器 `AuthUser(String)`:读 `Authorization: Bearer <jwt>`,校验通过产出
用户名,否则 `401`。受保护接口用它。
- **影响**token 格式变化,上线后现有登录态失效,需**重新登录一次**。
## 3. 后端接口与存储
存储布局:
- 根目录 `PHOTO_DIR`(默认 `photo-wall`,相对进程 cwd线上 cwd 为
`/opt/internet-project/`,即 `/opt/internet-project/photo-wall/`)。
- 每用户一子目录,目录名 = 用户名经字符集白名单 `[A-Za-z0-9_-]` 校验后使用;
含非法字符的用户名拒绝(`400`)。
- 每图文件名 `<16字节随机 hex>.<ext>``ext ∈ {jpg,png,gif,webp}`。
**无独立元数据存储**:列目录即得列表(无需数据库)。
- **图片 `id` 即完整文件名**(含扩展名,如 `a1b2…f9.jpg`)。`GET /:id` 据扩展名
决定 `Content-Type`。`id` 仅允许 `[0-9a-f]+\.(jpg|png|gif|webp)`,否则 `404`
接口(全部经 `AuthUser`,挂在 web 路由):
| 方法 | 路径 | 行为 | 返回 |
|---|---|---|---|
| GET | `/api/web/photos` | 列出当前用户图片 | `{ items: [{ id }], max: 10 }` |
| POST | `/api/web/photos` | 上传一张body `{ dataUrl }` base64 data URL | `201 { id }` |
| GET | `/api/web/photos/:id` | 流式返回字节(属主校验,文件须在该用户目录) | `200` + `Content-Type` |
| DELETE | `/api/web/photos/:id` | 删除一张 | `204` |
约束与错误:
- 满 10 张再传 → `409`(前端也禁用上传按钮)。
- 单图解码后 > 8 MB → `413`
- 类型不在白名单 → `415`
- `:id` 含路径分隔/`..` 或文件不存在 → `404`防目录穿越id 仅允许 hex+扩展名)。
- 放宽对应路由的 axum `DefaultBodyLimit` 至 ~16 MB容纳 base64
## 4. 前端数据流
- `useAuth`:在 `Auth` 接口里暴露 `token`(当前已存 session仅未对外返回
`App.tsx``auth.token` 传给 `PhotoWallPage`
- 新增 `Client/src/api/photos.ts``listPhotos(token)`、`uploadPhoto(token, dataUrl)`、
`deletePhoto(token, id)`、`fetchPhotoObjectUrl(token, id)`(带 `Bearer` 头 fetch →
`blob``URL.createObjectURL`)。
- `PhotoWallPage`
- 挂载时:`listPhotos` → 并发取每张 objectUrl → 建照片元素;按图片 id 从
localStorage 合并排版,缺失则自动散落(需图片自然宽高算比例,载入后取
`naturalWidth/Height`)。
- 持久化结构改为
`{ bgIndex, decorations: El[], photoLayout: { [id]: {x,y,rot,scale,z,caption,w} } }`
——**照片字节不再进 localStorage**。读到旧结构(照片带 base64时只取其装饰部分。
- 上传:选图 → 读 dataURL → `uploadPhoto` → 成功后插入照片元素(自动摆放)。满 10 禁用。
- 删除(右键):`deletePhoto` → 移除元素 + 清该 id 本地排版 + `revokeObjectURL`
- 卸载时释放所有 objectUrl。
- **右键菜单改进**(用户反馈:当前调整幅度太小):
- 默认步进调大:旋转 ±15°、照片放大/缩小 ±30px、其它元素 scale ±0.25。
- 新增「旋转角度」子项:预设角度(如 0/±15/±30/±45/90直接设定。
- 新增「放大比例」子项:预设比例(如 0.5×/0.75×/1×/1.5×/2×直接设定。
## 5. 部署与运维
- 这是**后端改动** → 全量重部署:重编 Rust → 传二进制 → `restart_rust.sh` 重启
(非 frontend-only
- 服务器 `weather.env` 增加 `JWT_SECRET=<随机串>`;可选 `PHOTO_DIR`(默认即对)。
- 首次上传自动创建 `photo-wall/<用户>/`
- CORS仅影响 dev 跨源):在 `routes/web.rs``CorsLayer` 增加 `DELETE` 方法与
`Authorization` 请求头;生产同源不受影响。
- 上线后提示用户**重新登录一次**。
## 6. 不做YAGNI
- 服务器端图片压缩/缩放小规模35GB 盘够用;前端显示尺寸小,将来需要再加)。
- 排版/装饰上云(明确只存浏览器)。
- 图片重命名/相册/分享链接等。