防复发:把「删数据集带走站点自建数据」做成结构性门禁 + 常驻体检
用户放弃继续救那批旧图,要求的是「别再发生同类事件」。所以不靠记性,靠结构:
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:
@@ -562,6 +562,37 @@ func dedupStrings(in []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// imageHealth 报告「库里的图片行」与「盘上的图片对象」对不对得上。
|
||||
//
|
||||
// 对不上(对象比行多)就说明有一批图其实还在、却看不见——正是那次事故的形状:
|
||||
// 行被级联删了、文件留在盘上。**让它随时可见**,下次再漂移当场就能发现,不必等照片消失。
|
||||
//
|
||||
// 只扫本地磁盘(走一次目录,毫秒级,适合每次进后台都算);对象存储要真正列一次桶才知道,
|
||||
// 那是一次 API 调用,放在「扫描并恢复」动作里,不放到每次进后台的开销里。
|
||||
func (s *Server) imageHealth(ctx context.Context) (rows, objects, orphans int) {
|
||||
if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM place_images`).Scan(&rows); err != nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
keys, err := s.mediaKeys()
|
||||
if err != nil {
|
||||
return rows, 0, 0
|
||||
}
|
||||
objects = len(keys)
|
||||
if objects == 0 {
|
||||
return rows, 0, 0
|
||||
}
|
||||
attached, err := s.attachedImageKeys(ctx)
|
||||
if err != nil {
|
||||
return rows, objects, 0
|
||||
}
|
||||
for _, k := range keys {
|
||||
if !attached[k] {
|
||||
orphans++
|
||||
}
|
||||
}
|
||||
return rows, objects, orphans
|
||||
}
|
||||
|
||||
// adminImagesRecover 是「扫描并恢复」按钮的处理函数。只插行、不删对象,可反复点。
|
||||
func (s *Server) adminImagesRecover(w http.ResponseWriter, r *http.Request) {
|
||||
rep, err := s.recoverPlaceImages(r.Context(), true)
|
||||
|
||||
Reference in New Issue
Block a user