图片恢复:坐标来源全部合并,认不出时说清已查过哪些来源

真数据实测(扫描 40 个对象、只挂回 7 张)暴露了第一版的漏洞:
报错「数据集 #1 里没有编号 389 的地点」——来源能读出来,却不是上传那会儿的编号空间。

根因:srcPKCoords 是「第一个能读到的来源就返回」,而**换基线会覆盖 baselines/<id>.sqlite**,
当前那份未必是当年上传时用的编号;旧编号往往还躺在同一份底座的 .bak 留档里。
教训很直白:**「能读出数据」不等于「读对了那一版」**。

改了三处:
1. 来源全部读入合并(先读的优先):库里 places → entity_raw 里 ZLOCATION 的整行留档
   (站点上删掉的地点,places 没了、留档可能还在)→ baselines/<id>.sqlite 及其每一份 .bak-*。
   读不动的文件跳过换下一个,不再「第一份成功就返回」。
2. 同一个编号保留多组候选坐标,逐个试到能在当前数据集里对上为止
   (站点纠过偏、或不同来源算出的坐标不一样,只认第一组会白白认不出)。
3. 认不出要能排查:理由写明「已查来源 …」与试过的坐标;提示条按**数据集归并**上报
   (「数据集 #1:31 张(编号 389、390…),已查来源 …」),不再刷「另有 31 张」;
   完整清单(前 30 条)进服务端日志。

新增用例:当前底座缺编号、换基线留档里才有(正是用户遇到的形状)⇒ 必须救得回;
以及「真的认不出」时要带出已查来源。全量验收 PASS=332 FAIL=0 SKIP=0。
This commit is contained in:
tamakyi
2026-10-01 00:11:04 +08:00
parent 770e752a7a
commit 6b665b6056
6 changed files with 333 additions and 56 deletions
+84 -2
View File
@@ -301,8 +301,9 @@ func TestRecoverPlaceImagesFallsBackToBaseline(t *testing.T) {
if gotKey != placeKeyOfCoords(lat, lon) {
t.Errorf("认出的地点不对:%q", gotKey)
}
if !strings.Contains(strings.Join(rep.Notes, " "), "底座库") {
t.Errorf("应当说明这份坐标是从底座库来的,实际 %+v", rep)
// 报告要写明这份坐标是从哪个底座库认出来的(换基线会覆盖底座文件,来源说不清没法排查)
if !strings.Contains(strings.Join(rep.Notes, " "), fmt.Sprintf("%d.sqlite", ghost)) {
t.Errorf("应当说明这份坐标是从底座库 %d.sqlite 认出来的,实际 %+v", ghost, rep)
}
}
@@ -455,3 +456,84 @@ func TestRecoverPlaceImagesReportsObjectStorageFailure(t *testing.T) {
}
}
}
// TestRecoverPlaceImagesFallsBackToSwappedBaselineBackup 钉住「换了底座就认不出编号」这个真实场景。
//
// 换基线会**覆盖** baselines/<id>.sqlite,当前那份未必是上传那会儿的编号空间。
// 用户的实测反馈就是这样:能读出 ZLOCATION,却报「数据集 #1 里没有编号 389 的地点」,
// 而旧编号很可能还在换基线留下的 `.bak` 留档里。所以坐标来源必须**全部读进来合并**,
// 不能「第一个能读的就算了」。
func TestRecoverPlaceImagesFallsBackToSwappedBaselineBackup(t *testing.T) {
f := newMovementFixture(t)
ctx, srv := f.ctx, f.srv
srcPK, _, lat, lon := pickPlaceWithCoords(t, f)
isolateImageState(t, f, lat, lon)
const ghost = 987655
base := srv.baselinePath(ghost)
if err := os.MkdirAll(filepath.Dir(base), 0o755); err != nil {
t.Fatal(err)
}
// 当前底座:换了别的一套编号,里面**没有**我们这个编号
writeGhostBaseline(t, base, 1001, 0.1, 0.1)
// 换基线留下的留档:才是上传那会儿的编号空间
bak := base + ".bak-20260101-000000"
writeGhostBaseline(t, bak, srcPK, lat, lon)
t.Cleanup(func() { os.Remove(base); os.Remove(bak) })
put := func(name string, ds int, pk int) string {
key := filepath.ToSlash(filepath.Join(strconv.Itoa(ds), strconv.Itoa(pk),
fmt.Sprintf("%s-%d.webp", name, time.Now().UnixNano())))
dst := filepath.Join(f.cfg.MediaDir(), filepath.FromSlash(key))
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(dst, fakeWebP(0x33, 64), 0o644); err != nil {
t.Fatal(err)
}
return key
}
want := put("swapped", ghost, srcPK)
// 另一个数据集连留档都没有:应当认不出,但要把「已查过哪些来源」说出来
const none = 987656
nobase := srv.baselinePath(none)
writeGhostBaseline(t, nobase, 2002, 0.2, 0.2)
t.Cleanup(func() { os.Remove(nobase) })
nope := put("unknown", none, srcPK)
rep, err := srv.recoverPlaceImages(ctx, true)
if err != nil {
t.Fatalf("恢复失败: %v", err)
}
if imageRows(t, f, want) != 1 {
t.Fatalf("没从底座留档里认回编号(认不出 %d 张:%+v)", len(rep.Unresolved), rep.Unresolved)
}
var gotKey string
if err := f.pg.QueryRowContext(ctx, `SELECT place_key FROM place_images WHERE object_key=$1`, want).
Scan(&gotKey); err != nil {
t.Fatal(err)
}
if gotKey != rond.PlaceCoordKey(lat, lon) {
t.Errorf("认出的地点不对:%q", gotKey)
}
if !strings.Contains(strings.Join(rep.Notes, " "), filepath.Base(bak)) {
t.Errorf("报告里应当写明用到了留档 %s,实际 %v", filepath.Base(bak), rep.Notes)
}
// 认不出的那张:理由要带上「已查来源」,否则站长判断不出该去找哪份包
var reason string
for _, u := range rep.Unresolved {
if u.ObjectKey == nope {
reason = u.Reason
}
}
if reason == "" {
t.Fatalf("这张本该认不出,实际没有出现在清单里:%+v", rep.Unresolved)
}
if !strings.Contains(reason, "已查来源") {
t.Errorf("认不出的理由里没写已查过哪些来源(没法排查):%q", reason)
}
if !containsStr(f.imageURLs(t, srcPK), "/media/"+want) {
t.Errorf("页面口径看不到这张图:%v", f.imageURLs(t, srcPK))
}
}