Files
Scoreboard/server/internal/rules/parse.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

72 lines
2.0 KiB
Go

package rules
import (
"bytes"
"errors"
"fmt"
"io"
"gopkg.in/yaml.v3"
)
// 导出文件的注释头:规则包可能含赛事敏感信息,不进版本库(rules/README.md)。
const header = `# 积分规则包 · scoreboard-rules v1
# 由系统导出,可手工编辑后再导入;导入前请先预览差异。
# 注意:本文件可能含赛事敏感信息,请勿提交到版本库。
`
// Parse 解析并校验规则包 YAML。未知字段一律报错,避免拼错的配置被静默忽略。
func Parse(data []byte) (*RulePack, error) {
if len(bytes.TrimSpace(data)) == 0 {
return nil, fmt.Errorf("%w: 内容为空", ErrNotRulePack)
}
dec := yaml.NewDecoder(bytes.NewReader(data))
dec.KnownFields(true)
var pack RulePack
if err := dec.Decode(&pack); err != nil {
if errors.Is(err, io.EOF) {
return nil, fmt.Errorf("%w: 内容为空", ErrNotRulePack)
}
return nil, fmt.Errorf("解析规则包 YAML: %w", err)
}
// 规则包只允许一个文档;多文档容易让人以为后面的也会生效。
var extra any
if err := dec.Decode(&extra); err == nil {
return nil, fmt.Errorf("%w: 只允许一个 YAML 文档", ErrNotRulePack)
} else if !errors.Is(err, io.EOF) {
return nil, fmt.Errorf("解析规则包 YAML: %w", err)
}
canonical := pack.Canonical()
if err := canonical.Validate(); err != nil {
return nil, err
}
return &canonical, nil
}
// Marshal 把规则包序列化成带注释头的 YAML;输出是规范化后的结果,可安全往返。
func Marshal(pack *RulePack) ([]byte, error) {
if pack == nil {
return nil, fmt.Errorf("%w: 规则包为空", ErrNotRulePack)
}
canonical := pack.Canonical()
if err := canonical.Validate(); err != nil {
return nil, err
}
var buf bytes.Buffer
buf.WriteString(header)
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
if err := enc.Encode(&canonical); err != nil {
return nil, fmt.Errorf("序列化规则包 YAML: %w", err)
}
if err := enc.Close(); err != nil {
return nil, fmt.Errorf("序列化规则包 YAML: %w", err)
}
return buf.Bytes(), nil
}