架构与请求生命周期
youteacher_admin 服务是面向运营方的后端,采用六边形架构(端口与适配器),分成四层同心结构。依赖方向一律朝内:外层认识内层,反之不然。
- interfaces(接口层)——Fastify 的 HTTP 表面:
createApp、路由注册器AdminHttpController,以及各类插件。 - application(应用层)——用例(use case),每一类操作一个(如
EmployerVerificationUseCase)。它只负责编排,不含任何框架代码和传输代码。 - domain(领域层)——聚合与值对象(
Employer、VerificationStatus、OwnershipStatus),以及应用层所依赖的仓储接口。规则归这一层所有。 - infrastructure(基础设施层)——满足那些接口的适配器:HTTP 仓储、
ServiceClient、认证辅助、内存态缓存与事件总线。
接线:先 bootstrap,再 createApp
构造过程是一个纯函数。bootstrapService(config) 接收各下游服务 URL 与一个共享的 JWT 密钥,一次性构建:
- 每个下游服务(profile、auth、content、job)一个
ServiceClient,都标注callerService: "admin"; - 非请求作用域的基础设施——仪表盘缓存与事件总线,二者默认走内存实现;
- 认证栈(
AuthSessionClient→UserAuthHelper); - 各 HTTP 仓储与 HTTP 服务,各自持有它所依赖的那个 client;
- 各用例,各自持有它所需要的领域接口。
结果是一个普通的 deps 依赖包。createApp({ jwtSecret, deps }) 随后注册 Fastify 插件,并调用 registerAdminRoutes(app, deps)。没有任何组件自己 new 依赖——一切都由这一次 bootstrap 注入,这正是领域层和应用层不沾传输细节的原因。
一个请求,从头到尾
- URL 改写。 Fastify 的
rewriteUrl给每条入站路径加上 API 前缀,只有 health、env-check 和内部(/__)路径除外,因此处理器可以按干净的路径来写。 - 请求上下文。 一个
onRequest钩子把入站会话 cookie 捕获进@fastify/request-context,这样仓储向下游转发调用者身份时,就不必把它穿过每一个函数参数。 - 认证。 先跑一个 service-auth 插件,接着控制器里第二个
onRequest钩子对每条路由强制 admin 认证——只有公共路由(health)和诊断用的 env-check 是例外。非 admin 永远到不了处理器。 - 处理器 → 用例。 路由处理器拆开请求,调用单个应用用例。
- 用例 → 领域。 用例通过仓储接口加载聚合,调用一个执行规则的领域方法(例如:employer 只能从 pending 状态被 verify),然后请仓储持久化新状态。
- 仓储 → 下游。 HTTP 仓储从请求上下文读取被转发的 header,通过其
ServiceClient调用 profile 服务,再用reconstitute把原始响应映射回领域聚合。下游返回 404 会被转成null,用例据此产生一个 not-found 错误,而不是崩溃。 - 响应 / 错误。 一个中央错误处理器把有类型的错误翻译成 HTTP:领域/HTTP 错误自带 status 与 code,校验错误变成 400,其余一律变成通用 500——所以不会有调用栈泄露给调用方。
这套形状对每个领域都重复(recruiter、quota、talent、claim、content、job、dashboard):处理器 → 用例 → 领域聚合 → HTTP 仓储 → 下游服务。该服务自己没有数据库;它是架在 profile、auth、content、job 各服务之前的一层编排与策略。
为什么这么建
领域规则只存在于一个地方——聚合——应用层只负责给调用排序,因此传输(到下游的 HTTP)是仓储接口背后一个可替换的适配器。让 header 走请求上下文转发、而不是走参数列表,把这条缝隙保持得很干净:用例永远看不到 cookie 或 JWT。