admin-moderation-workflows

管理后台审核流程与领域状态机

YouTeacher 的管理后台,是人工审核者处理那些不能交给开放自助流程的事情的地方:判定一所学校确实是它自称的那所、把一条雇主记录交给某个所有者、批准招聘者、调整某人的配额、以及隐藏一个职位或编辑一篇不该照原样留着的帖子。真正有意思的地方不是这堆按钮——而是"某个按钮什么时候被允许生效"这条规则,是以值对象状态机的形式活在领域层里的,而不是散落在各个 handler 中。一次审核动作,是传输层校验后向下传递的一个请求;这次状态迁移是否合法,由聚合本身来决定——它会抛错,而不会默默做错事。

雇主验证是一次带守卫的迁移

一个雇主的验证状态是四个值之一:unverifiedpendingverifiedrejectedVerificationStatus 值对象拥有这个集合——它是这些字符串唯一被允许存在的地方,fromString 对集合之外的任何值都会抛错拒绝。状态不是一个你随手赋值的普通字段;它会回答关于自身的问题。canVerify()canReject() 都只在 unverifiedpending 时返回 true。所以一个已经验证或已经拒绝的雇主,无法再次被验证或拒绝——在任何持久化发生之前,这扇门就已经在值对象这一层关上了。

Employer 聚合在此之上构建。调用 verify() 会先问 canVerify();如果答案是否,它就抛出一个错误,指明当前状态和它本该处于的状态。成功时它并不就地修改——而是返回一个新的 Employer,状态置为已验证、清空拒绝原因、刷新 updatedAtreject(reason) 与之对称:先用 canReject() 守卫,再返回一个标记为已拒绝、并存下原因的新雇主。聚合的构造函数是私有的,其 props 是只读的;已有记录通过 reconstitute 工厂方法回填。因此每一次迁移都是不可变的——你从不原地编辑一个雇主,而是推导出下一个——这让一次非法迁移变成单一收口处抛出的错误,而不是一个被污染的字段。

所有权是第二台独立的状态机

验证(这所学校是真的吗?)与所有权(有没有人拥有这条记录?)被分开对待。assignOwner(userId)canAssignOwner() 守卫,无法进行时抛出 "already claimed";成功时返回一个带上 user id、所有权状态为 claimed 的新雇主。removeOwner() 是它的逆操作——同样带守卫,当没有东西可移除时抛出 "not claimed",然后返回一个去掉 user id、所有权回到 unclaimed 的雇主。聚合把 isClaimed / isUnclaimed 作为委托给所有权值对象的计算读值暴露出来,与验证侧的 isPending / isVerified / isRejected 是同一套模式。两台正交的机器,各自拒绝自己的非法动作。

REST 边缘:先校验,再委派

雇主验证的这几条路由,展示了整个管理后台面的形状。有一个分页的 GET 用于待处理雇主(从 query 读取可选的 pagelimit)、一个 POST 用于验证、一个 POST 用于拒绝。验证和拒绝的 handler 自己几乎什么都不做:它们把请求体按一个 zod schema 解析、调用 use case、把结果发回去。来自领域层的错误——包括那些抛出的非法迁移消息——统一通过一个共享的错误处理器输出,而不是每条路由各自编造响应。

请求契约小而明确

所有管理后台的请求形状都是集中在一处的 zod schema,因此每一个审核动作的词汇一眼可辨:

  • 雇主——验证只要一个 employerId;拒绝要一个 employerId 加一个可选的 reason
  • 招聘者——同样的一对:一个 id 用于批准,一个 id 加可选原因用于拒绝。
  • 配额——充值(email、一个正数 amount)与设定(email、一个 ≥ 0 的 limit 和 ≥ 0 的 used),两者都带一个 quotaType,取值为 invitationjobPublish,默认 invitation。两种额度,一个 schema 家族。
  • 所有权——指派要一个所有者的 email 和可选 notes;移除要可选 notes;一个合并 schema 取 sourceIdtargetId,用于把一条雇主记录并入另一条。
  • 认领请求——批准(claimId、可选 notes)与拒绝(claimId、可选 reason、可选 adminNotes),这是有人申请拥有一条既有雇主记录的入口路径。
  • 内容——一个帖子更新 schema,其字段(标题、正文及其格式、摘要、封面图、分类、meta 标题与描述)全部可选,其中数个明确可为 null,因此审核者可以编辑或清空任意单个字段而不触碰其余。
  • 职位——一个可见性 schema,在 visiblehidden 之间翻转一个职位;以及一个只含 jobId 的 schema,用于仅凭 id 的动作。

贯穿始终的一条线:传输层的职责是证明请求格式良好(一个非空 id、一个合法 email、一个落在区间内的数、一个被允许的枚举值),领域层的职责是证明这次迁移合法。两者都不指望对方替自己完成那一半。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

admin-moderation-workflows