Skip to content
页面信息
📝 描述比赛模型,用于管理多种评分规则的比赛、排行榜生成、气球通知和答疑
📥 导入import { ContestModel } from 'hydrooj'

ContestModel

比赛模型,用于管理多种评分规则(ACM/ICPC、OI、IOI、IOI Strict、Ledo、Assignment)的比赛、排行榜生成、气球通知、答疑和打印任务。

ContestModel 是导出函数的普通模块(非类)。所有函数直接调用。它将 CRUD 和状态操作委托给共享的 document 模块,使用 TYPE_CONTEST = 30


类型导出

PrintTaskStatus

枚举值:pendingprintingprintedfailed。用于比赛打印任务状态追踪。


常量

RULES: ContestRules

将规则名映射到其规则定义的对象。键:acmoihomeworkioiledostrictioi。每个规则定义评分逻辑、排行榜渲染、可见性控制和记录投影行为。

buildContestRule<T>(def): ContestRule<T>

工厂函数,从部分定义构建新的比赛规则,继承并绑定基础规则中所有未指定的函数。内部用于创建内置规则。


方法

状态谓词

根据比赛的 beginAt/endAt 时间戳评估当前阶段的工具函数。

isNew(tdoc: Tdoc, days?: number): boolean

比赛在距今 days 天之后开始时返回 true

参数类型默认值说明
tdocTdoc比赛文档
daysnumber1天数阈值
返回值boolean

isUpcoming(tdoc: Tdoc, days?: number): boolean

比赛在 days 天内开始但尚未开始时返回 true

参数类型默认值说明
tdocTdoc比赛文档
daysnumber7天数阈值
返回值boolean

isNotStarted(tdoc: Tdoc): boolean

当前时间早于 tdoc.beginAt 时返回 true

参数类型默认值说明
tdocTdoc比赛文档
返回值boolean

isOngoing(tdoc: Tdoc, tsdoc?: any): boolean

当前时间在 beginAtendAt 之间时返回 true。对于限时比赛,还会检查用户的 startAt 未超过允许的时长。

参数类型默认值说明
tdocTdoc比赛文档
tsdocany用户比赛状态文档
返回值boolean

isDone(tdoc: Tdoc, tsdoc?: any): boolean

比赛已结束时返回 true。对于限时比赛,还会考虑用户的 startAt 加上时长。

参数类型默认值说明
tdocTdoc比赛文档
tsdocany用户比赛状态文档
返回值boolean

isLocked(tdoc: Tdoc, time?: Date): boolean

排行榜已锁定(lockAt 已设置且已过)且尚未解锁时返回 true

参数类型默认值说明
tdocTdoc比赛文档
timeDatenew Date()用于比较的时间点
返回值boolean

isExtended(tdoc: Tdoc): boolean

当前时间在罚时/延期时段(在 penaltySinceendAt 之间)时返回 true

参数类型默认值说明
tdocTdoc比赛文档
返回值boolean

statusText(tdoc: Tdoc, tsdoc?: any): string

返回可读的状态字符串:'New''Ready (☆▽☆)''Live...''Done'

参数类型默认值说明
tdocTdoc比赛文档
tsdocany用户比赛状态文档
返回值string

CRUD

add(domainId: string, title: string, content: string, owner: number, rule: string, beginAt?: Date, endAt?: Date, pids?: number[], rated?: boolean, data?: any): Promise<ObjectId>

创建新比赛。验证规则存在且 beginAt < endAt。触发 contest/before-addcontest/add 总线事件。

参数类型默认值说明
domainIdstring域 ID
titlestring比赛标题
contentstring比赛描述/正文
ownernumber创建者 UID
rulestring比赛规则名(acmoiioi 等)
beginAtDatenew Date()开始时间
endAtDatenew Date()结束时间
pidsnumber[][]题目 ID 列表
ratedbooleanfalse是否为 rated 比赛
dataPartial<Tdoc>{}附加数据(如比赛特定配置)
返回值Promise<ObjectId>新比赛 ID
typescript
// 创建 ACM 规则的比赛
const tid = await contest.add(
  'system',
  '2024 校内选拔赛',
  '## 比赛说明\n...',
  session.uid,
  'acm',
  new Date('2024-06-01T09:00:00'),
  new Date('2024-06-01T14:00:00'),
  [1001, 1002, 1003, 1004, 1005],
  true,
);

edit(domainId: string, tid: ObjectId, $set: Partial<Tdoc>): Promise<Tdoc>

