发布于 更新于 约 9 分钟读完 · 3,325 字

本文目录 11 节
  1. 事实记录(不可修改区)
  2. 一句话总结
  3. 可复用 insight
  4. 1. 下线动机决定下线深度:先写下「我在防谁」 ⚠️首次
  5. 2. 反馈和代码之间隔着时间,「按字面修」可能把显示错变成静默失败 ⚠️首次变体
  6. 3. 序号 / 标号类展示必须眼看一次输出,代码审查看不出来 ⚠️首次
  7. 4. 改默认值会把冷门路径变成热门路径,坑要一起填 ⚠️首次
  8. 5. 反馈里的「矛盾」和「无主语」是待澄清项,不是待推断项 ⚠️首次
  9. 顺手教训
  10. 下次改进
  11. 关联文档

公屏下线只删了界面 · 删界面不等于关链路

入档:2026-08-18 来源:PB Arena(pb.tiaozhuxiansheng.com)内测反馈 16 条的全量收口会话,当日 13 个提交(8 个功能修复),代码 7 文件 +1359/−414,后端用例 15 → 21,四次部署上线 状态:已上线并线上验证;insight 1 为本次首次发现 ⚠️,其余为既有规律的新变体 前情:零作品的比赛_界面演完不等于链路存在_v1(同项目,2026-07-25)——本篇是那一篇的镜像面 ⭐ 同族新变体(2026-08-19):核心律从「删界面不等于关链路」推广为「任何以『用户看不到』为前提的安全属性,只做在渲染层等于没做」。新实例:PB Arena 的「匿名投票」界面显示「作品 A/B/C」,但接口无条件下发 submissions[].userId,同一响应体里的 players[] 又是 id→昵称对照表,打开 DevTools 就能把作品对上人。通用判据:绕过界面直接打接口,看它还给不给。 修法上有个通用形状——前端要那个字段往往只是为了判断「这是不是我自己的」,换成一个布尔位就够,不必暴露全表身份。见 自检46条零驳回_同类中的异类才是缺陷藏身处_v1

事实记录(不可修改区)

  • 反馈来源:项目负责人 2026-08-08 晚在微信里提的 16 条,散在一条约 90 条消息的聊天记录里;由 Claude 桌面端 computer use 读屏整理成《问题清单》16 条 + 一份《回执》

  • 反馈日期与开工时 HEAD 之间隔了 约 20 个提交(含 08-11 的内测 12 项全量修复 1b0a90e)

  • 审计结果:16 条里 2 条已被后续提交修掉(投票倒计时归零不跳转、结算显示获胜者名字),1 条比反馈描述严重得多(积分登记整条链路没接后端,服务端连 /api/admin/score-entries 路由都不存在)

  • 收口结果:15 条修复上线,1 条(「新增轮次不好用」)因信息不足暂缓

  • 审计之外另查出并修掉 5 个不在原清单里的问题

  • 公屏下线的客观查证(2026-08-18,不加解释):第一版只删前端并部署后,对生产站实测——

    POST /api/lobby/messages  → 401   路由在,任何登录用户仍能写
    GET  /api/lobby/messages  → 200   不需要任何凭据,直接返回历史消息与用户名

    一条不带 Authorization 头的 curl 拿回了 5 条真实公屏消息(含 4 个真实昵称)

  • 同时核到:系统状态里 contentModeration 一直是 false,站内从未接入内容审核

  • 第二版删除两条路由后复测:GET/POST 均 404;对照组 GET /api/rooms/:id/messages 仍为 401(房间聊天不在下线范围内,路由保留)

  • 历史消息仍留在 chat_messages 表,但已无任何对外读取路径;是否清库未做,留作显式决定

一句话总结

下线一个功能要删到哪一层,由「下线动机」决定,不由「用户看不看得见」决定——动机是降噪,删界面就够了;动机是合规或安全,攻击面在 HTTP 层不在 DOM 层,必须 curl 一遍才算下线完。

