Skip to content
页面信息
📝 描述题目管理模型,提供增删改查操作、测试数据管理和导入导出
📥 导入import { ProblemModel } from 'hydrooj'

ProblemModel ​

题目管理模型,提供增删改查操作、测试数据/附件管理、导入导出以及每用户状态跟踪。

ProblemModel 是一个纯静态类。所有方法直接在类上调用(如 ProblemModel.get(...))。它封装了 document 子系统,文档类型为 TYPE_PROBLEM。


类型导出 ​

ProblemDoc ​

主要题目文档类型。继承 Document(提供 _id、docId、docType、domainId、owner、maintainer?)。通过 interface.ts 中的模块扩充声明的额外字段:

字段类型说明
pidstring题目标识符(如 "A"、"abc-123")
titlestring题目标题
contentstring题面(纯文本、HTML 或多语言 JSON)
nSubmitnumber总提交数
nAcceptnumber总通过数
tagstring[]标签/分类
dataFileInfo[]测试数据文件元数据
additional_fileFileInfo[]附加文件元数据
hiddenboolean?是否隐藏题目
htmlboolean?内容是否为原始 HTML
statsany?统计对象
difficultynumber?难度等级
sortstring?用于排序的排序键
configstring | ProblemConfig评测配置(YAML 字符串或解析后的配置对象)
reference{ domainId: string, pid: number }?源题目引用(用于复制的题目)

ProblemDict ​

typescript
type ProblemDict = NumericDictionary<ProblemDoc>

以 docId(数字)和 pid(字符串)为键的字典。

ProblemStatusDoc ​

每用户题目状态文档。继承 StatusDocBase。

字段类型说明
docIdnumber题目 docId
docType10始终为 TYPE_PROBLEM
uidnumber用户 ID
ridObjectId?最佳/最新提交的记录 ID
scorenumber?最佳分数
statusnumber?最佳状态码
starboolean?用户是否收藏了该题目

Field ​

typescript
type Field = keyof ProblemDoc;

所有 ProblemDoc 字段名的联合类型。用于投影数组。


常量 ​

投影常量 ​

为常见查询模式预构建的字段投影数组。

常量字段用途
PROJECTION_LIST_id, domainId, docType, docId, pid, owner, title, nSubmit, nAccept, difficulty, tag, hidden, stats题目列表页
PROJECTION_CONTEST_LISTPROJECTION_BASE + config比赛题目列表
PROJECTION_CONTEST_DETAILPROJECTION_CONTEST_LIST + content, html, data, additional_file, reference, maintainer比赛题目详情
PROJECTION_PUBLICPROJECTION_LIST + content, html, data, config, additional_file, reference, maintainer完整公开题目视图

方法 ​

增删改查 ​

add(domainId: string, pid: string = '', title: string, content: string, owner: number, tag?: string[], meta?: ProblemCreateOptions): Promise<number> ​

创建新题目,docId 自动递增。触发 problem/before-add 和 problem/add 事件。

参数类型默认值说明
domainIdstring—域 ID
pidstring''题目标识符
titlestring—题目标题
contentstring—题面
ownernumber—拥有者用户 ID
tagstring[][]标签
metaProblemCreateOptions{}附加选项
返回值Promise<number>新 docId
typescript
// 创建新题目
const docId = await ProblemModel.add(
  'system',             // domainId
  'A',                  // pid
  '两数之和',            // title
  '给定两个整数 a 和 b', // content
  uid,                  // owner
  ['数学', '入门'],      // tag
);

// 创建隐藏题目
const hiddenDocId = await ProblemModel.add(
  'system', 'B', '隐藏题', '题面内容', uid,
  [],
  { hidden: true },
);

addWithId(domainId: string, docId: number, pid: string = '', title: string, content: string, owner: number, tag?: string[], meta?: ProblemCreateOptions): Promise<number> ​

使用指定 docId 创建题目。由 add 内部使用。

参数类型默认值说明
domainIdstring—域 ID
docIdnumber—指定的 docId
pidstring''题目标识符
titlestring—题目标题
contentstring—题面
ownernumber—拥有者用户 ID
tagstring[][]标签
metaProblemCreateOptions{}附加选项
返回值Promise<number>新 docId

