package httpapi import ( "context" "io" "net/http" "git.yeij.top/AskaEth/Scoreboard/server/internal/auth" "git.yeij.top/AskaEth/Scoreboard/server/internal/rules" ) // rulesRoutes 挂载规则包的导出与导入(导入支持 ?preview=1 只预览差异)。 func (s *server) rulesRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/rules/export", s.read(auth.RoleAdmin, s.exportRules)) mux.HandleFunc("POST /api/rules/import", s.write(auth.RoleAdmin, s.importRules)) } func (s *server) exportRules(w http.ResponseWriter, r *http.Request) (any, error) { pack, err := rules.Export(s.ev, rules.Meta{}) if err != nil { return nil, internal(err) } body, err := rules.Marshal(pack) if err != nil { return nil, internal(err) } w.Header().Set("Content-Type", "application/yaml; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="rules.yaml"`) if _, err := w.Write(body); err != nil { s.logger.Error("规则包写出失败", "err", err) } return nil, errHandled } func (s *server) importRules(w http.ResponseWriter, r *http.Request) (any, error) { body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20)) if err != nil { return nil, badRequest("读取请求体失败", nil) } if len(body) == 0 { return nil, badRequest("请求体为空", nil) } pack, err := rules.Parse(body) if err != nil { return nil, badRequest(err.Error(), nil) } cur, err := rules.Load(s.ev) if err != nil { return nil, internal(err) } cs, err := rules.Diff(cur, pack) if err != nil { return nil, badRequest(err.Error(), nil) } // 先预览再落地:?preview=1 只返回差异。 if r.URL.Query().Get("preview") == "1" { return cs, nil } res, err := rules.Apply(s.ev, cs) if err != nil { return nil, internal(err) } // 分值一变,历史成绩的积分必须全量重算。 if err := s.recomputeAllEvents(r.Context(), middlewareUserID(r)); err != nil { return nil, internal(err) } s.audit(r.Context(), "rules.import", "rules", 0, nil, res) return res, nil } // recomputeAllEvents 逐项目重算积分流水,用于规则包导入等会改变分值的场景。 func (s *server) recomputeAllEvents(ctx context.Context, actorID int64) error { rows, err := s.ev.QueryContext(ctx, `SELECT id FROM events`) if err != nil { return err } var ids []int64 for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { _ = rows.Close() return err } ids = append(ids, id) } if err := rows.Close(); err != nil { return err } if err := rows.Err(); err != nil { return err } for _, eventID := range ids { tx, err := s.ev.BeginTx(ctx, nil) if err != nil { return err } if _, err := recomputeLedgerTx(ctx, tx, eventID, 0, actorID, "rules-import"); err != nil { _ = tx.Rollback() return err } // 重算改动了积分,必须写版本号并推送,否则大屏停在旧比分。 if err := s.bumpStandings(ctx, tx, actorID); err != nil { _ = tx.Rollback() return err } if err := tx.Commit(); err != nil { return err } } return nil }