重复到访:第二类(补录行被合并回来)不依赖基线,基线不可用时也照样能清
「补录行被合并回来」这一类的判据全在站点自己这边(派生 UUID 命中),本来不需要基线; 但原先的实现在读基线之前就 return 了,于是基线缺失 / 对不上的数据集连这一类都清不了。 现在先判第二类,基线的可用性检查只挡住第一类(站点侧副本),不可用时把说明补在卡片里 (有第二类就列出来 + 一行说明,没有就只给说明)。 模板相应合并成一个卡片:标题带对数、说明行只在有对时出现、判不了第一类时补一行灰字。
This commit is contained in:
@@ -212,60 +212,20 @@ func blobHex(s string) string {
|
|||||||
|
|
||||||
// detectDuplicateVisits 检出站点侧的重复到访,并给出每条该保留哪一行。
|
// detectDuplicateVisits 检出站点侧的重复到访,并给出每条该保留哪一行。
|
||||||
//
|
//
|
||||||
// 第二个返回值非空表示**判定不了**(基线不可用 / 跟本站到访对不上),此时页面只给一行
|
// 第二个返回值非空表示**第一类判定不了**(基线不可用 / 跟本站到访对不上)——那一类要拿基线
|
||||||
// 说明、不给按钮——宁可不动,也不能拿一份对不上的参照去删用户的数据。
|
// 当参照,参照不可靠就宁可不动;但第二类(补录行被合并回来)不依赖基线,照样要报出来。
|
||||||
func (s *Server) detectDuplicateVisits(ctx context.Context, datasetID int64) ([]EditDupVisit, string, error) {
|
func (s *Server) detectDuplicateVisits(ctx context.Context, datasetID int64) ([]EditDupVisit, string, error) {
|
||||||
path := s.baselinePath(datasetID)
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
return nil, "这份数据集还没有可用的基线库(当初上传的原始 .rondbackup 已不在),没法当参照,判定不了重复到访。", nil
|
|
||||||
}
|
|
||||||
base, err := readDupBaseline(ctx, path)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("读取基线库失败(数据集 %d,%s): %v", datasetID, path, err)
|
|
||||||
return nil, "读取基线库失败,判定不了重复到访。", nil
|
|
||||||
}
|
|
||||||
site, err := s.loadDupSiteVisits(ctx, datasetID)
|
site, err := s.loadDupSiteVisits(ctx, datasetID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var withUUID, matched int
|
|
||||||
for _, sv := range site {
|
|
||||||
if sv.uuid == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
withUUID++
|
|
||||||
if base.uuids[sv.uuid] {
|
|
||||||
matched++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 两道闸都防着「拿一份不相干的库当参照」:基线的 UUID 一个都比不上时,
|
|
||||||
// 所有站内到访都会被算成副本,照着删就是把整库搬走。
|
|
||||||
if len(base.uuids) == 0 {
|
|
||||||
return nil, "基线里没有带 UUID 的到访,没法当参照,判定不了重复到访。", nil
|
|
||||||
}
|
|
||||||
if withUUID > 0 && matched == 0 {
|
|
||||||
return nil, "基线跟本站到访一条都对不上,它多半不是这份数据集的手机包,判定不可靠,先不动它。", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留项:站点侧 UUID 在基线里能找到的那些(手机包里也有这条),按「时间 + 地点」建索引。
|
|
||||||
keepByKey := map[string]int{}
|
|
||||||
for _, sv := range site {
|
|
||||||
if sv.uuid == "" || !base.uuids[sv.uuid] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
secs := appleSeconds(sv.arrival)
|
|
||||||
dupSet(keepByKey, dupKey(secs, sv.place), sv.srcPK)
|
|
||||||
if sv.lat != 0 || sv.lon != 0 {
|
|
||||||
dupSet(keepByKey, dupKey(secs, dupCoordKey(sv.lat, sv.lon)), sv.srcPK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []EditDupVisit
|
var out []EditDupVisit
|
||||||
seen := map[int]bool{}
|
seen := map[int]bool{}
|
||||||
// 第二类:站点补录的行(留档里没有 UUID)导出过一次之后,包里那条带的是站点给它补的
|
// 第二类:站点补录的行(留档里没有 UUID)导出过一次之后,包里那条带的是站点给它补的
|
||||||
// 派生 UUID;这份包合并回站点时对不上(站点这侧本来是空的),于是又插了一条。
|
// 派生 UUID;这份包合并回站点时对不上(站点这侧本来是空的),于是又插了一条。
|
||||||
// 判据:站内某一行的 UUID 恰好等于「另一条无留档行」的派生 UUID。
|
// 判据全在站点自己这边(不需要基线),所以先判它,基线不可用时也不受影响。
|
||||||
|
// 派生 UUID 命中是很强的证据,排在「站点侧副本」之前。
|
||||||
soloByUUID := map[string]siteDupVisit{}
|
soloByUUID := map[string]siteDupVisit{}
|
||||||
for _, sv := range site {
|
for _, sv := range site {
|
||||||
if sv.uuid == "" && sv.srcPK > 0 {
|
if sv.uuid == "" && sv.srcPK > 0 {
|
||||||
@@ -283,6 +243,54 @@ func (s *Server) detectDuplicateVisits(ctx context.Context, datasetID int64) ([]
|
|||||||
seen[sv.srcPK] = true
|
seen[sv.srcPK] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 以下第一类需要基线:缺失 / 读不动 / 一条 UUID 都对不上,就只报第二类 + 一行说明。
|
||||||
|
fail := func(msg string) ([]EditDupVisit, string, error) {
|
||||||
|
sort.SliceStable(out, func(i, j int) bool { return out[i].Arrival.After(out[j].Arrival) })
|
||||||
|
return out, msg, nil
|
||||||
|
}
|
||||||
|
path := s.baselinePath(datasetID)
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return fail("这份数据集还没有可用的基线库(当初上传的原始 .rondbackup 已不在),没法当参照,判定不了「站点侧副本」那一类。")
|
||||||
|
}
|
||||||
|
base, err := readDupBaseline(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("读取基线库失败(数据集 %d,%s): %v", datasetID, path, err)
|
||||||
|
return fail("读取基线库失败,判定不了「站点侧副本」那一类。")
|
||||||
|
}
|
||||||
|
|
||||||
|
var withUUID, matched int
|
||||||
|
for _, sv := range site {
|
||||||
|
if sv.uuid == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
withUUID++
|
||||||
|
if base.uuids[sv.uuid] {
|
||||||
|
matched++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 两道闸都防着「拿一份不相干的库当参照」:基线的 UUID 一个都比不上时,
|
||||||
|
// 所有站内到访都会被算成副本,照着删就是把整库搬走。
|
||||||
|
if len(base.uuids) == 0 {
|
||||||
|
return fail("基线里没有带 UUID 的到访,没法当参照,判定不了「站点侧副本」那一类。")
|
||||||
|
}
|
||||||
|
if withUUID > 0 && matched == 0 {
|
||||||
|
return fail("基线跟本站到访一条都对不上,它多半不是这份数据集的手机包,判定不可靠,先不动它。")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留项:站点侧 UUID 在基线里能找到的那些(手机包里也有这条),按「时间 + 地点」建索引。
|
||||||
|
keepByKey := map[string]int{}
|
||||||
|
for _, sv := range site {
|
||||||
|
if sv.uuid == "" || !base.uuids[sv.uuid] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
secs := appleSeconds(sv.arrival)
|
||||||
|
dupSet(keepByKey, dupKey(secs, sv.place), sv.srcPK)
|
||||||
|
if sv.lat != 0 || sv.lon != 0 {
|
||||||
|
dupSet(keepByKey, dupKey(secs, dupCoordKey(sv.lat, sv.lon)), sv.srcPK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 第一类:站点的 UUID 在基线里找不到、而基线里有一条「同时刻同地点」的——站点侧副本
|
// 第一类:站点的 UUID 在基线里找不到、而基线里有一条「同时刻同地点」的——站点侧副本
|
||||||
for _, sv := range site {
|
for _, sv := range site {
|
||||||
if sv.uuid == "" || base.uuids[sv.uuid] || seen[sv.srcPK] {
|
if sv.uuid == "" || base.uuids[sv.uuid] || seen[sv.srcPK] {
|
||||||
|
|||||||
@@ -346,17 +346,13 @@
|
|||||||
{{else}}<div class="empty">没有匹配的地点</div>{{end}}
|
{{else}}<div class="empty">没有匹配的地点</div>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{/* 重复到访:站点侧跟手机包「同一到达时间 + 同一地点」的历史副本。判定见
|
{{/* 重复到访:两类(见 edit_duplicates.go)——站点侧副本、补录行被合并回来。
|
||||||
edit_duplicates.go(与 tools/dedup_site_visits.py 同一套语义)。
|
第二类不依赖基线,所以基线不可用时它照样列出来,另给一行说明解释第一类判不了。 */}}
|
||||||
只在检出 >0 对时出现;判定不了时只给一行说明、不给按钮。 */}}
|
{{if or .DupVisits .DupUnavailable}}
|
||||||
{{if .DupUnavailable}}
|
|
||||||
<div class="card" data-card="dup-visits">
|
<div class="card" data-card="dup-visits">
|
||||||
<h2>重复到访</h2>
|
<h2>重复到访{{if .DupVisits}} {{num (len .DupVisits)}} 对{{end}}</h2>
|
||||||
<div class="card-sub">{{.DupUnavailable}}</div>
|
{{if .DupUnavailable}}<div class="card-sub">{{.DupUnavailable}}</div>{{end}}
|
||||||
</div>
|
{{if .DupVisits}}
|
||||||
{{else if .DupVisits}}
|
|
||||||
<div class="card" data-card="dup-visits">
|
|
||||||
<h2>重复到访 {{num (len .DupVisits)}} 对</h2>
|
|
||||||
<div class="card-sub">
|
<div class="card-sub">
|
||||||
同一刻同一地点存了两条。成因有两种:<b>站点侧副本</b>(手机包里也有这条,但 UUID 不同——合并 / 差异按 UUID 认人,
|
同一刻同一地点存了两条。成因有两种:<b>站点侧副本</b>(手机包里也有这条,但 UUID 不同——合并 / 差异按 UUID 认人,
|
||||||
把它当成了「备份新增」)与<b>补录行被合并回来</b>(补录的行没有 UUID,导出时会被补一个派生 UUID,
|
把它当成了「备份新增」)与<b>补录行被合并回来</b>(补录的行没有 UUID,导出时会被补一个派生 UUID,
|
||||||
@@ -391,6 +387,7 @@
|
|||||||
<span class="muted">「全部保留」什么都不改,只是取消勾选。</span>
|
<span class="muted">「全部保留」什么都不改,只是取消勾选。</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user