防复发:把「删数据集带走站点自建数据」做成结构性门禁 + 常驻体检
用户放弃继续救那批旧图,要求的是「别再发生同类事件」。所以不靠记性,靠结构:
1) 归属铁律(写进 schema.sql 抬头 + README 新章节)
判断标准只有一句:**这份记录能不能从备份重新导入?**
能(places/visits/movements/weather/activities/tags/entity_raw)⇒ 可挂
`REFERENCES datasets(id) ON DELETE CASCADE`;
不能(place_images/place_notes/place_votes/topics)⇒ 只留 dataset_id 当出处、**不许挂外键**。
2) 两道门禁(dataset_ownership_test.go),都做了变异验证:
- 运行时:查 pg_constraint,凡挂 datasets 的表必须在白名单里;**反向也查**
(白名单里的表不许被顺手摘掉,否则删数据集会留成片孤儿行)。
- 静态:扫 schema.sql 的 CREATE TABLE。只看运行时有盲点——新建的库走 CREATE TABLE,
开发库可能压根没跑过那次迁移。变异(把 topics 的 CASCADE 补回去)当场红。
- 变异验证顺带发现:扫描必须先跳过 `--` 注释行,规矩的说明文字里就写着
`REFERENCES datasets(id)`,不跳过会把自己写的文档当成违规。
- 还发现运行时那道单独看**不够**:夹具每次跑迁移,迁移自己就把坏外键摘了,
它永远不会在不健康的库上报警 ⇒ 补了「注入事故原样的外键再跑迁移,断言被摘掉」
的自愈用例(并先确认变异真的生效,防假绿)。
3) 常驻体检:后台首页显示「库里 N 张 / 本地盘上 M 个 / 落单 K 个」,落单 >0 直接报警。
事故的本质不是「删数据集」本身,而是**行与对象悄悄对不上、没人发现**,等照片看不见了才追。
自愈用例又抓出一个此前漏掉的缺口:place_notes / place_votes 上的同类外键迁移并不会摘
(原先只写了 place_images 与 topics 两句 DROP)⇒ 补成五张自建数据表全覆盖。
⚠️ 排序坑(本项目第二次踩):摘外键那段最初放在 place_images 建表**之前**,
开发库毫无症状、**全新库起不来**(ALTER 报「关系不存在」),被
TestFreshDatabaseServesAppQueries 抓住。整段已挪到 place_images 之后,并把坑写进注释。
顺带订正「删除数据集」的二次确认文案(专题改完之后它还在说专题会一起删)。
全量验收 PASS=336 FAIL=0 SKIP=0。
This commit is contained in:
@@ -681,3 +681,47 @@ Core Data 元数据、索引、持久化历史、`ZRAWVISIT` 采样点、`ZTRIP*
|
||||
|
||||
教训(可复用):**「能读出数据」不等于「读对了那一版」**。凡是从「可能被覆盖过的留档」里还原编号/坐标,
|
||||
都要把历史版本一起纳入,并且在失败信息里列出已查过的来源 —— 否则用户只能看到「认不出」,无从下手。
|
||||
|
||||
### 三十五、防复发:归属口径做成门禁(2026-10-01,用户「我只想避免类似的事件再次发生」)
|
||||
|
||||
用户放弃继续救那批旧图,要求的是**结构性防复发**。所以把这次的教训固化成三件东西:
|
||||
|
||||
1. **归属铁律(写进 schema.sql 抬头 + README)**
|
||||
判断标准只有一句:**这份记录能不能从备份重新导入?**
|
||||
- 能(places/visits/movements/weather/activities/tags/entity_raw)⇒ 可以挂
|
||||
`REFERENCES datasets(id) ON DELETE CASCADE`,删数据集时跟着走是对的。
|
||||
- 不能(place_images/place_notes/place_votes/topics 等站点自建数据)⇒
|
||||
**只留 dataset_id 当出处、不许挂外键**。挂在备份里没有的记录上,删数据集=永久丢失。
|
||||
|
||||
2. **两道门禁(`internal/web/dataset_ownership_test.go`)**
|
||||
- 运行时 `TestOnlyBackupDerivedTablesCascadeFromDatasets`:查 `pg_constraint`,
|
||||
凡挂 `datasets` 的 FK 其表名必须在白名单里;**反向也查**(白名单里的表不许被顺手摘掉,
|
||||
否则删数据集会留成片孤儿行、或外键变 RESTRICT 之后干脆删不动)。
|
||||
- 静态 `TestSchemaDeclaresNoDatasetFKForUserData`:直接扫 `schema.sql` 的 `CREATE TABLE`。
|
||||
只看运行时有盲点——**新建的库走 CREATE TABLE**,开发库可能压根没跑过那次迁移。
|
||||
- 新加一张带 dataset_id 的表时,写 CREATE TABLE 的那一刻就会被拦住。
|
||||
|
||||
3. **体检常驻(真正的预防)**:`imageHealth` 每次进后台都算
|
||||
「库里 N 张 / 本地盘上 M 个 / 落单 K 个」,落单 > 0 直接在首页报警。
|
||||
那次事故的本质不是「删数据集」本身,而是**行与对象悄悄对不上、没人发现**,
|
||||
等照片看不见了才追——让它当场可见,同样的漂移下次立刻亮
|
||||
(`TestAdminHomeReportsOrphanImages` 守着这个提示)。
|
||||
只扫本地盘(毫秒级);对象存储要列一次桶,放在「扫描并恢复」动作里,不在每次进后台的开销里。
|
||||
|
||||
顺带订正:后台「删除数据集」的二次确认文案在专题改完之后已经说过时话(说专题会一起删),改成实话。
|
||||
|
||||
### 三十五补记:变异验证又抓出一个真缺口(2026-10-01)
|
||||
|
||||
给两道门禁做**变异验证**时(按项目纪律,新回归测试必须证明它会响):
|
||||
- 静态门禁:把 `topics` 的 CASCADE 外键补回去 ⇒ 立刻红,还原后绿 ✓
|
||||
(顺带发现扫描必须先跳过 `--` 注释行:规矩的说明文字里就写着 `REFERENCES datasets(id)`,
|
||||
不跳过会把自己写的文档当成违规误报。)
|
||||
- 运行时门禁:手动给 `topics` 加上外键后**没响**——因为夹具每次都跑 `db.Migrate`,
|
||||
迁移自己就把它摘了。⇒ 运行时判据单独看不够:它永远不会在不健康的库上报警。
|
||||
真正的自愈靠迁移,于是补了 `TestMigrationDropsStaleDatasetForeignKeys`:
|
||||
把事故原样的外键**注入回去**再跑一次迁移,断言它们没了(并先确认变异真的生效,防假绿)。
|
||||
|
||||
而这条自愈用例又抓出一个**此前漏掉的缺口**:`place_notes` / `place_votes` 上的同类外键
|
||||
(更早那次改坐标键时摘的,老库/生产库上未必摘干净)**迁移并不会摘**——
|
||||
原来只写了 `place_images` 与 `topics` 两句 DROP。已补成四张自建数据表全覆盖
|
||||
(含 `place_visitor_votes` 兜底)。**部署出去的库要能自愈,DROP 必须覆盖全部同类表,不是只覆盖这次动过的那两张。**
|
||||
|
||||
Reference in New Issue
Block a user