服务角色与六边形 + CQRS 架构
人才服务(talent service)是 YouTeacher 的一个模块,负责人才搜索、雇主/招聘方的邀请(invitation)、人才解锁(unlock)以及联系(contact)。它的代码按经典的六边形架构(端口与适配器)切分,命令与查询分离(CQRS),并由单一的 bootstrap 步骤在启动时把具体实现装配到一起。
四个分层
源码把关注点分成四个自外向内的环,外面再包一层 config 与 bootstrap。
- 领域层(
src/domain/) —— 纯业务对象,看不到任何框架或数据库。Invitation聚合根是最清楚的例子:私有构造函数;静态create工厂负责生成 id、用 Node 自带的crypto铸造一个密码学随机 token、并计算过期时间(默认七天);fromProps用于从存储重建,toProps用于写回;状态迁移方法(markAsViewed、accept、decline)负责守住规则 ——accept/decline在不满足canRespond时抛错,markAsViewed只在 pending 状态下生效。isExpired和isDuplicate也在这里。这个文件不 import 任何基础设施。 - 应用层(
src/application/) —— 命令与查询处理器(handler),按功能分组(invitation/、unlock/、search/、contact/、upsert/)。每个功能都把commands/和queries/分开:SendInvitation、AcceptInvitation、DeclineInvitation、UpdateInvitation、SendContact、UnlockTalent是命令;GetInvitationByToken、GetMyInvitations、GetSentInvitations、GetTalentDetail、SearchTalents、GetUnlockedTalents是查询。一个 handler 通过构造函数拿到它的协作对象,在execute里只做一件事。比如UnlockTalentHandler拿到一个UnlockRepository,先查是否已解锁(若已存在则返回alreadyUnlocked—— 天然幂等),否则构造领域对象TalentUnlock并持久化它的toProps()。 - 基础设施层(
src/infrastructure/) —— 满足应用层所声明接口的具体适配器:PrismaInvitationRepository、PrismaContactRepository、PrismaUnlockRepository用 Prisma 实现仓储端口;还有DatabaseBootstrapper、UserAuthHelper和文件存储。只有这一层知道数据库和外部系统的存在。 - 接口层(
src/interfaces/rest/) —— Fastify 交付层:TalentsHttpController和用于服务间调用的InternalController,外加一个serviceAuthPlugin。控制器把 HTTP 翻译成 handler 调用,再翻译回去。
端口归谁所有
依赖箭头一律指向内侧。应用层拥有它所需要的接口 —— InvitationRepository、ContactRepository、UnlockRepository 都放在 src/application/… 下,SearchCachePort 这类端口也是。基础设施层去实现它们。正是这一层反转,让领域层和 handler 得以对 Prisma 一无所知:handler 依赖的是 UnlockRepository,而不是 PrismaUnlockRepository。部分协作对象是可选的 —— FileStorage 和 SearchCachePort 以可选参数传入,缺席时代码会优雅降级(附件下载 handler 只在文件存储存在时才构造)。
路由来自环境变量
src/config/routes.ts 通过一个 requireEnv 助手从环境变量读取每一条路径,变量缺失时直接抛错。路由路径(search、detail、invitations、unlock、contact、内部 users 端点、API base)因此不是写死的 —— 它们是配置,配置出错的部署会在启动时快速失败,而不是去服务一条错误的路径。
Bootstrap:装配根
src/bootstrap/ 下的三个文件组装出可运行的服务,它们是唯一让具体类与抽象端口相遇的地方:
bootstrap.ts——bootstrapDatabase(prisma)确保表存在(DatabaseBootstrapper.ensureTables)并构造三个 Prisma 仓储。生产入口(main.ts)和测试走的是同一段代码。createHandlers.ts—— 接收仓储、一个TalentSearchService,以及可选的缓存端口和文件存储端口,返回整套 handler,为每个 handler 注入其依赖。仓储接口就在这里被绑定到具体实例。createApp.ts—— 构建 Fastify 实例:注册 CORS 和 service-auth 插件;安装单一的错误处理器,把HttpError和ZodError转成结构化的{ code, message }响应(其余一律转成通用的 500,原始堆栈永远不会到达客户端);暴露 health 与 env-check 端点;注册人才控制器。内部控制器只有在三个仓储都齐备时才挂载。一个rewriteUrl钩子把进来的路径按配置的 API base 归一化。
这种形状的收益:领域层可以独立测试,handler 可以对着假仓储运行,替换某个持久化适配器只是 bootstrap 里的一行改动 —— 领域层与应用层里没有任何东西需要动。