Skip to content
页面信息
📝 描述讨论(论坛)模型,用于管理线程化讨论、回复、表情回应和讨论节点
📥 导入import { DiscussionModel } from 'hydrooj'

DiscussionModel

讨论(论坛)模型,用于管理线程化讨论、回复、嵌套尾隔回复、表情回应、讨论节点(分类)和父实体解析。

DiscussionModel 是导出函数的普通模块(非类)。所有函数直接调用(如 DiscussionModel.add(...))。


类型导出

DiscussionDoc

继承 Document —— 讨论文档的形状。

Field

typescript
type Field = keyof DiscussionDoc

所有讨论文档字段名的联合类型。


常量

typeDisplay: Record<number, string>

将文档类型常量映射到可读名称:{ 10: 'problem', 20: 'node', 30: 'contest', 40: 'training' }

PROJECTION_LIST: Field[]

列表视图中返回的字段:_iddomainIddocTypedocIdhighlightnReplyviewspinupdateAtownerparentIdparentTypetitlehidden

PROJECTION_PUBLIC: Field[]

详情视图中返回的字段 —— 在 PROJECTION_LIST 基础上增加 contenteditedreactmaintainerlock

HISTORY_PROJECTION_PUBLIC: (keyof DiscussionHistoryDoc)[]

历史视图中返回的字段:titlecontentdocIduidtime


方法

讨论 CRUD

add(domainId: string, parentType: number, parentId: ObjectId | number | string, owner: number, title: string, content: string, ip: string | null = null, highlight: boolean, pin: boolean, hidden?: boolean): Promise<ObjectId>

在父实体(题目、比赛、训练计划或节点)下创建新讨论。触发 discussion/before-adddiscussion/add 总线事件。返回新讨论 ID。

参数类型默认值说明
domainIdstring域 ID
parentTypenumber父实体类型常量
parentIdObjectId | number | string父实体 ID
ownernumber创建者 UID
titlestring讨论标题
contentstring讨论内容
ipstring | nullnull创建者 IP 地址
highlightboolean是否高亮
pinboolean是否置顶
hiddenbooleanfalse是否隐藏
返回值Promise<ObjectId>新讨论 ID
typescript
// 在题目下创建讨论
const did = await DiscussionModel.add(
  'system',                          // domainId
  10,                                // parentType (TYPE_PROBLEM)
  problemId,                         // parentId
  12345,                             // owner
  '关于时间限制的疑问',               // title
  '这道题的 1s 时限是否合理?',       // content
  '127.0.0.1',                       // ip
  false,                             // highlight
  false,                             // pin
);

// 在讨论节点下创建置顶讨论
const pinnedDid = await DiscussionModel.add(
  'system',
  30,                                // parentType (TYPE_DISCUSSION_NODE)
  'general',                         // parentId (节点字符串 ID)
  12345,
  '版规公告',
  '请遵守社区规范...',
  undefined,                         // ip
  true,                              // highlight
  true,                              // pin
  false,                             // hidden
);

get<T extends Field>(domainId: string, did: ObjectId, projection?: T[]): Promise<Pick<DiscussionDoc, T>>

按 ID 获取单个讨论,支持指定字段投影。默认为 PROJECTION_PUBLIC

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
projectionT[]PROJECTION_PUBLIC返回字段列表
返回值Promise<Pick<DiscussionDoc, T>>

edit(domainId: string, did: ObjectId, $set: Partial<DiscussionDoc>): Promise<DiscussionDoc>

更新讨论字段。如果 content 有变更,自动在 coll 集合中插入历史记录。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
$setPartial<DiscussionDoc>要更新的字段
返回值Promise<DiscussionDoc>

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

删除讨论及所有关联的回复、状态和历史记录。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
返回值Promise<void>

inc(domainId: string, did: ObjectId, key: NumberKeys<DiscussionDoc>, value: number): Promise<DiscussionDoc | null>

原子性递增讨论上的数字字段(如 views)。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
keyNumberKeys<DiscussionDoc>要递增的字段名
valuenumber递增量
返回值Promise<DiscussionDoc | null>

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

返回匹配查询的讨论游标,按 pin 降序然后 docId 降序排列。默认为 PROJECTION_LIST

参数类型默认值说明
domainIdstring域 ID
queryFilter<DiscussionDoc>过滤条件
projectionField[]PROJECTION_LIST返回字段列表
返回值FindCursor<DiscussionDoc>

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

返回匹配给定查询的讨论数量。

参数类型默认值说明
domainIdstring域 ID
queryFilter<DiscussionDoc>过滤条件
返回值Promise<number>

回复 CRUD

addReply(domainId: string, did: ObjectId, owner: number, content: string, ip: string): Promise<ObjectId>

为讨论添加回复。原子性递增 nReply 并更新父讨论的 updateAt。返回新回复 ID。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
ownernumber回复者 UID
contentstring回复内容
ipstring回复者 IP 地址
返回值Promise<ObjectId>新回复 ID

