service-architecture-and-request-flow

服务架构与请求流转(youteacher_job)

youteacher_job 拥有 YouTeacher 网络对外暴露的每一条岗位记录——无论它来自雇主直接发布,还是来自 job_scrapers 项目维护的外部爬虫。下游各个界面(Web 前端、通知、搜索)都把这个服务当作唯一真相来源。它是一个基于 Fastify 的服务,按经典的 DDD 分层组织;而它最值得一看的地方,在于一次请求穿过这些层时,各层被隔离得有多干净。

四层,依赖朝内指

src/ 目录分四层,规则和前端一样:依赖只朝指。

  • interfaces/ —— Fastify 控制器。它们讲 HTTP,负责解析与校验输入、执行限流、把结果翻译回响应。它们完全不知道数据怎么存。
  • application/ —— 用例(这里叫 handler)、它们依赖的仓储接口、DTO,以及应用层服务(缓存、搜索、有效性、变更通知)。handler 只负责编排;它依赖的是像 JobRepository 这样的接口,绝不是具体的 Prisma 类。
  • domain/ —— 实体、值对象、业务规则,不 import 任何框架。
  • infrastructure/ —— 针对真实世界实现上述接口的适配器:Prisma 仓储、Redis 适配器、队列 provider、搜索客户端,以及调兄弟服务的 HTTP 客户端。

lint 禁止相对父级 import(跨目录一律走 @/... 别名),这样一次重构就不会悄悄把某个依赖指错方向。

一个镜像,两个进程

服务以单一容器镜像交付,运行成两种形态之一:

  • Fastify HTTP API,即处理请求的进程;以及
  • Bull ingest worker,一个后台进程,订阅共享的 job-updates 队列,把爬虫 payload 规范化后写入。

两者共用同一份代码与配置。worker 无需手写循环就满足了"while(true) 循环"的期待:它用 Bull 的 Queue.process 注册一个 handler,只要进程活着,Bull 就持续拉取任务,重试与退避(backoff)配置在队列上。

Bootstrap:先造零件,再接线

启动被刻意拆成几段,每一段都是一个小而纯的函数,好让测试里跑的接线和生产里完全一致。

  • bootstrapDatabase 先确保表存在,再构造具体的 Prisma 仓储(岗位、举报、收藏)并返回它们。
  • createHandlers 拿着这些仓储,加上应用层服务(搜索、缓存、变更通知、有效性服务),构造出每一个用例 handler,只把各自需要的协作者注入进去——比如 CreateDirectJobHandler 拿到岗位仓储和变更通知器,而 GetJobDetailHandler 拿到仓储、缓存和有效性服务。
  • createApp 构造 Fastify 实例,注册 CORS 与 service-auth 插件,装配好控制器依赖(各 handler、地理编码、一个 auth 助手、限流器,以及默认限流配置),然后把这一切交给控制器注册。

这里没有 DI 容器。这几个 bootstrap 函数就是组合根:它们是唯一同时知道接口与具体实现的地方,因此下面各层对 Prisma、Redis、Fastify 一无所知。

路由是配置,不是字面量

一个有辨识度的选择:没有任何路由路径是硬编码的。config/routes.ts 通过一个 requireEnv 助手从环境变量里读取每一条路径,变量缺失就抛错——于是一个配错的部署会在启动时大声失败,而不是去服务一个错误的 URL。同一个模块还声明了哪些路由是公开的(无需用户认证),并通过把 id 代入配置好的模板来派生带参数的路径(比如岗位详情 URL)。每一条生产路径都带版本号,让服务可以迭代而不破坏前端。

Fastify 实例上的一个 URL 重写钩子允许调用方省略 API base 前缀:内部、健康检查、以及已带前缀的 URL 原样放行,其余的自动补上前缀。

一次请求,端到端

控制器层本身也按关注点拆开。单个 registerJobsHttpController 组合了四个子控制器——岗位、搜索(同时也承载举报端点)、收藏、管理——每个只拿到自己需要的 handler 和限流分片。一次请求随后这样流转:

  1. 路由 → 控制器。 Fastify 匹配到配置好的路径,调用控制器。
  2. 控制器把关。 它对该端点施加限流(在校验之前,按合适的范围——发帖按认证身份,匿名端点按客户端 IP),校验 payload,并在需要认证时解析出调用者。
  3. 控制器 → handler。 它带着一个干净、已校验的命令或查询,调用对应的应用层 handler。
  4. handler → 仓储 / 服务。 handler 对着仓储接口和应用层服务跑业务逻辑,从不直接碰 Prisma 或 Redis。
  5. 响应。 handler 返回一个规范化的 JobDTO(或相应结果),由控制器序列化。控制器只抛出一小组带类型的错误,每种都映射成结构化的 { message, code } 响应体,限流拒绝还会附带重试提示。

读和写之所以能与缓存、搜索索引保持同步,是因为这些更新发生在 handler 和服务内部、就地完成,而不是走第二条队列:一次写入落进 Postgres 后,同一条代码路径会同步刷新 Redis 详情缓存和搜索文档。

关于服务间认证的一点说明

在架构层面,内部服务之间的调用靠请求上携带的一枚签名服务令牌来认证,由一个在应用启动时注册的 Fastify 插件校验;对需要用户身份的端点,一个 auth 助手会解析出调用者。这明确是一个过渡方案,直到一个专门的 auth 服务来签发受限范围的令牌。具体的签名方式、密钥处理和令牌内容属于部署机密,在此有意略去。

相关

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 →