修「补录行被合并回来」的重复:对账时认它的派生 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:
@@ -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{},
|
||||
|
||||
Reference in New Issue
Block a user