- 抽出 rond.MatchPlaces(stored, backup) PlaceMatch:坐标优先、退到「同名且相距 ≤1km」、 多值索引 + 认领即标记已用;DiffPlaces 与 web.mergePlaces 共用它。 - mergePlaces 原来是单槽位 byCoord/byName 自己再配一遍:同坐标/同名的多条备份地点全并到同一行 站内地点,到访被挂到错的分店(实测「茶颜悦色」两家分店相隔 8.3km,到访却指着同一条)。 现在直接用配对结果,再叠加 plan.AddPlaces 的新插入。 - loadStoredPlaces 与 mergePlaces 的地点查询都加 ORDER BY id:候选多于一个时靠顺序定谁配谁, 两侧必须一致,否则配对会飘。 - 删掉只写不读的 PlacePatch.CoordKey、ActivityPatch.Key(容易被误当成查找键)。 - 用例:TestMatchPlacesKeepsDuplicateNameGroupsDistinct(同一坐标/邻近同名都要一条配一条)、 TestMergePlacesMapsSameNameLocationsOneToOne(真库 + 真事务,两条同名备份地点必须映射到两条不同 地点);两处都做了变异验证(认领不标记已用 / 恢复单槽位映射 → 对应用例变红)。 - 探针库复测(从 dump 恢复、不动本地真库,49 组冲突全「用实机」):到访挂错地点(>1km)0/953 条、 地点被搬动 >1km 0 个;合并后再比对三类全 0(到访 0/47/0、地点 0/4/0、活动 0/0/0), 再点合并「按当前的选择没有要写入的改动」。全量验收 293 PASS / 0 FAIL / 0 SKIP。 已知取舍:勾「用实机」但没勾「并进来」时,若该到访在实机上的地点站内没有、且按 1km 规则也配不上, place_id 会被置空(旧行为是挂到一条远方的同名地点上——那正是这次要去掉的错配)。
170 lines
4.7 KiB
Go
170 lines
4.7 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 表示用备份侧的值覆盖库里的那一条。要改哪一条由配对结果决定
|
||
// (DiffPlaces/MatchPlaces 已经配好),所以这里只带备份侧的整条地点。
|
||
type PlacePatch struct {
|
||
L Location
|
||
}
|
||
|
||
// ActivityPatch 同理。
|
||
type ActivityPatch struct {
|
||
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{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{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) }
|