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)) }