- DiffPlaces 的 takeByName 只在两点相距 ≤1km 时才认领:名称不是唯一键,连锁店同名是常态 (真机里「中国体育彩票」3 条、「长沙南站」2 条、「茶颜悦色」2 条)。没有上限时,坐标配不上 的两条同名地点会被强行配成一对——实测把佛山的「中国体育彩票」配给了贺州那条(直线 236.8km), 站长勾「用实机」就把站内那条地点的坐标搬到另一座城市(实测 src_pk=71 从贺州搬到佛山)。 注意 geo.Haversine 返回的是公里,常量按公里写。 - visitFieldChanges 的「地点」在两边名称相同时用 coordSuffix 把坐标印出来:这一项比的是坐标, 只印名称会渲染成「X → X」,站长看不出差在哪(连锁店重名、同一地点两边坐标略有出入都会走到这里)。 - 新增用例 3 条(相隔 236.8km 不该配对、665m 仍要配对、名称相同时必须带坐标)+ 变异验证。 - 复测(回滚后按浏览器默认全选「用实机」重跑):被搬动超过 1km 的地点从 1 个变 0 个; 合并后再比对 到访 0/47/1、地点 0/4/1、活动 0/0/0;953 条 UUID 到访全命中、到达时间 0 偏差。
464 lines
18 KiB
Go
464 lines
18 KiB
Go
package rond
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func tp(s string) *time.Time {
|
|
t, err := time.Parse("2006-01-02 15:04", s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return &t
|
|
}
|
|
|
|
func ip(v int) *int { return &v }
|
|
|
|
// 两组编号刻意完全不同:rond 每次导出都会重排 Z_PK,对齐只能靠 UUID。
|
|
const (
|
|
uuidA = "11111111111111111111111111111111"
|
|
uuidB = "22222222222222222222222222222222"
|
|
uuidC = "33333333333333333333333333333333"
|
|
)
|
|
|
|
func sampleBackup() *Backup {
|
|
return &Backup{
|
|
Visits: []Visit{
|
|
{SrcPK: 555, Arrival: tp("2026-09-01 10:00"), Departure: tp("2026-09-01 11:00"),
|
|
LocationSrcPK: ip(3), ActivitySrcPK: ip(7)},
|
|
{SrcPK: 556, Arrival: tp("2026-09-02 10:00"), LocationSrcPK: ip(3)},
|
|
},
|
|
Locations: []Location{
|
|
{SrcPK: 3, Name: "家", Lat: 24.1, Lon: 109.1, City: "柳州市"},
|
|
{SrcPK: 4, Name: "公司", Lat: 24.2, Lon: 109.2, City: "柳州市"},
|
|
},
|
|
Activities: []Activity{
|
|
{SrcPK: 7, Name: "工作"},
|
|
{SrcPK: 8, Name: "散步"},
|
|
},
|
|
VisitUUID: map[int]string{555: uuidA, 556: uuidB},
|
|
ActivityUUID: map[int]string{7: uuidC},
|
|
}
|
|
}
|
|
|
|
func TestDiffVisitsAlignsByUUIDNotPK(t *testing.T) {
|
|
stored := []StoredVisit{
|
|
{SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 10:00"), Departure: tp("2026-09-01 11:00"),
|
|
PlaceKey: PlaceCoordKey(24.1, 109.1), ActivityName: "工作"},
|
|
{SrcPK: 2, UUID: uuidB, Arrival: tp("2026-09-02 10:00"),
|
|
PlaceKey: PlaceCoordKey(24.1, 109.1)},
|
|
}
|
|
d := DiffVisits(stored, sampleBackup())
|
|
if len(d.Added) != 0 || len(d.Removed) != 0 || len(d.Changed) != 0 {
|
|
t.Fatalf("两边一致时不该有差异: +%d -%d ~%d", len(d.Added), len(d.Removed), len(d.Changed))
|
|
}
|
|
if d.Same != 2 {
|
|
t.Fatalf("应配上 2 条,实际 %d", d.Same)
|
|
}
|
|
}
|
|
|
|
func TestDiffVisitsFieldChanges(t *testing.T) {
|
|
stored := []StoredVisit{{
|
|
SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 09:00"), Departure: tp("2026-09-01 11:00"),
|
|
PlaceKey: PlaceCoordKey(24.1, 109.1), ActivityName: "工作", Remark: "站点备注",
|
|
}}
|
|
d := DiffVisits(stored, sampleBackup())
|
|
if len(d.Changed) != 1 {
|
|
t.Fatalf("到达时间不同应算冲突,实际 %d 条", len(d.Changed))
|
|
}
|
|
f := d.Changed[0].Fields
|
|
if len(f) != 2 {
|
|
t.Fatalf("应报出到达时间与备注两项,实际 %v", f)
|
|
}
|
|
if f[0].Label != "到达时间" || f[1].Label != "备注" {
|
|
t.Fatalf("字段名不对: %v", f)
|
|
}
|
|
if f[1].From != "站点备注" || f[1].To != "" {
|
|
t.Fatalf("方向搞反了(From 应是库里的值): %+v", f[1])
|
|
}
|
|
}
|
|
|
|
// 站点新建的到访没有 UUID,而且它的 src_pk 很可能与真机新行的编号撞上——
|
|
// 绝不能因为编号相同就当成同一条。
|
|
func TestDiffVisitsIgnoresPKCollision(t *testing.T) {
|
|
stored := []StoredVisit{{
|
|
SrcPK: 555, UUID: "", Arrival: tp("2026-08-01 08:00"), UserAdded: true,
|
|
PlaceKey: PlaceCoordKey(30.0, 120.0),
|
|
}}
|
|
d := DiffVisits(stored, sampleBackup())
|
|
if len(d.Added) != 2 {
|
|
t.Fatalf("备份里两条都是新增,实际 %d", len(d.Added))
|
|
}
|
|
if len(d.Removed) != 1 {
|
|
t.Fatalf("站点补录那条应算「仅库里有」,实际 %d", len(d.Removed))
|
|
}
|
|
if !d.Removed[0].UserAdded {
|
|
t.Error("站点补录的标记要带上,页面上要区分开")
|
|
}
|
|
}
|
|
|
|
// 真机偶尔有不带 UUID 的到访,只按 UUID 配对会让同一条既算新增又算仅站内有。
|
|
func TestDiffVisitsFallsBackWhenUUIDMissing(t *testing.T) {
|
|
b := &Backup{
|
|
Visits: []Visit{
|
|
{SrcPK: 900, Arrival: tp("2026-09-01 10:00"), LocationSrcPK: ip(3)},
|
|
{SrcPK: 901, Arrival: tp("2026-09-02 10:00"), LocationSrcPK: ip(3)},
|
|
},
|
|
Locations: []Location{{SrcPK: 3, Name: "家", Lat: 24.1, Lon: 109.1}},
|
|
VisitUUID: map[int]string{}, // 一条都没有 UUID
|
|
ActivityUUID: map[int]string{},
|
|
}
|
|
stored := []StoredVisit{
|
|
{SrcPK: 1, Arrival: tp("2026-09-01 10:00"), PlaceKey: PlaceCoordKey(24.1, 109.1)},
|
|
{SrcPK: 2, Arrival: tp("2026-09-02 10:00"), PlaceKey: PlaceCoordKey(24.1, 109.1)},
|
|
}
|
|
d := DiffVisits(stored, b)
|
|
if len(d.Added) != 0 || len(d.Removed) != 0 {
|
|
t.Fatalf("没有 UUID 时应按「到达时间 + 地点」兜底配对: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
if d.Same != 2 {
|
|
t.Fatalf("两条都该配上,实际 %d", d.Same)
|
|
}
|
|
}
|
|
|
|
// 兜底配对要认得出「同一条但时间被改过」——时间不同就配不上,此时按新增 + 仅站内有报出,
|
|
// 总比静默当成同一条好。
|
|
func TestDiffVisitsFallbackNeedsSameTime(t *testing.T) {
|
|
b := &Backup{
|
|
Visits: []Visit{{SrcPK: 900, Arrival: tp("2026-09-01 10:00"), LocationSrcPK: ip(3)}},
|
|
Locations: []Location{{SrcPK: 3, Name: "家", Lat: 24.1, Lon: 109.1}},
|
|
VisitUUID: map[int]string{},
|
|
}
|
|
stored := []StoredVisit{{SrcPK: 1, Arrival: tp("2026-09-01 11:00"), PlaceKey: PlaceCoordKey(24.1, 109.1)}}
|
|
d := DiffVisits(stored, b)
|
|
if len(d.Added) != 1 || len(d.Removed) != 1 {
|
|
t.Fatalf("时间对不上时该分别报出: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
}
|
|
|
|
func TestDiffVisitsSkipsRowsWithoutArrival(t *testing.T) {
|
|
b := sampleBackup()
|
|
b.Visits = append(b.Visits, Visit{SrcPK: 557, LocationSrcPK: ip(3)})
|
|
d := DiffVisits(nil, b)
|
|
if len(d.Added) != 2 {
|
|
t.Fatalf("没有到达时间的行导入时就被跳过,不该算新增,实际 %d", len(d.Added))
|
|
}
|
|
}
|
|
|
|
func TestDiffPlacesByKey(t *testing.T) {
|
|
b := sampleBackup()
|
|
stored := []StoredPlace{
|
|
{SrcPK: 10, Key: PlaceCoordKey(24.1, 109.1), Name: "家", Lat: 24.1, Lon: 109.1, City: "柳州市"},
|
|
}
|
|
d := DiffPlaces(stored, b)
|
|
if len(d.Added) != 1 || d.Added[0].Name != "公司" {
|
|
t.Fatalf("应只有「公司」是新增: %+v", d.Added)
|
|
}
|
|
if len(d.Removed) != 0 || len(d.Changed) != 0 || d.Same != 1 {
|
|
t.Fatalf("「家」应配上且无差异: same=%d changed=%d removed=%d", d.Same, len(d.Changed), len(d.Removed))
|
|
}
|
|
}
|
|
|
|
func TestDiffPlacesReportsFieldChanges(t *testing.T) {
|
|
b := sampleBackup()
|
|
stored := []StoredPlace{
|
|
{SrcPK: 10, Key: PlaceCoordKey(24.1, 109.1), Name: "家", Lat: 24.1, Lon: 109.1, City: "桂林市"},
|
|
}
|
|
d := DiffPlaces(stored, b)
|
|
if len(d.Changed) != 1 {
|
|
t.Fatalf("城市不同应算冲突,实际 %d", len(d.Changed))
|
|
}
|
|
if f := d.Changed[0].Fields; len(f) != 1 || f[0].Label != "城市" || f[0].From != "桂林市" || f[0].To != "柳州市" {
|
|
t.Fatalf("字段差异不对: %+v", f)
|
|
}
|
|
}
|
|
|
|
func TestDiffActivities(t *testing.T) {
|
|
b := sampleBackup()
|
|
stored := []StoredActivity{
|
|
{SrcPK: 20, Name: "工作", UUID: uuidC},
|
|
{SrcPK: 21, Name: "站点自建"},
|
|
}
|
|
d := DiffActivities(stored, b)
|
|
if len(d.Added) != 1 || d.Added[0].Name != "散步" {
|
|
t.Fatalf("「散步」应是新增: %+v", d.Added)
|
|
}
|
|
if len(d.Removed) != 1 || d.Removed[0].Name != "站点自建" {
|
|
t.Fatalf("站点自建的活动应算仅库里有: %+v", d.Removed)
|
|
}
|
|
if d.Same != 1 {
|
|
t.Fatalf("「工作」应配上,实际 same=%d", d.Same)
|
|
}
|
|
}
|
|
|
|
// 活动改了名:UUID 还在,就该认出来是同一个,只报名称变化,而不是「删一个加一个」。
|
|
func TestDiffActivitiesRenameByUUID(t *testing.T) {
|
|
b := sampleBackup()
|
|
stored := []StoredActivity{{SrcPK: 20, Name: "上班", UUID: uuidC}}
|
|
d := DiffActivities(stored, b)
|
|
if len(d.Added) != 1 || len(d.Removed) != 0 {
|
|
t.Fatalf("只该多出「散步」,「上班」应认出是同一个: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
if len(d.Changed) != 1 || d.Changed[0].Fields[0].From != "上班" || d.Changed[0].Fields[0].To != "工作" {
|
|
t.Fatalf("应按 UUID 认出重命名: %+v", d.Changed)
|
|
}
|
|
}
|
|
|
|
func TestPlaceCoordKeyStable(t *testing.T) {
|
|
a := PlaceCoordKey(24.1, 109.1)
|
|
if a != PlaceCoordKey(24.1, 109.1) {
|
|
t.Fatal("同样的输入应得到同样的键")
|
|
}
|
|
if a == PlaceCoordKey(24.1, 109.2) {
|
|
t.Fatal("坐标不同不该算同一个地点")
|
|
}
|
|
}
|
|
|
|
// 站点把地点反查改名(坐标没动)时,到访层面不该报「换了地点」——
|
|
// 实测里这一条会让 17 条到访全变成假冲突。
|
|
func TestDiffVisitsIgnoresPlaceRename(t *testing.T) {
|
|
b := &Backup{
|
|
Visits: []Visit{{SrcPK: 5, Arrival: tp("2026-09-01 10:00"), LocationSrcPK: ip(3)}},
|
|
Locations: []Location{{SrcPK: 3, Name: "河池市国民体质监测中心", Lat: 24.691827, Lon: 108.035268}},
|
|
VisitUUID: map[int]string{5: uuidA},
|
|
}
|
|
stored := []StoredVisit{{
|
|
SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 10:00"),
|
|
PlaceKey: PlaceCoordKey(24.691827, 108.035268), PlaceName: "体育馆美食街",
|
|
}}
|
|
d := DiffVisits(stored, b)
|
|
if len(d.Changed) != 0 {
|
|
t.Fatalf("只改了地点名不该算到访冲突: %+v", d.Changed[0].Fields)
|
|
}
|
|
if d.Same != 1 {
|
|
t.Fatalf("应算作一致,实际 same=%d", d.Same)
|
|
}
|
|
}
|
|
|
|
// 反之,地点层面要把改名报出来。
|
|
func TestDiffPlacesReportsRename(t *testing.T) {
|
|
b := &Backup{Locations: []Location{
|
|
{SrcPK: 3, Name: "河池市国民体质监测中心", Lat: 24.691827, Lon: 108.035268, City: "河池市"},
|
|
}}
|
|
stored := []StoredPlace{{
|
|
SrcPK: 10, Key: PlaceCoordKey(24.691827, 108.035268), Name: "体育馆美食街",
|
|
Lat: 24.691827, Lon: 108.035268, City: "河池市",
|
|
}}
|
|
d := DiffPlaces(stored, b)
|
|
if len(d.Added) != 0 || len(d.Removed) != 0 {
|
|
t.Fatalf("坐标一样就是同一个地点: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
if len(d.Changed) != 1 {
|
|
t.Fatalf("改名应报成一条冲突,实际 %d", len(d.Changed))
|
|
}
|
|
f := d.Changed[0].Fields
|
|
if len(f) != 1 || f[0].Label != "名称" || f[0].From != "体育馆美食街" || f[0].To != "河池市国民体质监测中心" {
|
|
t.Fatalf("字段差异不对: %+v", f)
|
|
}
|
|
}
|
|
|
|
// 没有坐标的地点不能都落到同一个键上,否则两个不同地点会互相认领。
|
|
func TestDiffPlacesWithoutCoordDoesNotCollide(t *testing.T) {
|
|
b := &Backup{Locations: []Location{{SrcPK: 3, Name: "甲地"}}}
|
|
stored := []StoredPlace{
|
|
{SrcPK: 10, Name: "甲地"},
|
|
{SrcPK: 11, Name: "乙地"},
|
|
}
|
|
d := DiffPlaces(stored, b)
|
|
if len(d.Added) != 0 || len(d.Removed) != 1 {
|
|
t.Fatalf("应只配上同名的「甲地」: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
if d.Removed[0].Name != "乙地" {
|
|
t.Fatalf("没配上的应是「乙地」,实际 %q", d.Removed[0].Name)
|
|
}
|
|
if d.Same != 1 {
|
|
t.Fatalf("「甲地」应算一致,实际 %d", d.Same)
|
|
}
|
|
}
|
|
|
|
// 站点把坐标纠偏过(名称没变)也要认得出是同一个地点。
|
|
func TestDiffPlacesMatchesByNameWhenCoordMoved(t *testing.T) {
|
|
b := &Backup{Locations: []Location{{SrcPK: 3, Name: "家", Lat: 24.100001, Lon: 109.100001}}}
|
|
stored := []StoredPlace{{
|
|
SrcPK: 10, Key: PlaceCoordKey(24.1, 109.1), Name: "家", Lat: 24.1, Lon: 109.1,
|
|
}}
|
|
d := DiffPlaces(stored, b)
|
|
if len(d.Added) != 0 || len(d.Removed) != 0 {
|
|
t.Fatalf("名称一样就该配上: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
if len(d.Changed) != 1 || d.Changed[0].Fields[0].Label != "纬度" {
|
|
t.Fatalf("应报出坐标差异: %+v", d.Changed)
|
|
}
|
|
}
|
|
|
|
// 真机一个坐标下会有好几条同名同坐标的地点(实测「柳州站」同一个坐标 3 条,备份里这类分组共 10 组)。
|
|
// 用单槽位 map 时,同一坐标最多只能配上一条,第 3 条永远判「备份新增」→ 每次合并都再插一条
|
|
// 重复地点、差异永远归不了零。
|
|
func TestDiffPlacesAlignsDuplicateNameCoordGroups(t *testing.T) {
|
|
loc := func(pk int) Location {
|
|
return Location{SrcPK: pk, Name: "柳州站", Lat: 24.307787, Lon: 109.388309}
|
|
}
|
|
sp := func(pk int) StoredPlace {
|
|
return StoredPlace{SrcPK: pk, Key: PlaceCoordKey(24.307787, 109.388309),
|
|
Name: "柳州站", Lat: 24.307787, Lon: 109.388309}
|
|
}
|
|
b := &Backup{Locations: []Location{loc(41), loc(42), loc(43)}}
|
|
|
|
d := DiffPlaces([]StoredPlace{sp(11), sp(12), sp(13)}, b)
|
|
if len(d.Added) != 0 || len(d.Removed) != 0 || len(d.Changed) != 0 {
|
|
t.Fatalf("两边各 3 条同名同坐标应一一配上: +%d -%d ~%d", len(d.Added), len(d.Removed), len(d.Changed))
|
|
}
|
|
if d.Same != 3 {
|
|
t.Fatalf("应配上 3 条,实际 %d", d.Same)
|
|
}
|
|
|
|
// 反过来:站内真的少两条时,仍要老老实实报出「备份新增 2」——
|
|
// 多值配对不能变成「只要有同名同坐标就一律不算新增」。
|
|
d = DiffPlaces([]StoredPlace{sp(11)}, b)
|
|
if len(d.Added) != 2 || len(d.Removed) != 0 || d.Same != 1 {
|
|
t.Fatalf("站内只有 1 条时应报新增 2: +%d -%d same=%d", len(d.Added), len(d.Removed), d.Same)
|
|
}
|
|
}
|
|
|
|
// 活动改了名(UUID 还在):那是活动层的差异,不该在挂着它的每条到访上重复报一遍。
|
|
// 实测真机 16 个活动改名会把 355 条到访全变成假冲突,而且逐条选「用实机」也改不掉活动名。
|
|
func TestDiffVisitsIgnoresActivityRename(t *testing.T) {
|
|
b := &Backup{
|
|
Visits: []Visit{{SrcPK: 5, Arrival: tp("2026-09-01 10:00"), ActivitySrcPK: ip(7)}},
|
|
Activities: []Activity{{SrcPK: 7, Name: "吃饭"}},
|
|
VisitUUID: map[int]string{5: uuidA},
|
|
ActivityUUID: map[int]string{7: uuidC},
|
|
}
|
|
stored := []StoredVisit{{
|
|
SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 10:00"),
|
|
ActivityName: "餐厅", ActivityUUID: uuidC,
|
|
}}
|
|
d := DiffVisits(stored, b)
|
|
if len(d.Changed) != 0 {
|
|
t.Fatalf("同一个活动改名不该算到访冲突: %+v", d.Changed[0].Fields)
|
|
}
|
|
if d.Same != 1 {
|
|
t.Fatalf("应算作一致,实际 same=%d", d.Same)
|
|
}
|
|
}
|
|
|
|
// 换成了另一个活动才是真差异。
|
|
func TestDiffVisitsReportsActivityChange(t *testing.T) {
|
|
b := &Backup{
|
|
Visits: []Visit{{SrcPK: 5, Arrival: tp("2026-09-01 10:00"), ActivitySrcPK: ip(7)}},
|
|
Activities: []Activity{{SrcPK: 7, Name: "吃饭"}, {SrcPK: 8, Name: "散步"}},
|
|
VisitUUID: map[int]string{5: uuidA},
|
|
ActivityUUID: map[int]string{7: uuidC, 8: uuidB},
|
|
}
|
|
stored := []StoredVisit{{
|
|
SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 10:00"),
|
|
ActivityName: "散步", ActivityUUID: uuidB,
|
|
}}
|
|
d := DiffVisits(stored, b)
|
|
if len(d.Changed) != 1 {
|
|
t.Fatalf("换了活动该报冲突,实际 %d 条", len(d.Changed))
|
|
}
|
|
f := d.Changed[0].Fields
|
|
if len(f) != 1 || f[0].Label != "活动" || f[0].From != "散步" || f[0].To != "吃饭" {
|
|
t.Fatalf("活动差异不对: %+v", f)
|
|
}
|
|
}
|
|
|
|
// 站点自建的活动没有 UUID,退不回身份就按名称比(老数据要保持原样)。
|
|
func TestDiffVisitsActivityWithoutUUIDFallsBackToName(t *testing.T) {
|
|
b := &Backup{
|
|
Visits: []Visit{{SrcPK: 5, Arrival: tp("2026-09-01 10:00"), ActivitySrcPK: ip(7)}},
|
|
Activities: []Activity{{SrcPK: 7, Name: "吃饭"}},
|
|
VisitUUID: map[int]string{5: uuidA},
|
|
ActivityUUID: map[int]string{7: uuidC},
|
|
}
|
|
// 名称一致 → 不报
|
|
same := []StoredVisit{{
|
|
SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 10:00"), ActivityName: "吃饭",
|
|
}}
|
|
if d := DiffVisits(same, b); len(d.Changed) != 0 {
|
|
t.Fatalf("名称相同时不该报冲突: %+v", d.Changed[0].Fields)
|
|
}
|
|
// 名称不同 → 报
|
|
diff := []StoredVisit{{
|
|
SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 10:00"), ActivityName: "散步",
|
|
}}
|
|
d := DiffVisits(diff, b)
|
|
if len(d.Changed) != 1 || d.Changed[0].Fields[0].Label != "活动" {
|
|
t.Fatalf("没有 UUID 时应退回按名称比: %+v", d.Changed)
|
|
}
|
|
}
|
|
|
|
// 名称兜底配对必须有距离上限:连锁店同名(中国体育彩票、茶颜悦色…)时,坐标配不上的
|
|
// 两条相隔几百公里的同名地点绝不能配成一对——一勾「用实机」就把站内那条搬到另一座城市。
|
|
// 实测踩过:佛山的「中国体育彩票」(22.927572,113.222052) 被配给贺州那条
|
|
// (24.408376,111.554626)(直线 236.8km),合并后站内地点从贺州搬到了佛山。
|
|
func TestDiffPlacesNameFallbackNeedsNearby(t *testing.T) {
|
|
b := &Backup{Locations: []Location{
|
|
{SrcPK: 41, Name: "中国体育彩票", Lat: 24.408376, Lon: 111.554626, City: "贺州市"},
|
|
}}
|
|
stored := []StoredPlace{
|
|
{SrcPK: 11, Key: PlaceCoordKey(22.927572, 113.222052), Name: "中国体育彩票",
|
|
Lat: 22.927572, Lon: 113.222052, City: "佛山市"},
|
|
}
|
|
d := DiffPlaces(stored, b)
|
|
if len(d.Changed) != 0 {
|
|
t.Fatalf("相隔 236.8km 的同名地点不该被配对: %+v", d.Changed[0].Fields)
|
|
}
|
|
if len(d.Added) != 1 || len(d.Removed) != 1 {
|
|
t.Fatalf("应报成「备份新增 + 仅站内有」: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
}
|
|
|
|
// 但坐标纠偏那种(同一地点,两边差几百米)仍要配上——名称兜底本来就是为它准备的。
|
|
func TestDiffPlacesNameFallbackKeepsNearbyCoordFix(t *testing.T) {
|
|
b := &Backup{Locations: []Location{
|
|
{SrcPK: 41, Name: "茶颜悦色", Lat: 28.181954, Lon: 112.986995},
|
|
}}
|
|
stored := []StoredPlace{
|
|
{SrcPK: 11, Key: PlaceCoordKey(28.185360, 112.981422), Name: "茶颜悦色",
|
|
Lat: 28.185360, Lon: 112.981422},
|
|
}
|
|
d := DiffPlaces(stored, b)
|
|
if len(d.Added) != 0 || len(d.Removed) != 0 {
|
|
t.Fatalf("相距 665m 的同名地点应配上: +%d -%d", len(d.Added), len(d.Removed))
|
|
}
|
|
if len(d.Changed) != 1 || d.Changed[0].Fields[0].Label != "纬度" {
|
|
t.Fatalf("应报出坐标差异: %+v", d.Changed)
|
|
}
|
|
}
|
|
|
|
// 「地点」这一项比的是坐标,只印名称时同名不同坐标会渲染成「X → X」,站长看不出差在哪。
|
|
// 名称一样就必须把坐标带出来。
|
|
func TestDiffVisitsPlaceConflictShowsCoords(t *testing.T) {
|
|
b := &Backup{
|
|
Visits: []Visit{{SrcPK: 5, Arrival: tp("2026-09-01 10:00"), LocationSrcPK: ip(3)}},
|
|
Locations: []Location{{SrcPK: 3, Name: "长沙南站", Lat: 28.141772, Lon: 113.067569}},
|
|
VisitUUID: map[int]string{5: uuidA},
|
|
}
|
|
stored := []StoredVisit{{
|
|
SrcPK: 1, UUID: uuidA, Arrival: tp("2026-09-01 10:00"),
|
|
PlaceName: "长沙南站", PlaceKey: PlaceCoordKey(28.146535, 113.062164),
|
|
}}
|
|
d := DiffVisits(stored, b)
|
|
if len(d.Changed) != 1 {
|
|
t.Fatalf("坐标不同应算冲突,实际 %d 条", len(d.Changed))
|
|
}
|
|
f := d.Changed[0].Fields
|
|
if len(f) != 1 || f[0].Label != "地点" {
|
|
t.Fatalf("应只报一项地点差异: %+v", f)
|
|
}
|
|
for _, want := range []string{"长沙南站", "28.146535", "113.062164"} {
|
|
if !strings.Contains(f[0].From, want) {
|
|
t.Fatalf("站内那侧 %q 应含 %q(否则这一行看着像没变化)", f[0].From, want)
|
|
}
|
|
}
|
|
for _, want := range []string{"长沙南站", "28.141772", "113.067569"} {
|
|
if !strings.Contains(f[0].To, want) {
|
|
t.Fatalf("备份那侧 %q 应含 %q", f[0].To, want)
|
|
}
|
|
}
|
|
}
|