package rules import ( "database/sql" "fmt" "sort" "strings" ) // ApplyResult 汇总一次导入实际落库的行数。 type ApplyResult struct { ProfilesWritten int `json:"profilesWritten"` ProfilesDeleted int `json:"profilesDeleted"` ChannelsWritten int `json:"channelsWritten"` EventsUpdated int `json:"eventsUpdated"` SettingsWritten int `json:"settingsWritten"` EventsSkipped []string `json:"eventsSkipped,omitempty"` } // Apply 把差异预览落地。规则包语义是「全量覆盖」,因此按 Next 的目标状态同步, // 而不是逐条套用 diff 列表——这样重复导入同一份规则包是幂等的。 // 只填了 Next 的变更集(例如确认导入时重新 Parse 上传内容)同样可以调用,等价于强制全量导入。 // // 落地顺序:模板 → 通道 → 删除无主模板 → 项目开关 → 赛事设置。 // 通道先改完再删模板,避免「模板被换掉」时误判引用。 // Apply 不负责重算积分:分值一变,调用方必须全量重算(devdocs 02 §7.3)。 func Apply(db *sql.DB, cs *ChangeSet) (*ApplyResult, error) { if cs == nil || cs.Next == nil { return nil, ErrNilChangeSet } next := cs.Next.Canonical() if err := next.Validate(); err != nil { return nil, err } cur, err := Load(db) if err != nil { return nil, err } targets, unknown, err := resolveTargets(cur, &next) if err != nil { return nil, err } tx, err := db.Begin() if err != nil { return nil, fmt.Errorf("开启事务: %w", err) } defer func() { _ = tx.Rollback() }() res := &ApplyResult{EventsSkipped: unknown} // 1. 模板全量 upsert,并建立「编码 → id」映射供通道引用。 profileIDs := map[string]int64{} for _, prof := range next.Profiles { id, err := upsertProfile(tx, prof) if err != nil { return nil, err } profileIDs[prof.Code] = id res.ProfilesWritten++ } // 2. 通道:只覆盖规则包明确提到的项目。 eventIDs := map[string]int64{} for _, ev := range cur.Events { eventIDs[ev.Code] = ev.ID } codes := make([]string, 0, len(targets)) for code := range targets { codes = append(codes, code) } sort.Strings(codes) for _, code := range codes { eventID, ok := eventIDs[code] if !ok { continue } if _, err := tx.Exec(`DELETE FROM event_score_channels WHERE event_id = ?`, eventID); err != nil { return nil, fmt.Errorf("清理项目 %s 的计分通道: %w", code, err) } for i, ch := range targets[code] { profileID, ok := profileIDs[ch.Profile] if !ok { return nil, fmt.Errorf("项目 %s 的通道引用了不存在的模板 %q", code, ch.Profile) } if _, err := tx.Exec(`INSERT INTO event_score_channels (event_id, scope, basis, profile_id, sort_order) VALUES (?, ?, ?, ?, ?)`, eventID, string(ch.Scope), string(ch.Basis), profileID, i); err != nil { return nil, fmt.Errorf("写入项目 %s 的计分通道: %w", code, err) } } res.ChannelsWritten++ } // 3. 删除目标规则包里已没有的模板;仍被引用就报错,绝不静默拆掉别人的配置。 for _, prof := range cur.Pack.Profiles { if _, keep := profileIDs[prof.Code]; keep { continue } if err := deleteProfile(tx, prof.Code); err != nil { return nil, err } res.ProfilesDeleted++ } // 4. 项目开关:只改 override 里显式给出的字段。 for _, ov := range next.EventOverrides { eventID, ok := eventIDs[ov.EventCode] if !ok || (ov.Scored == nil && ov.Multiplier == nil) { continue } scored, err := currentScored(tx, eventID) if err != nil { return nil, err } if ov.Scored != nil { scored = *ov.Scored } multiplier, err := currentMultiplier(tx, eventID) if err != nil { return nil, err } if ov.Multiplier != nil { multiplier = *ov.Multiplier } if _, err := tx.Exec(`UPDATE events SET scored = ?, multiplier = ? WHERE id = ?`, boolToInt(scored), multiplier, eventID); err != nil { return nil, fmt.Errorf("更新项目 %s: %w", ov.EventCode, err) } res.EventsUpdated++ } // 5. 赛事设置。 settings := settingsFromPack(&next) for _, key := range settingsKeys() { if _, err := tx.Exec(`INSERT INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')`, key, settings[key]); err != nil { return nil, fmt.Errorf("写入赛事设置 %s: %w", key, err) } res.SettingsWritten++ } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("提交导入事务: %w", err) } return res, nil } func upsertProfile(tx *sql.Tx, prof Profile) (int64, error) { if _, err := tx.Exec(`INSERT INTO scoring_profiles (code, name, tie_mode, record_bonus_mode, award_ranks, last_place_zero) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(code) DO UPDATE SET name = excluded.name, tie_mode = excluded.tie_mode, record_bonus_mode = excluded.record_bonus_mode, award_ranks = excluded.award_ranks, last_place_zero = excluded.last_place_zero, updated_at = datetime('now')`, prof.Code, prof.Name, string(prof.TieMode), string(prof.RecordBonusMode), prof.AwardRanks, boolToInt(prof.LastPlaceZero)); err != nil { return 0, fmt.Errorf("写入积分模板 %s: %w", prof.Code, err) } var id int64 if err := tx.QueryRow(`SELECT id FROM scoring_profiles WHERE code = ?`, prof.Code).Scan(&id); err != nil { return 0, fmt.Errorf("读取积分模板 %s 的 id: %w", prof.Code, err) } if _, err := tx.Exec(`DELETE FROM scoring_profile_rules WHERE profile_id = ?`, id); err != nil { return 0, fmt.Errorf("清理积分模板 %s 的明细: %w", prof.Code, err) } for _, r := range prof.Rules { if _, err := tx.Exec(`INSERT INTO scoring_profile_rules (profile_id, rank, points) VALUES (?, ?, ?)`, id, r.Rank, r.Points); err != nil { return 0, fmt.Errorf("写入积分模板 %s 的名次 %d: %w", prof.Code, r.Rank, err) } } return id, nil } func deleteProfile(tx *sql.Tx, code string) error { var id int64 if err := tx.QueryRow(`SELECT id FROM scoring_profiles WHERE code = ?`, code).Scan(&id); err != nil { if err == sql.ErrNoRows { return nil } return fmt.Errorf("读取积分模板 %s: %w", code, err) } refs, err := profileRefs(tx, id) if err != nil { return err } if len(refs) > 0 { return fmt.Errorf("%w: 模板 %s 仍被 %s 引用", ErrProfileInUse, code, strings.Join(refs, "、")) } if _, err := tx.Exec(`DELETE FROM scoring_profile_rules WHERE profile_id = ?`, id); err != nil { return fmt.Errorf("删除积分模板 %s 的明细: %w", code, err) } if _, err := tx.Exec(`DELETE FROM scoring_profiles WHERE id = ?`, id); err != nil { return fmt.Errorf("删除积分模板 %s: %w", code, err) } return nil } func profileRefs(tx *sql.Tx, profileID int64) ([]string, error) { var refs []string var channels int if err := tx.QueryRow(`SELECT COUNT(*) FROM event_score_channels WHERE profile_id = ?`, profileID).Scan(&channels); err != nil { return nil, fmt.Errorf("统计模板引用: %w", err) } if channels > 0 { refs = append(refs, fmt.Sprintf("%d 个计分通道", channels)) } var sports int if err := tx.QueryRow(`SELECT COUNT(*) FROM sports WHERE default_profile_id = ?`, profileID).Scan(&sports); err != nil { return nil, fmt.Errorf("统计模板引用: %w", err) } if sports > 0 { refs = append(refs, fmt.Sprintf("%d 个大项的默认模板", sports)) } return refs, nil } func currentScored(tx *sql.Tx, eventID int64) (bool, error) { var v int if err := tx.QueryRow(`SELECT scored FROM events WHERE id = ?`, eventID).Scan(&v); err != nil { return false, fmt.Errorf("读取项目计分开关: %w", err) } return v != 0, nil } func currentMultiplier(tx *sql.Tx, eventID int64) (float64, error) { var v float64 if err := tx.QueryRow(`SELECT multiplier FROM events WHERE id = ?`, eventID).Scan(&v); err != nil { return 0, fmt.Errorf("读取项目权重: %w", err) } return v, nil } func boolToInt(v bool) int { if v { return 1 } return 0 }