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
This commit is contained in:
1 parent
d0e3b93e76
commit
bc8b1ee6c0
145 files changed
+25159
-321
No files matched your search
@@ -0,0 +1,256 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user