Files
Scoreboard/server/internal/rules/diff.go
T
AskaEth bc8b1ee6c0 feat: M3-M7 全模块落地 + 集成接线
- M3 认证/权限/审计/限流 + 全部 REST 接口
- M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制)
- M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配)
- M6 WebSocket 实时同步中心
- M7 xlsx 导出 + 赛事包 zip + 备份
- 规则包 YAML 导入导出(差异预览)
- 字体 vendor 子集化(4 套 x2 字重)
- 集成:realtime/export/rules 接入 main+httpapi;backup 子命令
- 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
2026-09-09 19:02:24 +08:00

257 lines
6.7 KiB
Go

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
}