验收脚本、工具整理与排查报告

- scripts/verify.sh:把「带齐六个环境变量、且必须 0 skip」这条纪律固化下来
  * DSN 自己从 conf/app-test.ini 取,不把口令写进脚本
  * 库名不含 test 就拒跑(指到维护库会多出「users 不存在」+ 一堆假 skip)
  * 数 SKIP,非 0 直接失败 —— 不带环境变量时 go test 依然回 ok,最容易被当成通过
  * 支持 --fast / --race / --cover / --pkg / --run
- tools/:删掉 6 个一次性探针(git 历史里可翻)和误入库的 __pycache__,
  只留在用的 4 个工具并补 README;.gitignore 补 __pycache__ / *.pyc
- audit-2026-09-21.md:全量排查报告(含 11 批修复记录与收尾审查)
This commit is contained in:
tamakyi
2026-09-24 20:38:18 +08:00
parent d5f6dfa381
commit d2e4392a88
11 changed files with 1172 additions and 928 deletions
+20
View File
@@ -0,0 +1,20 @@
# tools
留在这里的都是**还在用**的工具;一次性探针用完就删(历史里能翻到)。
| 工具 | 用途 |
|---|---|
| `verify_export_structure.py` | 结构级对比:导出的 `.rondbackup` / `.fwss` 与真机原版比表、列、行数、悬空引用。导出改动后必跑。 |
| `verify_rondbackup.py` | 逐字段对比两个 `.rondbackup`(含 `Z_METADATA` 等字节哈希),比上面那个更细。 |
| `photo_coord_audit.py` | 照片导入的坐标体检与批量换算(GCJ-02 / WGS-84)。 |
| `audit_db.sql` | 库内一致性审计:`psql "<dsn>" -f tools/audit_db.sql`。 |
## 验收
`../scripts/verify.sh`(封了六个环境变量,会数 SKIP,拒绝假绿):
```bash
./scripts/verify.sh # 全量,0 skip 才算过
./scripts/verify.sh --fast # gofmt + vet + build
./scripts/verify.sh --race --pkg ./internal/web/
```
Binary file not shown.
-89
View File
@@ -1,89 +0,0 @@
// 一次性探针:验证 ZLOCATION.ZPLACEID / ZVISIT.ZEVENTIDENTIFIER 是否跨备份稳定。
package main
import (
"fmt"
"os"
"rond-with-you/internal/rond"
)
type snap struct {
placeID map[string]int
eventID map[string]int
nulls int
total int
evTotal int
}
func load(p string) *snap {
dir, err := os.MkdirTemp("", "colprobe")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
db, err := rond.Open(p, dir)
if err != nil {
panic(err)
}
defer db.Close()
s := &snap{placeID: map[string]int{}, eventID: map[string]int{}}
rows, err := db.Query(`SELECT Z_PK, COALESCE(ZPLACEID,'') FROM ZLOCATION`)
if err != nil {
panic(err)
}
for rows.Next() {
var pk int
var id string
rows.Scan(&pk, &id)
s.total++
if id == "" {
s.nulls++
} else {
s.placeID[id] = pk
}
}
rows.Close()
rows, err = db.Query(`SELECT COALESCE(ZEVENTIDENTIFIER,'') FROM ZVISIT`)
if err != nil {
panic(err)
}
for rows.Next() {
var id string
rows.Scan(&id)
if id != "" {
s.eventID[id] = 1
s.evTotal++
}
}
rows.Close()
fmt.Printf("%s: ZLOCATION %d 个(ZPLACEID 空 %d),ZEVENTIDENTIFIER 非空 %d\n",
p, s.total, s.nulls, s.evTotal)
return s
}
func main() {
a := load(os.Args[1])
b := load(os.Args[2])
inter, samePK := 0, 0
for k, pa := range a.placeID {
if pb, ok := b.placeID[k]; ok {
inter++
if pa == pb {
samePK++
}
}
}
fmt.Printf("\nZPLACEID 交集 %d(A 独有 %d,B 独有 %d),其中 Z_PK 相同 %d\n",
inter, len(a.placeID)-inter, len(b.placeID)-inter, samePK)
ei := 0
for k := range a.eventID {
if _, ok := b.eventID[k]; ok {
ei++
}
}
fmt.Printf("ZEVENTIDENTIFIER 交集 %d(A %d / B %d)\n", ei, len(a.eventID), len(b.eventID))
}
-438
View File
@@ -1,438 +0,0 @@
// 一次性探针:把一份 .rondbackup 与 PG 里某个数据集的记录逐条对齐,输出差异画像。
// 用完即可删,不属于站点功能。
package main
import (
"context"
"database/sql"
"fmt"
"os"
"sort"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"rond-with-you/internal/rond"
)
func main() {
if len(os.Args) < 4 {
fmt.Println("用法: diffprobe <archive.rondbackup> <dsn> <datasetID>")
os.Exit(2)
}
archive, dsn := os.Args[1], os.Args[2]
var datasetID int64
fmt.Sscanf(os.Args[3], "%d", &datasetID)
tmp, err := os.MkdirTemp("", "diffprobe")
must(err)
defer os.RemoveAll(tmp)
sqlDB, err := rond.Open(archive, tmp)
must(err)
b, err := rond.Parse(sqlDB)
sqlDB.Close()
must(err)
pg, err := sql.Open("pgx", dsn)
must(err)
defer pg.Close()
ctx := context.Background()
must(pg.PingContext(ctx))
fmt.Printf("=== 备份 %s ===\n", archive)
fmt.Printf("备份: 到访 %d / 地点 %d / 位移 %d / 天气 %d / 活动 %d / 标签 %d / 原始定位 %d\n",
len(b.Visits), len(b.Locations), len(b.Movements), len(b.Weather),
len(b.Activities), len(b.Tags), b.RawVisitNum)
var bFirst, bLast sql.NullTime
for _, v := range b.Visits {
if v.Arrival == nil {
continue
}
if !bFirst.Valid || v.Arrival.Before(bFirst.Time) {
bFirst = sql.NullTime{Time: *v.Arrival, Valid: true}
}
if !bLast.Valid || v.Arrival.After(bLast.Time) {
bLast = sql.NullTime{Time: *v.Arrival, Valid: true}
}
}
fmt.Printf("备份时间跨度: %s ~ %s\n", ts(bFirst), ts(bLast))
fmt.Printf("\n=== 库内 dataset #%d ===\n", datasetID)
var name string
var cnt, pcnt, raw int
var gFirst, gLast sql.NullTime
must(pg.QueryRowContext(ctx, `SELECT original_name, visit_count, place_count, raw_visit_count,
first_visit_at, last_visit_at FROM datasets WHERE id=$1`, datasetID).
Scan(&name, &cnt, &pcnt, &raw, &gFirst, &gLast))
fmt.Printf("%s: 到访 %d / 地点 %d / 原始定位 %d,跨度 %s ~ %s\n", name, cnt, pcnt, raw, ts(gFirst), ts(gLast))
diffVisits(ctx, pg, datasetID, b)
diffPlaces(ctx, pg, datasetID, b)
diffMovements(ctx, pg, datasetID, b)
diffWeather(ctx, pg, datasetID, b)
diffActivities(ctx, pg, datasetID, b)
diffTags(ctx, pg, datasetID, b)
}
func ts(t sql.NullTime) string {
if !t.Valid {
return "-"
}
return t.Time.Local().Format("2006-01-02 15:04")
}
type visitRow struct {
pk, placeSrc, actSrc int
arrival, departure *time.Time
remark, emoji string
bookmarked, userAdd bool
}
func diffVisits(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
fmt.Printf("\n---------- 到访 ----------\n")
rows, err := pg.QueryContext(ctx, `SELECT v.src_pk, COALESCE(p.src_pk,0), COALESCE(a.src_pk,0),
v.arrival, v.departure, COALESCE(v.remark,''), COALESCE(v.emoji,''), v.bookmarked, v.is_user_added
FROM visits v LEFT JOIN places p ON p.id=v.place_id LEFT JOIN activities a ON a.id=v.activity_id
WHERE v.dataset_id=$1`, dsID)
must(err)
db := map[int]visitRow{}
for rows.Next() {
var r visitRow
var arr time.Time
var dep sql.NullTime
must(rows.Scan(&r.pk, &r.placeSrc, &r.actSrc, &arr, &dep, &r.remark, &r.emoji, &r.bookmarked, &r.userAdd))
r.arrival = &arr
if dep.Valid {
r.departure = &dep.Time
}
db[r.pk] = r
}
rows.Close()
bk := map[int]rond.Visit{}
for _, v := range b.Visits {
bk[v.SrcPK] = v
}
var onlyBackup, onlyDB, bothDiff, same []int
for pk, v := range bk {
r, ok := db[pk]
if !ok {
onlyBackup = append(onlyBackup, pk)
continue
}
if vdiff(v, r) != "" {
bothDiff = append(bothDiff, pk)
continue
}
same = append(same, pk)
}
for pk := range db {
if _, ok := bk[pk]; !ok {
onlyDB = append(onlyDB, pk)
}
}
sort.Ints(onlyBackup)
sort.Ints(onlyDB)
sort.Ints(bothDiff)
fmt.Printf("同号且一致: %d\n", len(same))
fmt.Printf("仅备份有(实机新增,库里没有): %d\n", len(onlyBackup))
fmt.Printf("仅库里有(站点侧新增/实机已删除): %d\n", len(onlyDB))
fmt.Printf("同号但内容不同(冲突): %d\n", len(bothDiff))
// 按到达时间做多重集匹配:判断「同号冲突」到底是同批数据换了主键,还是两批无关数据
byTime := map[string][]int{}
for pk, r := range db {
if r.arrival != nil {
byTime[r.arrival.UTC().Format(time.RFC3339)] = append(byTime[r.arrival.UTC().Format(time.RFC3339)], pk)
}
}
matched, miss := 0, 0
pair := [][2]int{}
for pk, v := range bk {
if v.Arrival == nil {
continue
}
k := v.Arrival.UTC().Format(time.RFC3339)
if list := byTime[k]; len(list) > 0 {
matched++
pair = append(pair, [2]int{pk, list[0]})
byTime[k] = list[1:]
} else {
miss++
}
}
fmt.Printf("\n[时间对齐] 备份到访按「到达时刻」能在库内找到同刻记录: %d 条,找不到: %d 条\n", matched, miss)
sort.Slice(pair, func(i, j int) bool { return pair[i][0] < pair[j][0] })
fmt.Printf("[时间对齐] 同刻对的 (备份pk→库pk) 前 12 组: ")
for i, p := range pair {
if i >= 12 {
break
}
fmt.Printf("%d→%d ", p[0], p[1])
}
fmt.Println()
nDiff := 0
for _, p := range pair {
if p[0] != p[1] {
nDiff++
}
}
fmt.Printf("[时间对齐] 同刻但主键不同: %d / %d\n", nDiff, len(pair))
show := func(label string, pks []int, f func(pk int) string) {
if len(pks) == 0 {
return
}
fmt.Printf("\n[%s] 共 %d 条,前 8 条:\n", label, len(pks))
for i, pk := range pks {
if i >= 8 {
break
}
fmt.Printf(" #%d %s\n", pk, f(pk))
}
}
show("仅备份有", onlyBackup, func(pk int) string {
v := bk[pk]
return fmt.Sprintf("到访 %s 离开 %s 地点src=%s", ft(v.Arrival), ft(v.Departure), ps(v.LocationSrcPK))
})
show("仅库里有", onlyDB, func(pk int) string {
r := db[pk]
return fmt.Sprintf("到访 %s 离开 %s 地点src=%d 站点新增=%v 备注=%q",
ft(r.arrival), ftp(r.departure), r.placeSrc, r.userAdd, r.remark)
})
show("冲突", bothDiff, func(pk int) string {
return vdiff(bk[pk], db[pk])
})
}
func vdiff(v rond.Visit, r visitRow) string {
var d string
if !eq(v.Arrival, r.arrival) {
d += fmt.Sprintf("到达 %s→%s ", ft(v.Arrival), ft(r.arrival))
}
if !eq(v.Departure, r.departure) {
d += fmt.Sprintf("离开 %s→%s ", ft(v.Departure), ft(r.departure))
}
if pv(v.LocationSrcPK) != r.placeSrc {
d += fmt.Sprintf("地点 %s→%d ", ps(v.LocationSrcPK), r.placeSrc)
}
if av(v.ActivitySrcPK) != r.actSrc {
d += fmt.Sprintf("活动 %s→%d ", ps(v.ActivitySrcPK), r.actSrc)
}
if v.Remark != r.remark {
d += fmt.Sprintf("备注 %q→%q ", v.Remark, r.remark)
}
if v.Emoji != r.emoji {
d += fmt.Sprintf("表情 %q→%q ", v.Emoji, r.emoji)
}
if v.Bookmarked != r.bookmarked {
d += fmt.Sprintf("收藏 %v→%v ", v.Bookmarked, r.bookmarked)
}
return d
}
func diffPlaces(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
fmt.Printf("\n---------- 地点 ----------\n")
type prow struct {
name, poi, cc, prov, city, dist, sub, thor, tz string
lat, lon float64
}
rows, err := pg.QueryContext(ctx, `SELECT src_pk, COALESCE(name,''), COALESCE(poi_category,''),
COALESCE(country_code,''), COALESCE(province,''), COALESCE(city,''), COALESCE(district,''),
COALESCE(sublocality,''), COALESCE(thoroughfare,''), COALESCE(timezone,''),
COALESCE(lat,0), COALESCE(lon,0) FROM places WHERE dataset_id=$1`, dsID)
must(err)
db := map[int]prow{}
for rows.Next() {
var pk int
var r prow
must(rows.Scan(&pk, &r.name, &r.poi, &r.cc, &r.prov, &r.city, &r.dist, &r.sub, &r.thor, &r.tz, &r.lat, &r.lon))
db[pk] = r
}
rows.Close()
bk := map[int]rond.Location{}
for _, l := range b.Locations {
bk[l.SrcPK] = l
}
var onlyB, onlyD, diff []int
for pk, l := range bk {
r, ok := db[pk]
if !ok {
onlyB = append(onlyB, pk)
continue
}
if l.Name != r.name || l.POICategory != r.poi || l.Province != r.prov || l.City != r.city ||
l.District != r.dist || l.Sublocality != r.sub || l.Thoroughfare != r.thor || l.Timezone != r.tz ||
l.CountryCode != r.cc || !near(l.Lat, r.lat) || !near(l.Lon, r.lon) {
diff = append(diff, pk)
}
}
for pk := range db {
if _, ok := bk[pk]; !ok {
onlyD = append(onlyD, pk)
}
}
sort.Ints(onlyB)
sort.Ints(onlyD)
sort.Ints(diff)
fmt.Printf("仅备份有: %d,仅库里有: %d,同号内容不同: %d\n", len(onlyB), len(onlyD), len(diff))
for i, pk := range onlyB {
if i >= 6 {
break
}
fmt.Printf(" 仅备份 #%d %s (%s %s %s)\n", pk, bk[pk].Name, bk[pk].Province, bk[pk].City, bk[pk].POICategory)
}
for i, pk := range onlyD {
if i >= 6 {
break
}
fmt.Printf(" 仅库内 #%d %s (%s %s)\n", pk, db[pk].name, db[pk].prov, db[pk].city)
}
for i, pk := range diff {
if i >= 6 {
break
}
l, r := bk[pk], db[pk]
fmt.Printf(" 冲突 #%d 备份[name=%q city=%q %.6f,%.6f] 库[name=%q city=%q %.6f,%.6f]\n",
pk, l.Name, l.City, l.Lat, l.Lon, r.name, r.city, r.lat, r.lon)
}
}
func diffMovements(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
fmt.Printf("\n---------- 位移 ----------\n")
rows, err := pg.QueryContext(ctx, `SELECT src_pk, COALESCE(transport_src,0),
COALESCE(from_visit_src,0), COALESCE(to_visit_src,0), started_at, ended_at
FROM movements WHERE dataset_id=$1`, dsID)
must(err)
type mrow struct {
t, f, to int
s, e *time.Time
}
db := map[int]mrow{}
for rows.Next() {
var pk int
var r mrow
var s, e sql.NullTime
must(rows.Scan(&pk, &r.t, &r.f, &r.to, &s, &e))
if s.Valid {
r.s = &s.Time
}
if e.Valid {
r.e = &e.Time
}
db[pk] = r
}
rows.Close()
var onlyB, onlyD int
for _, m := range b.Movements {
if _, ok := db[m.SrcPK]; !ok {
onlyB++
}
}
for pk := range db {
found := false
for _, m := range b.Movements {
if m.SrcPK == pk {
found = true
break
}
}
if !found {
onlyD++
}
}
fmt.Printf("备份 %d 条 / 库内 %d 条;仅备份有 %d,仅库里有 %d\n", len(b.Movements), len(db), onlyB, onlyD)
}
func diffWeather(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
var n int
must(pg.QueryRowContext(ctx, `SELECT count(*) FROM weather WHERE dataset_id=$1`, dsID).Scan(&n))
fmt.Printf("\n---------- 天气 ----------\n备份 %d 条 / 库内 %d 条\n", len(b.Weather), n)
}
func diffActivities(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
fmt.Printf("\n---------- 活动类型 ----------\n")
rows, err := pg.QueryContext(ctx, `SELECT src_pk, name FROM activities WHERE dataset_id=$1`, dsID)
must(err)
db := map[int]string{}
for rows.Next() {
var pk int
var n string
must(rows.Scan(&pk, &n))
db[pk] = n
}
rows.Close()
for _, a := range b.Activities {
n, ok := db[a.SrcPK]
if !ok {
fmt.Printf(" 仅备份 #%d %q\n", a.SrcPK, a.Name)
} else if n != a.Name {
fmt.Printf(" 冲突 #%d 备份=%q 库=%q\n", a.SrcPK, a.Name, n)
}
}
for pk, n := range db {
found := false
for _, a := range b.Activities {
if a.SrcPK == pk {
found = true
}
}
if !found {
fmt.Printf(" 仅库内 #%d %q\n", pk, n)
}
}
}
func diffTags(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
var n int
must(pg.QueryRowContext(ctx, `SELECT count(*) FROM tags WHERE dataset_id=$1`, dsID).Scan(&n))
fmt.Printf("\n---------- 标签 ----------\n备份 %d 条 / 库内 %d 条\n", len(b.Tags), n)
}
func eq(a, b *time.Time) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
return a.Equal(*b)
}
func pv(p *int) int {
if p == nil {
return 0
}
return *p
}
func av(p *int) int { return pv(p) }
func ps(p *int) string {
if p == nil {
return "-"
}
return fmt.Sprint(*p)
}
func ft(t *time.Time) string {
if t == nil {
return "-"
}
return t.Local().Format("01-02 15:04")
}
func ftp(t *time.Time) string { return ft(t) }
func near(a, b float64) bool {
d := a - b
return d < 1e-9 && d > -1e-9
}
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "错误:", err)
os.Exit(1)
}
}
-40
View File
@@ -1,40 +0,0 @@
// 一次性探针:调用站点的导出逻辑产出一份 .rondbackup,用于核对
// 「站点导出 → 导入 rond → rond 再导出」这条链路上 UUID 与 Z_PK 是否还连贯。
package main
import (
"context"
"database/sql"
"fmt"
"os"
"strconv"
_ "github.com/jackc/pgx/v5/stdlib"
"rond-with-you/internal/rond"
)
func main() {
if len(os.Args) < 5 {
fmt.Println("用法: exportprobe <dsn> <datasetID> <baseline.sqlite> <out.rondbackup>")
os.Exit(2)
}
dsID, err := strconv.ParseInt(os.Args[2], 10, 64)
must(err)
db, err := sql.Open("pgx", os.Args[1])
must(err)
defer db.Close()
f, err := os.Create(os.Args[4])
must(err)
defer f.Close()
must(rond.ExportRondbackup(context.Background(), db, dsID, os.Args[3], f))
fmt.Printf("已导出到 %s\n", os.Args[4])
}
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "错误:", err)
os.Exit(1)
}
}
-133
View File
@@ -1,133 +0,0 @@
// 一次性探针:把一份 .fwss 与 PG 里的 fog_blocks 逐块比对,量化「叠加 / 替换」两种模式的影响。
package main
import (
"database/sql"
"encoding/hex"
"fmt"
"os"
"strconv"
"strings"
_ "github.com/jackc/pgx/v5/stdlib"
"rond-with-you/internal/fog"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("用法: fogprobe <snapshot.fwss> <dsn> [userID]")
os.Exit(2)
}
uid := int64(1)
if len(os.Args) > 3 {
uid, _ = strconv.ParseInt(os.Args[3], 10, 64)
}
data, err := os.ReadFile(os.Args[1])
must(err)
inc, incCells, err := fog.Decode(data)
must(err)
fmt.Printf("快照 %s: %d 块 / %d 格\n", os.Args[1], len(inc), incCells)
pg, err := sql.Open("pgx", os.Args[2])
must(err)
defer pg.Close()
rows, err := pg.Query(`SELECT gbx, gby, bitmap FROM fog_blocks WHERE user_id=$1`, uid)
must(err)
local := map[[2]int][]byte{}
localCells := 0
for rows.Next() {
var gx, gy int
var bm []byte
must(rows.Scan(&gx, &gy, &bm))
local[[2]int{gx, gy}] = bm
localCells += popcount(bm)
}
rows.Close()
fmt.Printf("库内: %d 块 / %d 格\n", len(local), localCells)
var newBlocks, newCells int
var novBlock, overlap int
var overlapGain int
for _, b := range inc {
k := [2]int{b.GX, b.GY}
old, ok := local[k]
if !ok {
newBlocks++
newCells += popcount(b.Bitmap)
continue
}
novBlock++
gain := 0
for i := range b.Bitmap {
gain += poptab[b.Bitmap[i]&^old[i]]
}
if gain > 0 {
overlap++
overlapGain += gain
}
}
var lostBlocks, lostCells, lostFull int
for k, bm := range local {
found := false
for _, b := range inc {
if b.GX == k[0] && b.GY == k[1] {
found = true
break
}
}
if !found {
lostBlocks++
lostCells += popcount(bm)
lostFull++
}
}
fmt.Printf("\n【叠加合并 overlay】\n")
fmt.Printf(" 新增整块: %d 块(+%d 格)\n", newBlocks, newCells)
fmt.Printf(" 与已有块重叠: %d 块,其中 %d 块含新格(+%d 格)\n", novBlock, overlap, overlapGain)
fmt.Printf(" 合计新增格: %d,合并后总量: %d 块 / %d 格\n",
newCells+overlapGain, len(local)+newBlocks, localCells+newCells+overlapGain)
fmt.Printf("\n【整体替换 cover】\n")
fmt.Printf(" 库里现有 %d 块会被丢弃(快照里完全没有这些块),损失 %d 格\n", lostFull, lostCells)
fmt.Printf(" 替换后总量: %d 块 / %d 格\n", len(inc), incCells)
fmt.Printf(" 净变化: %+d 块 / %+d 格\n", len(inc)-len(local), incCells-localCells)
fmt.Printf("\n【其它图层】\n")
layers, err := fog.RawLayers(data)
if err == nil {
for k, v := range layers {
fmt.Printf(" %s (%d 字节, %s)\n", k, len(v), hex.EncodeToString(v[:min(8, len(v))]))
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
var poptab = func() [256]int {
var t [256]int
for i := range t {
t[i] = strings.Count(strconv.FormatUint(uint64(i), 2), "1")
}
return t
}()
func popcount(b []byte) int {
n := 0
for _, v := range b {
n += poptab[v]
}
return n
}
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "错误:", err)
os.Exit(1)
}
}
-171
View File
@@ -1,171 +0,0 @@
// 一次性探针:检查 rond 备份里是否存在「跨备份稳定」的对齐键。
// 用完即可删。
package main
import (
"database/sql"
"fmt"
"os"
"strings"
"rond-with-you/internal/rond"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("用法: idprobe <a.rondbackup> <b.rondbackup>")
os.Exit(2)
}
a := load(os.Args[1])
b := load(os.Args[2])
fmt.Printf("\n=== A=%s ===\n=== B=%s ===\n", os.Args[1], os.Args[2])
fmt.Printf("\n---------- ZVISIT.ZIDENTIFIER_ 稳定性 ----------\n")
fmt.Printf("A 有 UUID %d / %d,B 有 UUID %d / %d\n", len(a.visitUUID), len(a.visitPK), len(b.visitUUID), len(b.visitPK))
inter := 0
for u := range a.visitUUID {
if _, ok := b.visitUUID[u]; ok {
inter++
}
}
fmt.Printf("两次备份 UUID 交集: %d(A 独有 %d,B 独有 %d)\n",
inter, len(a.visitUUID)-inter, len(b.visitUUID)-inter)
// UUID 相同则对比 pk 是否相同
samePK, diffPK := 0, 0
for u, pa := range a.visitUUID {
if pb, ok := b.visitUUID[u]; ok {
if pa == pb {
samePK++
} else {
diffPK++
}
}
}
fmt.Printf("UUID 相同者中:Z_PK 也相同 %d,Z_PK 已变 %d\n", samePK, diffPK)
fmt.Printf("\n---------- ZACTIVITY.ZUID_ ----------\n")
fmt.Printf("A UUID %d / %d,B UUID %d / %d\n", len(a.actUUID), len(a.actPK), len(b.actUUID), len(b.actPK))
same, dif := 0, 0
for u, pa := range a.actUUID {
if pb, ok := b.actUUID[u]; ok {
if pa == pb {
same++
} else {
dif++
}
}
}
fmt.Printf("交集配对:Z_PK 相同 %d,Z_PK 已变 %d\n", same, dif)
fmt.Printf("\n---------- ZLOCATION 可用稳定列 ----------\n")
fmt.Printf("A 列: %s\n", strings.Join(a.locCols, ", "))
fmt.Printf("A 地点 %d,B 地点 %d\n", len(a.locPK), len(b.locPK))
// 名称+粗坐标是否可作键
type lk struct {
name string
lat float64
lon float64
}
keyA := map[string][]int{}
for pk, l := range a.locPK {
k := fmt.Sprintf("%s|%.5f|%.5f", l.name, l.lat, l.lon)
keyA[k] = append(keyA[k], pk)
}
hit, miss := 0, 0
for pk, l := range b.locPK {
k := fmt.Sprintf("%s|%.5f|%.5f", l.name, l.lat, l.lon)
if _, ok := keyA[k]; ok {
hit++
} else {
miss++
if miss <= 5 {
fmt.Printf(" B 独有地点 #%d %q %.6f,%.6f\n", pk, l.name, l.lat, l.lon)
}
}
}
fmt.Printf("按「名称+坐标(1e-5)」键:B 命中 A %d,未命中 %d\n", hit, miss)
}
type loc struct {
name string
lat, lon float64
}
type snap struct {
visitUUID map[string]int
visitPK map[int]bool
actUUID map[string]int
actPK map[int]bool
locPK map[int]loc
locCols []string
}
func load(path string) *snap {
dir, err := os.MkdirTemp("", "idprobe")
must(err)
defer os.RemoveAll(dir)
db, err := rond.Open(path, dir)
must(err)
defer db.Close()
s := &snap{visitUUID: map[string]int{}, visitPK: map[int]bool{},
actUUID: map[string]int{}, actPK: map[int]bool{}, locPK: map[int]loc{}}
rows, err := db.Query(`SELECT Z_PK, hex(ZIDENTIFIER_) FROM ZVISIT`)
must(err)
for rows.Next() {
var pk int
var u sql.NullString
must(rows.Scan(&pk, &u))
s.visitPK[pk] = true
if u.Valid && u.String != "" && strings.Trim(u.String, "0") != "" {
s.visitUUID[u.String] = pk
}
}
rows.Close()
rows, err = db.Query(`SELECT Z_PK, hex(ZUID_), COALESCE(ZNAME_,'') FROM ZACTIVITY`)
must(err)
for rows.Next() {
var pk int
var u sql.NullString
var n string
must(rows.Scan(&pk, &u, &n))
s.actPK[pk] = true
if u.Valid && u.String != "" && strings.Trim(u.String, "0") != "" {
s.actUUID[u.String] = pk
}
}
rows.Close()
rows, err = db.Query(`PRAGMA table_info(ZLOCATION)`)
must(err)
for rows.Next() {
var cid int
var name, typ string
var notnull int
var dflt sql.NullString
var pk int
must(rows.Scan(&cid, &name, &typ, &notnull, &dflt, &pk))
s.locCols = append(s.locCols, name)
}
rows.Close()
rows, err = db.Query(`SELECT Z_PK, COALESCE(ZNAME_,''), COALESCE(ZLATITUDE,0), COALESCE(ZLONGITUDE,0) FROM ZLOCATION`)
must(err)
for rows.Next() {
var pk int
var l loc
must(rows.Scan(&pk, &l.name, &l.lat, &l.lon))
s.locPK[pk] = l
}
rows.Close()
return s
}
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "错误:", err)
os.Exit(1)
}
}
-57
View File
@@ -1,57 +0,0 @@
// 一次性探针:在 rond 备份的 SQLite 上跑一条查询并打印结果(BLOB 打十六进制)。
package main
import (
"fmt"
"os"
"strings"
"rond-with-you/internal/rond"
)
func main() {
if len(os.Args) < 3 {
fmt.Println(`用法: sqlprobe <backup> "SELECT ..."`)
os.Exit(2)
}
dir, err := os.MkdirTemp("", "sqlprobe")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
db, err := rond.Open(os.Args[1], dir)
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query(os.Args[2])
if err != nil {
panic(err)
}
defer rows.Close()
cols, _ := rows.Columns()
fmt.Println(strings.Join(cols, " | "))
for rows.Next() {
vals := make([]any, len(cols))
ptrs := make([]any, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err != nil {
panic(err)
}
parts := make([]string, len(cols))
for i, v := range vals {
switch t := v.(type) {
case nil:
parts[i] = "-"
case []byte:
parts[i] = fmt.Sprintf("%x", t)
default:
parts[i] = fmt.Sprint(t)
}
}
fmt.Println(strings.Join(parts, " | "))
}
}