备份与迷雾的差异分析、合并执行,附属数据改挂坐标键

备份(.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:
tamakyi
2026-09-19 16:04:00 +08:00
parent 9e01923244
commit 30c502ce7b
43 changed files with 5968 additions and 170 deletions
+71
View File
@@ -14,6 +14,7 @@ import (
"rond-with-you/internal/auth"
"rond-with-you/internal/fog"
"rond-with-you/internal/rond"
"rond-with-you/internal/setting"
"rond-with-you/internal/stats"
"rond-with-you/internal/votes"
@@ -637,6 +638,74 @@ type AdminData struct {
DefaultPW bool
}
// VisitRow 是一条到访摊平后的展示行。地点与活动写名称而不是编号——
// 两边的编号空间完全不同,编号对人没有任何意义。
type VisitRow struct {
// Key 是这条冲突的对齐键(到访 UUID),合并表单拿它当字段名
Key string
// SrcPK 只有「仅站内有」的行有意义:要删哪条就勾哪条
SrcPK int
Arrival *time.Time
Departure *time.Time
Place string
Activity string
Remark string
Emoji string
UserAdded bool // 站点补录的行
Fields []rond.FieldChange
}
// BackupDiffData 是「备份差异预览」页的数据。
//
// 上限说明:列表按 diffListLimit 截断,页面上只提示「还有更多」,不做分页——
// 预演是给人下判断用的,看前 200 条足够判断要不要继续;真正要逐条取舍的是
// 后面那个合并流程(那里才有逐条操作)。
type BackupDiffData struct {
Page
Token string
DatasetName string
VisitCounts rond.Counts
PlaceCounts rond.Counts
ActCounts rond.Counts
VisitAdded, VisitGone, VisitClash []VisitRow
PlaceAdded []rond.Location
PlaceGone []rond.StoredPlace
PlaceClash []rond.PlaceConflict
ActAdded []rond.Activity
ActGone []rond.StoredActivity
ActClash []rond.ActivityConflict
// 两侧总量,用于「有没有少数据」的直觉判断
BackupVisits, StoredVisits int
BackupPlaces, StoredPlaces int
BackupActs, StoredActs int
BackupMovements, StoredMovements int
BackupWeather, StoredWeather int
BackupRaw int
Truncated bool
Identical bool
}
// FogPreviewData 是「迷雾导入预览」页的数据:先把差异摆出来,再决定叠加还是整体替换。
type FogPreviewData struct {
Page
Token string
Diff fog.Diff
// 快照里不参与渲染、但整体替换会一并换掉的图层条目数
TildeLayers int
HashLayers int
OtherLayers int
// 差异地图的初始视野:两边块集覆盖范围的包围盒(已按底图坐标系换算)。
// 没有任何块时 HasBounds 为假,模板不画地图。
HasBounds bool
MinLat, MinLon float64
MaxLat, MaxLon float64
}
type NotesData struct {
Page
Rows []NoteRow
@@ -1247,6 +1316,8 @@ func templateFuncs() template.FuncMap {
"km": func(f float64) string { return fmt.Sprintf("%.1f", f) },
"f1": func(f float64) string { return fmt.Sprintf("%.1f", f) },
"dt": func(t time.Time) string { return t.Local().Format("2006-01-02 15:04") },
// actKey 给活动冲突生成表单字段名,与处理函数共用同一套键
"actKey": rond.ActivityKey,
// dtt 接受 *time.Time,nil 返回空串,便于可选时间字段直接渲染
"dtt": func(t *time.Time) string {
if t == nil {