跳转至

并行通信:通知服务、双因子、附件上云与发布订阅

这一章回答一个问题:用户不在线、系统还忙的时候,变化怎么传达到人? 答案是「间接通信」四件套:邮件(持久、不打扰)、后台任务(不占请求线程)、 对象存储(大文件不压垮应用服务器)、发布订阅(订阅关系与事件队列的数据结构选择)。 主线例子:小李改了一张票 → 订阅者收到邮件 → 附件躺在对象存储里等下载, 全程主请求线程零阻塞。

1. 顶层全景

  间接消息:事件与通知解耦——事件进队列(保序),订阅存集合(去重)
    ↓
  两个独立组件:queue/subscription manager(记账)+ notification service(跑腿,定期跑)
    ↓
  基建三选:Redis(队列+集合自带)、MailHog+lettre(邮件)、fang+PostgreSQL(调度)
    ↓
  三个应用:2FA(refresh token 走邮件)、用户偏好(暗色模式)、附件(multipart → /tmp → S3 → 流式下载)
    ↓
  pub/sub 落地:subscribe/is_subscribed 端点 + RPUSH/LPOP 队列 + 哈希存消息 + 批量处理

  图说:先数据结构,再基建,再应用——每一层都在给「解耦」添一块砖。

2. 间接消息:为什么非得解耦

问题的形状: 改票的人不需要被通知(界面直接给了反馈);需要被通知的人此刻 不一定在线——通知必须持久、等用户回来再看。邮件恰好完美匹配: 它落进收件箱耐心等人,「我们把通知的所有权交给了接收者,而不逼人」1。

由此得出一个架构自由度:处理逻辑可以推迟,甚至可以按预定计划偶尔跑一次—— 这正是后台任务的生存空间2。

3. 订阅服务的架构:一个队列,一个集合

为什么不能同步发: 事件可能发生在限时事务里,而通知要渲染、要挨个发—— 让发起操作的用户等所有订阅者收完信?UX 灾难。所以事件与通知必须解耦; 同时顺序重要(先「推进评审」后「驳回重开」,倒过来读就误解了)—— 顺序重要 ⇒ 这是一条队列(先进先出,队首先服务); 订阅者互相不知道彼此、同一人不该收两封同样的信—— 成员要么在要么不在,这是集合的定义3。

集合的彩蛋:交集。 订阅「工单更新」是一个大集合,订阅「#42 的更新」是小集合; 想一键全关?把自己从大集合里拿走——与任何小集合的交集都为空,所有通知瞬间静默。 集合只是数据,改数据就改行为,不动一行代码、不改配置4。

两个泳道、两个组件5:

组件 职责
queue/subscription manager 登记事件(不阻塞主流)、存订阅
notification service 后台定期跑:捞一批事件 → 查订阅 → 渲染 → 发送

批量的智慧: 每轮只取预定上限(书里叫 batch size),剩下的留给下一轮。 数字书里给了个演算:一轮能处理 10 条,队列里 12 条、每轮新增 5 条—— 第一轮处理 10 剩 2,下一轮 2+5=7 全清空。不怕洪峰:波峰欠的账,波谷自己还上6。

4. 基建三选的账

Redis: 内存键值库,可选持久化,单次操作 ≤ 1 ms;更妙的是 队列(列表)和集合都是内置数据结构——「开箱即用,drop-in-and-forget」7。

调度——生态短板实话实说: Rust 的调度器生态落后;fang 可用, 但代价是它绑死 PostgreSQL(还要用 Diesel 跑它的迁移:装 diesel_cli、 建 extra_migrations/fang、写建表 SQL——表里任务状态是个枚举:new/in_progress/ failed/finished/retried)。书里给了条出路,也给了句实话: 「为调度单独养一个库未必划算,但系统越复杂,它越该独立」——这和微服务 「各服务自带数据库」的思路正好合拍;并且明写 fang 不是工业级方案, 真要全功能的,得出 Rust 域8。

