平台工程

账号系统对标一线 · 多账号抽屉与外部通道的半开陷阱

入档:2026-07-28 来源:蛛网之上(tiaozhuxiansheng.com)账号系统升级 + 自助重置密码上线(above-the-web 仓库 ce2e28729175e4),同日续 静态站接账号系统_内容归git状态归库的双源切分_v1 状态:已上线;服务端回归 27 项、真浏览器 19 + 17 项全过,线上以一次真实的「忘记密码」请求收口(审计 mailed:true)

一句话总结

功能开关要接在「能力被验证过」上,不能接在「配置填了」上。 发信通道半开时,页面照常说「信已经发出去了」而信根本没发出去——这种半开状态比功能压根没上线更糟:它把失败伪装成了成功,用户干等,站长无感。

背景与事实

六条可复用 insight

1. 半开比关着更糟(核心)

上线发信通道时的实际顺序是:拿到 API key → 写进服务器 .env → 重启 → 站点的 selfServiceReset 开关翻成 true。然后才发现发件域名还没验证,服务商一律 403 domain is not verified

此时站点处于最坏的一种状态:用户在「忘记密码」页填了邮箱,页面照常回「信已经发出去了,60 分钟内有效」,而信一封都没发出去。用户会一直等、翻垃圾箱、怀疑自己填错了邮箱;站长这边毫无动静。没有这个功能的时候,页面老老实实写「站内暂时不发信,找站长人工重置」,反而是可用的。

处置:立刻把 key 清空、重启,退回人工兜底那条路;等域名验证通过再填回去。

沉淀成两条可执行的判据:

这一族假成功信号在本库已经反复现形:云端定时内容生产连环坑复盘_全绿不等于已发_v1 的「全绿≠已发」、零作品的比赛_界面演完不等于链路存在_v1 的「界面演完≠链路存在」、ping通不等于路通_fake-ip假信号与节点带宽实测选型_v1 的「ping 通≠路通」。今天多一个变体:配置填了≠通道能用。共同的解法都是同一句:别信中间信号,去问最终产物(数据库里多了行没有?收件人收到没有?)。

2. 对外一句话,对内留全量

「忘记密码」接口有条铁律:不管这个邮箱在不在库里,回的东西必须一模一样,否则它就成了「查某个邮箱注册过没有」的探测器。本次实现里连发信失败都被裹进同一句回答。

但这条安全设计和上一条撞了——防枚举的沉默,和故障的沉默,是同一种沉默。信发不出去时,用户看到的和一切正常时完全一致,站长也就永远不会知道。

解法不是放弃沉默,而是把两条通道分开:

上线后的收口正是靠这条:发一次真实请求,然后去审计表里确认那行是 mailed:true 而不是 mailed:false——如果没有这条对内记录,“发出去了”就只是我方的一厢情愿。

同族的还有两条克制:同一账号一小时最多 3 封(挡「拿别人注册过的邮箱刷他收件箱」)、同一时间只有一张有效票(新发一张,旧的没用过的立刻作废)。

3. 一柜多抽屉:本机多账号的会话模型

「切换账号」不是 UI 糖,它要求本地会话模型从「一个 token」改成一柜多抽屉:

localStorage: { active: <userId>, list: [{ userId, token, expiresAt, user }, …] }

活跃的只有一个(active 指针),切号 = 换指针。这个模型一改,四个行为跟着自然落位:

代价很小:每个令牌在服务端本来就是一条独立会话记录,多账号只是让客户端同时持有几把。收益是”换个号看看”从「退出→重输密码」变成「点一下」。

4. 登录与注册分成两页,分的是两种心态

把两个表单拆成两个页面,不是排版偏好:

挤在一个页面的两个 tab 里时,这两套诉求会互相稀释,而且像「已经登录着又点了登录页」「要再加登一个账号(?add=1)」「登录后回到哪儿」这些状态没有地方放。拆开之后每页只有一条主线,状态机反而变简单了。

5. 断言测行为,截图测观感——两者不可互替

本次两个 bug,测试全绿,是看截图看出来的:

断言测的是行为,截图测的是观感,谁也替代不了谁。 凡是有视觉状态的功能,验证链里必须有一步是”真的看一眼”——本次还顺带看出日期在窄格子里断成两行、同一句错误提示在字段下和表单底重复出现两处。

这条与 全站文字截断体检_检测工具本身要先被证伪_v1 是一体两面:那篇说”检测工具的判据可能是错的”,这篇说”再对的断言也覆盖不到观感”。

6. 权限边界决定分工,交接靠一份带「不要做的事」的 runbook

本次卡在域名验证:那一步要在两个控制台(发信服务商 + DNS 服务商)点击操作,SSH 进不去;而本会话手里有服务器权限、没有浏览器。硬凑的办法是要一对 DNS 服务商的长期 AccessKey——为三条 DNS 记录发一把长期钥匙,权限代价远大于收益

实际做法:写一份 runbook 交给能操作浏览器的助手。这份文档里真正值钱的不是步骤,是边界:

回来的反馈直接可用,而且对方还补了两条我方不知道的现场信息(默认解析线路怎么选、他那边环境没有 dig 只能用服务商自己的校验结果)。跨会话协作的质量,取决于交接文档里”边界”和”判据”写得有多具体,而不是步骤有多详细。

上外部发信通道的顺序清单

顺序反了就会掉进 insight 1。写进了 .env.example 的注释里——把顺序写在配置文件旁边,比写在事后复盘里管用:

  1. 在发信服务商处添加发件域名,拿到 DKIM / SPF / MX 三条记录;
  2. 去 DNS 托管方加记录(本域 NS 在阿里云,记录加在云解析),等状态变 Verified;
  3. 最后才把 API key 填进服务器配置并重启。

三条顺手教训

关联文档

类型/平台工程