Files
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

77 lines
2.9 KiB
Go

package rond
import "testing"
func TestBuildMergePlanPicks(t *testing.T) {
vd := VisitDiff{
Added: []Visit{{SrcPK: 900, Arrival: tp("2026-09-09 10:00")}},
Removed: []StoredVisit{{SrcPK: 5, UUID: uuidA}},
Changed: []VisitConflict{
{Backup: Visit{SrcPK: 901, Arrival: tp("2026-09-01 12:00")}, DB: StoredVisit{SrcPK: 1, UUID: uuidA}},
{Backup: Visit{SrcPK: 902, Arrival: tp("2026-09-02 12:00")}, DB: StoredVisit{SrcPK: 2, UUID: uuidB}},
},
}
pd := PlaceDiff{Added: []Location{{SrcPK: 3, Name: "新地点"}}}
ad := ActivityDiff{Added: []Activity{{SrcPK: 8, Name: "散步"}}}
p := BuildMergePlan(vd, pd, ad, Choices{
AddVisits: true,
AddPlaces: true,
AddActs: false,
VisitClash: map[string]string{uuidA: PickBackup, uuidB: PickDB},
})
if len(p.AddVisits) != 1 || p.AddVisits[0].SrcPK != 900 {
t.Fatalf("新增到访应合入一条: %+v", p.AddVisits)
}
if len(p.UpdVisits) != 1 || p.UpdVisits[0].UUID != uuidA {
t.Fatalf("只有选了「用实机」的那条才该更新: %+v", p.UpdVisits)
}
if len(p.AddPlaces) != 1 {
t.Fatalf("勾了「并进来」就该带上新增地点: %d", len(p.AddPlaces))
}
// 即使没单独勾活动,合入到访时也要把它可能挂着的「库里还没有的活动」带上,
// 否则那条到访的 activity_id 无从映射,只能整条丢掉。
if len(p.AddActs) != 1 || p.AddActs[0].Name != "散步" {
t.Fatalf("合入到访应连带它需要的活动: %+v", p.AddActs)
}
// 「仅站内有」永远不进计划:删除是单独的事,绝不顺手做
if len(p.UpdPlaces)+len(p.UpdActs) != 0 {
t.Fatal("不该有地点/活动更新")
}
}
// 没提交的键、或提交了怪值,一律当成「保留站内」——不误伤。
func TestBuildMergePlanDefaultsToKeepDB(t *testing.T) {
vd := VisitDiff{Changed: []VisitConflict{
{Backup: Visit{SrcPK: 901}, DB: StoredVisit{SrcPK: 1, UUID: uuidA}},
}}
p := BuildMergePlan(vd, PlaceDiff{}, ActivityDiff{}, Choices{VisitClash: map[string]string{uuidA: "wat"}})
if len(p.UpdVisits) != 0 {
t.Fatalf("怪值不该被当成「用实机」: %+v", p.UpdVisits)
}
if !p.Empty() {
t.Fatal("什么都没选,计划应当是空的")
}
}
// 冲突选了「跳过」也不动。
func TestBuildMergePlanSkip(t *testing.T) {
vd := VisitDiff{Changed: []VisitConflict{
{Backup: Visit{SrcPK: 901}, DB: StoredVisit{SrcPK: 1, UUID: uuidA}},
}}
p := BuildMergePlan(vd, PlaceDiff{}, ActivityDiff{}, Choices{VisitClash: map[string]string{uuidA: PickSkip}})
if !p.Empty() {
t.Fatalf("跳过就不该产生改动: %+v", p)
}
}
// 活动改了名也要能用 UUID 当键对得上。
func TestActivityKeyPrefersUUID(t *testing.T) {
if got := ActivityKey(StoredActivity{SrcPK: 1, Name: "上班", UUID: uuidC}); got != uuidC {
t.Fatalf("有 UUID 就用 UUID,实际 %q", got)
}
if got := ActivityKey(StoredActivity{SrcPK: 1, Name: "上班"}); got != "n:上班" {
t.Fatalf("没有 UUID 退回名称,实际 %q", got)
}
}