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

5.5 KiB
Raw Permalink Blame History

设计:照片墙服务器存图(按用户、最多 10 张)

把照片墙从「纯前端 localStorage 存图」升级为「图片存服务器、按登录用户隔离」。 排版与装饰仍存浏览器。前置阅读:photo-wall.mdauth-login-gate.md

1. 目标与职责切分

存哪 内容 跨设备
服务器PHOTO_DIR/<用户>/≤10 张/用户) 图片字节本身 任意设备登录可见
浏览器 localStorage 背景、装饰(便签/胶带/贴纸/印章/手绘)、每张图的排版覆盖(位置/旋转/缩放/层级/标注/宽度,按图片 id 关联) 各浏览器独立

加载逻辑:登录后拉取本用户图片列表 → 每张取字节显示 → 若 localStorage 有该 id 的排版记录则套用,否则自动散落排版(沿用软木板随机摆放风格,按数量铺开避免重叠)。 图片严格私有:读取必须带登录凭证,后端校验身份后才返回字节。

2. 鉴权JWT

现状:登录返回的 token 是时间戳 SHA256无任何接口校验。本设计赋予其真实含义。

  • auth::issue_token(username) 改为签发 HS256 JWTjsonwebtoken,已是依赖): 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-Typeid 仅允许 [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.tsxauth.token 传给 PhotoWallPage
  • 新增 Client/src/api/photos.tslistPhotos(token)uploadPhoto(token, dataUrl)deletePhoto(token, id)fetchPhotoObjectUrl(token, id)(带 Bearer 头 fetch → blobURL.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.rsCorsLayer 增加 DELETE 方法与 Authorization 请求头;生产同源不受影响。
  • 上线后提示用户重新登录一次

6. 不做YAGNI

  • 服务器端图片压缩/缩放小规模35GB 盘够用;前端显示尺寸小,将来需要再加)。
  • 排版/装饰上云(明确只存浏览器)。
  • 图片重命名/相册/分享链接等。