邮件——必须出 Rust 域: 邮件本身就是产物,不是应用能包办的。 MailHog(Go 写的,别嫌弃)扮演假 SMTP:应用发给它的一切邮件它都收, 给个网页界面随便看——开发期不用注册一堆测试邮箱,也不会骚扰真人。 端口安排:SMTP 走常规 1025,网页客户端走 8025(避开常用 Web 端口)9。 发送用 lettre crate:本地 SMTP 或 Gmail 的远程 SMTP 都行; 配置(host/port/TLS 是否开启/账号密码)全从 .env 灌水;服务做成 Axum Extension, 任何 handler 想发信就发10。第一封测试邮件选在应用启动时发—— 既能验通链路,又能当「重启告警」11。书里照例提醒:SMTP 连不上会 panic, 演示可以,生产要把邮件服务跟应用解耦、错误要处理12。

5. 2FA:refresh token 走邮件

思路: 认证时不把 refresh token 直接给客户端,而是只发到用户的邮箱; 邮件里带 客户端地址/verify/{token} 链接。拿不到邮箱的人就拿不到 refresh token, 自然换不到 JWT——第二要素成立。前端在 /verify/:token 路由里取出 token 存 local storage,照常用它换 JWT13。

书里诚实的两条注脚: 一,这算「JWT 的误用」,真生产该用认证器 App 生成限时一次性密码(TOTP);二,邮件链接打开的是一个全新页面, 发起登录的那个旧页面不会自动更新——跨客户端实例的实时同步是另一章的话 (第 12 章)14。

顺带讲了个性化 HTML 邮件: 钓鱼邮件叫不出你的名字,而你有权使用用户 提供的个人资料——用 Askama 模板({{ user_name }}、{{ token }} 注入), ContentType::TEXT_HTML 头一设,信任感就有了15。

6. 用户偏好与暗色模式

偏好是一张挂用户的表:注册时预填默认值(或不存库、代码里给默认)。 认证 handler 查 MFA 偏好决定走不走双因子;前端按偏好换肤16。

暗色模式的工程做法: 样式写两份,暗色那份挂在 body.dark 选择器下; Yew 给包装组件加 class 即全局换肤。状态存本地(local storage/cookie), 偏好表里只存默认值——用户随手切着玩不打扰默认值,下次登录照旧还原17。 现成的主题有 Bulmaswatch 可以免费整体换肤18。

7. 附件:multipart 上传与对象存储

前端: 不需要表单——一个 type="file" 的 input,onchange 触发上传。 从事件里安全地抠出 File 对象(dyn_into + dyn_ref::<HtmlInputElement> + files().get(0));用 web-sys 的 FormData 打包(append_with_blob_and_filename), POST 出去。发出去的其实不是「文件」,是文件的二进制内容; FormData 自动处理 multipart 的 boundary 和 Content-Type,手动只管 Authorization19。

后端: Axum 默认不收 multipart,加 axum_typed_multipart。 #[derive(TryFromMultipart)] 定义表单:#[form_data(limit = "5MiB")] 限制文件 5 MB;TypedMultipart<UploadForm> 当提取器,文件落 /tmp20。

为什么要先落盘再上传(而不是直接转发到云): 云可能临时不可用—— 先把内容存在可靠的地方,再异步传21。

对象存储是什么: 「存下一个文件的全部,但不给文件系统」——二进制 + 元数据, 扁平命名空间,每个对象一个唯一标识;所谓「路径」只是字符串键, 支持按前缀列举(可还原目录假象)22。三大云都有:S3(AWS)、Blob Storage(Azure)、 Google Cloud Storage。各家 SDK 互不兼容——所以用 object_store crate: 统一接口 + feature 标志(aws/azure/gcp,默认还带本地文件系统和内存实现), 换云 = 换 feature + 换 builder + 换密钥23。

上云代码就六步: builder 从环境变量读凭证 → put_multipart 拿 writer → tokio::fs::read 读临时文件 → write_all → flush → shutdown。 包进 tokio::spawn 异步任务,请求线程零等待24。

两条安全红线(书里红字): 凭证硬编码进代码并提交 = 等着被劫持去挖矿; secret 只进环境变量/密钥管理器,.env 进 .gitignore,git-secrets 扫仓25。

下载:四步——建桶(同上);路径与 API 路由对齐 (/tickets/:id/attachments/:filename 与上云时的 tickets/{id}/attachments/{name} 同构,路径参数一取就能拼出对象路径);bucket.get(&path).into_stream() 拿流; Body::from_stream(stream) 装进响应26。

副作用是真金白银的: 客户端从头到尾没见过真实的桶 URL; S3 默认拒公开访问,就算猜到桶地址没有密钥也下不了—— 「这是我们为可移植性付过的最低代价」27。

