package web import ( "os" "path/filepath" "testing" "rond-with-you/internal/rond" ) // 交通方式是**全局表、以名称区分**:同名两条会被真机当成损坏数据,整包拒收 // (2026-09-29 生产导出就是这么被拒的:`ZTRANSPORT` 里两条「公交」)。 // // 站点的 transport_src 是另一套编号空间——把实机备份合并进来时,同一个 src 会带上两种含义 // (实测 src=4 既是「电🐔」又是「公交」)。导出必须按**名称**归并,并且把每条位移挂回 // 它自己那个名称对应的方式上;只按 src 取一个名字会让另一种含义的位移挂错方式。 func TestExportMergesDuplicateTransportNames(t *testing.T) { f := newMovementFixture(t) ctx, pg, dsID := f.ctx, f.pg, f.dsID // 造出「同一个 src、两种名称」的形状:正是合并实机备份留下的样子 var k1, k2 int if err := pg.QueryRowContext(ctx, `SELECT (SELECT src_pk FROM movements WHERE dataset_id=$1 AND transport_src IS NOT NULL ORDER BY id LIMIT 1), (SELECT src_pk FROM movements WHERE dataset_id=$1 AND transport_src IS NOT NULL ORDER BY id OFFSET 1 LIMIT 1)`, dsID).Scan(&k1, &k2); err != nil { t.Fatalf("挑位移失败: %v", err) } for _, v := range []struct { pk int name string color string icon string }{{k1, "电🐔", "purple", "moped.fill"}, {k2, "公交", "red", "bus.fill"}} { if _, err := pg.ExecContext(ctx, `UPDATE movements SET transport_src=900, transport_name=$3, transport_color=$4, transport_icon=$5 WHERE dataset_id=$1 AND src_pk=$2`, dsID, v.pk, v.name, v.color, v.icon); err != nil { t.Fatalf("预置失败: %v", err) } } t.Cleanup(func() { pg.ExecContext(ctx, `DELETE FROM movements WHERE dataset_id=$1 AND transport_src=900`, dsID) }) // 再把站点原本那条「公交」(src=2)改名,于是「公交」这个名字只剩注入的那条: // 归并后基线里 src=2 那行就成了「同名但编号已被归并掉」的旧行,必须被删掉, // 否则 keepMissing 会把它原样留下 → 包里又是两条同名方式 if _, err := pg.ExecContext(ctx, `UPDATE movements SET transport_name='巴士' WHERE dataset_id=$1 AND transport_src=2`, dsID); err != nil { t.Fatalf("预置改名失败: %v", err) } out := filepath.Join(t.TempDir(), "export.rondbackup") if err := os.WriteFile(out, f.export(t), 0o644); err != nil { t.Fatal(err) } edb, err := rond.Open(out, t.TempDir()) if err != nil { t.Fatalf("打开导出包失败: %v", err) } t.Cleanup(func() { edb.Close() }) // 1) 同名不许出现两条 rows, err := edb.Query(`SELECT ZNAME_ FROM ZTRANSPORT WHERE ZNAME_ IS NOT NULL GROUP BY ZNAME_ HAVING count(*) > 1`) if err != nil { t.Fatalf("读 ZTRANSPORT 失败: %v", err) } for rows.Next() { var name string if err := rows.Scan(&name); err != nil { t.Fatal(err) } t.Errorf("导出包里有两条同名交通方式 %q:真机会判成损坏数据、整包拒收", name) } rows.Close() if err := rows.Err(); err != nil { t.Fatal(err) } // 2) 每条位移必须挂回自己名称对应的那条方式 for _, want := range []struct { pk int name string }{{k1, "电🐔"}, {k2, "公交"}} { var got string if err := edb.QueryRow(`SELECT t.ZNAME_ FROM ZMOVEMENT m JOIN ZTRANSPORT t ON t.Z_PK = m.ZTRANSPORT_ WHERE m.Z_PK = ?`, want.pk).Scan(&got); err != nil { t.Fatalf("位移 #%d 的方式查不到: %v", want.pk, err) } if got != want.name { t.Errorf("位移 #%d 应挂到 %q,实际挂到了 %q(归并后要按位移自己的名称改挂)", want.pk, want.name, got) } } // 3) 归并后不许留下悬空的 ZTRANSPORT_ 引用 if n := countNull(t, edb, `SELECT count(*) FROM ZMOVEMENT m WHERE m.ZTRANSPORT_ IS NOT NULL AND m.ZTRANSPORT_ <> 0 AND NOT EXISTS (SELECT 1 FROM ZTRANSPORT t WHERE t.Z_PK = m.ZTRANSPORT_)`); n != 0 { t.Errorf("归并后有 %d 条位移指向不存在的方式", n) } }