合并:地点配对只留一份实现(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:
tamakyi
2026-09-29 10:46:54 +08:00
parent 370c4285f8
commit 8763b850be
7 changed files with 178 additions and 66 deletions
+6 -6
View File
@@ -19,9 +19,9 @@ func TestBuildMergePlanPlaceClash(t *testing.T) {
if len(p.UpdPlaces) != 1 {
t.Fatalf("只有选了「用实机」的地点该更新: %+v", p.UpdPlaces)
}
// 键必须是库里的坐标键:备份侧那条没有 Key 字段,拿它当键会让处理函数找不到行
if p.UpdPlaces[0].CoordKey != keyA || p.UpdPlaces[0].L.Name != "备份名A" {
t.Fatalf("应当用库里的坐标键 + 备份的值: %+v", p.UpdPlaces[0])
// 带的是备份侧那条的整份值;「改库里的哪一行」由配对(MatchPlaces)决定,计划里不再存键
if p.UpdPlaces[0].L.Name != "备份名A" {
t.Fatalf("应带备份侧的值: %+v", p.UpdPlaces[0])
}
if len(p.UpdVisits)+len(p.AddVisits)+len(p.UpdActs)+len(p.AddActs) != 0 {
t.Fatal("没勾新增就不该带出到访/活动改动")
@@ -40,9 +40,9 @@ func TestBuildMergePlanActivityClash(t *testing.T) {
if len(p.UpdActs) != 1 {
t.Fatalf("只有选了「用实机」的活动该更新: %+v", p.UpdActs)
}
// 改名也能对上:键取 UUID 而不是名称;没 UUID 的才退回 "n:"+名称
if p.UpdActs[0].Key != uuidC || p.UpdActs[0].A.Name != "散步" {
t.Fatalf("有 UUID 的活动应按 UUID 对上: %+v", p.UpdActs[0])
// 改名也能对上:选择是按「UUID(没有才退回名称)」提交的,配对早就发生在比对阶段
if p.UpdActs[0].A.Name != "散步" {
t.Fatalf("应带备份侧的值: %+v", p.UpdActs[0])
}
}