package rules import ( "errors" "fmt" "sort" ) // ChangeKind 是差异类型。 type ChangeKind string const ( ChangeAdded ChangeKind = "added" ChangeModified ChangeKind = "modified" ChangeRemoved ChangeKind = "removed" ) // ProfileChange 是一个积分模板的增/改/删。 type ProfileChange struct { Code string `json:"code"` Kind ChangeKind `json:"kind"` Before *Profile `json:"before,omitempty"` After *Profile `json:"after,omitempty"` // Fields 列出被修改的字段,便于前端只高亮变化项。 Fields []string `json:"fields,omitempty"` } // ChannelChange 是一个项目计分通道的变化。 type ChannelChange struct { EventCode string `json:"eventCode"` EventName string `json:"eventName,omitempty"` Kind ChangeKind `json:"kind"` Before []Channel `json:"before,omitempty"` After []Channel `json:"after,omitempty"` } // SettingChange 是一条赛事设置的变化。 type SettingChange struct { Key string `json:"key"` Before string `json:"before"` After string `json:"after"` } // ChangeSet 是导入预览的结果,也是 Apply 的输入。 type ChangeSet struct { Profiles []ProfileChange `json:"profiles,omitempty"` Channels []ChannelChange `json:"channels,omitempty"` Settings []SettingChange `json:"settings,omitempty"` // UnknownEvents 是规则包里出现、但赛事库中不存在的项目编码;Apply 会跳过它们。 UnknownEvents []string `json:"unknownEvents,omitempty"` // Next 是目标规则包;Apply 以它为完整目标状态落库。不参与 JSON 序列化。 Next *RulePack `json:"-"` } // Empty 判断这次导入是否真的会改动任何东西。 // 只用于预览展示;Apply 始终以 Next 为目标全量同步,不依赖它做短路。 func (cs *ChangeSet) Empty() bool { return cs == nil || (len(cs.Profiles) == 0 && len(cs.Channels) == 0 && len(cs.Settings) == 0) } // Diff 计算「当前赛事库 → 目标规则包」的差异,只读不落库。 func Diff(cur *Snapshot, next *RulePack) (*ChangeSet, error) { if cur == nil || cur.Pack == nil { return nil, errors.New("rules: 当前快照为空,无法比对差异") } if next == nil { return nil, fmt.Errorf("%w: 目标规则包为空", ErrNotRulePack) } target := next.Canonical() if err := target.Validate(); err != nil { return nil, err } cs := &ChangeSet{Next: &target} cs.Profiles = diffProfiles(cur.Pack, &target) targets, unknown, err := resolveTargets(cur, &target) if err != nil { return nil, err } cs.UnknownEvents = unknown cs.Channels = diffChannels(cur, targets) cs.Settings = diffSettings(cur.Pack, &target) return cs, nil } func diffProfiles(cur, next *RulePack) []ProfileChange { curIndex := cur.ProfileIndex() nextIndex := next.ProfileIndex() codes := make([]string, 0, len(curIndex)+len(nextIndex)) seen := map[string]bool{} for code := range curIndex { if !seen[code] { seen[code] = true codes = append(codes, code) } } for code := range nextIndex { if !seen[code] { seen[code] = true codes = append(codes, code) } } sort.Strings(codes) var out []ProfileChange for _, code := range codes { before, hasBefore := curIndex[code] after, hasAfter := nextIndex[code] switch { case !hasBefore: out = append(out, ProfileChange{Code: code, Kind: ChangeAdded, After: &after}) case !hasAfter: out = append(out, ProfileChange{Code: code, Kind: ChangeRemoved, Before: &before}) default: if fields := profileDiffFields(before, after); len(fields) > 0 { out = append(out, ProfileChange{Code: code, Kind: ChangeModified, Before: &before, After: &after, Fields: fields}) } } } return out } func profileDiffFields(a, b Profile) []string { var fields []string if a.Name != b.Name { fields = append(fields, "name") } if a.AwardRanks != b.AwardRanks { fields = append(fields, "awardRanks") } if a.LastPlaceZero != b.LastPlaceZero { fields = append(fields, "lastPlaceZero") } if a.TieMode != b.TieMode { fields = append(fields, "tieMode") } if a.RecordBonusMode != b.RecordBonusMode { fields = append(fields, "recordBonusMode") } if !rulesEqual(a.Rules, b.Rules) { fields = append(fields, "rules") } return fields } func rulesEqual(a, b []Rule) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // resolveTargets 把规则包展开成「项目编码 → 目标通道」。 // 优先级:eventOverrides 显式给了 channels > 第一条命中的 channelTemplates。 // 没被规则包提到的项目不出现在结果里,表示「保持原样」。 func resolveTargets(cur *Snapshot, next *RulePack) (map[string][]Channel, []string, error) { overrideByCode := make(map[string]EventOverride, len(next.EventOverrides)) for _, o := range next.EventOverrides { overrideByCode[o.EventCode] = o } targets := make(map[string][]Channel) for _, ev := range cur.Events { if ov, ok := overrideByCode[ev.Code]; ok && ov.Channels != nil { targets[ev.Code] = append([]Channel(nil), (*ov.Channels)...) continue } for _, t := range next.ChannelTemplates { if matchEvent(t.Match, ev) { targets[ev.Code] = append([]Channel(nil), t.Channels...) break } } } var unknown []string for _, o := range next.EventOverrides { if _, ok := cur.EventByCode(o.EventCode); !ok { unknown = append(unknown, o.EventCode) } } sort.Strings(unknown) return targets, unknown, nil } func diffChannels(cur *Snapshot, targets map[string][]Channel) []ChannelChange { curByCode := make(map[string]EventInfo, len(cur.Events)) for _, ev := range cur.Events { curByCode[ev.Code] = ev } codes := make([]string, 0, len(targets)) for code := range targets { codes = append(codes, code) } sort.Strings(codes) var out []ChannelChange for _, code := range codes { ev := curByCode[code] before := ev.Channels after := targets[code] if channelsEqual(before, after) { continue } kind := ChangeModified switch { case len(before) == 0: kind = ChangeAdded case len(after) == 0: kind = ChangeRemoved } out = append(out, ChannelChange{ EventCode: code, EventName: ev.Name, Kind: kind, Before: append([]Channel(nil), before...), After: append([]Channel(nil), after...), }) } return out } func channelsEqual(a, b []Channel) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func diffSettings(cur, next *RulePack) []SettingChange { curSettings := settingsFromPack(cur) nextSettings := settingsFromPack(next) var out []SettingChange for _, key := range settingsKeys() { before, after := curSettings[key], nextSettings[key] if before != after { out = append(out, SettingChange{Key: key, Before: before, After: after}) } } return out }