2026-09-23·by Sijie Wang#node#project#youteacher#talent

talent-sync-pipeline

人才同步管道

profile 服务持有教师的权威数据;talent 服务持有学校端查询的搜索索引。talent-search-sync-queue 那个节点讲的是生产者一端——档案改动如何被入队。本节讲消费者一端:talent 服务如何从那条队列取出任务,把它变成搜索索引里的一行。两端在同一条由 Redis 支撑、名为 talent-sync 的 BullMQ 队列上相遇。

消费者 worker:消费 talent-sync

TalentSyncWorker 包着一个绑定到 talent-sync 队列、跑在 Redis 连接上的 BullMQ Worker。它以并发 5 运行,因此多个人才更新可以并行入库。每个任务携带一份 TalentSyncJobData 载荷——档案里与搜索相关的投影:身份信息(id、username、标题、简介、头像)、学校端用来筛选的各个 facet(技能及其 id/name、语言、城市与国家、期望地点、可入职状态、经验级别与年限、期望薪资区间、资质),以及可见性字段(一个 public/limited/hidden 级别,外加按受众区分的布尔值)。

worker 本身除了「翻译」几乎什么都不做:记一条入场日志、把载荷映射成 UpsertTalentCommand、await 处理器、再记一条出场日志。它挂上 completedfailed 监听用于观测,并暴露一个 close() 供优雅停机。因为它是 await 处理器的,一次抛出会作为任务失败回传给 BullMQ——正是这一点,让队列(在生产者侧定义的)重试策略真正生效。

从任务到命令:那次映射

映射这一步是刻意的,不是原样透传。线上的 username 到命令里变成 displayName。可空的线上字段——avatarUrlyearsExperiencesalaryExpectationMin/Max——从 null 规范化为 undefined?? undefined),这样下游看到的是「缺省」而不是一个显式的 null。技能以三种并列形态到达(skillsskillIdsskillNames),原样带过去。这道接缝,就是 profile 服务的词汇与搜索服务的词汇在同一处被对齐的地方。

处理器:可见性决定 upsert 还是 delete

UpsertTalentHandler.execute 是进入索引的唯一写路径。它先守住输入——没有 id 的命令立即抛错。然后按可见性分支,这是那个承重的决策:

  • visibility === 'hidden',这位人才会被从搜索索引里删除,而不是写入。把档案设为隐藏,被表达为一次索引移除,于是一位转为隐藏的老师,就干脆不再可被检索。
  • 否则,构建一份 TalentDocumentupsert。用 upsert(而非 insert),正是让索引保持最终一致的关键——同一条代码路径既处理全新的老师,也处理某位老师的第一百次编辑。

处理器的注释把目的地写得很直白:它构建的文档,就是给 Meilisearch 的那份——Meilisearch 是人才这一面背后的搜索引擎。

写入时派生的字段

处理器不是把命令直接拷进文档——它派生出若干字段,让索引携带原始档案里没有的答案:

  • displayNamedisplayName → fullName → 'Unknown' 逐级兜底,索引里绝不会存下一条无名的行。
  • hasVerifiedCredentials 计算为「是否有任一资质的状态是 verified」——一个布尔值,搜索侧无需遍历资质数组即可据此筛选。
  • publicToEmployers 在可见性为 public limited 时为真;publicToRecruiters 只在 public 时为真。于是那一个可见性级别,在写入的当下就展开成按受众区分的访问标志,索引据此强制执行「谁能看见谁」。
  • experienceYears 作为 yearsExperience 的别名写入,用于筛选兼容;updatedAt 在每次写入时打上当前时间戳。

设计的形状:档案是真相之源,索引是派生视图,而这套派生集中在一个处理器里,于是每一条人才行都以同样的方式被构建出来。

管道周边

talent 服务还有另外两块基础设施,紧挨着同步路径。ProfileServiceClient反方向的——一个经过认证的服务间 HTTP 客户端,talent 服务用它回读 profile 服务:某个雇主的核验状态、某个招聘方的审批状态、以及调用者自己的人才档案。它把调用者的会话透传过去,并且在任何错误或非成功响应时记日志、返回 null 而不抛出——一次读取失败退化为「未知」,绝不变成崩溃。它抽出了一个接口,好让测试可以提供 mock。

TalentCleanupService 是个小小的机会主义清道夫:至多每小时一次,触发一趟不阻塞的后台清理(它自己的失败被捕获并记日志,绝不暴露给调用方),删除过期超过 90 天的邀请。它才是这个服务里真正「发射后不管」的部分,与那条被 await 的同步写入分开摆放。

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 →