Files
AskaEth 7f9701c465 fix: 审查修复波次(P0x4 + P1x30+)
- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力
- realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份
- httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表
- web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换
- console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复
- docs: 用户文档三份 + README
- 主代理: 破纪录加分接线(levels 未加载导致恒 0)
2026-09-09 21:06:40 +08:00

162 lines
5.0 KiB
Go

package rules
import (
"encoding/json"
"sort"
"strconv"
"strings"
"git.yeij.top/AskaEth/Scoreboard/server/internal/domain"
)
// 赛事库 settings 表里的键名;与 migrations/event/0011_seed.sql 保持一致。
const (
settingIndividualToTeamWeight = "standings.individual_to_team_weight"
settingTieBreak = "standings.tie_break"
settingMedalCountsTeamEvents = "standings.medal_counts_team_events"
settingShowIndividualBoard = "standings.show_individual_board"
settingShowIndividualMedals = "standings.show_individual_medals"
settingRecordBonusEnabled = "record_bonus.enabled"
settingRecordBonusMode = "record_bonus.mode"
settingRecordBonusLevels = "record_bonus.levels"
settingManualEnabled = "manual_adjustment.enabled"
settingManualReasonRequired = "manual_adjustment.reason_required"
)
// settingsFromPack 把规则包里能落到 settings 表的部分拍平成键值对。
// 破纪录档位库里没有表,用 JSON 存在 record_bonus.levels 里,避免导入时丢配置。
func settingsFromPack(pack *RulePack) map[string]string {
levels, err := json.Marshal(pack.RecordBonus.Levels)
if err != nil {
levels = []byte("[]")
}
return map[string]string{
settingIndividualToTeamWeight: formatFloat(pack.Standings.IndividualToTeamWeight),
settingTieBreak: strings.Join(pack.Standings.TieBreak, ","),
settingMedalCountsTeamEvents: formatBool(pack.Standings.MedalCountsTeamEventsEnabled()),
settingShowIndividualBoard: formatBool(pack.Standings.ShowIndividualBoardEnabled()),
settingShowIndividualMedals: formatBool(pack.Standings.ShowIndividualMedalsEnabled()),
settingRecordBonusEnabled: formatBool(pack.RecordBonus.Enabled),
settingRecordBonusMode: string(pack.RecordBonus.Mode),
settingRecordBonusLevels: string(levels),
settingManualEnabled: formatBool(pack.ManualAdjustment.Enabled),
settingManualReasonRequired: formatBool(pack.ManualAdjustment.ReasonRequired),
}
}
// settingsKeys 返回固定顺序的键名,保证差异预览与落库顺序稳定。
func settingsKeys() []string {
keys := []string{
settingIndividualToTeamWeight,
settingTieBreak,
settingMedalCountsTeamEvents,
settingShowIndividualBoard,
settingShowIndividualMedals,
settingRecordBonusEnabled,
settingRecordBonusMode,
settingRecordBonusLevels,
settingManualEnabled,
settingManualReasonRequired,
}
sort.Strings(keys)
return keys
}
func standingsFromSettings(m map[string]string) Standings {
s := Standings{
IndividualToTeamWeight: settingFloat(m, settingIndividualToTeamWeight, 1),
TieBreak: splitList(m[settingTieBreak]),
MedalCountsTeamEvents: BoolPtr(settingBool(m, settingMedalCountsTeamEvents, true)),
ShowIndividualBoard: BoolPtr(settingBool(m, settingShowIndividualBoard, true)),
ShowIndividualMedals: BoolPtr(settingBool(m, settingShowIndividualMedals, true)),
}
if len(s.TieBreak) == 0 {
s.TieBreak = append([]string(nil), DefaultTieBreak...)
}
return s
}
func recordBonusFromSettings(m map[string]string) RecordBonus {
rb := RecordBonus{
Enabled: settingBool(m, settingRecordBonusEnabled, false),
Mode: modeOrDefault(m[settingRecordBonusMode]),
}
if raw := strings.TrimSpace(m[settingRecordBonusLevels]); raw != "" {
var levels []RecordLevel
if err := json.Unmarshal([]byte(raw), &levels); err == nil {
rb.Levels = levels
}
}
return rb
}
// RecordBonusFromSettings 从赛事库 settings 键值对还原破纪录加分配置(含档位)。
// 上层加载器必须经此函数读取 record_bonus.levels,否则档位缺失、破纪录加分恒为 0。
func RecordBonusFromSettings(m map[string]string) domain.RecordBonusConfig {
return recordBonusFromSettings(m).ToDomain()
}
func manualAdjustmentFromSettings(m map[string]string) ManualAdjustment {
return ManualAdjustment{
Enabled: settingBool(m, settingManualEnabled, false),
ReasonRequired: settingBool(m, settingManualReasonRequired, true),
}
}
func settingFloat(m map[string]string, key string, def float64) float64 {
raw, ok := m[key]
if !ok {
return def
}
v, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
if err != nil {
return def
}
return v
}
func settingBool(m map[string]string, key string, def bool) bool {
raw, ok := m[key]
if !ok {
return def
}
switch strings.ToLower(strings.TrimSpace(raw)) {
case "true", "1", "yes", "on":
return true
case "false", "0", "no", "off":
return false
default:
return def
}
}
func formatFloat(v float64) string { return strconv.FormatFloat(v, 'g', -1, 64) }
func formatBool(v bool) string {
if v {
return "true"
}
return "false"
}
func splitList(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func modeOrDefault(raw string) domain.RecordBonusMode {
if strings.TrimSpace(raw) == "" {
return domain.RecordBonusMax
}
return domain.RecordBonusMode(strings.TrimSpace(raw))
}