package rules import ( "database/sql" "fmt" "strings" "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" ) // EventInfo 是展开通道模板所需的最小项目信息。 type EventInfo struct { ID int64 Code string Name string SportCode string EventType domain.EventType ResultType domain.ResultType Scored bool Multiplier float64 Channels []Channel } // Snapshot 是赛事库当前的规则状态:规则包 + 项目目录。 // 差异预览需要项目目录才能把 channelTemplates 展开到具体项目。 type Snapshot struct { Pack *RulePack Events []EventInfo } // EventByCode 按项目编码查找项目。 func (s *Snapshot) EventByCode(code string) (EventInfo, bool) { for _, e := range s.Events { if e.Code == code { return e, true } } return EventInfo{}, false } // Load 从赛事库读取当前规则状态。规则包元数据(meta)库里不存,保持为空。 func Load(db *sql.DB) (*Snapshot, error) { pack := &RulePack{ Format: FormatName, FormatVersion: FormatVersion, } profileByID, profiles, err := loadProfiles(db) if err != nil { return nil, err } pack.Profiles = profiles settings, err := loadSettings(db) if err != nil { return nil, err } pack.Standings = standingsFromSettings(settings) pack.RecordBonus = recordBonusFromSettings(settings) pack.ManualAdjustment = manualAdjustmentFromSettings(settings) events, err := loadEvents(db, profileByID) if err != nil { return nil, err } // 库里只有逐项目的通道、没有模板表,因此导出时把「有通道/不计分/权重非 1」的项目 // 全部写成 eventOverrides,保证「导出 → 导入」能还原当前配置。 for _, e := range events { needOverride := len(e.Channels) > 0 || !e.Scored || e.Multiplier != 1 if !needOverride { continue } ov := EventOverride{EventCode: e.Code, Name: e.Name} if !e.Scored { ov.Scored = BoolPtr(false) } if e.Multiplier != 1 { ov.Multiplier = Float64Ptr(e.Multiplier) } if len(e.Channels) > 0 { ov.Channels = ChannelListPtr(append([]Channel(nil), e.Channels...)) } pack.EventOverrides = append(pack.EventOverrides, ov) } canonical := pack.Canonical() return &Snapshot{Pack: &canonical, Events: events}, nil } // Export 从赛事库生成规则包;meta 由调用方提供(库里不存规则包元数据)。 func Export(db *sql.DB, meta Meta) (*RulePack, error) { snap, err := Load(db) if err != nil { return nil, err } pack := *snap.Pack pack.Meta = meta if strings.TrimSpace(pack.Meta.Code) == "" { pack.Meta.Code = "exported-rules" } if strings.TrimSpace(pack.Meta.Name) == "" { if name, err := settingByKey(db, "event.name"); err == nil && strings.TrimSpace(name) != "" { pack.Meta.Name = name } else { pack.Meta.Name = "未命名赛事 · 积分规则" } } canonical := pack.Canonical() return &canonical, nil } func loadProfiles(db *sql.DB) (map[int64]string, []Profile, error) { rows, err := db.Query(`SELECT id, code, name, tie_mode, record_bonus_mode, award_ranks, last_place_zero FROM scoring_profiles ORDER BY code`) if err != nil { return nil, nil, fmt.Errorf("查询积分模板: %w", err) } defer func() { _ = rows.Close() }() byID := map[int64]string{} byCode := map[string]int{} var profiles []Profile for rows.Next() { var ( id, awardRanks int64 code, name string tie, bonus string lastPlaceZeroRaw int ) if err := rows.Scan(&id, &code, &name, &tie, &bonus, &awardRanks, &lastPlaceZeroRaw); err != nil { return nil, nil, fmt.Errorf("读取积分模板: %w", err) } byID[id] = code byCode[code] = len(profiles) profiles = append(profiles, Profile{ Code: code, Name: name, AwardRanks: int(awardRanks), LastPlaceZero: lastPlaceZeroRaw != 0, TieMode: domain.TieMode(tie), RecordBonusMode: domain.RecordBonusMode(bonus), }) } if err := rows.Err(); err != nil { return nil, nil, fmt.Errorf("读取积分模板: %w", err) } ruleRows, err := db.Query(`SELECT profile_id, rank, points FROM scoring_profile_rules ORDER BY profile_id, rank`) if err != nil { return nil, nil, fmt.Errorf("查询积分模板明细: %w", err) } defer func() { _ = ruleRows.Close() }() for ruleRows.Next() { var ( profileID int64 rank int points float64 ) if err := ruleRows.Scan(&profileID, &rank, &points); err != nil { return nil, nil, fmt.Errorf("读取积分模板明细: %w", err) } code, ok := byID[profileID] if !ok { continue } idx := byCode[code] profiles[idx].Rules = append(profiles[idx].Rules, Rule{Rank: rank, Points: points}) } if err := ruleRows.Err(); err != nil { return nil, nil, fmt.Errorf("读取积分模板明细: %w", err) } return byID, profiles, nil } func loadSettings(db *sql.DB) (map[string]string, error) { rows, err := db.Query(`SELECT key, value FROM settings`) if err != nil { return nil, fmt.Errorf("查询赛事设置: %w", err) } defer func() { _ = rows.Close() }() out := map[string]string{} for rows.Next() { var k, v string if err := rows.Scan(&k, &v); err != nil { return nil, fmt.Errorf("读取赛事设置: %w", err) } out[k] = v } if err := rows.Err(); err != nil { return nil, fmt.Errorf("读取赛事设置: %w", err) } return out, nil } func settingByKey(db *sql.DB, key string) (string, error) { var value string if err := db.QueryRow(`SELECT value FROM settings WHERE key = ?`, key).Scan(&value); err != nil { return "", err } return value, nil } func loadEvents(db *sql.DB, profileByID map[int64]string) ([]EventInfo, error) { rows, err := db.Query(`SELECT e.id, e.code, e.name, s.code, e.event_type, e.result_type, e.scored, e.multiplier FROM events e JOIN sports s ON s.id = e.sport_id ORDER BY e.sort_order, e.code`) if err != nil { return nil, fmt.Errorf("查询项目: %w", err) } defer func() { _ = rows.Close() }() events := []EventInfo{} index := map[int64]int{} for rows.Next() { var ( id int64 code, name string sportCode string eventType string resultType string scoredRaw int multiplier float64 ) if err := rows.Scan(&id, &code, &name, &sportCode, &eventType, &resultType, &scoredRaw, &multiplier); err != nil { return nil, fmt.Errorf("读取项目: %w", err) } index[id] = len(events) events = append(events, EventInfo{ ID: id, Code: code, Name: name, SportCode: sportCode, EventType: domain.EventType(eventType), ResultType: domain.ResultType(resultType), Scored: scoredRaw != 0, Multiplier: multiplier, }) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("读取项目: %w", err) } chRows, err := db.Query(`SELECT c.event_id, c.scope, c.basis, p.code, c.sort_order FROM event_score_channels c JOIN scoring_profiles p ON p.id = c.profile_id ORDER BY c.event_id, c.sort_order, c.id`) if err != nil { return nil, fmt.Errorf("查询计分通道: %w", err) } defer func() { _ = chRows.Close() }() for chRows.Next() { var ( eventID int64 scope string basis string code string sortOrd int ) if err := chRows.Scan(&eventID, &scope, &basis, &code, &sortOrd); err != nil { return nil, fmt.Errorf("读取计分通道: %w", err) } idx, ok := index[eventID] if !ok { continue } events[idx].Channels = append(events[idx].Channels, Channel{ Scope: domain.Scope(scope), Basis: domain.Basis(basis), Profile: code, }) } if err := chRows.Err(); err != nil { return nil, fmt.Errorf("读取计分通道: %w", err) } return events, nil }