合并:地点配对只留一份实现(rond.MatchPlaces),合并侧不再自己配一遍
- 抽出 rond.MatchPlaces(stored, backup) PlaceMatch:坐标优先、退到「同名且相距 ≤1km」、 多值索引 + 认领即标记已用;DiffPlaces 与 web.mergePlaces 共用它。 - mergePlaces 原来是单槽位 byCoord/byName 自己再配一遍:同坐标/同名的多条备份地点全并到同一行 站内地点,到访被挂到错的分店(实测「茶颜悦色」两家分店相隔 8.3km,到访却指着同一条)。 现在直接用配对结果,再叠加 plan.AddPlaces 的新插入。 - loadStoredPlaces 与 mergePlaces 的地点查询都加 ORDER BY id:候选多于一个时靠顺序定谁配谁, 两侧必须一致,否则配对会飘。 - 删掉只写不读的 PlacePatch.CoordKey、ActivityPatch.Key(容易被误当成查找键)。 - 用例:TestMatchPlacesKeepsDuplicateNameGroupsDistinct(同一坐标/邻近同名都要一条配一条)、 TestMergePlacesMapsSameNameLocationsOneToOne(真库 + 真事务,两条同名备份地点必须映射到两条不同 地点);两处都做了变异验证(认领不标记已用 / 恢复单槽位映射 → 对应用例变红)。 - 探针库复测(从 dump 恢复、不动本地真库,49 组冲突全「用实机」):到访挂错地点(>1km)0/953 条、 地点被搬动 >1km 0 个;合并后再比对三类全 0(到访 0/47/0、地点 0/4/0、活动 0/0/0), 再点合并「按当前的选择没有要写入的改动」。全量验收 293 PASS / 0 FAIL / 0 SKIP。 已知取舍:勾「用实机」但没勾「并进来」时,若该到访在实机上的地点站内没有、且按 1km 规则也配不上, place_id 会被置空(旧行为是挂到一条远方的同名地点上——那正是这次要去掉的错配)。
This commit is contained in:
+43
-20
@@ -285,13 +285,22 @@ func placeLabel(name string, lat, lon float64) string {
|
||||
return fmt.Sprintf("%.5f, %.5f", lat, lon)
|
||||
}
|
||||
|
||||
// DiffPlaces 对齐地点。坐标优先(改名不影响配对),配不上再按名称找一遍
|
||||
// (覆盖「站点把坐标纠偏过」这种),两样都对不上才算新增。
|
||||
// PlaceMatch 是「备份地点编号 -> 库内地点」的配对结果。
|
||||
type PlaceMatch map[int]StoredPlace
|
||||
|
||||
// MatchPlaces 把备份地点配到库内地点:坐标优先(改名不影响配对),配不上再退到
|
||||
// 「同名且相距不超过 nameMatchMaxKm」。
|
||||
//
|
||||
// 两侧都用**多值**索引:真机一个坐标下会有好几条同名同坐标的地点(实测「柳州站」3 条,
|
||||
// 备份里这类分组共 10 组),单槽位 map 会让同一坐标最多只配上一条,剩下的一律判成
|
||||
// 「备份新增」——于是每次合并都再插一条重复地点、差异永远归不了零。
|
||||
func DiffPlaces(stored []StoredPlace, b *Backup) PlaceDiff {
|
||||
// 内部用**多值**索引:真机一个坐标下会有好几条同名同坐标的地点(实测「柳州站」3 条),
|
||||
// 单槽位 map 会让同一坐标最多只配上一条,剩下的一律判成「备份新增」——于是每次合并都再插
|
||||
// 一条重复地点、差异永远归不了零。认领过的一定要标记已用,否则多条会双双落到同一条上。
|
||||
//
|
||||
// 差异判定(DiffPlaces)与合并执行(internal/web)**必须共用这一份实现**:两处各配一遍就会
|
||||
// 漂移——合并侧原来是单槽位 map 自己再配一次,于是同一坐标/同名的多条备份地点全并到同一条
|
||||
// 站内地点上,到访被挂到错的分店(实测「茶颜悦色」两家分店相隔 8.3km)。
|
||||
//
|
||||
// 两个入参的顺序都要稳定(库内按 id、备份按解析顺序),否则候选多于一个时结果会飘。
|
||||
func MatchPlaces(stored []StoredPlace, backup []Location) PlaceMatch {
|
||||
byCoord := make(map[string][]StoredPlace, len(stored))
|
||||
byName := make(map[string][]StoredPlace, len(stored))
|
||||
for _, sp := range stored {
|
||||
@@ -304,7 +313,7 @@ func DiffPlaces(stored []StoredPlace, b *Backup) PlaceDiff {
|
||||
}
|
||||
}
|
||||
used := map[int]bool{}
|
||||
// take 认领第一条还没被用掉的候选,认领即置 used——同一个站内地点不能被两个备份地点配走。
|
||||
// take 认领第一条还没被用掉的候选,认领即置 used——同一条站内地点不能被两个备份地点配走。
|
||||
take := func(list []StoredPlace) (StoredPlace, bool) {
|
||||
for _, sp := range list {
|
||||
if !used[sp.SrcPK] {
|
||||
@@ -331,27 +340,41 @@ func DiffPlaces(stored []StoredPlace, b *Backup) PlaceDiff {
|
||||
}
|
||||
return StoredPlace{}, false
|
||||
}
|
||||
|
||||
out := make(PlaceMatch, len(backup))
|
||||
for _, l := range backup {
|
||||
if sp, ok := take(byCoord[PlaceCoordKey(l.Lat, l.Lon)]); ok {
|
||||
out[l.SrcPK] = sp
|
||||
continue
|
||||
}
|
||||
if sp, ok := takeByName(byName[l.Name], l); ok {
|
||||
out[l.SrcPK] = sp
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DiffPlaces 对齐地点。配对规则见 MatchPlaces:配上的比字段,配不上的两边分别是
|
||||
// 「备份新增」与「仅站内有」。
|
||||
func DiffPlaces(stored []StoredPlace, b *Backup) PlaceDiff {
|
||||
match := MatchPlaces(stored, b.Locations)
|
||||
matched := map[int]bool{}
|
||||
var d PlaceDiff
|
||||
classify := func(sp StoredPlace, l Location) {
|
||||
for _, l := range b.Locations {
|
||||
sp, ok := match[l.SrcPK]
|
||||
if !ok {
|
||||
d.Added = append(d.Added, l)
|
||||
continue
|
||||
}
|
||||
matched[sp.SrcPK] = true
|
||||
if fields := placeFieldChanges(sp, l); len(fields) > 0 {
|
||||
d.Changed = append(d.Changed, PlaceConflict{Backup: l, DB: sp, Fields: fields})
|
||||
} else {
|
||||
d.Same++
|
||||
}
|
||||
}
|
||||
for _, l := range b.Locations {
|
||||
if sp, ok := take(byCoord[PlaceCoordKey(l.Lat, l.Lon)]); ok {
|
||||
classify(sp, l)
|
||||
continue
|
||||
}
|
||||
if sp, ok := takeByName(byName[l.Name], l); ok {
|
||||
classify(sp, l)
|
||||
continue
|
||||
}
|
||||
d.Added = append(d.Added, l)
|
||||
}
|
||||
for _, sp := range stored {
|
||||
if !used[sp.SrcPK] {
|
||||
if !matched[sp.SrcPK] {
|
||||
d.Removed = append(d.Removed, sp)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user