8. pub/sub 的 Redis 实现

订阅端点两个: subscribe(切换式:是成员就移除、不是就加入—— sismember 查,sadd/srem 改;前端按钮只展示响应、不维护状态); is_subscribed(告诉前端按钮该亮不该亮)28。

集合的 key 与 member: key 形如 ticket:42:subscribers; member 用邮箱——书里如实列了它的四宗罪(会变、进日志、更长、可能与注销账号冲突), 但认了:「图简单,真出事就用一个 entry 的值当另一个 entry 的 key 来关联数据, Redis 快,多查几次没开销」29。总开关集合存在偏好里,用户关通知 = 从大集合移除30。

队列的实现细节(Redis 的三个数据结构各就各位):

结构 角色 命令
列表 队列本体 RPUSH 入队(尾部)、LPOP 出队(头部)——FIFO
哈希 消息本体 hset_multiple 存
队列里存什么 不是消息,是哈希的 key 因为 Redis 列表只存字符串——性能的代价

为什么 key 里要加时间戳: 同一张票改两次,若 key 只是 ticket id, 第二次会覆盖第一次的哈希——同一消息发两遍。引用必须唯一31。

预先渲染正文 vs 发送时个性化: 提前渲染省事但千人一面; 想个性化就得发信时连数据库取用户资料——在「个性化」和「省资源」之间自己找平衡32。

处理循环: while let Ok(key) = lpop(...) 逐条取;哈希 hgetall 读全; 两个集合 sinter 取交集(总开关 × 单票订阅集);对每个订阅者建信发送; counter 超 batch size 就 break,余者下轮再说33。

fang 调度收尾: 任务实现 Runnable trait——run(逻辑)、 cron(调度表达式)、uniq(同时只许一个实例)、max_retries(默认 20)等; 启动序列:AsyncQueue 连库 → AsyncWorkerPool 起 10 个 worker → start → queue.schedule_task(&task) 把任务交给 fang34。

防晕的一句话(原书注释):fang 的队列装「任务」,Redis 的队列装「数据」—— 两个队列,别混35。

9. 主线例子走一遍:一次改票的全链路

  小李把票 #42 从 REVIEWING 推到 TESTING(主请求线程)
      │  ① handler 里 RPUSH 队列一个引用 key;hset_multiple 写好
      │     {订阅集: ticket:42:subscribers, 主题, 正文}
      ▼
  请求立刻返回 200 —— 通知跟用户界面再无关系
      │  ② fang 的 Cron 到点,QueueMailer 醒来(独立线程/核)
      ▼
  LPOP 取出 key,hgetall 读消息;sinter(总开关集, 42 号订阅集) → ["ann@x.com","bob@x.com"]
      │  ③ 逐个 lettre 发信(走 MailHog 开发期可见)
      ▼
  订阅者邮箱躺着一封「#42 状态变更」——人不在线也算送达
  ——同一段时间,小张给 #42 传了个截图:FormData → /tmp → spawn 任务 put_multipart 上 S3,
    库表记 remote_path;小林点下载:GET 流式回传,桶地址全程不可见。

  图说:主线程只做「记账」,慢活全在后台——这就是「并行通信」四个字的形状。
  (人名、票号为演示编的;key 格式、命令名、worker 数出自原文。)

10. 作者的判断与证据(分开标)

有出处的: Redis ≤1 ms 与内置结构、MailHog 端口、fang 绑 PostgreSQL、 object_store 的 feature 清单、批量演算(10/12/5),都是原书原文或工具事实798236。

作者自己的评估:

  • 「Redis 队列 + 集合是绝配」「批量平滑洪峰」是经验性设计判断;
  • 「fang 不是工业方案」「邮件必须出 Rust 域」是生态观察,2026 年需重查;
  • 邮箱当订阅者标识被作者自己列为非最佳实践29;
  • 「2FA 用 refresh token 走邮件是 JWT 误用」是作者自评,生产请用 TOTP14。

11. 边界与局限

  • 邮件投递没有可靠性保证: 发送失败只记 let _ =(丢弃结果)——生产要重试/死信;
  • 队列无持久化承诺: Redis 配置快照/AOF 才保数据,默认重启即失;
  • 附件上传 5MiB 限制、临时文件清理都只在演示粒度;
  • fang + PostgreSQL + Diesel 迁移的三件套是为演示付的额外运维成本,书里自己点破了8。

