备份与迷雾的差异分析、合并执行,附属数据改挂坐标键
备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」: - 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上 用绿/灰/红标出会新增 / 不变 / 会丢的区域 - 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人), 新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行; 合入的行写回 entity_raw,保证再传同一份备份时差异归零 - 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的 到访全变成假冲突 - 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联; 访客票没有 dataset_id,回填按活跃数据集认领 - 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释 导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
This commit is contained in:
@@ -67,15 +67,41 @@ func insertEntityRaw(ctx context.Context, tx *sql.Tx, datasetID int64, rows map[
|
||||
}
|
||||
|
||||
// contentHash 给备份内容算指纹,用来识别「内容没变、只是重新导出了一次」的上传。
|
||||
// rond 每次导出连 WAL 一起打包,字节必然不同,文件摘要挡不住这种重复;
|
||||
// 但同一批到访的 Z_PK 是稳定的,所以按到访自然键排序后取摘要即可。
|
||||
// rond 每次导出连 WAL 一起打包,字节必然不同,文件摘要挡不住这种重复。
|
||||
//
|
||||
// 指纹按**到访/活动/地点的稳定标识**算,绝不能掺 Z_PK:rond 每次导出/同步都会重排
|
||||
// 编号(实测两份备份里 822 条到访的 UUID 完全一致、Z_PK 100% 不同),拿编号当指纹会把
|
||||
// 「同一批数据重导一次」判成新内容 —— 于是新建数据集,而备注与投票是按 dataset_id
|
||||
// 隔离的,换了数据集就再也看不见。
|
||||
//
|
||||
// 没有 UUID 的老备份退回「到达时间 + 地点坐标」,仍比编号可靠。
|
||||
func contentHash(b *rond.Backup) string {
|
||||
keys := make([]string, 0, len(b.Visits))
|
||||
locCoord := make(map[int]string, len(b.Locations))
|
||||
keys := make([]string, 0, len(b.Visits)+len(b.Locations)+len(b.Activities))
|
||||
for _, l := range b.Locations {
|
||||
locCoord[l.SrcPK] = fmt.Sprintf("%.6f/%.6f", l.Lat, l.Lon)
|
||||
keys = append(keys, "L"+l.Name+"/"+locCoord[l.SrcPK])
|
||||
}
|
||||
for _, v := range b.Visits {
|
||||
if v.Arrival == nil {
|
||||
continue // 导入时就被跳过的行,不该影响指纹
|
||||
}
|
||||
if u := b.VisitUUID[v.SrcPK]; u != "" {
|
||||
keys = append(keys, "V"+u)
|
||||
continue
|
||||
}
|
||||
keys = append(keys, fmt.Sprintf("%d|%d", v.SrcPK, v.Arrival.Unix()))
|
||||
loc := ""
|
||||
if v.LocationSrcPK != nil {
|
||||
loc = locCoord[*v.LocationSrcPK]
|
||||
}
|
||||
keys = append(keys, fmt.Sprintf("T%d/%s", v.Arrival.Unix(), loc))
|
||||
}
|
||||
for _, a := range b.Activities {
|
||||
if u := b.ActivityUUID[a.SrcPK]; u != "" {
|
||||
keys = append(keys, "A"+u)
|
||||
continue
|
||||
}
|
||||
keys = append(keys, "A"+a.Name)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
h := sha256.New()
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"rond-with-you/internal/rond"
|
||||
)
|
||||
|
||||
// sampleBackup 造一份最小的备份:到访 + 地点 + 活动,编号与 UUID 分别给定。
|
||||
func sampleBackup(pks []int, uuids []string) *rond.Backup {
|
||||
b := &rond.Backup{VisitUUID: map[int]string{}, ActivityUUID: map[int]string{}}
|
||||
for i, pk := range pks {
|
||||
at := time.Unix(int64(1700000000+i*3600), 0)
|
||||
b.Visits = append(b.Visits, rond.Visit{SrcPK: pk, Arrival: &at, LocationSrcPK: intp(1)})
|
||||
b.VisitUUID[pk] = uuids[i]
|
||||
}
|
||||
b.Locations = []rond.Location{{SrcPK: 1, Name: "家", Lat: 24.1, Lon: 109.1}}
|
||||
b.Activities = []rond.Activity{{SrcPK: 9, Name: "工作"}}
|
||||
b.ActivityUUID[9] = "cccccccccccccccccccccccccccccccc"
|
||||
return b
|
||||
}
|
||||
|
||||
func intp(v int) *int { return &v }
|
||||
|
||||
// 同一批数据重新导出一次:UUID 一个没变、Z_PK 全被打乱(顺序也变了),
|
||||
// 指纹必须一样。否则会被判成「新内容」→ 新建数据集 → 按 dataset_id 隔离的
|
||||
// 备注与投票全部失联。
|
||||
func TestContentHashIgnoresPKReorder(t *testing.T) {
|
||||
a := sampleBackup([]int{1, 2, 3}, []string{
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"dddddddddddddddddddddddddddddddd",
|
||||
})
|
||||
b := sampleBackup([]int{555, 276, 88}, []string{
|
||||
"dddddddddddddddddddddddddddddddd",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
})
|
||||
if contentHash(a) != contentHash(b) {
|
||||
t.Fatal("编号重排不该改变指纹")
|
||||
}
|
||||
}
|
||||
|
||||
// 内容真的变了(多一条到访)就得算不同的指纹,否则新数据会被当成重复上传而丢掉。
|
||||
func TestContentHashDetectsNewVisit(t *testing.T) {
|
||||
a := sampleBackup([]int{1, 2}, []string{
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
})
|
||||
b := sampleBackup([]int{1, 2, 3}, []string{
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"dddddddddddddddddddddddddddddddd",
|
||||
})
|
||||
if contentHash(a) == contentHash(b) {
|
||||
t.Fatal("多了一条到访就是不同内容")
|
||||
}
|
||||
}
|
||||
|
||||
// 只改了地点名(到访一条没动)也算内容变了:站点是靠地名认地点的。
|
||||
func TestContentHashDetectsPlaceRename(t *testing.T) {
|
||||
a := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
|
||||
b := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
|
||||
b.Locations[0].Name = "老家"
|
||||
if contentHash(a) == contentHash(b) {
|
||||
t.Fatal("地点改名应改变指纹")
|
||||
}
|
||||
}
|
||||
|
||||
// 没有 UUID 的老备份退回「到达时间 + 地点坐标」,同一批数据重导仍要认出是同一份。
|
||||
func TestContentHashFallsBackWithoutUUID(t *testing.T) {
|
||||
a := sampleBackup([]int{1, 2}, []string{"", ""})
|
||||
b := sampleBackup([]int{700, 900}, []string{"", ""})
|
||||
if contentHash(a) != contentHash(b) {
|
||||
t.Fatal("没有 UUID 时应按到达时间与地点兜底,不该受编号影响")
|
||||
}
|
||||
c := sampleBackup([]int{1, 2}, []string{"", ""})
|
||||
c.Visits[1].Arrival = ptrTime(time.Unix(1700000000, 0)) // 改掉第二条的时间
|
||||
if contentHash(a) == contentHash(c) {
|
||||
t.Fatal("时间不同应算不同内容")
|
||||
}
|
||||
}
|
||||
|
||||
// 没有到达时间的行导入时就被跳过,不该参与指纹。
|
||||
func TestContentHashSkipsRowsWithoutArrival(t *testing.T) {
|
||||
a := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
|
||||
b := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
|
||||
b.Visits = append(b.Visits, rond.Visit{SrcPK: 42})
|
||||
if contentHash(a) != contentHash(b) {
|
||||
t.Fatal("缺到达时间的行不该影响指纹")
|
||||
}
|
||||
}
|
||||
|
||||
// 指纹是稳定的十六进制摘要,不是随手拼的字符串。
|
||||
func TestContentHashShape(t *testing.T) {
|
||||
h := contentHash(sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}))
|
||||
if len(h) != 64 {
|
||||
t.Fatalf("应是 64 位 sha256 十六进制,实际 %d 位: %s", len(h), h)
|
||||
}
|
||||
if _, err := fmt.Sscanf(h, "%x", new(string)); err != nil {
|
||||
t.Fatalf("不是纯十六进制: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ptrTime(t time.Time) *time.Time { return &t }
|
||||
Reference in New Issue
Block a user