feat: 8 条功能 + 身份口径改为号码布为准(含批量退赛/移出、基础数据导入导出、登录有效期)
用户给出的 8 条功能 + 一条口径变更,全部带真机/真浏览器验证:
① 录入页姓名后直接显示号码布(原「号码」列保留)
② 录入页批次进度条:`第 2 批 已录 5/8 · 3 条有错`,录完变绿;已录判据复用
entryGuards.judgeEntryRow(与发布防呆同源,不会出现"进度条说录完、发布说缺 8 条")
⑦ 小项列表双击 → 跳到该小项的组别(编辑/删除按钮不误触)
③ 赛事开关 `standings.live_on_publish`(默认开=保持原行为):关闭后分批发布只上屏成绩,
名次/流水/榜单一起等该组录完再结算 —— 代理实读代码后纠正了我的前提:
奖牌榜读的是 results.rank(buildRanked)而非流水汇总,只延迟流水会出现
「有金牌但 0 分」。另提供 POST /api/results/settle 人工兜底 + GET /api/results/settlement 未结算清单
④ 组别「就地更新」PUT /api/events/{id}/categories/{categoryId}:有报名数据时也能改名/改年龄/
改性别/改权重(旧路径是整表替换,一改就被 409 拦下);改名不动报名/成绩/流水(真机逐字节比对);
权重真的变了才触发该组重算(容差 1e-9)
④辅 运动员列表多选+全选,POST /api/athletes/batch 一次请求完成整批停用/彻底删除(逐条回报,
dryRun 预览走同一段 SQL 但不提交、不写审计)——这才是绕开限流的正解
新增 报名项批量:POST /api/entries/batch(scratch/restore/remove)。退赛只动 registered、
跳过 dq/dns 并说明原因;移出是物理删除,必须显式清 score_ledger(该表 entry_id 是
ON DELETE SET NULL,不显式删会留下"人没了分还在"的孤儿流水,验证基线 orphan=0);
退赛同时清 results.rank(否则出现"0 分还挂着金牌")
⑤ GET /api/export/entries.xlsx 报名名单导出(13 列含批次,状态中文与录入页逐字一致)
⑥ 基础数据包:GET /api/export/basedata.xlsx + POST /api/export/basedata/import
(四张表、表头只有一份定义、表头行自动定位、列缺席≠写空、按编码 upsert、绝不删除、
行级中文错误带真实 Excel 行号、枚举走既有校验、新增单位一定是启用)
⑧ 登录有效期面板可配:系统设置新页签,1–720 小时,默认 12h → 24h;滑动续期 +
Cookie 同步刷新(只服务端续期是假的,浏览器侧的 Max-Age 到点仍会丢 Cookie)
身份口径:号码布为唯一标识、证件号退出匹配(号码布不同=同名的两个人,新建并单独报告);
同一份表重复证件号不再报错;两个不同的人共用证件号仍报行级错误(唯一索引保留)
其它对齐:分项成绩表草稿行不再消失(成绩列「分数未录入」、名次/积分 `—`,没成绩的行同占位);
大屏实时赛况单栏铺满(留白 370→78px)、字号上限 56→96px、≥7 行自动拆左右两块;
删除报名项/退赛/权重等改动都会重算并推榜单。
测试:服务端 13 包(连跑两遍)、前端 335 例、vue-tsc + build、Console 18 文件 —— 全绿。
文档:README、docs/ 四份(含 debug/使用说明 副本)、devdocs 05/08/04/13。
版本号 dev_0.1.5 → dev_0.1.6。
This commit is contained in:
1 parent
8dd599bd97
commit
631c8dd8e1
80 files changed
+12468
-519
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/domain"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/dto"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/export"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/middleware"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/rules"
|
||||
@@ -340,6 +341,22 @@ func computeEventScore(ctx context.Context, q queryer, eventID, categoryID int64
|
||||
// 重算再插一份导致该通道分数翻倍。聚合流水本身不归属组别,且重建通道会换掉通道 id
|
||||
// (source_id 随之失效),故按 event_id 全量作废;下方补算整项聚合流水时会重新插入全量。
|
||||
func recomputeLedgerTx(ctx context.Context, tx *sql.Tx, eventID, categoryID, actorID int64, reason string) (domain.EventScore, error) {
|
||||
score, err := computeEventScore(ctx, tx, eventID, categoryID)
|
||||
if err != nil {
|
||||
return domain.EventScore{}, err
|
||||
}
|
||||
if err := writeLedgerTx(ctx, tx, score, eventID, categoryID, actorID, reason); err != nil {
|
||||
return domain.EventScore{}, err
|
||||
}
|
||||
return score, nil
|
||||
}
|
||||
|
||||
// writeLedgerTx 把已算好的 score 落成流水:先作废范围内旧流水,再插入新流水。
|
||||
//
|
||||
// 与 recomputeLedgerTx 拆开是为了「延迟结算」:那时名次与流水都要等整组录完才落,
|
||||
// 但分数仍要能算出来(结算时才算,见 publishResults)。计算不读 score_ledger,
|
||||
// 所以「先算后作废」与老的「先作废后算」结果完全一致。
|
||||
func writeLedgerTx(ctx context.Context, tx *sql.Tx, score domain.EventScore, eventID, categoryID, actorID int64, reason string) error {
|
||||
scope := `source_type IN ('result', 'record_bonus') AND event_id = ?`
|
||||
args := []any{eventID}
|
||||
if categoryID > 0 {
|
||||
@@ -351,25 +368,21 @@ func recomputeLedgerTx(ctx context.Context, tx *sql.Tx, eventID, categoryID, act
|
||||
`UPDATE score_ledger SET voided = 1, voided_by = ?, voided_at = datetime('now'), void_reason = ?
|
||||
WHERE voided = 0 AND `+scope,
|
||||
append([]any{nullID(actorID), reason}, args...)...); err != nil {
|
||||
return domain.EventScore{}, internal(err)
|
||||
return internal(err)
|
||||
}
|
||||
|
||||
score, err := computeEventScore(ctx, tx, eventID, categoryID)
|
||||
if err != nil {
|
||||
return domain.EventScore{}, err
|
||||
}
|
||||
ledger := score.Ledger
|
||||
if categoryID > 0 {
|
||||
// 聚合流水无法按组别区分,上面已把本小项的全部聚合流水一并作废;
|
||||
// 这里补算整项的聚合流水,避免按组别重算丢掉其它组别的聚合得分。
|
||||
hasAggregate, err := hasAggregateChannel(ctx, tx, eventID)
|
||||
if err != nil {
|
||||
return domain.EventScore{}, err
|
||||
return err
|
||||
}
|
||||
if hasAggregate {
|
||||
all, err := computeEventScore(ctx, tx, eventID, 0)
|
||||
if err != nil {
|
||||
return domain.EventScore{}, err
|
||||
return err
|
||||
}
|
||||
ledger = make([]domain.LedgerEntry, 0, len(score.Ledger)+len(all.Ledger))
|
||||
for _, l := range score.Ledger {
|
||||
@@ -390,10 +403,10 @@ func recomputeLedgerTx(ctx context.Context, tx *sql.Tx, eventID, categoryID, act
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
l.TeamID, nullID(l.AthleteID), nullID(l.EventID), nullID(l.EntryID), nullID(l.ResultID),
|
||||
l.SourceType, l.SourceID, l.Points, boolToInt(l.CountToTeam), nullID(actorID)); err != nil {
|
||||
return domain.EventScore{}, internal(err)
|
||||
return internal(err)
|
||||
}
|
||||
}
|
||||
return score, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// isAggregateLedger 判断流水是否来自聚合通道:聚合名次的主体是单位/运动员,
|
||||
@@ -445,6 +458,106 @@ func applyComputedRanks(ctx context.Context, tx *sql.Tx, ranked []domain.RankedR
|
||||
// 而且编辑页会把整组当成「已发布」锁死,补录无门。
|
||||
const publishableResultCond = `(result_value IS NOT NULL OR rank IS NOT NULL OR status <> 'ok')`
|
||||
|
||||
// publishableResult 是 publishableResultCond 的 Go 版判据,供分批结算的纯函数使用。
|
||||
// 两处判据必须同源:SQL 那份决定哪些行能被发布,这份决定「这一条算不算已经出成绩」。
|
||||
func publishableResult(r domain.Result) bool {
|
||||
return r.ResultValue != nil || r.Rank > 0 || r.Status != domain.ResultStatusOK
|
||||
}
|
||||
|
||||
// groupStats 统计某组「该出成绩的报名项数」与「已经有可发布成绩的项数」。
|
||||
//
|
||||
// 非 registered 的报名项(scratched / dq / dns)本来就不该等它出成绩:
|
||||
// 计分把非 registered 视为 0 分且不占名次,等它只会让整组永远结算不了。
|
||||
func groupStats(entries []domain.Entry, results []domain.Result) (total, published int) {
|
||||
byEntry := make(map[int64]domain.Result, len(results))
|
||||
for _, r := range results {
|
||||
byEntry[r.EntryID] = r
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Status != domain.EntryStatusRegistered {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if r, ok := byEntry[e.ID]; ok && r.State == domain.ResultStatePublished && publishableResult(r) {
|
||||
published++
|
||||
}
|
||||
}
|
||||
return total, published
|
||||
}
|
||||
|
||||
// groupOf 从整项的行里挑出某个组别的报名与成绩。
|
||||
// 成绩表本身不带组别,只能靠报名项 id 反查,所以先收组内报名 id 再过滤成绩。
|
||||
func groupOf(entries []domain.Entry, results []domain.Result, categoryID int64) ([]domain.Entry, []domain.Result) {
|
||||
inGroup := make(map[int64]bool)
|
||||
groupEntries := make([]domain.Entry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.CategoryID != categoryID {
|
||||
continue
|
||||
}
|
||||
inGroup[e.ID] = true
|
||||
groupEntries = append(groupEntries, e)
|
||||
}
|
||||
groupResults := make([]domain.Result, 0, len(groupEntries))
|
||||
for _, r := range results {
|
||||
if inGroup[r.EntryID] {
|
||||
groupResults = append(groupResults, r)
|
||||
}
|
||||
}
|
||||
return groupEntries, groupResults
|
||||
}
|
||||
|
||||
// scopeStats 按发布 / 结算的作用域累加 groupStats:categoryID > 0 只看该组别,
|
||||
// categoryID = 0 看整项(逐组别累加,任何一组没录完都算没录完)。
|
||||
func scopeStats(ctx context.Context, q queryer, eventID, categoryID int64) (total, published int, err error) {
|
||||
entries, err := loadEntriesQ(ctx, q, eventID, 0)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
results, err := loadResultsQ(ctx, q, eventID, 0)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
categories := []int64{categoryID}
|
||||
if categoryID == 0 {
|
||||
cats, err := loadCategoriesQ(ctx, q, eventID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
categories = make([]int64, 0, len(cats))
|
||||
for _, cat := range cats {
|
||||
categories = append(categories, cat.ID)
|
||||
}
|
||||
}
|
||||
for _, id := range categories {
|
||||
groupEntries, groupResults := groupOf(entries, results, id)
|
||||
t, p := groupStats(groupEntries, groupResults)
|
||||
total += t
|
||||
published += p
|
||||
}
|
||||
return total, published, nil
|
||||
}
|
||||
|
||||
// scopeSettled 判断作用域内是否「所有批次都录完」,即可以结算。
|
||||
// 没有报名项(total = 0)视为录完:没有要等的东西,结算一次把范围清干净。
|
||||
func scopeSettled(ctx context.Context, q queryer, eventID, categoryID int64) (bool, error) {
|
||||
total, published, err := scopeStats(ctx, q, eventID, categoryID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return published == total, nil
|
||||
}
|
||||
|
||||
// liveOnPublishQ 读「发布后是否立即结算」:缺键 / 脏值一律按开启处理,保证历史行为。
|
||||
func liveOnPublishQ(ctx context.Context, q queryer) (bool, error) {
|
||||
var raw string
|
||||
err := q.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, dto.SettingKeyLiveOnPublish).Scan(&raw)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return false, internal(err)
|
||||
}
|
||||
return boolDefault(raw, true), nil
|
||||
}
|
||||
|
||||
func (s *server) publishResults(ctx context.Context, in publishPayload, userID int64) ([]resultDTO, error) {
|
||||
if in.EventID == 0 {
|
||||
return nil, badRequest("eventId 必填", nil)
|
||||
@@ -470,7 +583,8 @@ func (s *server) publishResults(ctx context.Context, in publishPayload, userID i
|
||||
|
||||
// 只发布「已有内容」的成绩行:现场分批比赛时(如 100 米预赛分 4 组、每组 8 条跑道),
|
||||
// 只跑完一组就要先把这一组上屏,没跑/没录的行必须留在 draft,不能被一起发布。
|
||||
// 判据见 publishableResultCond。名次与积分由下方重算按已发布部分计算,后续再发一批会自动纳入。
|
||||
// 判据见 publishableResultCond。名次与积分是否当场落库,取决于「发布后是否立即结算」
|
||||
// (见下方 liveOnPublishQ):实时模式按已发布部分重算,后续再发一批会自动纳入。
|
||||
setPublished := `UPDATE results SET state = 'published', verified_by = ?, verified_at = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE event_id = ? AND ` + publishableResultCond
|
||||
@@ -483,12 +597,33 @@ func (s *server) publishResults(ctx context.Context, in publishPayload, userID i
|
||||
return nil, internal(err)
|
||||
}
|
||||
|
||||
score, err := recomputeLedgerTx(ctx, tx, in.EventID, catID, userID, "发布重算")
|
||||
live, err := liveOnPublishQ(ctx, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := applyComputedRanks(ctx, tx, score.Ranked); err != nil {
|
||||
return nil, err
|
||||
settle := live
|
||||
if !live {
|
||||
// 延迟结算:等该 (小项, 组别) 所有批次都录完才结算一次。
|
||||
// 结算时按整组重算,之前各批的成绩一起纳入,避免名次停留在分批算的口径。
|
||||
settle, err = scopeSettled(ctx, tx, in.EventID, catID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if settle {
|
||||
// 名次也要等结算才写:奖牌榜统计读的就是 results.rank(见 buildRanked),
|
||||
// 分批写名次会让奖牌列跟着每批发布跳动,而总分来自流水、不写流水就不动,
|
||||
// 同一张榜上「有金牌但 0 分」自相矛盾。所以延迟期间只落成绩,名次一并等结算。
|
||||
score, err := computeEventScore(ctx, tx, in.EventID, catID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := applyComputedRanks(ctx, tx, score.Ranked); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writeLedgerTx(ctx, tx, score, in.EventID, catID, userID, "发布重算"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if catID == 0 {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE events SET status = 'published' WHERE id = ?`, in.EventID); err != nil {
|
||||
@@ -506,8 +641,11 @@ func (s *server) publishResults(ctx context.Context, in publishPayload, userID i
|
||||
if err := recordChanges(ctx, tx, "result", after, "upsert", userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.bumpStandings(ctx, tx, userID); err != nil {
|
||||
return nil, err
|
||||
// 没结算就不推榜单:延迟期间榜单保持上一次结算的状态,大屏不该因为一批成绩而跳动。
|
||||
if settle {
|
||||
if err := s.bumpStandings(ctx, tx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, internal(err)
|
||||
@@ -563,6 +701,9 @@ func (s *server) unpublishResults(ctx context.Context, in publishPayload, userID
|
||||
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
// 撤销发布是回滚动作,延迟结算也要立刻重算:这一组已经没有已发布成绩,
|
||||
// 留着上一次结算写下的流水就是「榜上有分、成绩却撤了」。重算本身会把旧流水作废、
|
||||
// 按当前已发布内容重写,之后再次发布时判据回到「未结算」,行为自然衔接。
|
||||
if _, err := recomputeLedgerTx(ctx, tx, in.EventID, catID, userID, "撤销发布"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -643,12 +784,28 @@ func (s *server) voidResult(ctx context.Context, resultID int64, reason string,
|
||||
return resultDTO{}, internal(err)
|
||||
}
|
||||
// 作废会让同组别名次前移,必须把重算出的名次回写,否则奖牌榜与积分榜对不上。
|
||||
score, err := recomputeLedgerTx(ctx, tx, eventID, catID, userID, "作废成绩:"+reason)
|
||||
if err != nil {
|
||||
// withdrawn 仍是「可发布成绩」,作废不会让该组变成没录完,所以录完过的组照样能结算;
|
||||
// 没录完的组按延迟结算处理:名次与流水都等结算,只把作废这件事落库。
|
||||
settle := true
|
||||
if live, err := liveOnPublishQ(ctx, tx); err != nil {
|
||||
return resultDTO{}, err
|
||||
} else if !live {
|
||||
settle, err = scopeSettled(ctx, tx, eventID, catID)
|
||||
if err != nil {
|
||||
return resultDTO{}, err
|
||||
}
|
||||
}
|
||||
if err := applyComputedRanks(ctx, tx, score.Ranked); err != nil {
|
||||
return resultDTO{}, err
|
||||
if settle {
|
||||
score, err := computeEventScore(ctx, tx, eventID, catID)
|
||||
if err != nil {
|
||||
return resultDTO{}, err
|
||||
}
|
||||
if err := applyComputedRanks(ctx, tx, score.Ranked); err != nil {
|
||||
return resultDTO{}, err
|
||||
}
|
||||
if err := writeLedgerTx(ctx, tx, score, eventID, catID, userID, "作废成绩:"+reason); err != nil {
|
||||
return resultDTO{}, err
|
||||
}
|
||||
}
|
||||
|
||||
after, err := scanResult(tx.QueryRowContext(ctx, `SELECT `+resultColumns+` FROM results WHERE id = ?`, resultID))
|
||||
@@ -658,8 +815,10 @@ func (s *server) voidResult(ctx context.Context, resultID int64, reason string,
|
||||
if err := recordChanges(ctx, tx, "result", []resultDTO{after}, "void", userID); err != nil {
|
||||
return resultDTO{}, err
|
||||
}
|
||||
if err := s.bumpStandings(ctx, tx, userID); err != nil {
|
||||
return resultDTO{}, err
|
||||
if settle {
|
||||
if err := s.bumpStandings(ctx, tx, userID); err != nil {
|
||||
return resultDTO{}, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return resultDTO{}, internal(err)
|
||||
@@ -669,6 +828,119 @@ func (s *server) voidResult(ctx context.Context, resultID int64, reason string,
|
||||
return s.getResult(ctx, resultID)
|
||||
}
|
||||
|
||||
// ---------- 分批结算 ----------
|
||||
|
||||
// settlementGroupDTO 是「未结算的 (小项, 组别)」:延迟结算下已发布成绩还没进榜单的那一组。
|
||||
// Required / Published 只数「该出成绩的报名项」,弃权 / 失格不计入,与结算判据同源。
|
||||
type settlementGroupDTO struct {
|
||||
EventID int64 `json:"eventId"`
|
||||
CategoryID int64 `json:"categoryId"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
Required int `json:"required"`
|
||||
Published int `json:"published"`
|
||||
}
|
||||
|
||||
// settlementStatusDTO 是 GET /api/results/settlement 的线上结构。
|
||||
type settlementStatusDTO struct {
|
||||
LiveOnPublish bool `json:"liveOnPublish"`
|
||||
Groups []settlementGroupDTO `json:"groups"`
|
||||
}
|
||||
|
||||
// settlementStatus 列出范围内尚未结算的组别,供录入页提示「榜单还没反映这批」。
|
||||
// 实时结算(默认)时清单恒为空:那种模式下每次发布都当场结算了。
|
||||
func (s *server) settlementStatus(ctx context.Context, eventID, catID int64) (settlementStatusDTO, error) {
|
||||
out := settlementStatusDTO{Groups: []settlementGroupDTO{}}
|
||||
live, err := liveOnPublishQ(ctx, s.ev)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.LiveOnPublish = live
|
||||
if live {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
categories, err := loadCategoriesQ(ctx, s.ev, eventID)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
entries, err := loadEntriesQ(ctx, s.ev, eventID, 0)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
results, err := loadResultsQ(ctx, s.ev, eventID, 0)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
for _, cat := range categories {
|
||||
if catID > 0 && cat.ID != catID {
|
||||
continue
|
||||
}
|
||||
groupEntries, groupResults := groupOf(entries, results, cat.ID)
|
||||
required, published := groupStats(groupEntries, groupResults)
|
||||
// 一条都没发布 = 还没开始结算;已录完 = 下次发布就会结算(或被立即结算过)。
|
||||
if required == 0 || published == 0 || published == required {
|
||||
continue
|
||||
}
|
||||
out.Groups = append(out.Groups, settlementGroupDTO{
|
||||
EventID: eventID,
|
||||
CategoryID: cat.ID,
|
||||
CategoryName: cat.Name,
|
||||
Required: required,
|
||||
Published: published,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// settleResults 立即结算:无视延迟结算设置,按当前已发布成绩重算积分流水并刷新榜单。
|
||||
//
|
||||
// 这是现场安全绳:只要有一条成绩永远录不出来(退赛却没标 scratched),
|
||||
// 延迟结算就永远不会自动触发,必须有一个显式入口把已发布部分结算掉。
|
||||
func (s *server) settleResults(ctx context.Context, in publishPayload, userID int64) ([]resultDTO, error) {
|
||||
if in.EventID == 0 {
|
||||
return nil, badRequest("eventId 必填", nil)
|
||||
}
|
||||
catID := int64(0)
|
||||
if in.CategoryID != nil {
|
||||
catID = *in.CategoryID
|
||||
}
|
||||
|
||||
tx, err := s.ev.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
before, err := queryResults(ctx, tx, `SELECT `+resultColumns+` FROM results WHERE event_id = ?`, in.EventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
score, err := recomputeLedgerTx(ctx, tx, in.EventID, catID, userID, "立即结算")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := applyComputedRanks(ctx, tx, score.Ranked); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
after, err := queryResults(ctx, tx, `SELECT `+resultColumns+` FROM results WHERE event_id = ?`, in.EventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := recordChanges(ctx, tx, "result", after, "upsert", userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.bumpStandings(ctx, tx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
|
||||
s.audit(ctx, "result.settle", "event", in.EventID, before, after)
|
||||
return s.resultsByEvent(ctx, in.EventID)
|
||||
}
|
||||
|
||||
// ---------- 变更日志与版本 ----------
|
||||
|
||||
func recordRevision(ctx context.Context, tx *sql.Tx, eventID int64, action string, results []resultDTO, actorID int64, message string) error {
|
||||
|
||||
Reference in new issue
Block a user