合并:地点配对只留一份实现(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:
@@ -5,6 +5,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"rond-with-you/internal/rond"
|
||||
)
|
||||
|
||||
// mergeBackup 走真实的合并处理函数;失败(重定向里带 err=)直接判错。
|
||||
@@ -363,3 +365,56 @@ func TestBackupMergeRefusesDeletingPlaceInUse(t *testing.T) {
|
||||
t.Errorf("被拒绝后到访数不该变,实际 %d(期望 %d)", got, beforeVisits+1)
|
||||
}
|
||||
}
|
||||
|
||||
// 两条同名备份地点在合并时必须各配一条站内地点。以前这里用**单槽位** map 自己再配一遍,
|
||||
// 两条同名地点会映射到同一条站内地点上,到访于是被挂到错的分店——实测「茶颜悦色」两家
|
||||
// 分店相隔 8.3km,到访却都指向同一条。现在配对直接沿用比对阶段那一份(rond.MatchPlaces)。
|
||||
func TestMergePlacesMapsSameNameLocationsOneToOne(t *testing.T) {
|
||||
f := newMovementFixture(t)
|
||||
|
||||
// 两条同名的站内地点,放在不太可能与真实数据撞车的坐标上
|
||||
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon)
|
||||
VALUES ($1, 920001, '同名测试店', 1.000000, 1.000000),
|
||||
($1, 920002, '同名测试店', 1.100000, 1.100000)`, f.dsID); err != nil {
|
||||
t.Fatalf("插入站内地点失败: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
f.pg.ExecContext(f.ctx, `DELETE FROM places WHERE dataset_id=$1 AND src_pk IN (920001,920002)`, f.dsID)
|
||||
})
|
||||
|
||||
// 备份各一条同名地点,分别距上面两条约 700m(属于坐标纠偏,该配上);
|
||||
// 与另一条相距 15km,远超 1km 上限,不该被配走
|
||||
b := &rond.Backup{Locations: []rond.Location{
|
||||
{SrcPK: 41, Name: "同名测试店", Lat: 1.004500, Lon: 1.004500},
|
||||
{SrcPK: 42, Name: "同名测试店", Lat: 1.095500, Lon: 1.095500},
|
||||
}}
|
||||
|
||||
var placeID map[int]int64
|
||||
if err := f.srv.withTx(f.ctx, func(ex execer) error {
|
||||
var err error
|
||||
placeID, err = f.srv.mergePlaces(f.ctx, ex, f.dsID, b, rond.MergePlan{}, &mergeStats{})
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("mergePlaces 失败: %v", err)
|
||||
}
|
||||
if len(placeID) != 2 {
|
||||
t.Fatalf("两条都该配上,实际 %d 条", len(placeID))
|
||||
}
|
||||
if placeID[41] == placeID[42] {
|
||||
t.Fatalf("两条同名备份地点被映射到同一条站内地点上: 41→%d 42→%d", placeID[41], placeID[42])
|
||||
}
|
||||
srcOf := func(id int64) int {
|
||||
t.Helper()
|
||||
var pk int
|
||||
if err := f.pg.QueryRowContext(f.ctx, `SELECT src_pk FROM places WHERE id=$1`, id).Scan(&pk); err != nil {
|
||||
t.Fatalf("反查 id=%d 失败: %v", id, err)
|
||||
}
|
||||
return pk
|
||||
}
|
||||
if got := srcOf(placeID[41]); got != 920001 {
|
||||
t.Errorf("备份 #41 应配到最近的 920001,实际 %d", got)
|
||||
}
|
||||
if got := srcOf(placeID[42]); got != 920002 {
|
||||
t.Errorf("备份 #42 应配到最近的 920002,实际 %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user