可复用 insight

1. 下线动机决定下线深度:先写下「我在防谁」 ⚠️首次

拿到「把交流社区去掉」这个指令时,我按产品需求处理它:删导航项、删视图、删相关前端代码、删 CSS,并且刻意保留了服务端接口和历史数据,理由写在提交信息里——「便于回退」。这个判断在 UX 语境下完全正确。

真正的动机是三小时后才补上的:防止有人在公屏发布违规内容。动机一换,同一个「保留接口便于回退」立刻从优点变成漏洞:

  • 防用户误点:删界面就够了,用户找不到入口就不会用;
  • 防有意为之的人:删界面等于零收益,因为他不从界面进。

而公屏恰恰是站内唯一「谁都能发、所有人都能看」的自由文本入口,且未接内容审核——它是这个站上暴露面最大的一处,第一版却只动了 DOM。

判据:下线前先把动机写成一句「我在防谁」。主语是「不小心的人」→ 删界面;主语是「有意的人」或「监管」→ 必须把这个功能的每一条 HTTP 路由 curl 一遍,未登录、已登录各试一次,看到 404 才算下线完。

这条是 零作品的比赛_界面演完不等于链路存在_v1 的镜像:那篇是界面全在而链路不存在(演得像有),这篇是界面没了而链路还在(演得像没有)。两篇合起来是同一个提醒——界面和链路是两套独立的事实,任何一方都不能替另一方作证。

2. 反馈和代码之间隔着时间,「按字面修」可能把显示错变成静默失败 ⚠️首次变体

反馈写于 08-08,开工是 08-18,中间 20 个提交。不做审计直接按字面修的话:

  • 2 条会白修一遍(已经修好了,反馈方看到的是旧版本);
  • 第 3 条会修出比原状更糟的结果。他说积分登记「依旧没有关联到我们的用户」,字面读就是「下拉数据源不对」——换个数据源即可。但真实情况是整条链路没接后端:换完数据源,提交时真实 UUID 在演示 store 里找不到,抛「用户不存在」,而当时的 saveScore 没有 try/catch,界面上什么都不会显示。原来是「显示错」,按字面修完变成「点了没反应」。

判据:反馈日期与当前 HEAD 之间隔了几个提交?隔得越多,越要先做一遍「这条现在还成立吗」的审计再排期。审计的产出不是结论,是排期本身——它会改变你先做什么、以及某几条要不要做。

同族但轴不同:内测修复逐项审计闭环_v1 的轴是「清单里哪些已在未提交改动中实现」,本篇的轴是**「反馈与 HEAD 之间的时间漂移」**——前者防重复劳动,后者防按过期描述修出新问题。

3. 序号 / 标号类展示必须眼看一次输出,代码审查看不出来 ⚠️首次

结算页作品排名的每一行都印着「作品 @」,本该是 A/B/C。代码是:

const label = String.fromCharCode(65 + submissions.indexOf(work));

而 work 来自 submissions.map(s => ({...s, voteCount, authorName})) —— 展开副本不是原对象,indexOf 恒返回 −1,charCode(64) 正好是 @。

这个 bug 在代码里长得和正确代码一模一样:indexOf 是合法调用、参数名对得上、没有任何静态检查会报。它只在渲染出来那一刻现形。它也不在反馈清单里——是我为了验收「并列点名」去截图时顺眼看见的。

判据:凡是「下标 / 序号 / 字母标号 / 排名」类的展示,验收时必须眼看一次真实输出。这类值错了不会抛错、不会变红,只会安静地印错一个字符。

4. 改默认值会把冷门路径变成热门路径,坑要一起填 ⚠️首次

把「冠军积分」新建默认值从 300 改成 1,是一行改动、零风险。但它改变了运营的下一步行为:默认值是 1 之后,想表达「本场不发积分」的第一反应就是把它填成 0。

