downstream-integration-and-caching

下游服务集成、配置驱动路由与仪表盘缓存

admin 服务本身不拥有数据。它是一层很薄的 backend-for-frontend,坐在若干下游微服务(profile、auth、content、job)前面,把它们的响应拼接成 admin 控制台需要的形状。让这件事既能跑通又不写死任何东西的,是三个机制:一个懂得服务间鉴权协议的通用 HTTP 客户端、一张完全由环境变量注入的路由表,以及一层挡在那个否则每次加载都要扇出的端点前面的小缓存。

ServiceClient——每个下游一个客户端

ServiceClient 是对 fetch 的一层通用封装。每个下游服务构造一个实例,各自携带自己的 base URL 和一个仅用于日志的服务名。它只暴露一个方法 request<T>(path, options)

  • 把 base URL 和调用方传入的 path 拼起来,并根据一个普通选项对象构建 query string(值为 undefined 的会被丢弃,这样调用方传可选过滤条件时不必分支处理);
  • 每次调用都新签一个短生命周期的服务令牌,作为 bearer 凭据附上,这样下游服务鉴别的是调用它的那个服务,而不是终端用户;
  • 默认收发 JSON,但当响应不是 JSON 时退回按文本读取;
  • 返回统一的 { status, data } 信封,而不是在非 2xx 时抛异常——这让仓储可以把「未找到」之类的结果当作数据处理(见下面的 404 处理),而不是当异常。

令牌每次请求现签,约一分钟内过期,因此线路上没有长期存活的秘密,也没有需要刷新或吊销的东西。构造函数拒绝在没有签名密钥的情况下建立客户端——缺密钥是启动期失败,而不是运行期的意外。

配置驱动路由——没有硬编码

admin 服务用到的每一个路径——既包括它自己的入站路由,也包括每个下游服务上的出站路径——都通过一个 requireEnv 辅助函数从环境变量读取,缺变量就抛错。这是刻意的 fail-fast:进程不会在一个只配了一半的环境里启动,于是缺失的路由会在部署时被发现,而不是等到第一个需要它的请求。

路由表存的是完整路径,而不是 base 加相对片段,并按关注点分组(雇主归属与认证、招聘者审批、认领请求、内容审核、职位审核、人才、仪表盘)。需要 id 的动态路径由小的 builder 函数生成,而不是在每个调用点做字符串拼接,这样 id 插值就集中在一处。少数几条路由(健康检查)被标为 public,豁免服务令牌要求。

仓储——线上格式与领域之间的防腐层

HttpEmployerRepository 这样的仓储实现的是领域层接口,但底层由指向对应下游的 ServiceClient 支撑。它们的职责是翻译:下游服务返回的原始 DTO 被逐字段映射进一个丰富的领域聚合,走的是聚合的 reconstitute 工厂——字符串状态变成值对象,布尔 claimed 标志变成归属状态,时间戳变成 Date。这层映射就是防腐层:下游的线上格式可以漂移而不渗进领域,因为只有映射器同时知道两边。

仓储方法里反复出现两条约定。列表查询同时返回映射后的实体和原封不动的下游分页 meta,这样控制台可以分页而 admin 服务不必重新计数。以及,404 状态被翻译成 null 而非报错,因为「这个雇主不存在」是 findById 的一个合法答案,不是失败。

仪表盘缓存——Redis,带内存兑底

仪表盘汇总的是一批计数(待认证、未认领雇主、待审/已拒招聘者、待处理认领),这些数字否则每次加载都要向下游扇出。这些计数被缓存在一个统一的 DashboardCache 接口(get / set / invalidate)后面,有两个可互换的实现:

  • RedisDashboardCache 把整个统计对象序列化成 JSON,用一个短 TTL(默认 60 秒)存在单个 key 下。每个 Redis 操作都被包起来,让缓存故障退化为 miss:get 出错时返回 nullsetinvalidate 记一条 warning 后照常继续。因此缓存挂了只会让仪表盘变慢,绝不会让它坏掉。
  • InMemoryDashboardCache 用相同接口、单个进程内槽位和基于时间的过期实现——当没有 Redis 可用时(本地开发,或选择不跑 Redis 的部署)的兑底。

短 TTL 正是整个设计的要点:允许仪表盘数字有一分钟的陈旧,换来不必每次请求都扇出。invalidate 的存在,是为了让一个明显会改变计数的写操作可以立刻清缓存,而不必等 TTL 过完。

为什么这几块凑在一起

这四块构成同一个模式:admin 服务不持有自己的状态,通过配置好的路径和一个已鉴权的客户端触达一切,在仓储边界把每个下游格式翻译进领域,只缓存那次昂贵的汇总读。加一个新的下游调用是一致的——在环境里声明它的路径,通过对应的 ServiceClient 调用,在仓储里映射响应。

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 →