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) }