公屏下线只删了界面 · 删界面不等于关链路
入档: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。
- 反馈含糊时列候选让人选,不要替提出者做决定——尤其候选之间代价差很大时。
关联文档
- ⭐⭐ 零作品的比赛_界面演完不等于链路存在_v1 —— 本篇的镜像(同项目,2026-07-25):那篇是界面全在而链路不存在,本篇是界面删了而链路还开着;两篇合起来是同一个提醒——界面和链路是两套独立事实,任何一方都不能替另一方作证
- ⚠️ 内测修复逐项审计闭环_v1 —— 同族不同轴:那篇防「重复修已实现的项」,本篇防「按过期反馈修出新问题」;两篇的共同前提是动手前先审计
- 内测反馈渠道上线_匿名兜底与422探针验证_v1 —— 同项目更早一环:本次这 16 条走的是微信口头反馈,那篇建的站内表单正是为了收齐排查字段
- 反馈收集双文档体系_v1 —— 同项目反馈流程的另一半
- ⭐ 交付前实测证伪律_v1 —— 再验变体:本篇的「探针」是一条不带凭据的 curl,用来证伪「删了界面就等于下线了」这个应该能行
- 打磨不等于校验律_v1 —— 同族:删干净前端是一道看得见的工序,它不产生任何关于「这个功能是否真的关闭」的证据
- 成稿链接存进库没人读_写入方不等于展示方_v1 —— 状态坑家族:写入方 / 展示方 / 推进方 / 链路存在与否,四者互不代表
- 赛事状态机到点不切换_写入方不等于推进方_v1 —— 同项目同族更早一篇
- 复盘事实先行原则 —— 本篇顶部事实冻结区按它执行
- 09_平台工程索引 —— 平台工程区入口