get(domainId: string, pid: string | number, projection?: Projection<ProblemDoc>, rawConfig?: boolean): Promise<ProblemDoc | null> ​

通过数字 docId 或字符串 pid 获取单个题目。除非 rawConfig 为 true,否则自动解析评测配置。

参数类型默认值说明
domainIdstring—域 ID
pidstring | number—题目 ID 或 pid
projectionProjection<ProblemDoc>PROJECTION_PUBLIC要返回的字段
rawConfigbooleanfalse跳过配置解析
返回值Promise<ProblemDoc | null>

getMulti(domainId: string, query: Filter<ProblemDoc>, projection?: Field[]): FindCursor<ProblemDoc> ​

获取查询多个题目的 MongoDB 游标,按 sort 字段排序。

参数类型默认值说明
domainIdstring—域 ID
queryFilter<ProblemDoc>—MongoDB 过滤器
projectionField[]PROJECTION_LIST要返回的字段
返回值FindCursor<ProblemDoc>

list(domainId: string, query: Filter<ProblemDoc>, page: number, pageSize: number, projection?: Field[]): Promise<[ProblemDoc[], number, number]> (已弃用) ​

分页题目列表。返回 [docs, numPages, count](题目数组、总页数、总数)。

参数类型默认值说明
domainIdstring—域 ID
queryFilter<ProblemDoc>—MongoDB 过滤器
pagenumber—页码
pageSizenumber—每页数量
projectionField[]PROJECTION_LIST要返回的字段
返回值Promise<[ProblemDoc[], number, number]>[docs, numPages, count](题目数组、总页数、总数)

edit(domainId: string, _id: number, $set: Partial<ProblemDoc>): Promise<ProblemDoc> ​

更新题目字段。pid 变更时重新计算 sort 键。触发 problem/before-edit 和 problem/edit 事件。

参数类型默认值说明
domainIdstring—域 ID
_idnumber—题目 docId
$setPartial<ProblemDoc>—要更新的字段
返回值Promise<ProblemDoc>更新后的文档

del(domainId: string, docId: number): Promise<boolean> ​

删除题目、其状态和关联的存储文件。触发 problem/before-del 和 problem/delete 事件。

参数类型默认值说明
domainIdstring—域 ID
docIdnumber—题目 docId
返回值Promise<boolean>是否有内容被删除

count(domainId: string, query: Filter<ProblemDoc>): Promise<number> ​

统计匹配过滤条件的题目数量。

参数类型默认值说明
domainIdstring—域 ID
queryFilter<ProblemDoc>—MongoDB 过滤器
返回值Promise<number>

copy(domainId: string, _id: number, target: string, pid?: string, hidden?: boolean): Promise<number> ​

将题目复制到另一个域,创建指向原始题目的引用链接。返回新题目的 docId。

参数类型默认值说明
domainIdstring—源域 ID
_idnumber—题目 docId
targetstring—目标域 ID
pidstring—目标域中的题目 PID
hiddenboolean—是否在目标域中隐藏
返回值Promise<number>新题目的 docId

random(domainId: string, query: Filter<ProblemDoc>): Promise<string | number | null> ​

获取匹配过滤条件的随机题目 pid 或 docId。未找到则返回 null。

参数类型默认值说明
domainIdstring—域 ID
queryFilter<ProblemDoc>—MongoDB 过滤器
返回值Promise<string | number | null>

批量查找 ​

getList(domainId: string, pids: number[], canViewHidden?: number | boolean, doThrow?: boolean, projection?: Field[], indexByDocIdOnly?: boolean): Promise<ProblemDict> ​

获取多个题目作为 ProblemDict。解析引用、解析配置,可选对缺失题目抛出异常。

参数类型默认值说明
domainIdstring—域 ID
pidsnumber[]—docId 数组
canViewHiddennumber | booleanfalseUID(检查拥有者/维护者关系)或 true 跳过检查
doThrowbooleantrue题目缺失时抛出异常
projectionField[]PROJECTION_PUBLIC要返回的字段
indexByDocIdOnlybooleanfalse仅按 docId 索引,跳过 pid 键
返回值Promise<ProblemDict>

状态跟踪 ​

getStatus(domainId: string, docId: number, uid: number): Promise<ProblemStatusDoc | null> ​

