人才同步管道
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 处理器、再记一条出场日志。它挂上 completed 与 failed 监听用于观测,并暴露一个 close() 供优雅停机。因为它是 await 处理器的,一次抛出会作为任务失败回传给 BullMQ——正是这一点,让队列(在生产者侧定义的)重试策略真正生效。
从任务到命令:那次映射
映射这一步是刻意的,不是原样透传。线上的 username 到命令里变成 displayName。可空的线上字段——avatarUrl、yearsExperience、salaryExpectationMin/Max——从 null 规范化为 undefined(?? undefined),这样下游看到的是「缺省」而不是一个显式的 null。技能以三种并列形态到达(skills、skillIds、skillNames),原样带过去。这道接缝,就是 profile 服务的词汇与搜索服务的词汇在同一处被对齐的地方。
处理器:可见性决定 upsert 还是 delete
UpsertTalentHandler.execute 是进入索引的唯一写路径。它先守住输入——没有 id 的命令立即抛错。然后按可见性分支,这是那个承重的决策:
- 若
visibility === 'hidden',这位人才会被从搜索索引里删除,而不是写入。把档案设为隐藏,被表达为一次索引移除,于是一位转为隐藏的老师,就干脆不再可被检索。 - 否则,构建一份
TalentDocument并upsert。用 upsert(而非 insert),正是让索引保持最终一致的关键——同一条代码路径既处理全新的老师,也处理某位老师的第一百次编辑。
处理器的注释把目的地写得很直白:它构建的文档,就是给 Meilisearch 的那份——Meilisearch 是人才这一面背后的搜索引擎。
写入时派生的字段
处理器不是把命令直接拷进文档——它派生出若干字段,让索引携带原始档案里没有的答案:
displayName按displayName → fullName → 'Unknown'逐级兜底,索引里绝不会存下一条无名的行。hasVerifiedCredentials计算为「是否有任一资质的状态是verified」——一个布尔值,搜索侧无需遍历资质数组即可据此筛选。publicToEmployers在可见性为public或limited时为真;publicToRecruiters只在public时为真。于是那一个可见性级别,在写入的当下就展开成按受众区分的访问标志,索引据此强制执行「谁能看见谁」。experienceYears作为yearsExperience的别名写入,用于筛选兼容;updatedAt在每次写入时打上当前时间戳。
设计的形状:档案是真相之源,索引是派生视图,而这套派生集中在一个处理器里,于是每一条人才行都以同样的方式被构建出来。
管道周边
talent 服务还有另外两块基础设施,紧挨着同步路径。ProfileServiceClient 是反方向的——一个经过认证的服务间 HTTP 客户端,talent 服务用它回读 profile 服务:某个雇主的核验状态、某个招聘方的审批状态、以及调用者自己的人才档案。它把调用者的会话透传过去,并且在任何错误或非成功响应时记日志、返回 null 而不抛出——一次读取失败退化为「未知」,绝不变成崩溃。它抽出了一个接口,好让测试可以提供 mock。
TalentCleanupService 是个小小的机会主义清道夫:至多每小时一次,触发一趟不阻塞的后台清理(它自己的失败被捕获并记日志,绝不暴露给调用方),删除过期超过 90 天的邀请。它才是这个服务里真正「发射后不管」的部分,与那条被 await 的同步写入分开摆放。