而填 0 撞上一个存量 bug:Math.max(0, Number(updates.reward ?? current.reward_points) || current.reward_points) —— || 把合法的 0 判成假值,静默落回旧值。改成 0 存不进去,还不报错。这个 bug 在默认值是 300 的年代几乎没人踩(谁会把 300 改成 0),改完默认值它就从冷门路径变成高频路径。

判据:改一个默认值 / 入口 / 排序之前,问一句「这个改动会让人更容易走到哪条以前很少走的路」,然后去看那条路上有没有坑。 一行改动的风险不在这一行,在它诱导出的新行为。

5. 反馈里的「矛盾」和「无主语」是待澄清项,不是待推断项 ⚠️首次

16 条里有三条不可直接执行:

  • 自相矛盾:21:47 说「匹配对战给放出来吧」,21:48 说「你都没隐藏干净」;
  • 无主语:「聊天功能去掉」——站内实有四处聊天(比赛房间、大厅等待房间、交流社区、好友私聊);
  • 无法定性:「新增轮次不好用」——是坏了、是缺功能、还是操作繁琐,三种修法完全不同。

我在「聊天」那条上做了推断并给出推荐:从他当时在主持赛事、界面投在大屏的场景出发,推断是比赛房间聊天,还论证了它与三天前刚做的 BUG-10(专为官方赛事补的 battle 维度聊天通道)冲突。推断错了——正确答案是交流社区,四处里暴露面最大、也是唯一未登录可读的那一处。

如果不问直接做,会撤掉三天前刚交付的功能,同时把真正该关的洞留着。

判据:反馈里出现「矛盾 / 无主语 / 无法定性」时,不要用「最可能」去补全。把全部候选连同各自代价列出来让提出者选——尤其当不同候选的代价差好几个数量级时(删一个聊天面板 vs 删一整个带历史数据的功能区)。

顺手教训

  • 新增的测试用例要验反向。 每条新用例写完后 git stash 撤掉对应修复,确认它会失败,再 pop 回来复跑。正反都绿的测试等于没写。本次「删轮次守卫」那条就是这样验的——撤掉修复后确实红。
  • 删功能比加功能更容易留下会抛错的悬空引用。 删掉交流社区后,全局点击处理器里还有一句 $("#emojiPicker").hidden = true,节点没了就是 null.hidden,直接抛错并中断后面的逻辑。node --check 查不出来(语法完全合法)。可靠的探针是加载页面收集 console 错误与未捕获异常,逐个视图走一遍。
  • 删共用样式前先确认「共用」的边界。 交流社区的 .chat-* 一组 CSS 里,.chat-header 和好友面板的 .friends-header 是合并选择器。全删会静默弄坏好友面板。判断是:死 CSS 无害,拆合并选择器有风险,所以只删了明确专属的 .community-*,通用那组留着单独清理。
  • 修复清单本身也会写错,要回头订正。 我在审计清单里写过「300 是有人特意设过的」,后续 git log -S 核实是初始交接包带进来的、从未被调整——纯属臆测。已在原文档里标注订正而不是悄悄改掉。
  • 对外报告要去名。 报告初稿把反馈逐条挂在提出者名下,交付前被要求去名,统一写作「内测反馈」「反馈方」。内部审计记录保留原文与署名(后续追问要靠它),对外转发件去名——两者是不同受众,不该同一套处理。

下次改进

  • 收到「去掉某功能」的指令,先反问一句动机:降噪、合规、还是安全?动机决定要不要 curl。
  • 修一批有日期的反馈前,先数一下反馈日期到 HEAD 之间有多少提交,超过一次迭代就先审计再排期。
  • 验收带序号 / 字母标号的列表时,眼看一次输出,别只读代码。
  • 改默认值时顺手问「这会让人更容易做什么」,去看那条新路径上的存量 bug。
  • 反馈含糊时列候选让人选,不要替提出者做决定——尤其候选之间代价差很大时。

关联文档

类型/平台工程