Files
rond-with-you/internal/web/place_identity_test.go
tamakyi 30c502ce7b 备份与迷雾的差异分析、合并执行,附属数据改挂坐标键
备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」:

- 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上
  用绿/灰/红标出会新增 / 不变 / 会丢的区域
- 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人),
  新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行;
  合入的行写回 entity_raw,保证再传同一份备份时差异归零
- 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的
  到访全变成假冲突
- 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联;
  访客票没有 dataset_id,回填按活跃数据集认领
- 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释

导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
2026-09-19 16:04:00 +08:00

150 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("坐标不同的地点不该认领到别人的备注")
}
}