更新比赛字段。如果规则有变更则验证。触发 contest/before-editcontest/edit 总线事件。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
$setPartial<Tdoc>要更新的字段
返回值Promise<Tdoc>更新后的比赛文档
typescript
// 延长比赛结束时间
const updated = await contest.edit(
  'system',
  tid,
  { endAt: new Date('2024-06-01T15:00:00') },
);

// 修改比赛规则并更新题目列表
await contest.edit('system', tid, {
  rule: 'oi',
  pids: [1001, 1002, 1003],
});

del(domainId: string, tid: ObjectId): Promise<void>

删除比赛及所有关联的用户状态。触发 contest/del 总线事件。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
返回值Promise<void>

get(domainId: string, tid: ObjectId): Promise<Tdoc>

按 ID 获取单个比赛。未找到时抛出 ContestNotFoundError

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
返回值Promise<Tdoc>比赛文档

getMulti(domainId: string, query?: any): FindCursor<Tdoc>

返回匹配查询的比赛游标,按 beginAt 降序排列。

参数类型默认值说明
domainIdstring域 ID
queryanyMongoDB 查询过滤器
返回值FindCursor<Tdoc>比赛游标

getRelated(domainId: string, pid: number, rule?: string): Promise<Tdoc[]>

查找包含指定题目(pids 中包含 pid)的比赛。除非指定了 rule,否则过滤隐藏规则。