getReply(domainId: string, drid: ObjectId): Promise<DiscussionReplyDoc | null>

按 ID 获取单个回复。

参数类型默认值说明
domainIdstring域 ID
dridObjectId回复 ID
返回值Promise<DiscussionReplyDoc | null>

editReply(domainId: string, drid: ObjectId, content: string, uid: number, ip: string): Promise<DiscussionReplyDoc | null>

更新回复内容。自动插入历史记录并设置 edited: true

参数类型默认值说明
domainIdstring域 ID
dridObjectId回复 ID
contentstring新内容
uidnumber编辑者 UID
ipstring编辑者 IP 地址
返回值Promise<DiscussionReplyDoc | null>

delReply(domainId: string, drid: ObjectId): Promise<void>

删除回复及所有尾隔回复和历史记录。原子性递减父讨论的 nReply。回复不存在时抛出 DocumentNotFoundError

参数类型默认值说明
domainIdstring域 ID
dridObjectId回复 ID
返回值Promise<void>

getMultiReply(domainId: string, did: ObjectId): FindCursor<DiscussionReplyDoc>

返回讨论的回复游标,按 _id 降序排列。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
返回值FindCursor<DiscussionReplyDoc>

getListReply(domainId: string, did: ObjectId): Promise<DiscussionReplyDoc[]>

以数组形式返回讨论的所有回复(getMultiReply 的便捷封装)。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
返回值Promise<DiscussionReplyDoc[]>

尾隔回复

尾隔回复是嵌套在顶级回复中的二级回复,存储在 DiscussionReplyDocreply 数组字段中。

addTailReply(domainId: string, drid: ObjectId, owner: number, content: string, ip: string): Promise<[DiscussionReplyDoc, ObjectId]>

为顶级回复添加嵌套回复。同时更新父讨论的 updateAt。返回更新后的父回复文档和新尾隔回复 ID。

参数类型默认值说明
domainIdstring域 ID
dridObjectId父回复 ID
ownernumber回复者 UID
contentstring回复内容
ipstring回复者 IP 地址
返回值Promise<[DiscussionReplyDoc, ObjectId]>父回复文档和新尾隔回复 ID
typescript
// 向回复添加尾隔回复(二级嵌套)
const [updatedReply, tailReplyId] = await DiscussionModel.addTailReply(
  'system',                          // domainId
  replyId,                           // drid (父回复 ID)
  12345,                             // owner
  '感谢解答,非常清楚!',             // content
  '127.0.0.1',                       // ip
);
console.log('新尾隔回复 ID:', tailReplyId);

getTailReply(domainId: string, drid: ObjectId, drrid: ObjectId): Promise<[DiscussionReplyDoc, DiscussionTailReplyDoc] | [null, null]>

获取父回复中的指定尾隔回复。未找到时返回 [null, null]

参数类型默认值说明
domainIdstring域 ID
dridObjectId父回复 ID
drridObjectId尾隔回复 ID
返回值Promise<[DiscussionReplyDoc, DiscussionTailReplyDoc] | [null, null]>

editTailReply(domainId: string, drid: ObjectId, drrid: ObjectId, content: string, uid: number, ip: string): Promise<DiscussionTailReplyDoc>

更新尾隔回复内容。自动插入历史记录并设置 edited: true

参数类型默认值说明
domainIdstring域 ID
dridObjectId父回复 ID
drridObjectId尾隔回复 ID
contentstring新内容
uidnumber编辑者 UID
ipstring编辑者 IP 地址
返回值Promise<DiscussionTailReplyDoc>

delTailReply(domainId: string, drid: ObjectId, drrid: ObjectId): Promise<[void, void]>

删除尾隔回复及其关联的历史记录。

参数类型默认值说明
domainIdstring域 ID
dridObjectId父回复 ID
drridObjectId尾隔回复 ID
返回值Promise<[void, void]>

表情回应

react(domainId: string, docType: keyof DocType, did: ObjectId, id: string, uid: number, reverse?: boolean): Promise<[any, any]>

切换用户对讨论或回复的表情回应(id)。如果 reverse 为 true,则移除回应。返回 [updatedDoc, statusDoc]

参数类型默认值说明
domainIdstring域 ID
docTypekeyof DocType文档类型
didObjectId讨论/回复 ID
idstringemoji ID
uidnumber用户 ID
reversebooleanfalsetrue 时移除回应
返回值Promise<[any, any]>[updatedDoc, statusDoc]
typescript
// 对讨论点赞(emoji ID "like")
const [doc, status] = await DiscussionModel.react(
  'system',                          // domainId
  21,                                // docType (TYPE_DISCUSSION)
  discussionId,                      // did
  'like',                            // id (emoji ID)
  12345,                             // uid
);

