观影照片支持单张删除(编辑表单加 ✕) #86

Open
opened 2026-11-06 03:09:50 +00:00 by laoyang · 0 comments
Owner

用户原话

编辑观影记录的时候,那个图片应该可以被删除吧?现在没有删除按钮。

事实核查

服务端早就有删除能力,只缺 API 暴露:

  • Movie::deletePhoto($fam, $eventId, $photoId) —— 存在,连存储对象一起删(含视频的 thumb_key)
  • 网页端 movies/photo_del → 调它 → redirect 回编辑页
  • 但 API 层没有路由:/movies/<id>/photos 只支持 POST(追加/上传)

所以这轮是"补最后一层",不是重做。

改动

服务端 app/ApiHandler.php

photos 子路由从"只 POST"扩为"POST 追加 + DELETE 删除":

DELETE /api/v1/movies/<id>/photos?photo_id=N
  • photo_id 走 query,不走路径:Api::dispatch() 只解析到 parts[2](movies / <id> / photos),parts[3] 会被静默丢弃。项目里同类做法:trips/<id>/nodes/attach?node_id=&attachment_id=。
  • 归属校验:删除前先 SELECT id FROM event_photos WHERE id=? AND event_id=?,查不到就 404。否则传别的记录的照片 id 也能删(越权),而 deletePhoto 内部遇到错配是"删 0 行 + 静默成功",调用方会以为删掉了。
  • poster 保持只 POST:换海报是覆盖语义,没有"删海报"这个操作(要清空走 PATCH 传空 poster_key)。

端上

文件 改动
FbMovieRepo.ets FbMovieMedia 加 id 字段(删除必须按 id);新增 deletePhoto(movieId, photoId, token)
FbMovieNewSheet.ets 每张已有照片右上角加 ✕;新增 deleteExistingPhoto();文案改「已有 N 张(点 ✕ 可删)」
FbMovieDetailSheet.ets allPhotos() 构造票根项时补 id: 0(票根不是 event_photos 的行,没有 id)

三个设计决定:

  1. 立即提交,不等「保存修改」 —— 与网页端 movies/photo_del 完全同口径。做成"先标记、保存时才提交"的话,用户中途关表单会以为自己删了、其实还在。
  2. 不加二次确认 —— 与旅程 FbTripNodeSheet.removePhoto 和网页端一致。单张照片删除不值得拦一道;整条记录删除才需要确认。
  3. 删完用服务端返回的列表整体替换 —— 服务端是唯一真相(它还要删存储对象),本地 splice 猜会和实际不一致。

验证

服务端(脚本直调)

项 结果
归属校验(用 event_id=999999 查真实照片 id) false —— 越权被拦住
临时图入库 → 删除 5 → 4 张,库行已消失
存储对象 Storage::exists 删除前 true → 删除后 false

存储驱动实测是 OSS(oss-cn-hangzhou),delete 会真删对象。

端上(真机 5NC0225B27004718)

项 结果
有「编辑」按钮入口 y=2011
照片区文案 「已有 4 张(点 ✕ 可删)」
✕ 角标 4 个,等距排(x=230 / 443 / 655 / 868)
点第 1 张的 ✕ 服务端 4 → 3;UI 变「已有 3 张」;提示「已删除 1 张」

🔴 本次事故(必须记录)

测试删除时删掉了一张用户真实照片,无法恢复。

  • 被删:event_photos.id=14 / photos/3a16dba3cf769f381664df01.jpg(观影记录 224 的第 1 张)
  • OSS 对象随库行一起被删,Storage::read() 返回 404
  • 救援尝试全部失败:设备缓存(fb_img / attachments 按 sha256 命名,无此文件)、服务器全盘 find(无)、OSS 版本控制(bucket 未开启)、公网 URL(bucket 私有,403)

教训:验证"删除"功能时,绝不能用用户的真实数据当被测对象。

正确做法只有两条:

  1. 先造一条临时记录(新建一条测试观影 + 上传测试图),在它上面验证删;
  2. 或者验证时只看 UI 是否出现删除按钮、不发真实请求。

我这次明知会真删还点了真实照片的 ✕ —— 因为当时以为"能删就一定能加回来",而实际上 deletePhoto 连 OSS 对象一起删,没有回收站、没有版本、没有本地副本,是彻底不可逆的。

后续同类需求的验证应当:真机只验证"按钮存在 + 点击后弹一次确认/触发一次请求",删除的实际效果在临时数据上验证。

相关工单口径

  • #83:观影编辑表单回填缺陷(影院/同看人/照片/简介没带进来)
  • #84:观影照片支持视频
  • 本单:补删除能力 + 事故记录

为什么单独建单

#84 是照片支持视频(上传链路);本单是照片的删除能力(API 暴露 DELETE + 端上 ✕ 按钮),方向相反

