feat: M3-M7 全模块落地 + 集成接线
- M3 认证/权限/审计/限流 + 全部 REST 接口 - M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制) - M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配) - M6 WebSocket 实时同步中心 - M7 xlsx 导出 + 赛事包 zip + 备份 - 规则包 YAML 导入导出(差异预览) - 字体 vendor 子集化(4 套 x2 字重) - 集成:realtime/export/rules 接入 main+httpapi;backup 子命令 - 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
This commit is contained in:
1 parent
d0e3b93e76
commit
bc8b1ee6c0
145 files changed
+25159
-321
No files matched your search
@@ -0,0 +1,231 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/domain"
|
||||
)
|
||||
|
||||
// ---------- 成员姓名 JSON 数组 ----------
|
||||
|
||||
func parseNames(ns sql.NullString) []string {
|
||||
if !ns.Valid || ns.String == "" {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
if err := json.Unmarshal([]byte(ns.String), &out); err != nil {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func marshalNames(names []string) any {
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
raw, err := json.Marshal(names)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// ---------- results ----------
|
||||
|
||||
const resultColumns = `id, event_id, entry_id, rank, result_text, result_value, unit, is_tie,
|
||||
points_override, status, state, recorded_by, recorded_at, verified_by, verified_at, note`
|
||||
|
||||
func scanResult(row interface{ Scan(...any) error }) (resultDTO, error) {
|
||||
var (
|
||||
r resultDTO
|
||||
rank sql.NullInt64
|
||||
text, unit sql.NullString
|
||||
value, override sql.NullFloat64
|
||||
recordedBy, verifiedBy sql.NullInt64
|
||||
recordedAt, verifiedAt, note sql.NullString
|
||||
)
|
||||
if err := row.Scan(&r.ID, &r.EventID, &r.EntryID, &rank, &text, &value, &unit, &r.IsTie,
|
||||
&override, &r.Status, &r.State, &recordedBy, &recordedAt, &verifiedBy, &verifiedAt, ¬e); err != nil {
|
||||
return r, err
|
||||
}
|
||||
r.Rank = intPtr(rank)
|
||||
r.ResultText = text.String
|
||||
r.ResultValue = f64Ptr(value)
|
||||
r.Unit = unit.String
|
||||
r.PointsOverride = f64Ptr(override)
|
||||
r.RecordedBy = i64Ptr(recordedBy)
|
||||
r.RecordedAt = strPtr(recordedAt)
|
||||
r.VerifiedBy = i64Ptr(verifiedBy)
|
||||
r.VerifiedAt = strPtr(verifiedAt)
|
||||
r.Note = strPtr(note)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *server) resultsByEvent(ctx context.Context, eventID int64) ([]resultDTO, error) {
|
||||
return queryResults(ctx, s.ev, `SELECT `+resultColumns+` FROM results WHERE event_id = ? ORDER BY id`, eventID)
|
||||
}
|
||||
|
||||
func queryResults(ctx context.Context, q queryer, query string, args ...any) ([]resultDTO, error) {
|
||||
rows, err := q.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
out := make([]resultDTO, 0)
|
||||
for rows.Next() {
|
||||
r, err := scanResult(rows)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *server) getResult(ctx context.Context, id int64) (resultDTO, error) {
|
||||
r, err := scanResult(s.ev.QueryRowContext(ctx, `SELECT `+resultColumns+` FROM results WHERE id = ?`, id))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return resultDTO{}, notFound("成绩不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return resultDTO{}, internal(err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *server) listResultsPage(ctx context.Context, eventID int64, state string, page, size int) (pageResult[resultDTO], error) {
|
||||
where := []string{"1 = 1"}
|
||||
args := []any{}
|
||||
if eventID > 0 {
|
||||
where = append(where, "event_id = ?")
|
||||
args = append(args, eventID)
|
||||
}
|
||||
if state != "" {
|
||||
where = append(where, "state = ?")
|
||||
args = append(args, state)
|
||||
}
|
||||
clause := " WHERE " + joinAnd(where)
|
||||
|
||||
var total int
|
||||
if err := s.ev.QueryRowContext(ctx, `SELECT COUNT(*) FROM results`+clause, args...).Scan(&total); err != nil {
|
||||
return pageResult[resultDTO]{}, internal(err)
|
||||
}
|
||||
|
||||
query := `SELECT ` + resultColumns + ` FROM results` + clause + ` ORDER BY id LIMIT ? OFFSET ?`
|
||||
args = append(args, size, (page-1)*size)
|
||||
items, err := queryResults(ctx, s.ev, query, args...)
|
||||
if err != nil {
|
||||
return pageResult[resultDTO]{}, err
|
||||
}
|
||||
return pageResult[resultDTO]{Items: items, Total: total, Page: page, Size: size}, nil
|
||||
}
|
||||
|
||||
// upsertDraft 批量保存草稿;已发布的成绩保留 published 状态,避免静默撤下大屏。
|
||||
func (s *server) upsertDraft(ctx context.Context, eventID int64, userID int64, items []draftResultInput) ([]resultDTO, error) {
|
||||
tx, err := s.ev.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
for i, it := range items {
|
||||
if it.EntryID == 0 {
|
||||
return nil, badRequest("results[].entryId 必填", i)
|
||||
}
|
||||
var entryEvent int64
|
||||
if err := tx.QueryRowContext(ctx, `SELECT event_id FROM entries WHERE id = ?`, it.EntryID).Scan(&entryEvent); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, badRequest("entryId 不存在", it.EntryID)
|
||||
}
|
||||
return nil, internal(err)
|
||||
}
|
||||
if entryEvent != eventID {
|
||||
return nil, badRequest("entryId 不属于该小项", it.EntryID)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO results (event_id, entry_id, rank, result_text, result_value, unit, is_tie,
|
||||
points_override, status, state, recorded_by, recorded_at, note, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft', ?, datetime('now'), ?, datetime('now'))
|
||||
ON CONFLICT(entry_id) DO UPDATE SET
|
||||
rank = excluded.rank,
|
||||
result_text = excluded.result_text,
|
||||
result_value = excluded.result_value,
|
||||
unit = excluded.unit,
|
||||
is_tie = excluded.is_tie,
|
||||
points_override = excluded.points_override,
|
||||
status = excluded.status,
|
||||
note = excluded.note,
|
||||
recorded_by = excluded.recorded_by,
|
||||
recorded_at = excluded.recorded_at,
|
||||
updated_at = datetime('now')`,
|
||||
eventID, it.EntryID, nullInt(it.Rank), nullStr(it.ResultText), nullF64(it.ResultValue),
|
||||
nullStr(it.Unit), it.IsTie, nullF64(it.PointsOverride), orDefault(it.Status, "ok"),
|
||||
nullID(userID), nullStr(it.Note)); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
return s.resultsByEvent(ctx, eventID)
|
||||
}
|
||||
|
||||
func nullID(id int64) any {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func joinAnd(parts []string) string {
|
||||
out := ""
|
||||
for i, p := range parts {
|
||||
if i > 0 {
|
||||
out += " AND "
|
||||
}
|
||||
out += p
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------- ledger ----------
|
||||
|
||||
func (s *server) activeLedger(ctx context.Context) ([]domain.LedgerEntry, error) {
|
||||
return queryLedger(ctx, s.ev, `SELECT id, team_id, athlete_id, event_id, entry_id, result_id,
|
||||
source_type, source_id, points, voided, created_at FROM score_ledger WHERE voided = 0`)
|
||||
}
|
||||
|
||||
func queryLedger(ctx context.Context, q queryer, query string, args ...any) ([]domain.LedgerEntry, error) {
|
||||
rows, err := q.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
out := make([]domain.LedgerEntry, 0)
|
||||
for rows.Next() {
|
||||
var (
|
||||
l domain.LedgerEntry
|
||||
athlete sql.NullInt64
|
||||
event sql.NullInt64
|
||||
entry sql.NullInt64
|
||||
result sql.NullInt64
|
||||
created sql.NullString
|
||||
)
|
||||
if err := rows.Scan(&l.ID, &l.TeamID, &athlete, &event, &entry, &result,
|
||||
&l.SourceType, &l.SourceID, &l.Points, &l.Voided, &created); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
l.AthleteID = athlete.Int64
|
||||
l.EventID = event.Int64
|
||||
l.EntryID = entry.Int64
|
||||
l.ResultID = result.Int64
|
||||
l.CreatedAt = created.String
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in new issue
Block a user