package web import ( "fmt" "testing" "time" "rond-with-you/internal/votes" ) // testPlace 在两个数据集里各建一个**同坐标**的地点,返回它们的编号与第二个数据集。 // // 坐标是随手挑的专属值:备注按坐标共享,而读取按设计不按 user_id 过滤(单站长站点), // 借用真实数据里的坐标会把别的测试残留的记录串进来。 func testPlace(t *testing.T, f *movementFixture, uid int64, lat, lon float64) (pkA, pkB int, dsB int64) { t.Helper() const pkAVal, pkBVal = 660001, 660002 if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon) VALUES ($1,$2,'甲侧地点',$3,$4)`, f.dsID, pkAVal, lat, lon); err != nil { t.Fatalf("建地点失败: %v", err) } sha := fmt.Sprintf("%064d", time.Now().UnixNano()) if err := f.pg.QueryRowContext(f.ctx, `INSERT INTO datasets (user_id, original_name, sha256, status, is_active) VALUES ($1,'另一个数据集',$2,'done',false) RETURNING id`, uid, sha).Scan(&dsB); err != nil { t.Fatalf("建数据集失败: %v", err) } if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon) VALUES ($1,$2,'乙侧地点',$3,$4)`, dsB, pkBVal, lat, lon); err != nil { t.Fatalf("建地点失败: %v", err) } key := fmt.Sprintf("%.6f|%.6f", lat, lon) t.Cleanup(func() { f.pg.ExecContext(f.ctx, `DELETE FROM place_notes WHERE place_key=$1`, key) f.pg.ExecContext(f.ctx, `DELETE FROM place_votes WHERE place_key=$1`, key) f.pg.ExecContext(f.ctx, `DELETE FROM place_visitor_votes WHERE place_key=$1`, key) f.pg.ExecContext(f.ctx, `DELETE FROM datasets WHERE id=$1`, dsB) }) return pkAVal, pkBVal, dsB } // 访客票也一样:它是站点自己的数据(rondbackup 里没有这一项), // 但同样要认得出「是哪个地点」,所以也挂坐标键。 func TestVisitorVotesFollowPlaceAcrossDatasets(t *testing.T) { f := newMovementFixture(t) uid := userFrom(f.ctx).ID pkA, pkB, dsB := testPlace(t, f, uid, 77.777777, 88.888888) if _, mine, err := f.srv.votes.Cast(f.ctx, f.dsID, pkA, "visitor-a", 1); err != nil || mine != 1 { t.Fatalf("投票失败: mine=%d err=%v", mine, err) } tally, err := f.srv.votes.Tally(f.ctx, dsB, pkB) if err != nil { t.Fatalf("读票数失败: %v", err) } if tally.Up != 1 || tally.Down != 0 { t.Fatalf("换数据集后票数应照旧,实际 %+v", tally) } if v := f.srv.votes.Voter(f.ctx, dsB, pkB, "visitor-a"); v != 1 { t.Fatalf("「我投的」也该认得出来,实际 %d", v) } // 再点一次是撤销:从哪一侧点都该撤掉同一张票 if _, mine, err := f.srv.votes.Cast(f.ctx, dsB, pkB, "visitor-a", 1); err != nil || mine != 0 { t.Fatalf("再点一次应撤销: mine=%d err=%v", mine, err) } if tally, err := f.srv.votes.Tally(f.ctx, f.dsID, pkA); err != nil || tally.Up != 0 { t.Fatalf("撤销后另一边也该归零: %+v err=%v", tally, err) } } // 备注、结论、图片挂的是坐标键,所以**换数据集之后仍然跟着同一个地点**—— // 这正是这次迁移要解决的问题。老结构挂 (dataset_id, src_pk), // 换一份备份同编号就是另一个地点,内容全部失联。 func TestNotesAndVerdictsFollowPlaceAcrossDatasets(t *testing.T) { f := newMovementFixture(t) uid := userFrom(f.ctx).ID pkA, pkB, dsB := testPlace(t, f, uid, 11.111111, 22.222222) if err := f.srv.notes.Save(f.ctx, uid, f.dsID, pkA, "别名甲", "备注甲"); err != nil { t.Fatalf("写备注失败: %v", err) } if err := f.srv.votes.SaveVerdict(f.ctx, uid, f.dsID, pkA, votes.VerdictRecommend); err != nil { t.Fatalf("写结论失败: %v", err) } // 从「另一个数据集」读:编号完全不同,但坐标一致,应当照样读得到 all, err := f.srv.notes.All(f.ctx, dsB) if err != nil { t.Fatalf("读备注失败: %v", err) } got, ok := all[pkB] if !ok { t.Fatal("换数据集后应当仍能读到这个地点的备注") } if got.Alias != "别名甲" || got.Content != "备注甲" { t.Fatalf("备注应原样跟过来,实际 alias=%q note=%q", got.Alias, got.Content) } v, err := f.srv.votes.Verdict(f.ctx, dsB, pkB) if err != nil { t.Fatalf("读结论失败: %v", err) } if v != votes.VerdictRecommend { t.Fatalf("结论应跟过来,实际 %d", v) } } // 两个数据集里的编号完全不同,但备注与结论是同一份——改一处两边都变。 func TestNotesUpdateVisibleFromOtherDataset(t *testing.T) { f := newMovementFixture(t) uid := userFrom(f.ctx).ID pkA, pkB, dsB := testPlace(t, f, uid, 33.333333, 44.444444) if err := f.srv.notes.Save(f.ctx, uid, dsB, pkB, "别名乙", "备注乙"); err != nil { t.Fatalf("在新数据集写备注失败: %v", err) } back, err := f.srv.notes.All(f.ctx, f.dsID) if err != nil { t.Fatalf("回读失败: %v", err) } if back[pkA].Content != "备注乙" { t.Fatalf("备注是同一份数据,改一处两边都该变,实际 %q", back[pkA].Content) } } // 坐标不同的地点绝不能互相认领——键是坐标,不是「大概位置」。 func TestNotesDoNotLeakBetweenDifferentPlaces(t *testing.T) { f := newMovementFixture(t) uid := userFrom(f.ctx).ID pkA, _, dsB := testPlace(t, f, uid, 55.555555, 66.666666) if err := f.srv.notes.Save(f.ctx, uid, f.dsID, pkA, "别名甲", "备注甲"); err != nil { t.Fatalf("写备注失败: %v", err) } // 在乙数据集里放一个坐标差 0.001 度(约 100m)的地点,是完全不同的地方 const otherPK = 660003 if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon) VALUES ($1,$2,'隔壁那家', 55.556555, 66.666666)`, dsB, otherPK); err != nil { t.Fatalf("建地点失败: %v", err) } all, err := f.srv.notes.All(f.ctx, dsB) if err != nil { t.Fatalf("读备注失败: %v", err) } if _, ok := all[otherPK]; ok { t.Fatal("坐标不同的地点不该认领到别人的备注") } }