12. 可带走的

主线例子一行走完: 改票 → RPUSH 记账 → 请求返回 → Cron 醒来 → LPOP + hgetall + sinter → 逐个发信;附件走 FormData → /tmp → spawn 上 S3 → 流式下载。

  1. 事件与通知解耦; 顺序重要 ⇒ 队列;互不重复 ⇒ 集合。
  2. 集合的交集就是总开关; 数据即行为,改数据不改代码。
  3. 批量处理让系统消化洪峰: 每轮限量,波峰欠账波谷还。
  4. 开发期邮件用 MailHog(1025/8025),发送用 lettre,配置全走 .env。
  5. 调度生态是 Rust 短板: fang 可用但绑 PostgreSQL 且非工业级。
  6. 2FA 教学版 = refresh token 走邮件;生产版 = TOTP 认证器。
  7. 大文件三跳: multipart 进、临时落盘、异步转对象存储;下载用流式响应,顺手藏桶地址。
  8. object_store 一个接口通吃 S3/Azure/GCS——可移植性的成本降到换 feature。
  9. Redis 三个结构各就各位: 列表当队列、哈希存消息、队列里只存引用的 key。
  10. 密钥永不入库; .env 进 .gitignore,git-secrets 扫仓。

13. 原文地图

主题 原书章 原文位置
间接消息的所有权 Parallel Communications text/61-fm-indirect-messaging.txt:7(搜「ownership of the notification」)
队列与集合的推导 同上(Architecture) text/62-fm-architecture-of-a-subscription-service.txt:30(搜「queue」)、:36(搜「sets work」)
交集总开关 同上 text/62-fm-architecture-of-a-subscription-service.txt:39(搜「kill switch」)
批量平滑演算 同上 66-fm-implementing-the-pub-sub-system.txt:36(搜「12 in the queue」)、:37(搜「two remaining」)
Redis ≤1ms 与内置结构 同上 text/62-fm-architecture-of-a-subscription-service.txt:87(搜「lightning-fast」)
fang 绑 PostgreSQL 同上 text/62-fm-architecture-of-a-subscription-service.txt:90(搜「PostgreSQL」)、:206(搜「not an industrial solution」)
MailHog 端口 同上(The mailer) text/62-fm-architecture-of-a-subscription-service.txt:227(搜「1025」)
lettre 与 .env 配置 同上 text/62-fm-architecture-of-a-subscription-service.txt:237(搜「lettre」)、:487(搜「SMTP_TLS_OFF」)
启动邮件与 panic 注 同上 text/62-fm-architecture-of-a-subscription-service.txt:508(搜「started or restarted」)、:553(搜「decouple」)
2FA 思路与误用自评 同上(Enhancing security) text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:7(搜「misuse」)
Askama 个性化邮件 同上 text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:13(搜「telltale sign」)
verify 路由与旧页不更新 同上 text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:105(搜「route」)、:146(搜「another chapter」)
偏好表与暗色模式 同上(Implementing user preferences) text/64-fm-implementing-user-preferences.txt:4(搜「yet another table」)、:19(搜「double the used styling」)、:22(搜「default state」)
发的是内容不是文件 同上(Ticket attachments) text/65-fm-ticket-attachments.txt:37(搜「blob」)
先落盘再上传的理由 同上 text/65-fm-ticket-attachments.txt:47(搜「temporarily unavailable」)
multipart 表单与 5MiB 同上 text/65-fm-ticket-attachments.txt:212(搜「axum_typed_multipart」)、:227(搜「5MiB」)
对象存储定义 同上 text/65-fm-ticket-attachments.txt:295(搜「flat namespace」)
object_store 与换云成本 同上 text/65-fm-ticket-attachments.txt:324(搜「object_store」)、:563(搜「lowest cost」)
密钥红线 同上 text/65-fm-ticket-attachments.txt:366(搜「never commit」)
流式下载四步 同上 text/65-fm-ticket-attachments.txt:479(搜「stream body」)、:540(搜「from_stream」)
订阅端点与切换 同上(Implementing pub/sub) text/66-fm-implementing-the-pub-sub-system.txt:14(搜「Subscribe」)、:57(搜「sismember」)
邮箱当 member 的四宗罪 同上 text/66-fm-implementing-the-pub-sub-system.txt:87(搜「de facto standard identifiers」)
RPUSH/LPOP 与 key 唯一性 同上 text/66-fm-implementing-the-pub-sub-system.txt:106(搜「tail」)、:207(搜「overwrite」)
预先渲染 vs 个性化 同上 text/66-fm-implementing-the-pub-sub-system.txt:220(搜「personalize」)
处理循环与 sinter 同上 text/66-fm-implementing-the-pub-sub-system.txt:255(搜「lpop」)、:294(搜「sinter」)、:348(搜「BATCH_SIZE」)
fang Runnable 与双队列 同上 text/66-fm-implementing-the-pub-sub-system.txt:408(搜「run」)、:535(搜「queue is for tasks」)

