Files
rond-with-you/internal/rond/merge.go
T
tamakyi 30c502ce7b 备份与迷雾的差异分析、合并执行,附属数据改挂坐标键
备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」:

- 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上
  用绿/灰/红标出会新增 / 不变 / 会丢的区域
- 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人),
  新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行;
  合入的行写回 entity_raw,保证再传同一份备份时差异归零
- 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的
  到访全变成假冲突
- 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联;
  访客票没有 dataset_id,回填按活跃数据集认领
- 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释

导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
2026-09-19 16:04:00 +08:00

171 lines
4.6 KiB
Go

package rond
// 合并计划:把「差异画像 + 站长在页面上做的选择」翻译成一份可执行的改动清单。
// 这里只做判定,不碰数据库、也不写任何东西。
// 站长对一条冲突的处理方式。
const (
PickBackup = "backup" // 用备份(实机)的值覆盖站内
PickDB = "db" // 保留站内的值
PickSkip = "skip" // 这条不动
)
// Choices 是差异页提交上来的选择。冲突是逐条选的,键分别是到访 UUID、
// 地点坐标键、活动 UUID。
type Choices struct {
AddVisits bool // 把备份里多出来的到访并进来
AddPlaces bool
AddActs bool
VisitClash map[string]string
PlaceClash map[string]string
ActClash map[string]string
// 删除:「仅站内有」的记录按库里编号勾选,默认一个都不删。
// 键是 src_pk 而不是 UUID——站点补录的行压根没有 UUID。
DelVisits map[int]bool
DelPlaces map[int]bool
DelActs map[int]bool
}
func (c Choices) visitPick(key string) string { return pickOf(c.VisitClash, key) }
func (c Choices) placePick(key string) string { return pickOf(c.PlaceClash, key) }
func (c Choices) activityPick(key string) string { return pickOf(c.ActClash, key) }
func pickOf(m map[string]string, key string) string {
switch m[key] {
case PickBackup, PickDB, PickSkip:
return m[key]
}
return PickDB // 没提交或提交了怪值:默认保留站内的,不误伤
}
// VisitPatch 表示用备份侧的值覆盖库里的那一条。
type VisitPatch struct {
UUID string // 对齐键
V Visit
}
// PlacePatch 同理,键是坐标键。
type PlacePatch struct {
CoordKey string
L Location
}
// ActivityPatch 键是活动 UUID(没有 UUID 时退回名称)。
type ActivityPatch struct {
Key string
A Activity
}
// MergePlan 是一次合并要做的全部改动。
type MergePlan struct {
AddPlaces []Location
UpdPlaces []PlacePatch
DelPlaces []StoredPlace
AddActs []Activity
UpdActs []ActivityPatch
DelActs []StoredActivity
AddVisits []Visit
UpdVisits []VisitPatch
DelVisits []StoredVisit
}
// Empty 报告这份计划什么事都不用做。
func (p MergePlan) Empty() bool {
return len(p.AddPlaces)+len(p.UpdPlaces)+len(p.DelPlaces)+
len(p.AddActs)+len(p.UpdActs)+len(p.DelActs)+
len(p.AddVisits)+len(p.UpdVisits)+len(p.DelVisits) == 0
}
// BuildMergePlan 按选择把差异筛成改动清单。
func BuildMergePlan(vd VisitDiff, pd PlaceDiff, ad ActivityDiff, c Choices) MergePlan {
var p MergePlan
if c.AddPlaces {
p.AddPlaces = pd.Added
}
for _, cf := range pd.Changed {
if c.placePick(cf.DB.Key) == PickBackup {
p.UpdPlaces = append(p.UpdPlaces, PlacePatch{CoordKey: cf.DB.Key, L: cf.Backup})
}
}
for _, sp := range pd.Removed {
if c.DelPlaces[sp.SrcPK] {
p.DelPlaces = append(p.DelPlaces, sp)
}
}
if c.AddActs {
p.AddActs = ad.Added
}
for _, cf := range ad.Changed {
if c.activityPick(activityKey(cf.DB)) == PickBackup {
p.UpdActs = append(p.UpdActs, ActivityPatch{Key: activityKey(cf.DB), A: cf.Backup})
}
}
for _, sa := range ad.Removed {
if c.DelActs[sa.SrcPK] {
p.DelActs = append(p.DelActs, sa)
}
}
if c.AddVisits {
p.AddVisits = vd.Added
// 新到访可能挂在一个「备份里新出现、库里还没有」的地点/活动上:
// 不把它们一起建出来,到访的 place_id / activity_id 就无从映射,只能整条丢掉。
p.AddPlaces = appendMissingPlaces(p.AddPlaces, pd.Added)
p.AddActs = appendMissingActs(p.AddActs, ad.Added)
}
for _, cf := range vd.Changed {
if c.visitPick(cf.DB.UUID) == PickBackup {
p.UpdVisits = append(p.UpdVisits, VisitPatch{UUID: cf.DB.UUID, V: cf.Backup})
}
}
for _, sv := range vd.Removed {
if c.DelVisits[sv.SrcPK] {
p.DelVisits = append(p.DelVisits, sv)
}
}
return p
}
func appendMissingPlaces(have, more []Location) []Location {
seen := make(map[int]bool, len(have))
for _, l := range have {
seen[l.SrcPK] = true
}
for _, l := range more {
if !seen[l.SrcPK] {
have = append(have, l)
seen[l.SrcPK] = true
}
}
return have
}
func appendMissingActs(have, more []Activity) []Activity {
seen := make(map[int]bool, len(have))
for _, a := range have {
seen[a.SrcPK] = true
}
for _, a := range more {
if !seen[a.SrcPK] {
have = append(have, a)
seen[a.SrcPK] = true
}
}
return have
}
// activityKey 是活动冲突在页面上的标识:优先 UUID(改名也能对上),没有才用名称。
func activityKey(sa StoredActivity) string {
if sa.UUID != "" {
return sa.UUID
}
return "n:" + sa.Name
}
// ActivityKey 供页面与处理函数共用同一套键。
func ActivityKey(sa StoredActivity) string { return activityKey(sa) }