修「补录行被合并回来」的重复:对账时认它的派生 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:
+14
-6
@@ -169,11 +169,12 @@ func DiffVisits(stored []StoredVisit, b *Backup) VisitDiff {
|
||||
byUUID := make(map[string]int, len(stored))
|
||||
byTime := map[string][]int{}
|
||||
for i, sv := range stored {
|
||||
if sv.UUID != "" {
|
||||
byUUID[sv.UUID] = i
|
||||
continue
|
||||
// 站点补录的行没有留档 UUID,但它导出过一次之后就带着派生 UUID 在包里
|
||||
// (见 VisitMatchUUID)——不拿它当键,同一行会被判成「备份新增」再插一条。
|
||||
if u := VisitMatchUUID(sv); u != "" {
|
||||
byUUID[u] = i
|
||||
}
|
||||
if sv.Arrival != nil {
|
||||
if sv.UUID == "" && sv.Arrival != nil {
|
||||
k := timeKey(*sv.Arrival, sv.PlaceKey)
|
||||
byTime[k] = append(byTime[k], i)
|
||||
}
|
||||
@@ -206,8 +207,15 @@ func DiffVisits(stored []StoredVisit, b *Backup) VisitDiff {
|
||||
if v.LocationSrcPK != nil {
|
||||
bpk = placeKey[*v.LocationSrcPK]
|
||||
}
|
||||
if list := byTime[timeKey(*v.Arrival, bpk)]; len(list) > 0 {
|
||||
byTime[timeKey(*v.Arrival, bpk)] = list[1:]
|
||||
// 照旧按「时间 + 地点」兜底,但要跳过已经被 UUID 那一路认领的行——
|
||||
// 同一条站点行可能两个索引里都有,重复 classify 会把它算成两条。
|
||||
k := timeKey(*v.Arrival, bpk)
|
||||
list := byTime[k]
|
||||
for len(list) > 0 && used[list[0]] {
|
||||
list = list[1:]
|
||||
}
|
||||
if len(list) > 0 {
|
||||
byTime[k] = list[1:]
|
||||
classify(list[0], v)
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user