// 取消点赞
await DiscussionModel.react(
  'system',
  21,
  discussionId,
  'like',
  12345,
  true,                              // reverse = true → 移除
);

getReaction(domainId: string, docType: keyof DocType, did: ObjectId, uid: number): Promise<Record<string, number>>

返回用户对文档的回应状态,以 emoji ID 到值的映射返回。

参数类型默认值说明
domainIdstring域 ID
docTypekeyof DocType文档类型
didObjectId文档 ID
uidnumber用户 ID
返回值Promise<Record<string, number>>emoji ID → 值的映射

历史

getHistory(domainId: string, docId: ObjectId, query?: Filter<DiscussionHistoryDoc>, projection?: (keyof DiscussionHistoryDoc)[]): Promise<DiscussionHistoryDoc[]>

返回讨论或回复的编辑历史记录,按 time 降序排列。

参数类型默认值说明
domainIdstring域 ID
docIdObjectId讨论/回复 ID
queryFilter<DiscussionHistoryDoc>过滤条件
projection(keyof DiscussionHistoryDoc)[]返回字段列表
返回值Promise<DiscussionHistoryDoc[]>

用户状态

setStar(domainId: string, did: ObjectId, uid: number, star: boolean): Promise<void>

设置或清除用户对讨论的星标。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
uidnumber用户 ID
starbooleantrue 星标,false 取消
返回值Promise<void>

getStatus(domainId: string, did: ObjectId, uid: number): Promise<any>

获取用户对讨论的状态记录(包括星标、回应等)。

参数类型默认值说明
domainIdstring域 ID
didObjectId讨论 ID
uidnumber用户 ID
返回值Promise<any>

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

覆盖用户对讨论的状态字段。

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

节点(分类)

讨论节点作为顶层分类,用于组织讨论。

addNode(domainId: string, _id: string, category: string, args?: any): Promise<any>

创建具有指定 ID 和分类名称的讨论节点。可选 args 用于附加字段。

参数类型默认值说明
domainIdstring域 ID
_idstring节点 ID
categorystring分类名称
argsany附加字段
返回值Promise<any>

getNode(domainId: string, _id: string): Promise<any>

按字符串 ID 获取单个讨论节点。

参数类型默认值说明
domainIdstring域 ID
_idstring节点 ID
返回值Promise<any>

getNodes(domainId: string): Promise<any[]>

以数组形式返回域的所有讨论节点。

参数类型默认值说明
domainIdstring域 ID
返回值Promise<any[]>

flushNodes(domainId: string): Promise<any>

删除域的所有讨论节点。

参数类型默认值说明
domainIdstring域 ID
返回值Promise<any>

虚拟节点(父实体)

虚拟节点解析讨论所附属的父实体(题目、比赛、训练计划或讨论节点)。

getVnode(domainId: string, type: number, id: string, uid?: number): Promise<any>

解析讨论的父实体。处理题目(按数字 ID)、比赛/训练计划(按 ObjectId)和讨论节点(按字符串 ID)。可选地为指定用户填充 attend 状态。未找到时抛出 DiscussionNodeNotFoundError

参数类型默认值说明
domainIdstring域 ID
typenumber父实体类型
idstring父实体 ID
uidnumber用于填充 attend 状态的用户 ID
返回值Promise<any>

getListVnodes(domainId: string, ddocs: any, getHidden?: boolean, assign?: string[]): Promise<Record<number, Record<string, any>>>

批量解析多个讨论的父实体。返回嵌套映射 { [parentType]: { [parentId]: vnode } }。默认过滤隐藏节点和受作业组限制的项目。

参数类型默认值说明
domainIdstring域 ID
ddocsany讨论文档数组
getHiddenbooleanfalse是否包含隐藏节点
assignstring[]作业组限制过滤
返回值Promise<Record<number, Record<string, any>>>

checkVNodeVisibility(type: number, vnode: any, user: User): boolean

用户被允许查看父实体时返回 true。检查隐藏题目可见性和比赛/训练计划的作业组限制。

参数类型默认值说明
typenumber父实体类型
vnodeany虚拟节点对象
userUser当前用户
返回值boolean是否可见

属性

属性类型说明
collCollection<DiscussionHistoryDoc>MongoDB discussion.history 集合 —— 存储编辑历史记录

备注

  • 讨论是文档类型模型(TYPE_DISCUSSION = 21)。CRUD 和状态操作委托给共享的 document 模块。
  • 模型支持三级嵌套:讨论回复尾隔回复
  • 所有内容变更(讨论编辑、回复编辑、尾隔回复编辑)会自动在 discussion.history 集合中插入历史记录。
  • add() 触发总线事件(discussion/before-adddiscussion/add),使插件可以拦截或响应讨论创建。
  • apply(ctx) 注册生命周期钩子:题目删除时级联删除关联讨论,题目编辑时同步 hidden 状态到关联讨论。