8.1 通知系统的数据流全图(订阅到送达的完整账本)

  写路径(事件记账,毫秒级)
      改票 handler ──► push_to_queue:
          hset_multiple(key, {订阅集, 主题, 正文})   # 哈希存消息本体
          rpush(TICKET_UPDATES_QUEUE, key)            # 队列只存引用
      ◄── 立刻返回 200;发信与本次请求无关
  读路径(订阅维护,随时)
      POST /subscribe:    sismember? → sadd : srem     # 切换式
      GET  /is_subscribed: sismember                    # 按钮状态
  送路径(fang 定时唤醒,秒~分钟级)
      QueueMailer::run(Cron 到点)
        ├─ lpop 取 key ─► hgetall 读消息
        ├─ sinter(总开关集, 单票订阅集) → 收件人列表
        ├─ 逐个 lettre 发信(经 MailHog 可见)
        └─ counter 到 BATCH_SIZE 即停,余者下轮

  图说:三条路径互不等待 —— 这就是「事件与通知解耦」的完整形状。
  (命令与 key 名出自原书 Code listing 8.27–8.29。)

为什么消息本体不进队列(Redis 的本质限制):「Redis 列表只是 字符串值的链表——这是为性能付的价」;于是哈希装消息、队列装 key, 引用间接一层,数据结构各得其所。key 必须唯一(同票改两次会互相 覆盖),「加个时间戳就够,它只是引用」36。

总开关(preferences 集)与单票集的关系再敲一次: 收件人 = TICKET_SUBSCRIBER_SET ∩ ticket:42:subscribers;用户关掉通知 = 从总开关集移除,「与任何单票集都不再有交集」——数据即开关, 代码零改动37。

9. 本章四个「何时用谁」的速查

需求 本章的答案 理由(原书口径)
用户不在线也要送达 邮件(MailHog 开发期) 「邮件在邮箱里耐心等待,通知所有权交给接收者」
慢活不能堵请求 spawn_local / tokio::spawn 后台任务 「库都支持异步;Tokio 连阻塞操作都有专用线程」
大文件存取 对象存储(object_store) 「应用服务器只做中转;桶地址不对客户端可见」
定期批量 fang + Cron(绑 PostgreSQL) 「Rust 调度生态落后;fang 不是工业方案但够演示」38

与前后章的接缝: 邮件设施(lettre + MailHog)是第 13 章注册邮件验证 的伏笔;Redis 的队列/集合手艺在第 10 章(本组 14 篇)换身缓存再战; 附件的 multipart 手艺(S 篇)与流式响应(第 12 章下载)在 lighttpd 反代下原样工作。一章四技,技技有下文39。

7.1 附件上传的完整时序(一次截图的全旅程)

  用户点「选择文件」→ input onchange 触发
      │
      ├─ 前端:dyn_into 抠出 File → FormData::new()
      │        append_with_blob_and_filename("file", &file, &file.name())
      │        Request::post(/tickets/{id}/upload).body(payload).send()
      │        (boundary 与 Content-Type 由 FormData 自动料理)
      ▼
  后端:TypedMultipart<UploadForm> 提取(5MiB 上限)
      ├─ file.metadata.file_name → 拼 /tmp/<名> 落盘(临时!)
      ├─ 返回 ok —— 响应不等云
      └─ tokio::spawn 后台:put_multipart 六步传 S3
             → 库表记 remote_path → 删临时文件
      ▼
  别人点下载:GET /tickets/{id}/attachments/{file_name}
      Path 参数拼出对象路径 → get().into_stream() → Body::from_stream
      ——客户端眼里就是个静态文件;桶地址全程隐身

  图说:上传三段(收/暂存/上云),下载四步(建桶/拼路径/取流/回传);
  每段都短,因为重活都搬去了后台。
  (端点与键名出自原书 Code listing 8.14–8.26。)