获取指定用户在特定题目上的状态记录。

参数类型默认值说明
domainIdstring—域 ID
docIdnumber—题目 docId
uidnumber—用户 ID
返回值Promise<ProblemStatusDoc | null>

getMultiStatus(domainId: string, query: Filter<ProblemStatusDoc>): FindCursor<ProblemStatusDoc> ​

获取查询题目状态文档的游标。

参数类型默认值说明
domainIdstring—域 ID
queryFilter<ProblemStatusDoc>—MongoDB 过滤器
返回值FindCursor<ProblemStatusDoc>

getListStatus(domainId: string, uid: number, pids: number[]): Promise<NumericDictionary<ProblemStatusDoc>> ​

获取多个题目的状态记录,以 docId 为键的字典。

参数类型默认值说明
domainIdstring—域 ID
uidnumber—用户 ID
pidsnumber[]—docId 数组
返回值Promise<NumericDictionary<ProblemStatusDoc>>

updateStatus(domainId: string, pid: number, uid: number, rid: ObjectId, status: number, score: number): Promise<boolean> ​

更新用户在题目上的状态。仅在新状态更优时更新(通过始终优先)。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
uidnumber—用户 ID
ridObjectId—记录 ID
statusnumber—状态码
scorenumber—分数
返回值Promise<boolean>状态是否被更新

incStatus(domainId: string, pid: number, uid: number, key: NumberKeys<ProblemStatusDoc>, count: number): Promise<ProblemStatusDoc> ​

递增用户题目状态上的数字字段。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
uidnumber—用户 ID
keyNumberKeys<ProblemStatusDoc>—要递增的字段名
countnumber—递增量
返回值Promise<ProblemStatusDoc>更新后的状态文档

setStar(domainId: string, pid: number, uid: number, star: boolean): Promise<ProblemStatusDoc> ​

设置或取消用户题目状态上的收藏标记。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
uidnumber—用户 ID
starboolean—true 收藏,false 取消收藏
返回值Promise<ProblemStatusDoc>更新后的状态文档

测试数据管理 ​

addTestdata(domainId: string, pid: number, name: string, f: Readable | Buffer | string, operator?: number): Promise<void> ​

上传测试数据文件。更新题目文档中的 data 数组。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
namestring—文件名
fReadable | Buffer | string—文件内容或路径
operatornumber1操作者用户 ID

renameTestdata(domainId: string, pid: number, file: string, newName: string, operator?: number): Promise<void> ​

在存储和文档元数据中重命名测试数据文件。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
filestring—原文件名
newNamestring—新文件名
operatornumber1操作者用户 ID

delTestdata(domainId: string, pid: number, name: string | string[], operator?: number): Promise<void> ​

删除一个或多个测试数据文件。name 可以是单个字符串或数组。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
namestring | string[]—要删除的文件名
operatornumber1操作者用户 ID

附加文件管理 ​

addAdditionalFile(domainId: string, pid: number, name: string, f: Readable | Buffer | string, operator?: number, skipUpload?: boolean): Promise<void> ​

上传附加文件(如附件)。类似 addTestdata,但操作 additional_file 数组。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
namestring—文件名
fReadable | Buffer | string—文件内容或路径
operatornumber1操作者用户 ID
skipUploadbooleanfalse跳过存储上传(仅元数据)

renameAdditionalFile(domainId: string, pid: number, file: string, newName: string, operator?: number): Promise<void> ​

重命名附加文件。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
filestring—原文件名
newNamestring—新文件名
operatornumber1操作者用户 ID

delAdditionalFile(domainId: string, pid: number, name: string | string[], operator?: number): Promise<void> ​

删除一个或多个附加文件。name 接受 string | string[]。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
namestring | string[]—要删除的文件名
operatornumber1操作者用户 ID

子文档辅助 ​

push(domainId: string, _id: number, key: ArrayKeys<ProblemDoc>, value: ProblemDoc[typeof key][0]): Promise<[ProblemDoc, ObjectId]> ​

向数组字段(data 或 additional_file)追加元素。

参数类型默认值说明
domainIdstring—域 ID
_idnumber—题目 docId
keyArrayKeys<ProblemDoc>—数组字段名
valueProblemDoc[typeof key][0]—要追加的元素
返回值Promise<[ProblemDoc, ObjectId]>更新后的文档和新元素 ID

