重复到访:第二类(补录行被合并回来)不依赖基线,基线不可用时也照样能清
「补录行被合并回来」这一类的判据全在站点自己这边(派生 UUID 命中),本来不需要基线; 但原先的实现在读基线之前就 return 了,于是基线缺失 / 对不上的数据集连这一类都清不了。 现在先判第二类,基线的可用性检查只挡住第一类(站点侧副本),不可用时把说明补在卡片里 (有第二类就列出来 + 一行说明,没有就只给说明)。 模板相应合并成一个卡片:标题带对数、说明行只在有对时出现、判不了第一类时补一行灰字。
This commit is contained in:
@@ -212,60 +212,20 @@ func blobHex(s string) string {
|
||||
|
||||
// detectDuplicateVisits 检出站点侧的重复到访,并给出每条该保留哪一行。
|
||||
//
|
||||
// 第二个返回值非空表示**判定不了**(基线不可用 / 跟本站到访对不上),此时页面只给一行
|
||||
// 说明、不给按钮——宁可不动,也不能拿一份对不上的参照去删用户的数据。
|
||||
// 第二个返回值非空表示**第一类判定不了**(基线不可用 / 跟本站到访对不上)——那一类要拿基线
|
||||
// 当参照,参照不可靠就宁可不动;但第二类(补录行被合并回来)不依赖基线,照样要报出来。
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
seen := map[int]bool{}
|
||||
// 第二类:站点补录的行(留档里没有 UUID)导出过一次之后,包里那条带的是站点给它补的
|
||||
// 派生 UUID;这份包合并回站点时对不上(站点这侧本来是空的),于是又插了一条。
|
||||
// 判据:站内某一行的 UUID 恰好等于「另一条无留档行」的派生 UUID。
|
||||
// 判据全在站点自己这边(不需要基线),所以先判它,基线不可用时也不受影响。
|
||||
// 派生 UUID 命中是很强的证据,排在「站点侧副本」之前。
|
||||
soloByUUID := map[string]siteDupVisit{}
|
||||
for _, sv := range site {
|
||||
if sv.uuid == "" && sv.srcPK > 0 {
|
||||
@@ -283,6 +243,54 @@ func (s *Server) detectDuplicateVisits(ctx context.Context, datasetID int64) ([]
|
||||
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 在基线里找不到、而基线里有一条「同时刻同地点」的——站点侧副本
|
||||
for _, sv := range site {
|
||||
if sv.uuid == "" || base.uuids[sv.uuid] || seen[sv.srcPK] {
|
||||
|
||||
Reference in New Issue
Block a user