architecture-and-request-lifecycle

架构与请求生命周期

youteacher_admin 服务是面向运营方的后端,采用六边形架构(端口与适配器),分成四层同心结构。依赖方向一律朝内:外层认识内层,反之不然。

  • interfaces(接口层)——Fastify 的 HTTP 表面:createApp、路由注册器 AdminHttpController,以及各类插件。
  • application(应用层)——用例(use case),每一类操作一个(如 EmployerVerificationUseCase)。它只负责编排,不含任何框架代码和传输代码。
  • domain(领域层)——聚合与值对象(EmployerVerificationStatusOwnershipStatus),以及应用层所依赖的仓储接口。规则归这一层所有。
  • infrastructure(基础设施层)——满足那些接口的适配器:HTTP 仓储、ServiceClient、认证辅助、内存态缓存与事件总线。

接线:先 bootstrap,再 createApp

构造过程是一个纯函数。bootstrapService(config) 接收各下游服务 URL 与一个共享的 JWT 密钥,一次性构建:

  1. 每个下游服务(profile、auth、content、job)一个 ServiceClient,都标注 callerService: "admin"
  2. 非请求作用域的基础设施——仪表盘缓存与事件总线,二者默认走内存实现;
  3. 认证栈(AuthSessionClientUserAuthHelper);
  4. 各 HTTP 仓储与 HTTP 服务,各自持有它所依赖的那个 client;
  5. 各用例,各自持有它所需要的领域接口。

结果是一个普通的 deps 依赖包。createApp({ jwtSecret, deps }) 随后注册 Fastify 插件,并调用 registerAdminRoutes(app, deps)。没有任何组件自己 new 依赖——一切都由这一次 bootstrap 注入,这正是领域层和应用层不沾传输细节的原因。

一个请求,从头到尾

  1. URL 改写。 Fastify 的 rewriteUrl 给每条入站路径加上 API 前缀,只有 health、env-check 和内部(/__)路径除外,因此处理器可以按干净的路径来写。
  2. 请求上下文。 一个 onRequest 钩子把入站会话 cookie 捕获进 @fastify/request-context,这样仓储向下游转发调用者身份时,就不必把它穿过每一个函数参数。
  3. 认证。 先跑一个 service-auth 插件,接着控制器里第二个 onRequest 钩子对每条路由强制 admin 认证——只有公共路由(health)和诊断用的 env-check 是例外。非 admin 永远到不了处理器。
  4. 处理器 → 用例。 路由处理器拆开请求,调用单个应用用例。
  5. 用例 → 领域。 用例通过仓储接口加载聚合,调用一个执行规则的领域方法(例如:employer 只能从 pending 状态被 verify),然后请仓储持久化新状态。
  6. 仓储 → 下游。 HTTP 仓储从请求上下文读取被转发的 header,通过其 ServiceClient 调用 profile 服务,再用 reconstitute 把原始响应映射回领域聚合。下游返回 404 会被转成 null,用例据此产生一个 not-found 错误,而不是崩溃。
  7. 响应 / 错误。 一个中央错误处理器把有类型的错误翻译成 HTTP:领域/HTTP 错误自带 status 与 code,校验错误变成 400,其余一律变成通用 500——所以不会有调用栈泄露给调用方。

这套形状对每个领域都重复(recruiter、quota、talent、claim、content、job、dashboard):处理器 → 用例 → 领域聚合 → HTTP 仓储 → 下游服务。该服务自己没有数据库;它是架在 profile、auth、content、job 各服务之前的一层编排与策略。

为什么这么建

领域规则只存在于一个地方——聚合——应用层只负责给调用排序,因此传输(到下游的 HTTP)是仓储接口背后一个可替换的适配器。让 header 走请求上下文转发、而不是走参数列表,把这条缝隙保持得很干净:用例永远看不到 cookie 或 JWT。

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 →