并行通信:通知服务、双因子、附件上云与发布订阅¶
这一章回答一个问题:用户不在线、系统还忙的时候,变化怎么传达到人? 答案是「间接通信」四件套:邮件(持久、不打扰)、后台任务(不占请求线程)、 对象存储(大文件不压垮应用服务器)、发布订阅(订阅关系与事件队列的数据结构选择)。 主线例子:小李改了一张票 → 订阅者收到邮件 → 附件躺在对象存储里等下载, 全程主请求线程零阻塞。
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 → 流式下载。
- 事件与通知解耦; 顺序重要 ⇒ 队列;互不重复 ⇒ 集合。
- 集合的交集就是总开关; 数据即行为,改数据不改代码。
- 批量处理让系统消化洪峰: 每轮限量,波峰欠账波谷还。
- 开发期邮件用 MailHog(1025/8025),发送用 lettre,配置全走 .env。
- 调度生态是 Rust 短板: fang 可用但绑 PostgreSQL 且非工业级。
- 2FA 教学版 = refresh token 走邮件;生产版 = TOTP 认证器。
- 大文件三跳: multipart 进、临时落盘、异步转对象存储;下载用流式响应,顺手藏桶地址。
- object_store 一个接口通吃 S3/Azure/GCS——可移植性的成本降到换 feature。
- Redis 三个结构各就各位: 列表当队列、哈希存消息、队列里只存引用的 key。
- 密钥永不入库; .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. 可带走的(补两条)¶
- 事件记账两命令:
hset_multiple存消息、rpush存引用;队列永不装本体。 - 上传三段论: 收(multipart)→ 暂存(/tmp)→ 上云(spawn);响应不等云,回执走通知。
5.6 订阅数据结构的最终账(一张表记住 Redis 三结构)¶
| Redis 结构 | 键 | 值 | 服务谁 |
|---|---|---|---|
| 集合 | ticket:{id}:subscribers |
用户邮箱(member) | 单票订阅;交集的右 operand |
| 集合 | 偏好里的总开关集 | 用户邮箱 | 通知总闸;交集的左 operand |
| 哈希 | 消息 key(含时间戳) | {订阅集 key, 主题, 正文} | 消息本体 |
| 列表 | TICKET_UPDATES_QUEUE | 消息 key | 顺序与削峰 |
「fang 的队列装任务,Redis 的队列装数据」——两个队列并存不矛盾, 一个管「何时醒」,一个管「醒了干什么」。41
12. 本篇卷尾补记(通知系统验收单)¶
收卷前三问:主线程零阻塞吗(事件只记账)?废票与积压有出口吗 (批量平滑)?密钥进环境变量了吗?三问全过,并行通信才算落地。42
-
出处:「Indirect messaging」(
text/61-fm-indirect-messaging.txt:7,搜「ownership of the notification」)。 ↩ -
出处:「Indirect messaging」(
text/61-fm-indirect-messaging.txt:10,搜「postponed」)。 ↩ -
出处:「Architecture of a subscription service」(
text/62-fm-architecture-of-a-subscription-service.txt:30,搜「queue」)、:36(搜「in or out」)。 ↩ -
出处:「Architecture of a subscription service」(
text/62-fm-architecture-of-a-subscription-service.txt:39,搜「kill switch」)。 ↩ -
出处:「Architecture of a subscription service」(
text/62-fm-architecture-of-a-subscription-service.txt:61,搜「queue/subscription manager」)。 ↩ -
出处:「Architecture of a subscription service」(
text/66-fm-implementing-the-pub-sub-system.txt:118,搜「12 in the queue」)、:122(搜「remaining two」)。 ↩↩ -
出处:「Architecture of a subscription service」(
text/62-fm-architecture-of-a-subscription-service.txt:87,搜「lightning-fast」)。 ↩↩ -
出处:「Architecture of a subscription service」(
text/62-fm-architecture-of-a-subscription-service.txt:90,搜「Their choice is PostgreSQL」)、:206(搜「not an industrial solution」)。 ↩↩↩ -
出处:「The mailer」(
text/62-fm-architecture-of-a-subscription-service.txt:227,搜「1025」)。 ↩↩ -
出处:「Setting up lettre」(
text/62-fm-architecture-of-a-subscription-service.txt:237,搜「lettre」)、:487(搜「SMTP_TLS_OFF」)、:570(搜「Extension」)。 ↩ -
出处:「Sending our first email」(
text/62-fm-architecture-of-a-subscription-service.txt:508,搜「started or restarted」)。 ↩ -
出处:「Sending our first email」(
text/62-fm-architecture-of-a-subscription-service.txt:553,搜「decouple」)。 ↩ -
出处:「Enhancing security with two-factor authentication」(
text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:7,搜「personal email account」)。 ↩ -
出处:「Enhancing security with two-factor authentication」(
text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:7,搜「authenticator app」)、:146(搜「scope of another chapter」)。 ↩↩ -
出处:「Enhancing security with two-factor authentication」(
text/63-fm-enhancing-security-with-two-factor-authenticatio.txt:13,搜「telltale sign」)、:55(搜「ContentType::TEXT_HTML」)。 ↩ -
出处:「Implementing user preferences」(
text/64-fm-implementing-user-preferences.txt:4,搜「yet another table」)、:7(搜「branch the code」)。 ↩ -
出处:「Dark mode」(
text/64-fm-implementing-user-preferences.txt:19,搜「double the used styling」)、:22(搜「default state」)。 ↩ -
出处:「Dark mode」(
text/64-fm-implementing-user-preferences.txt:69,搜「Bulmaswatch」)。 ↩ -
出处:「Ticket attachments」(
text/65-fm-ticket-attachments.txt:37,搜「blob」)、:62(搜「onchange」)、:190(搜「boundary」)。 ↩ -
出处:「File upload on the backend」(
text/65-fm-ticket-attachments.txt:212,搜「axum_typed_multipart」)、:227(搜「5MiB」)、:259(搜「persist」)。 ↩ -
出处:「Ticket attachments」(
text/65-fm-ticket-attachments.txt:47,搜「temporarily unavailable」)。 ↩ -
出处:「Object storage」(
text/65-fm-ticket-attachments.txt:295,搜「flat namespace」)。 ↩ -
出处:「Rust Object Store」(
text/65-fm-ticket-attachments.txt:327,搜「object_store」)、:337(搜「aws」)。 ↩↩ -
出处:「Sending a multipart request to the cloud」(
text/65-fm-ticket-attachments.txt:439,搜「put_multipart」)、:473(搜「spawn」)。 ↩ -
出处:「Rust Object Store」(
text/65-fm-ticket-attachments.txt:366,搜「never commit」)、:374(搜「git-secrets」)。 ↩ -
出处:「Fetching objects from the cloud」(
text/65-fm-ticket-attachments.txt:479,搜「stream body」)、:517(搜「into_stream」)、:540(搜「from_stream」)。 ↩ -
出处:「Fetching objects from the cloud」(
text/65-fm-ticket-attachments.txt:560,搜「restricts blob access」)、:563(搜「lowest cost」)。 ↩ -
出处:「Implementing the pub/sub system」(
text/66-fm-implementing-the-pub-sub-system.txt:14,搜「Subscribe」)、:57(搜「sismember」)、:39(搜「toggle」)。 ↩ -
出处:「Storing subscriptions」(
text/66-fm-implementing-the-pub-sub-system.txt:87,搜「de facto standard identifiers」)。 ↩↩ -
出处:「Storing subscriptions」(
text/66-fm-implementing-the-pub-sub-system.txt:90,搜「intersection」)。 ↩ -
出处:「Queue items」(
text/66-fm-implementing-the-pub-sub-system.txt:106,搜「tail」)、:207(搜「overwrite」)。 ↩ -
出处:「Queue items」(
text/66-fm-implementing-the-pub-sub-system.txt:220,搜「personalize」)。 ↩ -
出处:「Processing the queue」(
text/66-fm-implementing-the-pub-sub-system.txt:255,搜「lpop」)、:294(搜「sinter」)、:348(搜「BATCH_SIZE」)。 ↩ -
出处:「Scheduling background tasks with fang」(
text/66-fm-implementing-the-pub-sub-system.txt:408,搜「run」)、:494(搜「number_of_workers」)。 ↩ -
出处:「Scheduling background tasks with fang」(
text/66-fm-implementing-the-pub-sub-system.txt:566,搜「queue is for tasks」)。 ↩ -
出处:「Queue items」(
text/66-fm-implementing-the-pub-sub-system.txt:210,搜「linked lists of string values」)、:207(搜「overwrite」)。 ↩ -
出处:「Storing subscriptions」(
text/66-fm-implementing-the-pub-sub-system.txt:90,搜「intersection」)。 ↩ -
速查表为我们的产品化整理;理由引文见
text/61-fm-indirect-messaging.txt:7、:206(搜「not an industrial solution」)。 ↩ -
接缝为全书画线对照;lettre/MailHog 伏笔见
text/52-fm-user-registration.txt:4(搜「email verification」)。 ↩ -
时序依据:「Ticket attachments」(
text/65-fm-ticket-attachments.txt:62,搜「onchange」)、:470(搜「send a notification」)。 ↩ -
三结构表依据原书 Code listing 8.27–8.30;「两个队列」见
text/66-fm-implementing-the-pub-sub-system.txt:535。 ↩ -
验收单为我们的产品化整理;三问对应本篇 §3、§8、§7。 ↩