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

ContestModel ​

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

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


类型导出 ​

PrintTaskStatus ​

枚举值:pending、printing、printed、failed。用于比赛打印任务状态追踪。

ScoreboardConfig ​

排行榜配置对象:{ isExport: boolean; showDisplayName: boolean; lockAt?: Date }。用于 getScoreboard。


常量 ​

RULES: ContestRules ​

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

buildContestRule<T>(def: Optional<ContestRule<T>, 'applyProjection'>): ContestRule<T> ​

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

重载:buildContestRule<T>(def: Partial<ContestRule<T>>, baseRule: ContestRule<T>): ContestRule<T> —— 基于给定的 baseRule 构建,def 中未指定的函数从 baseRule 继承并绑定。


方法 ​

状态谓词 ​

根据比赛的 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?: ContestStatusDoc): boolean ​

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

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

isDone(tdoc: Tdoc, tsdoc?: ContestStatusDoc): boolean ​

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

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

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

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

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

isExtended(tdoc: Tdoc): boolean ​

当前时间在罚时/延期时段(在 penaltySince 和 endAt 之间)时返回 true。

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

statusText(tdoc: Tdoc, tsdoc?: ContestStatusDoc): string ​

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

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

CRUD ​

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

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

参数类型默认值说明
domainIdstring—域 ID
titlestring—比赛标题
contentstring—比赛描述/正文
ownernumber—创建者 UID
rulestring—比赛规则名(acm、oi、ioi 等)
beginAtDatenew Date()开始时间
endAtDatenew Date()结束时间
pidsnumber[][]题目 ID 列表
ratedbooleanfalse是否为 rated 比赛
dataPartial<Tdoc>{}附加数据(如比赛特定配置)
返回值Promise<ObjectId>新比赛 ID
typescript
// 创建 ACM 规则的比赛
const tid = await ContestModel.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-edit 和 contest/edit 总线事件。

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

// 修改比赛规则并更新题目列表
await ContestModel.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: Filter<DocType['30']> = {}): FindCursor<Tdoc> ​

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

参数类型默认值说明
domainIdstring—域 ID
queryFilter<DocType['30']>{}MongoDB 查询过滤器
返回值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
queryany—MongoDB 查询过滤器
返回值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
queryany—MongoDB 查询过滤器
返回值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, $unset?: any): Promise<ContestStatusDoc> ​

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

参数类型默认值说明
domainIdstring—域 ID
tidObjectId—比赛 ID
uidnumber—用户 ID
$setany—要设置的状态字段
$unsetany—要移除的状态字段
返回值Promise<ContestStatusDoc>更新后的比赛状态

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 ContestModel.updateStatus(
  'system',
  tid,
  session.uid,
  rid,
  1001,
  { status: STATUS.STATUS_ACCEPTED, score: 100 },
);

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

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

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

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

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

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

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

cancelAttend(domainId: string, tid: ObjectId, uid: number): Promise<{}> ​

取消用户对比赛的报名。删除该用户的比赛状态记录,并将比赛的 attend 计数减一。用户未报名时静默成功(deleteMultiStatus 匹配零条)。

参数类型默认值说明
domainIdstring—域 ID
tidObjectId—比赛 ID
uidnumber—用户 ID
返回值Promise<{}>

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>

排行榜 ​

检查用户是否可以查看某些比赛信息的函数。除 getScoreboard 使用 this: Handler 外,均使用 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: ScoreboardConfig): Promise<[Tdoc, ScoreboardRow[], BaseUserDict, ProblemDict]> ​

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

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

// 每行是 ScoreboardNode 数组,udict 为用户信息字典,pdict 为题目信息字典
for (const row of rows.slice(1)) {
  const rank = row.find((node) => node.type === 'rank')?.value;
  const uid = row.find((node) => node.type === 'user')?.raw;
  console.log(rank, udict[uid]?.uname);
}

气球 ​

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 | null> ​

按 ID 获取单个气球。未找到时返回 null。

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

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

返回比赛的气球游标。

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

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

更新气球字段。findOneAndUpdate 未指定 returnDocument: 'after',驱动默认返回更新前的气球文档。

参数类型默认值说明
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 | null> ​

按 ID 获取单个答疑。未找到时返回 null。

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

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<ContestPrintDoc | null> ​

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

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

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

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

参数类型默认值说明
domainIdstring—域 ID
tidObjectId—比赛 ID
queryany—MongoDB 查询过滤器
返回值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)、oi、ioi、strictioi、ledo、homework(隐藏)。每个规则定义 stat、scoreboard、scoreboardRow、scoreboardHeader、showScoreboard、showRecord、showSelfRecord、applyProjection 和 check。
  • updateStatus 使用基于修订号的状态(revPushStatus + revSetStatus)实现日志更新的乐观并发控制。
  • attend 使用 cappedIncStatus(上限为 1)原子性防止重复报名;重新抛出为 ContestAlreadyAttendedError。
  • add 和 edit 触发前/后总线事件(contest/before-add、contest/add、contest/before-edit、contest/edit)。
  • 答疑使用 TYPE_CONTEST_CLARIFICATION 作为独立 docType,通过父引用关联比赛。
  • 打印任务使用 TYPE_CONTEST_PRINT 作为独立 docType,通过父引用关联比赛。