pull(domainId: string, pid: number, key: ArrayKeys<ProblemDoc>, values: ProblemDoc[typeof key][0][]): Promise<ProblemDoc> ​

按值从数组字段中移除元素。

参数类型默认值说明
domainIdstring—域 ID
pidnumber—题目 docId
keyArrayKeys<ProblemDoc>—数组字段名
valuesProblemDoc[typeof key][0][]—要移除的值
返回值Promise<ProblemDoc>更新后的文档

inc(domainId: string, _id: number, field: NumberKeys<ProblemDoc> | string, n: number): Promise<ProblemDoc> ​

递增数字字段(如 nSubmit、nAccept)。

参数类型默认值说明
domainIdstring—域 ID
_idnumber—题目 docId
fieldNumberKeys<ProblemDoc> | string—要递增的字段名
nnumber—递增量(负数为递减)
返回值Promise<ProblemDoc>更新后的文档

权限检查 ​

canViewBy(pdoc: ProblemDoc, udoc: User): boolean ​

检查用户是否可以查看题目。若用户拥有 PERM_VIEW_PROBLEM 且拥有/维护该题目,或拥有 PERM_VIEW_PROBLEM_HIDDEN,或题目未隐藏,则返回 true。

参数类型默认值说明
pdocProblemDoc—题目文档
udocUser—用户文档
返回值boolean

导入 / 导出 ​

import(domainId: string, filepath: string, options?: ProblemImportOptions): Promise<void> ​

从 ZIP 归档或目录导入题目。支持 Hydro、ICPC 和 DOMjudge 包格式。触发进度回调并处理配置合并。

参数类型默认值说明
domainIdstring—目标域
filepathstring—.zip 文件或目录路径
options.preferredPrefixstring?—导入时替换 PID 前缀
options.progressFunction?—进度回调
options.overridebooleanfalse覆盖已有题目
options.operatornumber1操作者用户 ID
options.delSourceboolean?—导入后删除源文件
options.hiddenboolean?—将导入的题目标记为隐藏
typescript
// 从 ZIP 文件导入题目
await ProblemModel.import('system', '/tmp/problems.zip');

// 导入时指定前缀和进度回调
await ProblemModel.import('system', '/tmp/problems.zip', {
  preferredPrefix: 'contest-',
  override: true,
  progress: (current, total) => {
    console.log(`导入进度: ${current}/${total}`);
  },
});

// 导入目录并标记为隐藏
await ProblemModel.import('system', '/data/problems/', {
  hidden: true,
  operator: uid,
});

export(domainId: string, pidFilter?: string): Promise<void> ​

导出所有题目(或匹配 PID 正则过滤器的题目)为 ZIP 归档,保存到当前工作目录。输出文件路径打印到控制台。

参数类型默认值说明
domainIdstring—域 ID
pidFilterstring''PID 正则过滤器(留空导出全部)
返回值Promise<void>无返回值
typescript
// 导出域内所有题目(无返回值,ZIP 输出到当前工作目录,路径打印到控制台)
await ProblemModel.export('system');

// 仅导出匹配前缀的题目
await ProblemModel.export('system', '^contest-');

属性 ​

属性类型说明
defaultProblemDoc模板 ProblemDoc,所有字段设为安全默认值
deletedProblemDoc哨兵 ProblemDoc,用作已删除题目的占位符

事件 ​

以下事件在 ProblemModel 操作期间通过 bus 触发:

事件参数触发时机
problem/before-adddomainId, content, owner, docId, doc创建题目前
problem/adddoc, docId创建题目后
problem/before-editdoc, $unset编辑题目前
problem/editdoc编辑题目后
problem/before-deldomainId, docId删除题目前
problem/deletedomainId, docId删除题目后
problem/addTestdatadomainId, docId, name, payload添加测试数据后
problem/renameTestdatadomainId, docId, name, newName重命名测试数据后
problem/delTestdatadomainId, docId, name删除测试数据后
problem/addAdditionalFiledomainId, docId, name, payload添加附加文件后
problem/renameAdditionalFiledomainId, docId, name, newName重命名附加文件后
problem/delAdditionalFiledomainId, docId, name删除附加文件后