修「补录行被合并回来」的重复:对账时认它的派生 UUID

成因链:站点补录的到访没有留档行(entity_raw 里没有它),也就没有 UUID;导出时会被补上
一个确定性 UUID v5("ZVISIT/<src_pk>") 写进包里,手机于是把它当成一条真记录;这份包再
合并回站点时,站点这侧没有 UUID 可对齐,DiffVisits 直接判成「备份新增」又插一条 ——
时间线上出现两条一模一样的到访(实测数据集 9 有 43 对,生产时间线一屏就有 53 对)。

- rond:把派生 UUID 抽成 DerivedUUID / DerivedVisitUUID / VisitMatchUUID(导出侧 coreDataUUID
  复用同一个函数),差异与合并都拿它当对账键(diff.go 的 byUUID、backup_merge 的 storedByUUID)。
- 「时间 + 地点」兜底配对补 used 检查:同一条站点行现在两个索引里都有,不查会被 classify 两次。
- 数据编辑页的「重复到访」卡片认得这类(成因标「你补录的那条被合并回来的一份」),
  清理时把副本名下的位移 / 天气**改挂**到保留项——补录行自己名下没有位移,跟着删就白丢里程;
  派生 UUID 判定排在「站点侧副本」之前(证据更强)。

用例:rond 两条(正例 + 派生 UUID 不相配的反例)、web 一条端到端(造补录行与副本 → 检出 →
清理 → 断言位移与天气改挂、留档被清)。变异验证:去掉改挂语义、关掉该判定,两条都立刻红。
This commit is contained in:
tamakyi
2026-09-30 15:02:45 +08:00
parent 881f2933ff
commit 8dec973db1
7 changed files with 329 additions and 21 deletions
+42 -2
View File
@@ -123,10 +123,50 @@ func TestDiffVisitsFallsBackWhenUUIDMissing(t *testing.T) {
}
}
// 站点补录的行(没有留档 UUID)导出过一次之后,包里那条带着的是站点给它补的派生 UUID。
// 这份包再合并回来时,必须认出「这就是我导出去的那条」——不认就会当成「备份新增」又插一条,
// 时间线上于是出现两条一模一样的到访(实测数据集 9 有 43 对)。
func TestDiffVisitsAlignsSiteCreatedRowByDerivedUUID(t *testing.T) {
const srcPK = 42
b := &Backup{
Visits: []Visit{{SrcPK: 900, Arrival: tp("2026-09-01 10:00"), LocationSrcPK: ip(3)}},
Locations: []Location{{SrcPK: 3, Name: "家", Lat: 24.1, Lon: 109.1}},
VisitUUID: map[int]string{900: DerivedVisitUUID(srcPK)}, // 导出时补给这条的
ActivityUUID: map[int]string{},
}
stored := []StoredVisit{{
SrcPK: srcPK, UUID: "", // 站点补录:entity_raw 里没有这一行
Arrival: tp("2026-09-01 10:00"), PlaceKey: PlaceCoordKey(24.1, 109.1),
}}
d := DiffVisits(stored, b)
if len(d.Added) != 0 || len(d.Removed) != 0 {
t.Fatalf("该认出是同一行(派生 UUID 对上了): +%d -%d", len(d.Added), len(d.Removed))
}
if d.Same != 1 {
t.Fatalf("应配上 1 条,实际 same=%d changed=%d", d.Same, len(d.Changed))
}
}
// 反例:派生 UUID 是别的编号的,就不该被认领(否则会把两条不同的到访混成一条)。
func TestDiffVisitsDoesNotAlignOtherDerivedUUID(t *testing.T) {
const srcPK = 42
b := &Backup{
Visits: []Visit{{SrcPK: 900, Arrival: tp("2026-09-01 10:00"), LocationSrcPK: ip(3)}},
Locations: []Location{{SrcPK: 3, Name: "家", Lat: 24.1, Lon: 109.1}},
VisitUUID: map[int]string{900: DerivedVisitUUID(srcPK + 1)},
}
stored := []StoredVisit{{
SrcPK: srcPK, Arrival: tp("2026-09-01 10:00"), PlaceKey: PlaceCoordKey(24.1, 109.1),
}}
d := DiffVisits(stored, b)
if len(d.Added) != 1 || len(d.Removed) != 1 {
t.Fatalf("派生 UUID 对不上就该分别报出: +%d -%d", len(d.Added), len(d.Removed))
}
}
// 兜底配对要认得出「同一条但时间被改过」——时间不同就配不上,此时按新增 + 仅站内有报出,
// 总比静默当成同一条好。
func TestDiffVisitsFallbackNeedsSameTime(t *testing.T) {
b := &Backup{
func TestDiffVisitsFallbackNeedsSameTime(t *testing.T) { b := &Backup{
Visits: []Visit{{SrcPK: 900, Arrival: tp("2026-09-01 10:00"), LocationSrcPK: ip(3)}},
Locations: []Location{{SrcPK: 3, Name: "家", Lat: 24.1, Lon: 109.1}},
VisitUUID: map[int]string{},