合并:重复地点不再反复判「备份新增」,到访的活动改按身份比

- DiffPlaces 的 byCoord/byName 改为多值索引 + take() 逐条认领未用项:真机一个坐标下会有
  多条同名同坐标的地点(实测「柳州站」3 条),单槽位时同一坐标最多只配上 2 条,第 3 条
  永远判「备份新增」⇒ 每次合并都再插一条重复地点、差异永远归不了零(实测 368→438→439)。
  修完:地点新增 70→67、合并后再比对归零、再点合并直接「没有要写入的改动」。
- 到访的「活动」由按名称比改为按身份(ZUID_)比,两边都缺 UUID 才退回名称:16 个活动改名
  曾把 355 条到访全变成同因冲突,且逐条选「用实机」只写回 activity_id(映射回同一个活动)
  改不掉名字。loadStoredVisits 为此多 JOIN 一次 entity_raw 取 ZACTIVITY.ZUID_。
- 回归用例 4 条(重复地点一一配上、活动改名不算冲突、换活动仍要报、缺 UUID 退回名称)+
  「合并后地点差异归零」断言 3 处;两处改动都做过变异验证。
- 实测(09-28 备份并进 09-14 数据集):到访冲突 253→6,合并两次地点数 438→439 变为 435→435。
This commit is contained in:
tamakyi
2026-09-28 19:31:21 +08:00
parent f2ce6646c4
commit 10d23e03cf
5 changed files with 172 additions and 22 deletions
+39 -14
View File
@@ -27,6 +27,7 @@ type StoredVisit struct {
PlaceKey string // 地点对齐键,空 = 该到访没有地点
PlaceName string // 仅用于展示
ActivityName string
ActivityUUID string // 该到访所挂活动的对齐键,空 = 站点新建的活动
}
// StoredPlace 是库里的一个地点。
@@ -170,7 +171,7 @@ func DiffVisits(stored []StoredVisit, b *Backup) VisitDiff {
classify := func(i int, v Visit) {
used[i] = true
sv := stored[i]
if fields := visitFieldChanges(sv, v, placeKey, placeName, actName); len(fields) > 0 {
if fields := visitFieldChanges(sv, v, placeKey, placeName, actName, b.ActivityUUID); len(fields) > 0 {
d.Changed = append(d.Changed, VisitConflict{Backup: v, DB: sv, Fields: fields})
} else {
d.Same++
@@ -215,7 +216,7 @@ func timeKey(t time.Time, place string) string {
return fmt.Sprintf("%d|%s", t.Unix(), place)
}
func visitFieldChanges(sv StoredVisit, v Visit, placeKey, placeName, actName map[int]string) []FieldChange {
func visitFieldChanges(sv StoredVisit, v Visit, placeKey, placeName, actName, actUUID map[int]string) []FieldChange {
var out []FieldChange
add := func(label, from, to string) {
if from != to {
@@ -228,7 +229,7 @@ func visitFieldChanges(sv StoredVisit, v Visit, placeKey, placeName, actName map
if !sameTime(sv.Departure, v.Departure) {
add("离开时间", fmtTime(sv.Departure), fmtTime(v.Departure))
}
// 地点按坐标判、显示名称;活动按名称判。都不能比 src_pk——两边编号空间不同。
// 地点按坐标、活动按身份比对,都不能比 src_pk——两边编号空间不同。
bp, bn := "", ""
if v.LocationSrcPK != nil {
bp, bn = placeKey[*v.LocationSrcPK], placeName[*v.LocationSrcPK]
@@ -237,11 +238,21 @@ func visitFieldChanges(sv StoredVisit, v Visit, placeKey, placeName, actName map
if sv.PlaceKey != bp {
out = append(out, FieldChange{Label: "地点", From: sv.PlaceName, To: bn})
}
bl := ""
// 活动按**身份**判,不按名称:站内与真机对同一个活动改名属于活动层的差异,
// 挂在它名下的每条到访都跟着报一遍就会把真差异淹掉(实测 16 个活动改名 → 355 条假冲突),
// 而且逐条选「用实机」也改不掉活动名,冲突永远收敛不了。
// 两边都有 UUID 才比 UUID;缺 UUID 的老数据退回按名称比。
bl, bu := "", ""
if v.ActivitySrcPK != nil {
bl = actName[*v.ActivitySrcPK]
bl, bu = actName[*v.ActivitySrcPK], actUUID[*v.ActivitySrcPK]
}
if sv.ActivityUUID != "" && bu != "" {
if sv.ActivityUUID != bu {
add("活动", sv.ActivityName, bl)
}
} else {
add("活动", sv.ActivityName, bl)
}
add("活动", sv.ActivityName, bl)
add("备注", sv.Remark, v.Remark)
add("表情", sv.Emoji, v.Emoji)
add("收藏", fmtBool(sv.Bookmarked), fmtBool(v.Bookmarked))
@@ -258,21 +269,35 @@ func placeLabel(name string, lat, lon float64) string {
// DiffPlaces 对齐地点。坐标优先(改名不影响配对),配不上再按名称找一遍
// (覆盖「站点把坐标纠偏过」这种),两样都对不上才算新增。
//
// 两侧都用**多值**索引:真机一个坐标下会有好几条同名同坐标的地点(实测「柳州站」3 条,
// 备份里这类分组共 10 组),单槽位 map 会让同一坐标最多只配上一条,剩下的一律判成
// 「备份新增」——于是每次合并都再插一条重复地点、差异永远归不了零。
func DiffPlaces(stored []StoredPlace, b *Backup) PlaceDiff {
byCoord := make(map[string]StoredPlace, len(stored))
byName := map[string]StoredPlace{}
byCoord := make(map[string][]StoredPlace, len(stored))
byName := make(map[string][]StoredPlace, len(stored))
for _, sp := range stored {
if sp.Lat != 0 || sp.Lon != 0 {
byCoord[PlaceCoordKey(sp.Lat, sp.Lon)] = sp
k := PlaceCoordKey(sp.Lat, sp.Lon)
byCoord[k] = append(byCoord[k], sp)
}
if _, dup := byName[sp.Name]; !dup && sp.Name != "" {
byName[sp.Name] = sp
if sp.Name != "" {
byName[sp.Name] = append(byName[sp.Name], sp)
}
}
used := map[int]bool{}
// take 认领第一条还没被用掉的候选,认领即置 used——同一个站内地点不能被两个备份地点配走。
take := func(list []StoredPlace) (StoredPlace, bool) {
for _, sp := range list {
if !used[sp.SrcPK] {
used[sp.SrcPK] = true
return sp, true
}
}
return StoredPlace{}, false
}
var d PlaceDiff
classify := func(sp StoredPlace, l Location) {
used[sp.SrcPK] = true
if fields := placeFieldChanges(sp, l); len(fields) > 0 {
d.Changed = append(d.Changed, PlaceConflict{Backup: l, DB: sp, Fields: fields})
} else {
@@ -280,11 +305,11 @@ func DiffPlaces(stored []StoredPlace, b *Backup) PlaceDiff {
}
}
for _, l := range b.Locations {
if sp, ok := byCoord[PlaceCoordKey(l.Lat, l.Lon)]; ok && !used[sp.SrcPK] {
if sp, ok := take(byCoord[PlaceCoordKey(l.Lat, l.Lon)]); ok {
classify(sp, l)
continue
}
if sp, ok := byName[l.Name]; ok && !used[sp.SrcPK] {
if sp, ok := take(byName[l.Name]); ok {
classify(sp, l)
continue
}