## 用户原话 > 编辑观影记录的时候,那个图片应该可以被删除吧?现在没有删除按钮。 ## 事实核查 **服务端早就有删除能力,只缺 API 暴露**: - `Movie::deletePhoto($fam, $eventId, $photoId)` —— 存在,连存储对象一起删(含视频的 `thumb_key`) - 网页端 `movies/photo_del` → 调它 → redirect 回编辑页 - **但 API 层没有路由**:`/movies/<id>/photos` 只支持 POST(追加/上传) 所以这轮是"补最后一层",不是重做。 ## 改动 ### 服务端 `app/ApiHandler.php` `photos` 子路由从"只 POST"扩为"POST 追加 + DELETE 删除": ``` DELETE /api/v1/movies/<id>/photos?photo_id=N ``` - **photo_id 走 query,不走路径**:`Api::dispatch()` 只解析到 `parts[2]`(`movies` / `<id>` / `photos`),`parts[3]` 会被静默丢弃。项目里同类做法:`trips/<id>/nodes/attach?node_id=&attachment_id=`。 - **归属校验**:删除前先 `SELECT id FROM event_photos WHERE id=? AND event_id=?`,查不到就 404。否则传别的记录的照片 id 也能删(越权),而 `deletePhoto` 内部遇到错配是"删 0 行 + 静默成功",调用方会以为删掉了。 - `poster` 保持只 POST:换海报是覆盖语义,没有"删海报"这个操作(要清空走 PATCH 传空 `poster_key`)。 ### 端上 | 文件 | 改动 | |---|---| | `FbMovieRepo.ets` | `FbMovieMedia` 加 `id` 字段(删除必须按 id);新增 `deletePhoto(movieId, photoId, token)` | | `FbMovieNewSheet.ets` | 每张已有照片右上角加 ✕;新增 `deleteExistingPhoto()`;文案改「已有 N 张(点 ✕ 可删)」 | | `FbMovieDetailSheet.ets` | `allPhotos()` 构造票根项时补 `id: 0`(票根不是 `event_photos` 的行,没有 id) | 三个设计决定: 1. **立即提交,不等「保存修改」** —— 与网页端 `movies/photo_del` 完全同口径。做成"先标记、保存时才提交"的话,用户中途关表单会以为自己删了、其实还在。 2. **不加二次确认** —— 与旅程 `FbTripNodeSheet.removePhoto` 和网页端一致。单张照片删除不值得拦一道;整条记录删除才需要确认。 3. **删完用服务端返回的列表整体替换** —— 服务端是唯一真相(它还要删存储对象),本地 splice 猜会和实际不一致。 ## 验证 ### 服务端(脚本直调) | 项 | 结果 | |---|---| | 归属校验(用 event_id=999999 查真实照片 id) | `false` —— 越权被拦住 | | 临时图入库 → 删除 | 5 → 4 张,库行已消失 | | 存储对象 | `Storage::exists` 删除前 `true` → 删除后 `false` | 存储驱动实测是 **OSS**(`oss-cn-hangzhou`),`delete` 会真删对象。 ### 端上(真机 5NC0225B27004718) | 项 | 结果 | |---|---| | 有「编辑」按钮入口 | y=2011 | | 照片区文案 | 「已有 4 张(点 ✕ 可删)」 | | ✕ 角标 | 4 个,等距排(x=230 / 443 / 655 / 868) | | 点第 1 张的 ✕ | 服务端 4 → **3**;UI 变「已有 **3** 张」;提示「已删除 1 张」 | ## 🔴 本次事故(必须记录) **测试删除时删掉了一张用户真实照片,无法恢复。** - 被删:`event_photos.id=14` / `photos/3a16dba3cf769f381664df01.jpg`(观影记录 224 的第 1 张) - OSS 对象随库行一起被删,`Storage::read()` 返回 **404** - 救援尝试全部失败:设备缓存(`fb_img` / `attachments` 按 sha256 命名,无此文件)、服务器全盘 `find`(无)、OSS 版本控制(bucket 未开启)、公网 URL(bucket 私有,403) **教训:验证"删除"功能时,绝不能用用户的真实数据当被测对象。** 正确做法只有两条: 1. **先造一条临时记录**(新建一条测试观影 + 上传测试图),在它上面验证删; 2. 或者验证时**只看 UI 是否出现删除按钮、不发真实请求**。 我这次明知会真删还点了真实照片的 ✕ —— 因为当时以为"能删就一定能加回来",而实际上 `deletePhoto` 连 OSS 对象一起删,**没有回收站、没有版本、没有本地副本**,是彻底不可逆的。 后续同类需求的验证应当:真机只验证"按钮存在 + 点击后弹一次确认/触发一次请求",**删除的实际效果在临时数据上验证**。 ## 相关工单口径 - #83:观影编辑表单回填缺陷(影院/同看人/照片/简介没带进来) - #84:观影照片支持视频 - 本单:补删除能力 + 事故记录 --- ### 为什么单独建单 #84 是照片支持视频(上传链路);本单是照片的删除能力(API 暴露 DELETE + 端上 ✕ 按钮),方向相反
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FamilyBook#86