职位生命周期:有效性、过期与清理
系统从不假设一条职位还真实存在。每次要把某条职位提供出去时,它都必须重新证明自己仍然有效——而如何证明,取决于它从哪里来。系统按来源类型(origin type)把职位分开,为每一类跑一套不同的检查。检查一旦失败,这条职位不会被打标记、也不会被隐藏,而是当场硬删除,并告知调用方该职位已不存在。
两种来源,两条规则
JobValidityService 是一层建立在策略模式(Strategy)之上的薄路由。它持有一张从来源类型到 JobValidator 的映射;ensureJobIsValid 只做一件事:按职位的 originType 查出对应的校验器并委派过去。构造时注册了两个校验器——"direct" 与 "aggregated"——而任何没有注册校验器的来源类型都被默认视为有效,因此新增来源种类无需改动路由本身。另有一个 registerValidator 方法,允许调用方以同样的方式挂上更多校验器(对扩展开放,对修改封闭)。
直接来源(direct)的职位自带过期时间。DirectJobValidator 把职位的 expiresAt 时间戳与当前时间比较:若仍在未来,职位原样放行;若已过去,校验器就删除该职位、通知变更处理器,并抛出 JobInvalidError.expired。这条规则完全是本地的——不发任何网络请求,只做一次日期比较。
聚合来源(aggregated)的职位是从别人的招聘板抓来的,所以它的有效性由外部定义:只要它的源 URL 仍可访问,这条职位就有效。AggregatedJobValidator 先要求 sourceUrl 必须存在——没有源 URL 的职位会被立即删除并报为不可达(unreachable)。否则,它再判断是否到了该重新核查的时候。
重新核查,但不去砸源站
如果每次请求都重新核验每条聚合职位,就意味着每看一次就要一个网络往返,所以校验器给自己加了节流。shouldRecheck 只在三种情况之一成立时返回 true:源从未被核查过、上次核查已在 recheckMinutes 之前、或上次记录的状态不健康(非 2xx)。三者都不成立,就沿用缓存的判定,不发出任何请求。
当确实需要重新核查时,校验器调用源核验器,随后通过 updateValidity 持久化结果——sourceLastCheckedAt 与 sourceLastStatus——好让节流机制下次有新鲜的状态可依据。若核验器报告源已不 OK,职位就被删除,并抛出 JobInvalidError.unreachable。
SourceVerificationService 是一个单方法端口:verify(url) 返回 { ok, statusCode? }。其 HTTP 实现发一个 HEAD 请求(很便宜——只取响应头、不取正文),套在带短超时的 AbortController 里,并把 2xx 或 3xx 响应视为 OK。任何抛出的错误——超时、DNS 失败、连接被拒——都会收敛成 { ok: false } 且不带状态码,因此"不可达的源"与"已死的源"在结果上无从区分,这正是设计意图。(注意这里有个细微的不对称:核验器把 2xx–3xx 都算作 "OK",而校验器触发重核查时只把 2xx 算作"健康"——所以一条以 3xx 响应的职位能存活,但会被更积极地反复核查。)
删除,在各处是同一个动作
两个校验器在判定职位无效时都收敛到同一形状:先从仓储删除它,再调用变更处理器的 jobDeleted 钩子。这个钩子正是下游消费者——缓存、搜索索引——得知该行已消失的接缝,于是一次有效性失败会向外扩散,而不是留下过期的副本。这个通知器是可选的;缺席时删除照样发生,只是不再向外扩散。
清扫长尾
逐请求校验能在有人碰到某条已过期或已死的职位时立刻把它清掉,但那些再也没人看的职位会一直滞留。JobCleanupService 就是这条长尾的清道夫。triggerCleanup 被机会性地调用,且是非阻塞的——它记下时间、在后台启动清扫、随即返回;出错只记日志,绝不抛给调用方。它还自我节流为至多每小时一次,因此热路径可以放心随手调用。
后台清扫会删除过期已超过一年的职位;若存在报告仓储,还会删除处理已逾九十天的职位举报。每一遍都会记录清掉了多少行。这是刻意从宽的:近期历史被保留,只有真正陈旧的尾巴才被回收。
为什么是这个形状
这套设计守住一条不变式:无效的职位就是被删除的职位,而不是被打标记的职位。下游不存在"已过期但仍可见"这种需要额外推理的状态。有效性是在使用点惰性检查的,因此在最要紧的时刻应用最新鲜的判定;而每小时一次的清理,只是为了收拾那些惰性检查永远不会再回访的部分。按来源拆分规则,意味着每一类职位都只用真正定义它的那个信号来评判——直接来源看它自报的过期时间,聚合来源看它当下是否可达。