基础设施集成
内容服务依赖三块共享基础设施:服务间鉴权、一层 Redis 读缓存、以及 S3/MinIO 对象存储。每一块都被包在各自的适配器里,业务代码只面对一个很小的接口,而不直接面对厂商 SDK。
服务鉴权
每一个进来的 HTTP 请求,都被当作来自另一个内部服务的调用,而不是来自浏览器。一个 Fastify 插件注册了在路由处理器之前运行的 onRequest 钩子。钩子要求带 Authorization: Bearer … 头;没有 bearer token、或 token 无法通过校验的请求,一律以 401 Unauthorized 加一个简短的原因码(missing_token / invalid_token)拒绝。通过校验的 token 会在请求上挂一份小小的鉴权上下文——调用方的服务名加上它的 claims——供处理器读取。
这个 token 是一个紧凑的 HMAC 签名 JWT,携带 service 名以及标准的签发时间 / 过期时间,用服务环境里的一把对称密钥来验证。验证会检查签名,并检查 token 未过期。同一个模块既能签发也能验证,于是一个服务铸造一个短期 token(默认一小时),它的对端来核验。
健康检查路由默认被豁免,注册插件时调用方还可以再传入额外的忽略路径。匹配是带前缀的:一个被忽略的路由同时覆盖它的子路径与带 query 的变体。
Redis 内容缓存
ContentCache 包住 Redis 客户端,只缓存公开读取——已发布的文章(按 id、按 slug、按 alias)以及分类树 / 分类列表。草稿从不缓存;文章的写入方法在条目未发布时直接短路返回,所以未发布内容永远不会从缓存里泄漏出去。键按用途分命名空间(文章用按查询方式区分的前缀,分类树与列表用固定键)。文章缓存五分钟过期,分类十分钟过期。
一致性规则被有意设计成不对称的:
- 读取失败则软降级。 Redis 报错或超时,在几次短重试之后返回
null,让调用方回落到数据库。缓存宕机损失的是延迟,不是正确性。 - 写入失败则硬报错。 缓存的 set 与失效操作在重试用尽后抛错,好让"缓存只写了一半"的状态暴露出来,而不是被藏起来。
每一次改动都会失效它所触及的东西:创建、更新、删除、发布、取消发布一篇文章,都会删掉该文章的 id/slug/alias 键;任何分类变更都会清掉分类键。缓存是派生副本;数据库始终是唯一真相来源,写时失效让这份副本不至于变陈旧。
Redis 客户端本身是一个从环境里的连接 URL 惰性创建的单例,带有生命周期日志(连接、就绪、重连、关闭、结束)以及一条干净的关停路径。
S3 / MinIO 存储
媒体文件存放在对象存储里,背后是一个实现了小接口 StoragePort 的 S3StorageAdapter——应用依赖这个 port,而不是 AWS SDK。适配器用 endpoint、bucket、region、公开基础 URL 和一组凭据来配置,并以 path-style 寻址运行,于是同一份代码在开发期跑 MinIO、在生产跑兼容 S3 的存储都成立。
它自己处理供给:ensureBucket 在 head 请求发现 bucket 不存在时创建它;ensurePublicReadPolicy 把公开读取的语句合并进 bucket 策略——只针对存放公开资产的前缀,且只补上尚不存在的语句。上传落在带前缀、抗碰撞的路径下——特色图片在 images/ 前缀下,附件在 attachments/ 下,每个都带一段随机 id——并同时返回存储用的 key 与由配置基础 URL 拼出的公开 URL。下载与删除按这个 key 寻址,找不到的对象会读回 null 而不是报错。
三者如何拼合
这三个适配器共享同一种形状:一个厂商特定的实现藏在一个窄接口后面,各自按任务选择失败语义。鉴权失败即关闭(没 token,不放行)。缓存在读上失败即打开、在写上失败即关闭。存储对自己的初始化是幂等的。服务自身的逻辑对 Fastify 钩子、Redis 重试、S3 bucket 策略一概无感。