参数类型默认值说明
domainIdstring域 ID
pidnumber题目 ID
rulestring按规则过滤(如 'acm'
返回值Promise<Tdoc[]>关联的比赛列表

count(domainId: string, query: any): Promise<number>

返回匹配查询的比赛数量。

参数类型默认值说明
domainIdstring域 ID
queryanyMongoDB 查询过滤器
返回值Promise<number>匹配数量

状态管理

getStatus(domainId: string, tid: ObjectId, uid: number): Promise<Tsdoc | null>

获取单个用户的比赛状态。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
uidnumber用户 ID
返回值Promise<Tsdoc | null>用户状态或 null

getMultiStatus(domainId: string, query: any): FindCursor

返回匹配查询的比赛状态游标。

参数类型默认值说明
domainIdstring域 ID
queryanyMongoDB 查询过滤器
返回值FindCursor状态游标

getListStatus(domainId: string, uid: number, tids: ObjectId[]): Promise<Record<string, Tsdoc>>

批量获取指定用户多场比赛的状态,以 tid.toHexString() 为键的映射返回。

参数类型默认值说明
domainIdstring域 ID
uidnumber用户 ID
tidsObjectId[]比赛 ID 数组
返回值Promise<Record<string, Tsdoc>>以十六进制 ID 为键的状态映射

setStatus(domainId: string, tid: ObjectId, uid: number, $set: any): Promise<void>

覆盖用户在指定比赛上的状态字段。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
uidnumber用户 ID
$setany要设置的状态字段
返回值Promise<void>

updateStatus(domainId: string, tid: ObjectId, uid: number, rid: ObjectId, pid: number, { status?, score?, subtasks?, lang? }?: { status?: STATUS, score?: number, subtasks?: Record<number, SubtaskResult>, lang?: string }): Promise<Tsdoc>

推入新的日志条目(提交结果),并使用比赛规则的 stat 函数重新计算用户统计。使用基于修订号的状态更新以确保并发安全。同时对通过的提交触发气球创建。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
uidnumber用户 ID
ridObjectId评测记录 ID
pidnumber题目 ID
opts{ status?: STATUS, score?: number, subtasks?: Record<number, SubtaskResult>, lang?: string }{}额外选项(评测状态、分数、子任务结果、语言)
返回值Promise<Tsdoc>更新后的用户状态
typescript
// 提交通过后更新比赛状态
const tsdoc = await contest.updateStatus(
  'system',
  tid,
  session.uid,
  rid,
  1001,
  { status: STATUS.STATUS_ACCEPTED, score: 100 },
);

countStatus(domainId: string, query: any): Promise<number>

返回匹配查询的比赛状态数量。

参数类型默认值说明
domainIdstring域 ID
queryanyMongoDB 查询过滤器
返回值Promise<number>匹配数量

attend(domainId: string, tid: ObjectId, uid: number, payload?: any): Promise<{}>

为用户报名比赛。已报名时抛出 ContestAlreadyAttendedError。使用 cappedIncStatus 原子性防止重复报名。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
uidnumber用户 ID
payloadany附加报名信息
返回值Promise<{}>
typescript
// 用户报名比赛
await contest.attend('system', tid, session.uid);

// 带附加信息报名(如队伍名)
await contest.attend('system', tid, session.uid, {
  teamName: '测试小队',
});

getAndListStatus(domainId: string, tid: ObjectId): Promise<[Tdoc, Tsdoc[]]>

获取比赛文档及按规则的 statusSort 排序的所有用户状态。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
返回值Promise<[Tdoc, Tsdoc[]]>比赛文档和排序后的状态列表

recalcStatus(domainId: string, tid: ObjectId): Promise<Tsdoc[]>

使用比赛规则的 stat 函数从日志重新计算所有用户状态。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
返回值Promise<Tsdoc[]>重新计算后的状态列表

unlockScoreboard(domainId: string, tid: ObjectId): Promise<void>

解锁已锁定的排行榜,设置 unlocked: true 并重新计算所有状态。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
返回值Promise<void>

排行榜

检查用户是否可以查看某些比赛信息的函数。均使用 this 上下文,包含 { user: User }

canViewHiddenScoreboard(this: { user }, tdoc: Tdoc): boolean

用户拥有比赛或具有 PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD(作业为 PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD)时返回 true

参数类型默认值说明
this{ user: User }上下文,包含当前用户
tdocTdoc比赛文档
返回值boolean

canShowRecord(this: { user }, tdoc: Tdoc, allowPermOverride?: boolean): boolean

比赛规则允许在当前时间显示所有记录,或用户具有排行榜覆盖权限时返回 true

参数类型默认值说明
this{ user: User }上下文,包含当前用户
tdocTdoc比赛文档
allowPermOverridebooleantrue是否允许权限覆盖
返回值boolean

canShowSelfRecord(this: { user }, tdoc: Tdoc, allowPermOverride?: boolean): boolean

比赛规则允许显示用户自己的记录,或用户具有排行榜覆盖权限时返回 true

参数类型默认值说明
this{ user: User }上下文,包含当前用户
tdocTdoc比赛文档
allowPermOverridebooleantrue是否允许权限覆盖
返回值boolean

canShowScoreboard(this: { user }, tdoc: Tdoc, allowPermOverride?: boolean): boolean

比赛规则允许显示排行榜,或用户具有排行榜覆盖权限时返回 true

参数类型默认值说明
this{ user: User }上下文,包含当前用户
tdocTdoc比赛文档
allowPermOverridebooleantrue是否允许权限覆盖
返回值boolean

getScoreboard(this: Handler, domainId: string, tid: ObjectId, config: any): Promise<[Tdoc, ScoreboardRow[], BaseUserDict, ProblemDict]>

使用规则的 scoreboard 函数构建完整排行榜。排行榜不可见时抛出 ContestScoreboardHiddenError。触发 contest/scoreboard 总线事件。

参数类型默认值说明
thisHandler请求处理上下文
domainIdstring域 ID
tidObjectId比赛 ID
configany排行榜配置选项
返回值Promise<[Tdoc, ScoreboardRow[], BaseUserDict, ProblemDict]>比赛文档、排行榜行、用户字典、题目字典
typescript
// 获取完整排行榜
const [tdoc, rows, udict, pdict] = await contest.getScoreboard.call(
  handler,
  'system',
  tid,
  { showDisplayName: true },
);

// rows 为排行数据,udict 为用户信息字典,pdict 为题目信息字典
for (const row of rows) {
  console.log(row.rank, udict[row.uid]?.uname, row.score);
}

气球

ACM 风格首 A 通知的气球管理。

addBalloon(domainId: string, tid: ObjectId, uid: number, rid: ObjectId, pid: number): Promise<ObjectId | null>

为通过的提交添加气球。判断是否为该题目的首次通过。触发 contest/balloon 事件。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
uidnumber用户 ID
ridObjectId评测记录 ID
pidnumber题目 ID
返回值Promise<ObjectId | null>气球 ID,非首次通过返回 null

getBalloon(domainId: string, tid: ObjectId, _id: ObjectId): Promise<BalloonDoc>

按 ID 获取单个气球。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
_idObjectId气球 ID
返回值Promise<BalloonDoc>气球文档

getMultiBalloon(domainId: string, tid: ObjectId, query?: any): FindCursor

返回比赛的气球游标。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
queryanyMongoDB 查询过滤器
返回值FindCursor气球游标

updateBalloon(domainId: string, tid: ObjectId, _id: ObjectId, $set: any): Promise<BalloonDoc>

更新气球字段。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
_idObjectId气球 ID
$setany要更新的字段
返回值Promise<BalloonDoc>更新后的气球文档

答疑

比赛答疑(提问/回答)管理,以 TYPE_CONTEST_CLARIFICATION 类型的子文档存储。

addClarification(domainId: string, tid: ObjectId, owner: number, content: string, ip: string, subject?: number): Promise<ObjectId>

在比赛上创建新的答疑问题。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
ownernumber提问者 UID
contentstring问题内容
ipstring提问者 IP 地址
subjectnumber0答疑主题
返回值Promise<ObjectId>答疑 ID

addClarificationReply(domainId: string, did: ObjectId, owner: number, content: string, ip: string): Promise<[any, ObjectId]>

为已有答疑追加回复。

参数类型默认值说明
domainIdstring域 ID
didObjectId答疑 ID
ownernumber回复者 UID
contentstring回复内容
ipstring回复者 IP 地址
返回值Promise<[any, ObjectId]>更新后的文档与回复 ID

getClarification(domainId: string, did: ObjectId): Promise<ClarificationDoc>

按 ID 获取单个答疑。

参数类型默认值说明
domainIdstring域 ID
didObjectId答疑 ID
返回值Promise<ClarificationDoc>答疑文档

getMultiClarification(domainId: string, tid: ObjectId, owner?: number): Promise<ClarificationDoc[]>

列出比赛的答疑。如果指定 owner,则仅包含该用户可见的答疑(owner $in: [owner, 0])。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
ownernumber过滤可见答疑的用户 ID
返回值Promise<ClarificationDoc[]>答疑列表

打印任务

现场比赛的打印任务管理。使用 TYPE_CONTEST_PRINT

addPrintTask(domainId: string, tid: ObjectId, uid: number, name: string, content: string): Promise<ObjectId>

创建 pending 状态的新打印任务。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
uidnumber提交者 UID
namestring打印任务名称
contentstring打印内容
返回值Promise<ObjectId>打印任务 ID

updatePrintTask(domainId: string, tid: ObjectId, taskId: ObjectId, $set: any): Promise<boolean>

更新打印任务字段。修改成功返回 true

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
taskIdObjectId打印任务 ID
$setany要更新的字段
返回值Promise<boolean>是否修改成功

allocatePrintTask(domainId: string, tid: ObjectId): Promise<PrintDoc | null>

原子性地领取下一个待处理打印任务,将其状态设为 printing

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
返回值Promise<PrintDoc | null>打印任务文档或 null(无待处理任务)

getMultiPrintTask(domainId: string, tid: ObjectId, query?: any): FindCursor

返回比赛的打印任务游标,按 _id 升序排列。

参数类型默认值说明
domainIdstring域 ID
tidObjectId比赛 ID
queryanyMongoDB 查询过滤器
返回值FindCursor打印任务游标

其他

applyProjection(tdoc: Tdoc, rdoc: RecordDoc, udoc: User): RecordDoc

应用比赛规则的 applyProjection,在比赛进行期间脱敏记录中的敏感字段(分数、时间、内存、测试用例等)。

参数类型默认值说明
tdocTdoc比赛文档
rdocRecordDoc评测记录文档
udocUser用户文档
返回值RecordDoc脱敏后的记录文档

apply(ctx: Context): Promise<void>

生命周期钩子。注册 contest/balloon 事件监听器(发送首 A 消息)并确保气球集合上的数据库索引。

参数类型默认值说明
ctxContext插件上下文
返回值Promise<void>

备注

  • 比赛是文档类型模型(TYPE_CONTEST = 30)。CRUD 和状态操作委托给共享的 document 模块。
  • 六种内置规则:acm(XCPC)、oiioistrictioiledohomework(隐藏)。每个规则定义 statscoreboardscoreboardRowscoreboardHeadershowScoreboardshowRecordshowSelfRecordapplyProjectioncheck
  • updateStatus 使用基于修订号的状态(revPushStatus + revSetStatus)实现日志更新的乐观并发控制。
  • attend 使用 cappedIncStatus(上限为 1)原子性防止重复报名;重新抛出为 ContestAlreadyAttendedError
  • addedit 触发前/后总线事件(contest/before-addcontest/addcontest/before-editcontest/edit)。
  • 答疑使用 TYPE_CONTEST_CLARIFICATION 作为独立 docType,通过父引用关联比赛。
  • 打印任务使用 TYPE_CONTEST_PRINT 作为独立 docType,通过父引用关联比赛。