修「补录行被合并回来」的重复:对账时认它的派生 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
+8 -5
View File
@@ -358,19 +358,22 @@
<div class="card" data-card="dup-visits">
<h2>重复到访 {{num (len .DupVisits)}} 对</h2>
<div class="card-sub">
这些到访跟手机包里的某条<b>到达时间 + 地点完全相同、但 UUID 不同</b>,是站点侧的历史副本,
导出到 rond 后会变成成对的两条。默认全选:删掉勾选的副本会连它的位移 / 天气 / 留档一起清,
并把指向它的合并标记改指到保留项(不动就会在导出时留下悬空引用)。
同一刻同一地点存了两条。成因有两种:<b>站点侧副本</b>(手机包里也有这条,但 UUID 不同——合并 / 差异按 UUID 认人,
把它当成了「备份新增」)与<b>补录行被合并回来</b>(补录的行没有 UUID,导出时会被补一个派生 UUID,
这份包再合并回站点时对不上,于是又插了一条)。默认全选:删掉勾选的副本会连它的位移 / 天气 / 留档一起清
(「补录行被合并回来」那类会把位移 / 天气<b>改挂</b>到保留项,不白丢里程),
并把指向它的合并标记改指到保留项。导出到 rond 后会变成成对的两条,清完就没了。
</div>
<form method="post" action="/admin/edit/dup/clean">
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>到达时间</th><th>地点</th><th>保留(手机来源)</th><th>删除(站点副本)</th></tr></thead>
<thead><tr><th>到达时间</th><th>地点</th><th>保留</th><th>删除(多出来的那条)</th></tr></thead>
<tbody>
{{range .DupVisits}}
<tr>
<td class="muted mono" data-label="到达时间">{{.ArrivalStr}}</td>
<td data-label="地点">{{if .PlaceName}}{{.PlaceName}}{{else}}<span class="muted">(未命名地点)</span>{{end}}</td>
<td data-label="地点">{{if .PlaceName}}{{.PlaceName}}{{else}}<span class="muted">(未命名地点)</span>{{end}}
{{if .Reason}}<span class="tag" title="这一对是怎么重复的">{{.Reason}}</span>{{end}}</td>
<td class="mono" data-label="保留">#{{.Keep}}</td>
<td data-label="删除">
<label class="check"><input type="checkbox" name="victim" value="{{.Victim}}" checked>