备份与迷雾的差异分析、合并执行,附属数据改挂坐标键
备份(.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:
@@ -0,0 +1,170 @@
|
||||
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) }
|
||||
Reference in New Issue
Block a user