备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」: - 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上 用绿/灰/红标出会新增 / 不变 / 会丢的区域 - 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人), 新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行; 合入的行写回 entity_raw,保证再传同一份备份时差异归零 - 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的 到访全变成假冲突 - 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联; 访客票没有 dataset_id,回填按活跃数据集认领 - 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释 导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
172 lines
4.1 KiB
Go
172 lines
4.1 KiB
Go
// 一次性探针:检查 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, ¬null, &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)
|
||
}
|
||
}
|