package web import ( "database/sql" "fmt" "os" "path/filepath" "testing" "rond-with-you/internal/rond" ) // 导出的**编号必须与基线对齐**。 // // 为什么:Core Data 的 store 里地点与活动**没有 UUID**,app 只能靠 Z_PK 解引用 // (`ZVISIT.ZLOCATION` / `ZVISIT.ZACTIVITY_`)。站点这边的编号是从「导入→导出」循环里滚出来的, // 与手机 store 的编号不是一套——2026-09-29 实测:同一批到访里 920/920 的地点引用、903/920 的 // 活动引用与真机包不同(引用到的地点按名称+坐标完全一致),结果是「导入成功但看不到数据」。 // // 这里把库里的编号整体平移(模拟「数据来自编号完全不同的备份」),再导出,要求导出结果回到基线的编号。 func TestExportAlignsPKsToBaseline(t *testing.T) { f := newMovementFixture(t) f.shiftSrcPKs(t, 500000) // 库内编号整体平移 ⇒ 与基线完全不同 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() }) base := f.openSource(t) // 基线 = 导入用的那份包 type ref struct { placePK, actPK sql.NullInt64 } load := func(db *sql.DB) map[string]ref { rows, err := db.Query(`SELECT hex(ZIDENTIFIER_), ZLOCATION, ZACTIVITY_ FROM ZVISIT WHERE ZIDENTIFIER_ IS NOT NULL`) if err != nil { t.Fatalf("读到访失败: %v", err) } defer rows.Close() m := map[string]ref{} for rows.Next() { var u string var r ref if err := rows.Scan(&u, &r.placePK, &r.actPK); err != nil { t.Fatal(err) } m[u] = r } return m } describe := func(db *sql.DB, placePK, actPK sql.NullInt64) (string, string) { p := "<空>" if placePK.Valid { var n string var la, lo float64 if err := db.QueryRow(`SELECT COALESCE(ZNAME_,''), COALESCE(ZLATITUDE,0), COALESCE(ZLONGITUDE,0) FROM ZLOCATION WHERE Z_PK=?`, placePK.Int64).Scan(&n, &la, &lo); err != nil { return "<地点不存在>", "" } p = fmt.Sprintf("%s %.5f,%.5f", n, la, lo) } a := "<空>" if actPK.Valid { var n string if err := db.QueryRow(`SELECT COALESCE(ZNAME_,'') FROM ZACTIVITY WHERE Z_PK=?`, actPK.Int64).Scan(&n); err != nil { return p, "<活动不存在>" } a = n } return p, a } baseVisits, expVisits := load(base), load(edb) if len(baseVisits) == 0 || len(expVisits) == 0 { t.Fatalf("包里的到访为空:基线 %d 条 / 导出 %d 条", len(baseVisits), len(expVisits)) } common, placeOK, actOK := 0, 0, 0 for u, br := range baseVisits { er, ok := expVisits[u] if !ok { continue } common++ bp, ba := describe(base, br.placePK, br.actPK) ep, ea := describe(edb, er.placePK, er.actPK) // 关键:**编号本身**要和基线一致(app 就是拿这个编号解引用的)。只看「名称+坐标」 // 查不出问题——编号不一致时引用依然自洽,但手机那边解不出来。 samePK := br.placePK.Valid == er.placePK.Valid && (!br.placePK.Valid || br.placePK.Int64 == er.placePK.Int64) if samePK { placeOK++ } else if placeOK+3 > common { // 只记前几条,避免刷屏 // 少数不一致是正常的:真机自己就有重名的重复行(如同名车站两条), // 地点没有 UUID,1:1 归属只能任选其一。所以例子只记日志,断言看比例。 t.Logf("到访 %s 的地点编号不同:基线 #%v(%q) / 导出 #%v(%q)", u[:8], br.placePK.Int64, bp, er.placePK.Int64, ep) } sameAct := br.actPK.Valid == er.actPK.Valid && (!br.actPK.Valid || br.actPK.Int64 == er.actPK.Int64) if sameAct { actOK++ } else if actOK+3 > common { t.Logf("到访 %s 的活动编号不同:基线 #%v(%q) / 导出 #%v(%q)", u[:8], br.actPK.Int64, ba, er.actPK.Int64, ea) } } if common == 0 { t.Fatal("基线与导出没有可对齐的到访") } if placeOK*100 < common*95 { t.Errorf("地点引用与基线一致率过低:%d/%d(应 ≥95%%)", placeOK, common) } if actOK*100 < common*95 { t.Errorf("活动引用与基线一致率过低:%d/%d(应 ≥95%%)", actOK, common) } t.Logf("按 UUID 对齐共同到访 %d 条:地点引用一致 %d、活动引用一致 %d", common, placeOK, actOK) // 对齐之后不许出现悬空引用(含自引用 ZMERGEDTO / ZPARENT) for _, q := range []struct{ name, sql string }{ {"visit→location", `SELECT count(*) FROM ZVISIT a WHERE a.ZLOCATION IS NOT NULL AND a.ZLOCATION<>0 AND NOT EXISTS (SELECT 1 FROM ZLOCATION b WHERE b.Z_PK=a.ZLOCATION)`}, {"visit→activity", `SELECT count(*) FROM ZVISIT a WHERE a.ZACTIVITY_ IS NOT NULL AND a.ZACTIVITY_<>0 AND NOT EXISTS (SELECT 1 FROM ZACTIVITY b WHERE b.Z_PK=a.ZACTIVITY_)`}, {"visit→mergedTo", `SELECT count(*) FROM ZVISIT a WHERE a.ZMERGEDTO IS NOT NULL AND a.ZMERGEDTO<>0 AND NOT EXISTS (SELECT 1 FROM ZVISIT b WHERE b.Z_PK=a.ZMERGEDTO)`}, {"visit→parent", `SELECT count(*) FROM ZVISIT a WHERE a.ZPARENT IS NOT NULL AND a.ZPARENT<>0 AND NOT EXISTS (SELECT 1 FROM ZVISIT b WHERE b.Z_PK=a.ZPARENT)`}, {"movement→visit", `SELECT count(*) FROM ZMOVEMENT a WHERE a.ZVISITFROM_ IS NOT NULL AND a.ZVISITFROM_<>0 AND NOT EXISTS (SELECT 1 FROM ZVISIT b WHERE b.Z_PK=a.ZVISITFROM_)`}, {"movement→transport", `SELECT count(*) FROM ZMOVEMENT a WHERE a.ZTRANSPORT_ IS NOT NULL AND a.ZTRANSPORT_<>0 AND NOT EXISTS (SELECT 1 FROM ZTRANSPORT b WHERE b.Z_PK=a.ZTRANSPORT_)`}, {"weather→visit", `SELECT count(*) FROM ZHOURLYWEATHER a WHERE a.ZVISIT IS NOT NULL AND a.ZVISIT<>0 AND NOT EXISTS (SELECT 1 FROM ZVISIT b WHERE b.Z_PK=a.ZVISIT)`}, } { if n := countNull(t, edb, q.sql); n != 0 { t.Errorf("编号对齐后出现 %d 条悬空引用:%s", n, q.name) } } }