「响应只回状态码」在附件场景的复用: 上传成功只需「收到了、在传」, 真正的成败由通知系统(第 8 章设施)事后告知——「发送通知关于上传结果」 是 spawn 任务的最后一步。请求-响应管「接单」,通知管「回执」, 两条通道各管一段,互不拖累40。

10. 可带走的(补两条)

  1. 事件记账两命令: hset_multiple 存消息、rpush 存引用;队列永不装本体。
  2. 上传三段论: 收(multipart)→ 暂存(/tmp)→ 上云(spawn);响应不等云,回执走通知。

5.6 订阅数据结构的最终账(一张表记住 Redis 三结构)

Redis 结构 键 值 服务谁
集合 ticket:{id}:subscribers 用户邮箱(member) 单票订阅;交集的右 operand
集合 偏好里的总开关集 用户邮箱 通知总闸;交集的左 operand
哈希 消息 key(含时间戳) {订阅集 key, 主题, 正文} 消息本体
列表 TICKET_UPDATES_QUEUE 消息 key 顺序与削峰

「fang 的队列装任务,Redis 的队列装数据」——两个队列并存不矛盾, 一个管「何时醒」,一个管「醒了干什么」。41

12. 本篇卷尾补记(通知系统验收单)

收卷前三问:主线程零阻塞吗(事件只记账)?废票与积压有出口吗 (批量平滑)?密钥进环境变量了吗?三问全过,并行通信才算落地。42


  1. 出处:「Indirect messaging」(text/61-fm-indirect-messaging.txt:7,搜「ownership of the notification」)。 ↩

  2. 出处:「Indirect messaging」(text/61-fm-indirect-messaging.txt:10,搜「postponed」)。 ↩

  3. 出处:「Architecture of a subscription service」(text/62-fm-architecture-of-a-subscription-service.txt:30,搜「queue」)、:36(搜「in or out」)。 ↩

  4. 出处:「Architecture of a subscription service」(text/62-fm-architecture-of-a-subscription-service.txt:39,搜「kill switch」)。 ↩

  5. 出处:「Architecture of a subscription service」(text/62-fm-architecture-of-a-subscription-service.txt:61,搜「queue/subscription manager」)。 ↩

  6. 出处:「Architecture of a subscription service」(text/66-fm-implementing-the-pub-sub-system.txt:118,搜「12 in the queue」)、:122(搜「remaining two」)。 ↩↩

  7. 出处:「Architecture of a subscription service」(text/62-fm-architecture-of-a-subscription-service.txt:87,搜「lightning-fast」)。 ↩↩

  8. 出处:「Architecture of a subscription service」(text/62-fm-architecture-of-a-subscription-service.txt:90,搜「Their choice is PostgreSQL」)、:206(搜「not an industrial solution」)。 ↩↩↩

  9. 出处:「The mailer」(text/62-fm-architecture-of-a-subscription-service.txt:227,搜「1025」)。 ↩↩

  10. 出处:「Setting up lettre」(text/62-fm-architecture-of-a-subscription-service.txt:237,搜「lettre」)、:487(搜「SMTP_TLS_OFF」)、:570(搜「Extension」)。 ↩

  11. 出处:「Sending our first email」(text/62-fm-architecture-of-a-subscription-service.txt:508,搜「started or restarted」)。 ↩

  12. 出处:「Sending our first email」(text/62-fm-architecture-of-a-subscription-service.txt:553,搜「decouple」)。 ↩

  13. 出处:「Enhancing security with two-factor authentication」(text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:7,搜「personal email account」)。 ↩

  14. 出处:「Enhancing security with two-factor authentication」(text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:7,搜「authenticator app」)、:146(搜「scope of another chapter」)。 ↩↩

  15. 出处:「Enhancing security with two-factor authentication」(text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:13,搜「telltale sign」)、:55(搜「ContentType::TEXT_HTML」)。 ↩

  16. 出处:「Implementing user preferences」(text/64-fm-implementing-user-preferences.txt:4,搜「yet another table」)、:7(搜「branch the code」)。 ↩

  17. 出处:「Dark mode」(text/64-fm-implementing-user-preferences.txt:19,搜「double the used styling」)、:22(搜「default state」)。 ↩

  18. 出处:「Dark mode」(text/64-fm-implementing-user-preferences.txt:69,搜「Bulmaswatch」)。 ↩

  19. 出处:「Ticket attachments」(text/65-fm-ticket-attachments.txt:37,搜「blob」)、:62(搜「onchange」)、:190(搜「boundary」)。 ↩

  20. 出处:「File upload on the backend」(text/65-fm-ticket-attachments.txt:212,搜「axum_typed_multipart」)、:227(搜「5MiB」)、:259(搜「persist」)。 ↩

  21. 出处:「Ticket attachments」(text/65-fm-ticket-attachments.txt:47,搜「temporarily unavailable」)。 ↩

  22. 出处:「Object storage」(text/65-fm-ticket-attachments.txt:295,搜「flat namespace」)。 ↩

  23. 出处:「Rust Object Store」(text/65-fm-ticket-attachments.txt:327,搜「object_store」)、:337(搜「aws」)。 ↩↩

  24. 出处:「Sending a multipart request to the cloud」(text/65-fm-ticket-attachments.txt:439,搜「put_multipart」)、:473(搜「spawn」)。 ↩

  25. 出处:「Rust Object Store」(text/65-fm-ticket-attachments.txt:366,搜「never commit」)、:374(搜「git-secrets」)。 ↩

  26. 出处:「Fetching objects from the cloud」(text/65-fm-ticket-attachments.txt:479,搜「stream body」)、:517(搜「into_stream」)、:540(搜「from_stream」)。 ↩

  27. 出处:「Fetching objects from the cloud」(text/65-fm-ticket-attachments.txt:560,搜「restricts blob access」)、:563(搜「lowest cost」)。 ↩

  28. 出处:「Implementing the pub/sub system」(text/66-fm-implementing-the-pub-sub-system.txt:14,搜「Subscribe」)、:57(搜「sismember」)、:39(搜「toggle」)。 ↩

  29. 出处:「Storing subscriptions」(text/66-fm-implementing-the-pub-sub-system.txt:87,搜「de facto standard identifiers」)。 ↩↩

  30. 出处:「Storing subscriptions」(text/66-fm-implementing-the-pub-sub-system.txt:90,搜「intersection」)。 ↩

  31. 出处:「Queue items」(text/66-fm-implementing-the-pub-sub-system.txt:106,搜「tail」)、:207(搜「overwrite」)。 ↩

  32. 出处:「Queue items」(text/66-fm-implementing-the-pub-sub-system.txt:220,搜「personalize」)。 ↩

  33. 出处:「Processing the queue」(text/66-fm-implementing-the-pub-sub-system.txt:255,搜「lpop」)、:294(搜「sinter」)、:348(搜「BATCH_SIZE」)。 ↩

  34. 出处:「Scheduling background tasks with fang」(text/66-fm-implementing-the-pub-sub-system.txt:408,搜「run」)、:494(搜「number_of_workers」)。 ↩

  35. 出处:「Scheduling background tasks with fang」(text/66-fm-implementing-the-pub-sub-system.txt:566,搜「queue is for tasks」)。 ↩

  36. 出处:「Queue items」(text/66-fm-implementing-the-pub-sub-system.txt:210,搜「linked lists of string values」)、:207(搜「overwrite」)。 ↩

  37. 出处:「Storing subscriptions」(text/66-fm-implementing-the-pub-sub-system.txt:90,搜「intersection」)。 ↩

  38. 速查表为我们的产品化整理;理由引文见 text/61-fm-indirect-messaging.txt:7、:206(搜「not an industrial solution」)。 ↩

  39. 接缝为全书画线对照;lettre/MailHog 伏笔见 text/52-fm-user-registration.txt:4(搜「email verification」)。 ↩

  40. 时序依据:「Ticket attachments」(text/65-fm-ticket-attachments.txt:62,搜「onchange」)、:470(搜「send a notification」)。 ↩

  41. 三结构表依据原书 Code listing 8.27–8.30;「两个队列」见 text/66-fm-implementing-the-pub-sub-system.txt:535。 ↩

  42. 验收单为我们的产品化整理;三问对应本篇 §3、§8、§7。 ↩