图片不再随数据集删除:加「图片挂载恢复」,place_images 摘掉外键
用户报「之前上传的照片找不到了,应该是因为清了应用数据换基线」。查下来与 App 侧无关, 是站点自己两套口径打架: - place_images.dataset_id 带 ON DELETE CASCADE,而读取按 place_key 坐标键、跨数据集。 - 于是删掉旧数据集时图片**行**被级联删了,**文件却留在 media 目录里**。 - 实测(本机):datasets 只剩 155 号、place_images 0 行、盘上 4 个 .webp 好端端躺着。 同一批附属数据里备注(17)与结论(3+1)因为早就迁到坐标键,毫发无损——只有图片倒霉。 三件事: 1) 恢复:后台首页「图片挂载恢复 → 扫描并恢复」(POST /admin/images/recover, internal/web/image_recovery.go)。只插行、不删文件、不看数据集,按 object_key 判重可反复点。 认地点两处来源:数据集还在就查它的 places;数据集已删就读它留下的 baselines/<id>.sqlite 还原「地点号 → 坐标」。对象键两种历史布局都认(<ds>/<YYYYMM>/<srcPK>-<hash>.webp 与 <ds>/<srcPK>/<hash>.webp);判据先看文件名有没有 `<数字>-` 前缀,再看中间目录—— 地点号也可能刚好 6 位,只看位数会认错。 2) 防复发:CREATE TABLE place_images 去掉外键 + 幂等 DROP CONSTRAINT;删图接口不再按 dataset_id 过滤(数据集删了也得能删图);「删除数据集」的二次确认文案改成实话。 3) 报告同类陷阱(未改):topics 仍带 CASCADE,专题随数据集一起删且没有恢复来源。 测试踩到的坑:图片「按坐标键跨数据集读」+「按 object_key 判重」都不看数据集,用例之间会 互相污染两处——盘上文件(报「应当 2 张、实际 10 张」)与库里的行(Already:1 让兜底分支 根本没跑到,用例差点假绿)。加 isolateImageState(换空 DataDir + 按 place_key 清历史行), 并给夹具补收尾清理(外键没了必须自己清)。 新增用例 4 条;验收:./scripts/verify.sh 全量 PASS=323 FAIL=0 SKIP=0。
This commit is contained in:
@@ -583,3 +583,52 @@ Core Data 元数据、索引、持久化历史、`ZRAWVISIT` 采样点、`ZTRIP*
|
||||
记档优先重建(**带反证**:清掉记档后同一条路必须走不通,防止用例被别的兜底弄绿)、
|
||||
部分重合需确认、一条都对不上 force 也不给过、非包文件第一步就挡住、回滚还原且当前那份也留档、
|
||||
页面渲染到底(尾部 `</html>`)。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 三十四、图片挂载恢复 + `place_images` 摘掉外键(2026-09-30)
|
||||
|
||||
### 症状与根因(用户报「之前上传的照片找不到了」)
|
||||
`place_images.dataset_id` 原本带 `ON DELETE CASCADE`,而**读取按 `place_key` 坐标键、跨数据集**。
|
||||
两套口径打架:删掉旧数据集时图片**行**被级联删掉,**文件却留在 `<data_dir>/media/<数据集>/…`**。
|
||||
实测(本机 `rond_with_you`):`datasets` 只剩 155 号,`place_images` 0 行,盘上 4 个 `.webp` 好端端躺着。
|
||||
同一批附属数据里,备注(17 行)、结论(3+1 行)因为早就迁到坐标键,**毫发无损**——只有图片倒霉。
|
||||
|
||||
### 恢复动作
|
||||
- 入口:后台首页「图片挂载恢复 → 扫描并恢复」⇒ `POST /admin/images/recover`(`internal/web/image_recovery.go`)。
|
||||
只**插入**行:不删文件、不动已有行、可反复点(按 `object_key` 判重)。`apply=false` 是预演。
|
||||
- 认地点的两处来源:① 数据集还在库里 ⇒ 查它的 `places`(最准);② 数据集已删 ⇒ 读它留下的
|
||||
**底座库** `baselines/<id>.sqlite`(换基线留下的 `.bak-*` 也算,新的优先)→ 还原「地点号 → 坐标」。
|
||||
⚠️ **底座文件是救图的唯一依据**:清掉 baselines 就永远认不出这些图属于哪个地点(只报「认不出」)。
|
||||
- 对象键**两种布局都要认**(历史原因):
|
||||
`<dataset>/<YYYYMM>/<srcPK>-<内容哈希>.webp`(现在)与 `<dataset>/<srcPK>/<内容哈希>.webp`(早期,目录名就是地点号)。
|
||||
判据不能只看「中间那层是不是 6 位数字」——地点号也可能刚好 6 位:先看文件名有没有 `<数字>-` 前缀,再退到目录名。
|
||||
- `width/height` 无从得知、留 0:页面只用 `URL`(模板里没有宽高),`bytes` 用文件大小。
|
||||
|
||||
### 防复发
|
||||
- `schema.sql`:`CREATE TABLE place_images` **去掉外键**,另加幂等迁移
|
||||
`ALTER TABLE place_images DROP CONSTRAINT IF EXISTS place_images_dataset_id_fkey;`。
|
||||
`dataset_id` 退化成「出处」这一列——图片认地点本来就不看它。
|
||||
- `adminPlaceImageDelete` 不再按 `dataset_id` 过滤(数据集删了也得能删图),SELECT/DELETE 都只按 `id`。
|
||||
- 后台「删除数据集」的二次确认文案改成实话:删的是到访/地点/位移/原始行留档 + **专题**,
|
||||
备注/结论/图片按坐标键保留。
|
||||
- ⚠️ **同类陷阱还剩一个**:`topics` 仍带 CASCADE。专题内容是「时间 + 区域」派生的,本可以跨数据集活,
|
||||
但列表/详情都按 `dataset_id` 过滤,去掉外键就得同时改成用户级查询——**未做,待用户拍板**。
|
||||
已丢的那批专题(本机 0 行)没有本地恢复来源。
|
||||
|
||||
### ⚠️ 测试隔离(这批用例踩出来的)
|
||||
图片「按坐标键跨数据集读」+「按 object_key 判重」,都**不看数据集** ⇒ 用例之间会互相污染,两处都要隔离:
|
||||
1. **盘上文件**:共用 media 目录里任何一张「坐标相同」的图都会被算成该地点的图
|
||||
⇒ 实测「应当 2 张、实际 10 张」(上一轮跑剩的文件全在那张坐标上)。
|
||||
2. **库里的行**:上一轮留下的 `object_key` 会让新文件被当成「已经在库里」直接跳过
|
||||
⇒ 实测 `Already:1`,**兜底分支根本没被执行到**,而用例只看「挂回 1 张」差点就放过(假绿)。
|
||||
做法:`isolateImageState(t, f, lat, lon)` —— 换空 `DataDir`(`t.TempDir()`)+ 按 `place_key`
|
||||
清历史行(前置清一次、`t.Cleanup` 再清一次)。改名用的文件名再带 `UnixNano` 后缀避免撞键。
|
||||
夹具 `newMovementFixture` 也补了收尾:显式 `DELETE FROM place_images WHERE dataset_id=$1` + 删 media 子目录
|
||||
(**不再级联,必须自己清**),注册得最晚 ⇒ 在删用户之前跑。
|
||||
|
||||
### 用例 `internal/web/image_recovery_test.go`(4 条)
|
||||
行没了文件还在 ⇒ 按坐标挂回(含预演不写库、幂等、挂回后**页面口径** `placeImagesOf` 看得见)、
|
||||
早期键布局与 `parseImageKey` 的表驱动、**数据集已删 ⇒ 靠底座库认回**(并在报告里说明来源)、
|
||||
删数据集不再带走图片行与文件。
|
||||
|
||||
Reference in New Issue
Block a user