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)
This commit is contained in:
AskaEth committed 2026-09-09 21:06:40 +08:00
1 parent 8dae63ee70
commit 7f9701c465
61 files changed
+3844 -333

No files matched your search

+103 -16
View File
@@ -225,24 +225,24 @@ func boolDefault(v string, def bool) bool {
// ---------- standings ----------
func (s *server) loadStandingsInput(ctx context.Context) (domain.StandingsInput, error) {
teams, err := s.domainTeams(ctx)
func (s *server) loadStandingsInput(ctx context.Context, q queryer) (domain.StandingsInput, error) {
teams, err := s.domainTeams(ctx, q)
if err != nil {
return domain.StandingsInput{}, err
}
athletes, err := s.domainAthletes(ctx)
athletes, err := s.domainAthletes(ctx, q)
if err != nil {
return domain.StandingsInput{}, err
}
ledger, err := s.activeLedger(ctx)
ledger, err := s.activeLedger(ctx, q)
if err != nil {
return domain.StandingsInput{}, err
}
ranked, err := s.buildRanked(ctx)
ranked, err := s.buildRanked(ctx, q)
if err != nil {
return domain.StandingsInput{}, err
}
values, err := loadSettingsMap(ctx, s.ev)
values, err := loadSettingsMap(ctx, q)
if err != nil {
return domain.StandingsInput{}, err
}
@@ -259,8 +259,31 @@ func (s *server) loadStandingsInput(ctx context.Context) (domain.StandingsInput,
}, nil
}
func (s *server) domainTeams(ctx context.Context) ([]domain.Team, error) {
rows, err := s.ev.QueryContext(ctx,
// standingsSnapshot 构造与 GET /api/standings 完全同构的整张榜单,
// 供 change_log 的 standings replace 载荷使用(契约要求 data 是榜单数组)。
func (s *server) standingsSnapshot(ctx context.Context, q queryer) ([]standingRowDTO, error) {
in, err := s.loadStandingsInput(ctx, q)
if err != nil {
return nil, err
}
teams, err := teamMapQ(ctx, q)
if err != nil {
return nil, err
}
return standingRowsDTO(domain.BuildTeamStandings(in), teams), nil
}
// bumpStandings 在写事务内把整张榜单写入 change_log;榜单变化后必须调用,否则大屏不会刷新。
func (s *server) bumpStandings(ctx context.Context, tx *sql.Tx, actorID int64) error {
snapshot, err := s.standingsSnapshot(ctx, tx)
if err != nil {
return err
}
return bumpVersion(ctx, tx, "standings", 0, "replace", snapshot, actorID)
}
func (s *server) domainTeams(ctx context.Context, q queryer) ([]domain.Team, error) {
rows, err := q.QueryContext(ctx,
`SELECT id, code, name, short_name, color, sort_order, active FROM teams WHERE active = 1`)
if err != nil {
return nil, internal(err)
@@ -284,8 +307,8 @@ func (s *server) domainTeams(ctx context.Context) ([]domain.Team, error) {
return out, rows.Err()
}
func (s *server) domainAthletes(ctx context.Context) ([]domain.Athlete, error) {
rows, err := s.ev.QueryContext(ctx, `SELECT id, name, gender, team_id FROM athletes WHERE active = 1`)
func (s *server) domainAthletes(ctx context.Context, q queryer) ([]domain.Athlete, error) {
rows, err := q.QueryContext(ctx, `SELECT id, name, gender, team_id FROM athletes WHERE active = 1`)
if err != nil {
return nil, internal(err)
}
@@ -310,8 +333,8 @@ func (s *server) domainAthletes(ctx context.Context) ([]domain.Athlete, error) {
// buildRanked 从已发布成绩重建名次明细(用于奖牌统计与个人榜)。
// 一条成绩在每个通道下各产出一条明细,与计分引擎的口径一致。
func (s *server) buildRanked(ctx context.Context) ([]domain.RankedResult, error) {
rows, err := s.ev.QueryContext(ctx,
func (s *server) buildRanked(ctx context.Context, q queryer) ([]domain.RankedResult, error) {
rows, err := q.QueryContext(ctx,
`SELECT r.id, r.event_id, e.category_id, r.rank, r.result_value, r.status,
e.id, e.team_id, e.athlete_id, COALESCE(a.name, ''), c.scope, c.id
FROM results r
@@ -495,9 +518,17 @@ func (s *server) approveRecordBreak(ctx context.Context, id, userID int64) (reco
}
defer func() { _ = tx.Rollback() }()
var state string
var eventID int64
if err := tx.QueryRowContext(ctx, `SELECT state, event_id FROM record_breaks WHERE id = ?`, id).Scan(&state, &eventID); err != nil {
var (
state string
eventID int64
entryID int64
level string
resultVal sql.NullFloat64
resultText sql.NullString
)
if err := tx.QueryRowContext(ctx,
`SELECT state, event_id, entry_id, level, result_value, result_text FROM record_breaks WHERE id = ?`, id).
Scan(&state, &eventID, &entryID, &level, &resultVal, &resultText); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return recordBreakDTO{}, notFound("破纪录登记不存在")
}
@@ -514,9 +545,23 @@ func (s *server) approveRecordBreak(ctx context.Context, id, userID int64) (reco
if _, err := recomputeLedgerTx(ctx, tx, eventID, 0, userID, "确认破纪录加分"); err != nil {
return recordBreakDTO{}, err
}
// 确认即更新纪录库,并把引擎算出的加分回填到登记上(devdocs/02 第 3 节)。
if err := upsertCurrentRecord(ctx, tx, eventID, entryID, level, resultVal, resultText); err != nil {
return recordBreakDTO{}, err
}
if _, err := tx.ExecContext(ctx,
`UPDATE record_breaks SET bonus_points = COALESCE((
SELECT SUM(points) FROM score_ledger
WHERE source_type = 'record_bonus' AND event_id = ? AND entry_id = ? AND voided = 0), 0)
WHERE id = ?`, eventID, entryID, id); err != nil {
return recordBreakDTO{}, internal(err)
}
if err := bumpVersion(ctx, tx, "record", id, "upsert", map[string]any{"id": id}, userID); err != nil {
return recordBreakDTO{}, err
}
if err := s.bumpStandings(ctx, tx, userID); err != nil {
return recordBreakDTO{}, err
}
if err := tx.Commit(); err != nil {
return recordBreakDTO{}, internal(err)
}
@@ -524,6 +569,44 @@ func (s *server) approveRecordBreak(ctx context.Context, id, userID int64) (reco
return s.getRecordBreak(ctx, id)
}
// upsertCurrentRecord 把确认后的破纪录写入 records:同小项同档位的旧纪录改为非当前,
// 新纪录标为 current(devdocs/04 records 表;source=current 表示本届产生)。
func upsertCurrentRecord(ctx context.Context, tx *sql.Tx, eventID, entryID int64, level string, value sql.NullFloat64, text sql.NullString) error {
var (
teamID int64
athleteID sql.NullInt64
athleteName sql.NullString
teamName sql.NullString
)
if err := tx.QueryRowContext(ctx,
`SELECT e.team_id, e.athlete_id, a.name, t.name
FROM entries e
LEFT JOIN athletes a ON a.id = e.athlete_id
LEFT JOIN teams t ON t.id = e.team_id
WHERE e.id = ?`, entryID).Scan(&teamID, &athleteID, &athleteName, &teamName); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return notFound("报名项不存在")
}
return internal(err)
}
holder := teamName.String
if athleteID.Valid && athleteName.String != "" {
holder = athleteName.String
}
if _, err := tx.ExecContext(ctx,
`UPDATE records SET is_current = 0 WHERE event_id = ? AND level = ? AND is_current = 1`,
eventID, level); err != nil {
return internal(err)
}
if _, err := tx.ExecContext(ctx,
`INSERT INTO records (event_id, level, result_value, result_text, holder_name, holder_team_id, achieved_at, is_current, source)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 1, 'current')`,
eventID, level, value, text, nullStr(&holder), nullID(teamID)); err != nil {
return internal(err)
}
return nil
}
// ---------- adjustments ----------
func (s *server) listAdjustments(ctx context.Context, teamID int64, page, size int) (pageResult[adjustmentDTO], error) {
@@ -614,6 +697,10 @@ func (s *server) createAdjustment(ctx context.Context, in adjustmentInput, userI
in.TeamID, nullI64(in.EventID), id, in.Delta, nullID(userID)); err != nil {
return adjustmentDTO{}, internal(err)
}
// 调整分进积分榜,必须写版本号,否则大屏不会刷新。
if err := s.bumpStandings(ctx, tx, userID); err != nil {
return adjustmentDTO{}, err
}
if err := tx.Commit(); err != nil {
return adjustmentDTO{}, internal(err)
}
@@ -669,7 +756,7 @@ func (s *server) voidAdjustment(ctx context.Context, id, userID int64, reason st
nullID(userID), reason, id); err != nil {
return adjustmentDTO{}, internal(err)
}
if err := bumpVersion(ctx, tx, "standings", 0, "replace", map[string]any{"adjustmentId": id}, userID); err != nil {
if err := s.bumpStandings(ctx, tx, userID); err != nil {
return adjustmentDTO{}, err
}
if err := tx.Commit(); err != nil {