Files
rond-with-you/internal/rond/diff_test.go
T

549 lines
22 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)
}
}
// 站点补录的行(没有留档 UUID)导出过一次之后,包里那条带着的是站点给它补的派生 UUID。
// 这份包再合并回来时,必须认出「这就是我导出去的那条」——不认就会当成「备份新增」又插一条,
// 时间线上于是出现两条一模一样的到访(实测数据集 9 有 43 对)。
func TestDiffVisitsAlignsSiteCreatedRowByDerivedUUID(t *testing.T) {
const srcPK = 42
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{900: DerivedVisitUUID(srcPK)}, // 导出时补给这条的
ActivityUUID: map[int]string{},
}
stored := []StoredVisit{{
SrcPK: srcPK, UUID: "", // 站点补录:entity_raw 里没有这一行
Arrival: tp("2026-09-01 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 != 1 {
t.Fatalf("应配上 1 条,实际 same=%d changed=%d", d.Same, len(d.Changed))
}
}
// 反例:派生 UUID 是别的编号的,就不该被认领(否则会把两条不同的到访混成一条)。
func TestDiffVisitsDoesNotAlignOtherDerivedUUID(t *testing.T) {
const srcPK = 42
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{900: DerivedVisitUUID(srcPK + 1)},
}
stored := []StoredVisit{{
SrcPK: srcPK, Arrival: tp("2026-09-01 10:00"), PlaceKey: PlaceCoordKey(24.1, 109.1),
}}
d := DiffVisits(stored, b)
if len(d.Added) != 1 || len(d.Removed) != 1 {
t.Fatalf("派生 UUID 对不上就该分别报出: +%d -%d", len(d.Added), len(d.Removed))
}
}
// 兜底配对要认得出「同一条但时间被改过」——时间不同就配不上,此时按新增 + 仅站内有报出,
// 总比静默当成同一条好。
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)
}
}
// 多条同名备份地点必须**一条配一条**(各配最近的那条),不能全并到同一条站内地点上:
// 合并执行(mergePlaces)沿用的就是这份配对,并错了到访就会挂到错的分店
// (实测「茶颜悦色」两家分店相隔 8.3km,到访却指向同一条)。
func TestMatchPlacesKeepsDuplicateNameGroupsDistinct(t *testing.T) {
mk := func(pk int, lat, lon float64) StoredPlace {
sp := StoredPlace{SrcPK: pk, Name: "茶颜悦色", Lat: lat, Lon: lon}
sp.Key = PlaceCoordKey(lat, lon)
return sp
}
stored := []StoredPlace{
mk(11, 28.185360, 112.981422), // 与备份 #41 相距约 665m
mk(12, 28.245842, 113.031303), // 与备份 #42 相距约 670m
}
backup := []Location{
{SrcPK: 41, Name: "茶颜悦色", Lat: 28.181954, Lon: 112.986995},
{SrcPK: 42, Name: "茶颜悦色", Lat: 28.242581, Lon: 113.037061},
}
m := MatchPlaces(stored, backup)
if len(m) != 2 {
t.Fatalf("两条都该配上,实际 %d", len(m))
}
if m[41].SrcPK == m[42].SrcPK {
t.Fatalf("两条同名备份地点被并到了同一条站内地点上: 41→%d 42→%d", m[41].SrcPK, m[42].SrcPK)
}
if m[41].SrcPK != 11 || m[42].SrcPK != 12 {
t.Fatalf("应各配最近的一条(41→11、42→12),实际 41→%d 42→%d", m[41].SrcPK, m[42].SrcPK)
}
// 两家分店挨得近(都在 1km 内)时也要一条配一条:坐标键对不上、只能靠名称兜底,
// 若认领了却不标记已用,两条会双双落到第一条上。
near := []StoredPlace{mk(21, 1.000000, 1.000000), mk(22, 1.005000, 1.005000)}
closeBy := []Location{
{SrcPK: 51, Name: "茶颜悦色", Lat: 1.002400, Lon: 1.002400},
{SrcPK: 52, Name: "茶颜悦色", Lat: 1.002600, Lon: 1.002600},
}
m2 := MatchPlaces(near, closeBy)
if len(m2) != 2 {
t.Fatalf("两条都该配上,实际 %d", len(m2))
}
if m2[51].SrcPK == m2[52].SrcPK {
t.Fatalf("两条同名备份地点被并到了同一条站内地点上: 51→%d 52→%d", m2[51].SrcPK, m2[52].SrcPK)
}
}
// 「地点」这一项比的是坐标,只印名称时同名不同坐标会渲染成「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)
}
}
}